Skip to content

Content Model

This page explains how content is structured in Naluma --- the custom post types, taxonomies, ACF fields, and multilingual layer that together form the content model. Understanding the "why" behind these choices is essential for anyone adding new content types, modifying the REST API, or working on the content automation pipeline.

Design Principles

The content model was shaped by three requirements:

  1. Medical information architecture. Tinnitus content needs domain-specific categorization --- by tinnitus type, patient stage, treatment approach, and audience. Generic WordPress categories and tags are too coarse.
  2. Dual-channel delivery. The same content serves both a web frontend (FSE block theme) and a Flutter mobile app (REST API). The model must be API-friendly, with structured fields rather than unstructured post content alone.
  3. Automated content pipeline. A separate Python pipeline produces content and publishes it via the REST API. Field definitions must be precise and machine-writable, not just human-editable.

Custom Post Types

Naluma replaces the default WordPress post type (which is disabled entirely) with three purpose-built content types:

Post Type Slug Purpose Has Archive?
Article article In-depth patient-facing content on tinnitus topics Yes
Research Summary research Accessible summaries of recent research findings Yes
Landing Page landing Conversion-focused pages (lead gen, email signup) No

All three share the same base configuration: public, REST API-enabled, and supporting title, editor, thumbnail, excerpt, and custom fields. The differences are in their associated field groups and taxonomies.

Why disable the default Post type?

The built-in post type carries assumptions (categories, tags, date-based archives) that do not fit a medical content site. Disabling it prevents confusion and enforces the use of purpose-built content types with proper domain-specific metadata.

Taxonomies

Five hierarchical taxonomies provide the classification system:

Taxonomy Slug Applies To Purpose
Cornerstones cornerstone Articles only Identifies pillar content clusters
Tinnitus Types tinnitus_type Articles, Research Subjective, objective, pulsatile, etc.
Tinnitus Stages tinnitus_stage Articles, Research Acute, subacute, chronic
Treatment Modalities treatment_modality Articles, Research CBT, sound therapy, medication, etc.
Audiences audience Articles, Research Patients, caregivers, newly diagnosed

All taxonomies are hierarchical (like categories, not tags) and REST API-enabled, which allows them to be used as structured filters in both the web frontend and the Flutter app.

Underscore slugs

Taxonomy slugs use underscores (tinnitus_type, not tinnitus-type). This is deliberate: PHP converts hyphens in $_GET parameter keys to underscores, which silently breaks REST API taxonomy filtering if the slug contains hyphens.

Entity Relationships

erDiagram
    ARTICLE ||--o{ CORNERSTONE : "assigned to"
    ARTICLE ||--o{ TINNITUS_TYPE : "classified by"
    ARTICLE ||--o{ TINNITUS_STAGE : "classified by"
    ARTICLE ||--o{ TREATMENT_MODALITY : "classified by"
    ARTICLE ||--o{ AUDIENCE : "targeted at"
    ARTICLE ||--o{ SOURCE : "cites"
    ARTICLE ||--o{ FAQ : "includes"
    ARTICLE ||--|| CONTENT_METADATA : "has"

    RESEARCH ||--o{ TINNITUS_TYPE : "classified by"
    RESEARCH ||--o{ TINNITUS_STAGE : "classified by"
    RESEARCH ||--o{ TREATMENT_MODALITY : "classified by"
    RESEARCH ||--o{ AUDIENCE : "targeted at"
    RESEARCH ||--o{ RESEARCH_ITEM : "contains"
    RESEARCH ||--|| CONTENT_METADATA : "has"

    RESEARCH_ITEM ||--o{ SOURCE : "cites"

    LANDING ||--|| CONTENT_METADATA : "has (partial)"

    CONTENT_METADATA {
        string summary
        int reading_time
        boolean app_featured
        date last_reviewed
    }

    SOURCE {
        string source_publication
        url source_publication_link
        string source_article_title
        url source_article_link
        string source_authors
        string source_article_year
    }

    FAQ {
        string question
        string answer
    }

    RESEARCH_ITEM {
        string item_title
        wysiwyg item_summary
        wysiwyg item_what_this_means
    }

ACF Field Groups

ACF Pro provides the structured field layer. All field groups are registered in code (acf-field-groups.php), not through the ACF GUI, ensuring they are version-controlled and reproducible across environments.

Content Metadata (Shared)

Applied to both Articles and Research Summaries, this group provides the fields that both the web frontend and the mobile app need:

Field Type Purpose
summary Textarea (280 chars) Short description for app list views and meta descriptions
reading_time Number (1--120 min) Estimated reading time, displayed in article headers
app_featured True/False Controls whether the content appears on the app homepage
last_reviewed Date picker Date of last medical/editorial review (E-E-A-T signal)

Article Settings

A single field specific to the Article CPT:

Field Type Purpose
is_cornerstone True/False Marks the article as the pillar article for its cornerstone cluster

Article Sources (Repeater)

A repeater field that captures cited sources with full bibliographic detail:

Sub-field Type Purpose
source_publication Text Journal or publication name
source_publication_link URL Link to the publication homepage
source_article_title Text Title of the specific study
source_article_link URL DOI or journal URL
source_authors Text Author names
source_article_year Text Publication year

This structured format (rather than a freeform "references" textarea) allows the content pipeline to produce machine-formatted citations and the frontend to render them consistently.

Article FAQ (Repeater)

Question/answer pairs for FAQ schema markup:

Sub-field Type Purpose
faq_question Text The question (required)
faq_answer Textarea The answer (required)

FAQ items generate structured data (JSON-LD) for search engine rich results.

Research Digest Items (Repeater)

Research summaries are collections of individual findings, each with its own sources:

Sub-field Type Purpose
item_title Text (80 chars) Short title for the finding
item_summary WYSIWYG Patient-accessible summary
item_what_this_means WYSIWYG Plain-language takeaway
sources Nested repeater Per-item source citations (same structure as article sources)

This nested structure allows a single Research Summary post to contain multiple independent findings, each with its own citation trail --- mirroring how medical research digests work in practice.

Cornerstone Content Strategy

The cornerstone taxonomy implements a pillar/cluster content strategy designed for SEO and information architecture:

graph TD
    C["Cornerstone Term<br/>(e.g., 'Tinnitus Treatments')"]
    P["Pillar Article<br/>(is_cornerstone = true)<br/>Comprehensive overview"]
    A1["Cluster Article<br/>CBT for Tinnitus"]
    A2["Cluster Article<br/>Sound Therapy Guide"]
    A3["Cluster Article<br/>Medication Options"]
    R1["Research Summary<br/>Latest CBT Study"]

    C --> P
    C --> A1
    C --> A2
    C --> A3
    P ---|"internal links"| A1
    P ---|"internal links"| A2
    P ---|"internal links"| A3
    A1 -.->|"links back"| P
    A2 -.->|"links back"| P
    A3 -.->|"links back"| P

Each cornerstone term represents a topic cluster. One article within that cluster is marked as the pillar article (is_cornerstone = true) --- a comprehensive overview that links to all cluster members. The cluster articles link back to the pillar, creating a hub-and-spoke internal linking pattern that signals topical authority to search engines.

The cornerstone-redirect.php mu-plugin reinforces this by 301-redirecting cornerstone taxonomy archive URLs to the corresponding pillar article. This consolidates link equity and prevents duplicate content --- visitors and search engines always land on the authoritative pillar page, not a generic taxonomy archive.

Cornerstone vs. taxonomy assignment

An article being in the "Tinnitus Treatments" cornerstone cluster (taxonomy assignment) is distinct from being the pillar article for that cluster (is_cornerstone ACF field). Many articles share a cornerstone term; only one per term should be the pillar.

Polylang: Multilingual Content

Polylang Pro provides the multilingual layer. The site launches in German (primary) and English. Polylang works by creating translation pairs --- each post exists once per language, linked together.

Key integration points:

  • Taxonomy terms are translated per-language. A "Sound Therapy" term in English has a linked "Klangtherapie" term in German.
  • ACF fields are per-post, so each translation has its own field values (its own sources, FAQ items, reading time, etc.).
  • REST API supports a lang parameter for filtered queries. The custom /tinnitus/v1/ endpoints respect the active Polylang language.
  • hreflang tags are generated automatically via polylang-customizations.php, telling search engines about the language relationship between pages.

The tinnitus-ai-translate mu-plugin accelerates the translation workflow by using AI (OpenAI or Anthropic) to translate post content, taxonomy terms, and Polylang-registered strings. This is particularly important for the content pipeline: the Python automation publishes German content first, then the AI translation plugin produces the English version.

Content Pipeline Integration

The content automation pipeline (a separate Python repository) is the primary content producer. Understanding the content model is essential because the pipeline writes to it programmatically:

  1. Pipeline publishes articles via the standard WordPress REST API (/wp/v2/article/) with application password authentication.
  2. ACF fields are set through the REST API --- each field has show_in_rest => 1 specifically for this purpose.
  3. Taxonomy terms are assigned by slug via the REST API.
  4. Rank Math SEO meta (focus keyword, SEO title, meta description) is registered as post meta with REST API exposure, allowing the pipeline to set SEO fields programmatically.

The entire field registration is code-defined rather than GUI-configured because the content pipeline depends on exact field keys. A field renamed in the GUI would silently break automated publishing.