← all notes

Building a publishing platform, Part 3: Content types for geospatial publishing

In Part 1 I covered the monorepo structure, and in Part 2 the deploy pipeline. This final note is about the layer in between: the Strapi content model that shapes what editors can create and what the frontend can render.

Start with the list, not the fields

Before defining a single field, I wrote out every type of content the platform would host. That list became the collection types:

Article        Long-form feature articles
NewsItem       Short-form industry news
CaseStudy      Sponsored/partner content
MagazineIssue  Digital editions with PDF viewer
Event          Industry events calendar
Video          Embedded video content
PodcastEpisode Podcast episodes with show notes
ProductShowcase Partner product highlight pages
Author         Author profiles
Member         Tiered company directory (gold/silver/bronze)

The temptation is to start with Article and copy-paste it for everything else. Resist that. NewsItem does not need an issueDate. Event needs startDate and endDate, not publishedAt. MagazineIssue has a pdfFile and a flipbookUrl, neither of which makes sense on an article. Each type earns its own schema.

Components for shared building blocks

Several fields appear on nearly every type: slug, excerpt, featuredImage, seo. Strapi 5 components let you define these once and reuse them. The seo component is the most valuable one:

Component: seo
  metaTitle       Text
  metaDescription Text (long)
  ogImage         Media (single)
  canonicalURL    Text
  noindex         Boolean

Attach it to any content type and the frontend has a single interface for rendering meta tags, OpenGraph data, and canonical URLs. When I needed to add noindex to case studies (sponsored content should not compete with editorial in search), it was a one-field change to the component, not ten edits across ten types.

Taxonomy is the spine

A publishing platform lives or dies by how readers find content. The platform uses a Topic taxonomy type that relates to almost every collection type: articles, news, events, videos, podcasts. An editor tags a piece with "Remote Sensing" and it shows up in that topic's feed across all content types, not just articles.

The key decision was making Topic a single flat list rather than a nested hierarchy. Hierarchies feel smart in the CMS and become a nightmare on the frontend: breadcrumbs, active states, URL slugs, and "should this article show under the parent topic too?" Flat tags with a good search interface are simpler and scale better. If you need grouping, add a Theme type that groups topics, rather than nesting topics inside each other.

Relations that mirror the editorial workflow

Article has a relation to Author[] (multiple authors for collaborative pieces). CaseStudy has a relation to Member (the sponsoring company). MagazineIssue has a relation to Article[] (the articles in that issue). PodcastEpisode has a relation to Author[] for guests.

The trap is over-relating. Early on I linked NewsItem to Article so news could reference related features. Editors never used it. The relation added complexity to the API responses and the frontend for zero editorial value. I removed it. If a news item needs to reference an article, the editor pastes the URL into a sourceUrl text field. Not every connection needs to be a database relation.

The Member directory: tiers as enums, not separate types

Companies partner with the publication at three tiers: gold, silver, bronze. The obvious wrong move is three separate collection types. The right move is one Member type with a tier enum field:

Member:
  name         Text (required)
  slug         UID
  logo         Media (single)
  description  Rich Text (Blocks)
  website      Text
  tier         Enum: gold | silver | bronze
  isFeatured   Boolean

The frontend filters by tier when rendering the directory. Promoting a member from silver to gold is a dropdown change, not a data migration.

What I would do differently

I should have started with the seo component from day one instead of adding it after the first three types were already in production. Retrofitting it meant writing a script to populate empty SEO fields on existing content. I also should have kept Author and Member separate from the beginning rather than initially trying to model companies as authors with an isOrganization boolean. That boolean infected the frontend for months: every author page had to branch on "is this a person or a company?" before rendering bio, photo, or social links. Separate types would have been cleaner from the start.

Takeaways

Model each content type with its own schema. Share structure through components, not copy-paste. Keep taxonomy flat. Prefer enums over separate types for tiers and categories. And start with SEO from the first content type, because retrofitting it is painful. The schema decisions you make early are the ones you live with longest, so spend time on the model before you build the UI.

That wraps up this series. Read Part 1 on the monorepo and Part 2 on deploys if you have not already.