Blog /
Reading your WordPress taxonomy from an app: a recipe-filter walkthrough
Saturday’s post shipped a recipe-filter app that reads from your posts and your tags. That version hardcoded the top 12 tags as a chip row. It worked.
It also did something subtly wrong that almost every “small filter widget” gets wrong: it treated the tag list as a snapshot. If the food blogger publishes a recipe next week with a brand-new “kimchi” tag, the filter never picks it up. The tag list was frozen on the day the app shipped.
A grown-up filter reads the current taxonomy from the site, every time the page loads. If the writer published “kimchi” yesterday, today’s filter has a kimchi chip. No re-deploys, no manual updates.
This post walks the taxonomy surface in the bridge, with the recipe filter as the running example. Same wedge as Saturday; deeper bridge tour.
What “taxonomy” means in WordPress
If you came from React + Vercel, taxonomy is roughly “the system of tags and categories.” It is a generic primitive: any post type can have any number of taxonomies attached, each of which is a hierarchy of terms.
Default WordPress ships with two taxonomies on the post type:
category(hierarchical, exclusive)post_tag(flat, multiple)
A real site has more. WooCommerce attaches product_cat and product_tag to the product type. LearnDash attaches ld_course_category. An IDX plugin attaches property_type, location, bedrooms. A food blog might register an ingredient and meal_type taxonomy via Custom Post Type UI.
Every one of those is queryable through the same bridge surface.
The two methods that matter
// List terms in a taxonomy:
const tags = await dsgo.taxonomy.terms({
taxonomy: 'post_tag',
orderby: 'count',
order: 'desc',
per_page: 20,
});
// [{ id, name, slug, count, taxonomy }, ...]
// Get a single term by slug:
const term = await dsgo.taxonomy.term({
taxonomy: 'ingredient',
slug: 'gluten-free',
});
// { id, name, slug, count, taxonomy } or null
For the recipe filter, taxonomy.terms is the workhorse. You call it once on page load with the right taxonomy name, and you have the live list.
The corrected recipe filter
Here is the version from Saturday’s post, with the hardcoded chips replaced by a live read:
import { dsgo } from '@designsetgo/app-client';
await dsgo.ready;
// Read recent recipes.
const recipes = await dsgo.posts.list({
category: 'recipe',
per_page: 100,
_embed: true,
});
// Read tags by usage (live, not hardcoded).
const tags = await dsgo.taxonomy.terms({
taxonomy: 'post_tag',
orderby: 'count',
order: 'desc',
per_page: 12,
});
renderChips(tags);
renderCards(recipes);
The next time the writer publishes a recipe tagged “kimchi,” next morning’s filter has a kimchi chip. The chip appears because the term’s count went from 0 to 1, and the orderby surfaces it once it edges into the top 12.
For a filter that uses a custom taxonomy instead of generic tags (the better long-term shape for a food blog), the change is one string:
const ingredients = await dsgo.taxonomy.terms({
taxonomy: 'ingredient', // your custom taxonomy slug
orderby: 'count',
order: 'desc',
per_page: 20,
});
The bridge does not care which taxonomy. The site’s CPT UI plugin or theme is what registered it. The bridge enumerates whatever is registered with show_in_rest: true.
How filtering happens
The chips are just the UI. The filter happens on the next bridge call.
When the user clicks a chip, you re-query posts with that term as a filter:
async function applyFilter(tagSlug: string) {
const filtered = await dsgo.posts.list({
category: 'recipe',
tags: [tagSlug], // for post_tag
per_page: 100,
});
renderCards(filtered);
}
For a custom taxonomy:
async function applyIngredientFilter(slug: string) {
const filtered = await dsgo.posts.list({
category: 'recipe',
taxonomy: { ingredient: [slug] },
per_page: 100,
});
renderCards(filtered);
}
Multi-term filtering (recipes that have both chicken and lemon) uses an array:
const filtered = await dsgo.posts.list({
category: 'recipe',
taxonomy: { ingredient: ['chicken', 'lemon'] },
per_page: 100,
});
The default boolean is AND. For OR, pass relation: 'OR':
const filtered = await dsgo.posts.list({
category: 'recipe',
taxonomy: { ingredient: ['chicken', 'tofu'], relation: 'OR' },
per_page: 100,
});
The manifest
Two permissions:
{
"permissions": {
"read": ["posts", "taxonomy"],
"write": []
}
}
posts to list recipes. taxonomy to read term lists. Nothing else. The install dialog will say:
This app wants to read: posts (including: recipes), taxonomies.
If the site has a ingredient taxonomy registered with show_in_rest: true, it is included in the “taxonomies” line automatically (the bridge enumerates them at install time, as covered Friday).
Two patterns the recipe filter does not need but other apps do
Hierarchical taxonomies. Categories are hierarchical (Asia > East Asia > Korea). If you want to render a tree, ask for the full list and reconstruct the parent-child structure from the parent field on each term:
const cats = await dsgo.taxonomy.terms({ taxonomy: 'category', per_page: 100 });
// cats[i].parent is the parent term ID; 0 if top-level.
const tree = buildTree(cats);
The bridge does not currently return a pre-built tree (because what shape you want depends on whether you are rendering a dropdown, a tree, or a breadcrumb). The flat list with parent IDs is enough to build any of those.
Counts per filter intersection. “How many recipes have chicken AND are gluten-free?” requires actually running the query and counting. The bridge does not pre-compute facet counts. For small post collections (under a few hundred), running the query is fine. For larger collections, accept that the chip count is “total recipes with this tag” not “recipes with this tag intersected with the current filter.” Faceted search at scale is a separate problem.
Where this goes
The taxonomy surface is the wedge for a whole category of apps you can ship in an afternoon:
- A blog category browser that reads your live category list.
- A team-member directory grouped by department (a
departmenttaxonomy on ateam_memberCPT). - A product filter by attribute (WooCommerce attributes are taxonomies).
- A course catalog grouped by skill level (a
skill_leveltaxonomy on courses). - A locations browser grouped by region.
All of those are the same pattern: read the taxonomy, render the filter, re-query posts on filter change. The bridge call shape is identical across them. The only thing that changes per app is the post type and the taxonomy name.
The shorter version
dsgo.taxonomy.terms({ taxonomy: '...' }) reads the live taxonomy. dsgo.posts.list({ taxonomy: { ... } }) filters posts by it. Together they are 90% of what filter UIs need. Stop hardcoding tag lists.
Further reading
- A recipe filter that actually uses your recipes: the wedge tutorial with the hardcoded chips.
- The bridge cookbook: includes the “dataset-driven-detail-page” pattern that uses taxonomies.
- BRIDGE-API.md §taxonomy: the full surface.