Skip to content
Numan Zahid
Get in touch
All work

Podcast and video site on WordPress and Next.js

Media and publishing · Front end and CMS architecture

WPGraphQLACFWordPressNext.js 16React 19Tailwind CSS v4
0Rebuilds needed to publish, via on-demand revalidation
25Routes, covering episodes, videos, articles and taxonomy archives
6Sitemaps generated from the CMS, behind one index
Starting point
  • Episodes, videos and written articles were three different shapes of content that needed to share taxonomies, archives and related-content logic across the site.
  • Some articles originate in Substack and some in WordPress, and both needed to appear in one feed without duplicating or competing for the same search result.
  • Editorial fields for an episode -- transcripts, timestamped sections, resource links, takeaways -- had no home in the default post model.
What I did
  • Defined the content model as a plugin: two custom post types, five shared taxonomies, and ACF field groups written as version-controlled PHP rather than clicked into a database.
  • Put the related-content logic in the CMS as a GraphQL resolver, so one taxonomy-aware implementation serves podcasts, videos and posts instead of three in the front end.
  • Set a revalidation window per content type rather than one global number, tuned to how often each actually changes.
  • Bridged Substack through post meta, letting synced articles carry their original URL as a canonical so the two publications never compete.
Measured outcome
  • Adding an episode is filling in a form the plugin defines, and the site updates without a deploy.
  • Every legacy URL from the previous structure redirects to its new home, so nothing that had accumulated ranking was dropped.
  • Related content stayed consistent across all three content types, because there is only one implementation of it.
Technical detail
  • ACF field groups live as PHP in the repository and register on boot. A field added in staging arrives in production through a deploy rather than through someone remembering to repeat the clicks.
  • The related-content resolver falls through three strategies in order -- same series, then shared topic, then most recent -- so a new episode with no taxonomy overlap still renders a populated section instead of an empty one.
  • WPGraphQL has no total count on a connection, which makes numbered pagination impossible. The plugin adds one, and the front end refuses any query requesting it unless the caller opts in, because it forces an expensive count that list views do not need.
  • Substack-synced articles carry the original URL as their canonical. The site renders them in full but tells search engines where the authoritative copy lives, which is the difference between syndication and duplicate content.
  • The author field is stripped from both the GraphQL and REST responses. Attribution on this site is a host taxonomy, not a WordPress user, and exposing the underlying account would have leaked editorial logins.
  • Anonymous REST is blocked entirely while GraphQL stays open, matching a front end that only speaks GraphQL and shrinking the public surface to what is actually consumed.

Got a project?

Email me with where things are now and what you'd like to change. If I'm not the right person for it, I'll tell you, and I'll usually know someone who is.

hello@zahid.dev