# SEO pipeline

## Stages

1. **Route resolution** produces a page type (`home`, `question`, `topic`, `category`, `tag`, `profile`, `search`, …) and its data.
2. **Canonical policy** produces the canonical path and normalised query.
3. **SEO builder** produces the title, description, robots directives, social tags and JSON-LD.
4. **Filters** (`seo.meta`, `seo.structured_data`) let plugins adjust the result.
5. **Layout** renders the values into the document head.

## Canonical policy

One component owns canonical construction, so the rule is not re-implemented per route.

**Preserved** (they change what the page shows):

- `?page=N` on listings, when N > 1
- `answers_page=N` on questions, when N > 1
- `comments_page=N` on topics, when N > 1

**Stripped** (they do not change content):

`created`, `voted`, `followed`, `reported`, `answer`, `commented`, `utm_*`, and any other tracking or status parameter.

Page 1 omits its page parameter entirely.

The effect: every paginated page self-canonicalises, and a status parameter after a successful post never mints a second URL for the same content.

## Structured data

| Page | Emitted |
|---|---|
| Question | `QAPage` containing `Question` and `Answer` |
| Topic (member) | `DiscussionForumPosting`, body in `text` |
| Topic (editorial/system) | Ordinary page schema |
| Profile (indexable) | `ProfilePage` |
| All | `BreadcrumbList` |

Two rules are load-bearing:

- **`answerCount` is the total** across all pages, not the count rendered on the current page. Reporting the page count tells a search engine a 130-answer question has 50 answers.
- **`text`, not `articleBody`,** in `DiscussionForumPosting`. `articleBody` is not valid for that type.

Comments enter JSON-LD only where the structure stays bounded and valid. Thousands of comments are never dumped into head markup.

## Indexability

| Surface | Policy |
|---|---|
| Public content | Indexable |
| Empty category or tag | Not indexable, not in the sitemap |
| Empty profile | Not indexable, not in the sitemap |
| Editorial/system profile | `noindex` by default |
| Search results | `noindex` |
| Admin, auth, account, API | Excluded and `noindex` |

A profile is indexable when it has at least one published question, answer or topic, or a meaningful non-empty bio.

## Extending

`seo.meta` filters safe public meta values before rendering. `seo.structured_data` filters the JSON-LD. Both run after core policy, so a plugin can adjust but not accidentally publish something the core marked `noindex`.
