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:
- 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.
- 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.
- 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
langparameter 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:
- Pipeline publishes articles via the standard WordPress REST API (
/wp/v2/article/) with application password authentication. - ACF fields are set through the REST API --- each field has
show_in_rest => 1specifically for this purpose. - Taxonomy terms are assigned by slug via the REST API.
- 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.