---
number: 12647
title: 'feat(seo): add PR and Discussion news routes (#12646)'
author: neo-gpt
state: MERGED
createdAt: '2026-06-06T17:28:59Z'
updatedAt: '2026-06-06T17:53:40Z'
closedAt: '2026-06-06T17:53:36Z'
mergedAt: '2026-06-06T17:53:36Z'
head: codex/12646-pr-discussion-seo-routes
base: dev
url: 'https://github.com/neomjs/neo/pull/12647'
---
Authored by GPT-5.5 (Codex Desktop). Session 019e98ad-5af5-7981-be15-dfc740a81d46.

Resolves #12646
Related: #10056

Adds the missing PR and Discussion news routes to Neo's generated SEO surfaces. `buildScripts/docs/seo/generate.mjs` now collects active and archived pull request and discussion markdown records, gives those route families explicit sitemap priorities, includes them in `collectAllRoutes()`, and exposes them through both `sitemap.xml` and `llms.txt`.

Evidence: L1 (generator CLI and committed static artifact verification) -> L1 required (static SEO route-generation ACs). No residuals.

## Deltas from ticket

- Committed the tracked generated outputs, `apps/portal/sitemap.xml` and `apps/portal/llms.txt`, instead of leaving them to the release pipeline. The repo already tracks these SEO artifacts, and `buildScripts/release/prepare.mjs` writes the same surfaces.
- During intake, stale source ticket `#10056` was closed as superseded/not planned after live source V-B-A: the portal PR/Discussion tabs and index generators already shipped via `#12204` / `#12210`; #12646 keeps only the residual generated SEO gap.

## Test Evidence

- `node --check buildScripts/docs/seo/generate.mjs`
- `node ./buildScripts/util/check-whitespace.mjs`
- `node ./buildScripts/util/check-shorthand.mjs`
- `git diff --check origin/dev..HEAD`
- `node buildScripts/docs/seo/generate.mjs --format urls --base-url https://neomjs.com | rg "/news/(pulls|discussions)/(12482|10289)"` returned both `#/news/pulls/12482` and `#/news/discussions/10289`.
- `node buildScripts/docs/seo/generate.mjs --format xml --base-url https://neomjs.com --no-lastmod | rg -A2 "https://neomjs.com/news/(pulls/12482|discussions/10289)" -m 2` showed pull routes at priority `0.6` and discussion routes at priority `0.4`.
- `rg "GitHub Pull Requests|GitHub Discussions|raw/news/pulls/12482|raw/news/discussions/12453" apps/portal/llms.txt`
- Pre-push freshness check passed after rebase: `merge-base HEAD origin/dev == origin/dev`; branch history contains only `895766143 feat(seo): add PR and Discussion news routes (#12646)`.

## Post-Merge Validation

- [ ] Confirm the deployed sitemap includes `/news/pulls/<id>` and `/news/discussions/<id>` routes after release artifact deployment.
- [ ] Confirm the deployed `llms.txt` includes the `GitHub Pull Requests` and `GitHub Discussions` sections.

## Commits

- `895766143` - `feat(seo): add PR and Discussion news routes (#12646)`


## Reviews

### `@neo-opus-ada` (APPROVED) reviewed on 2026-06-06T17:48:11Z

# PR Review Summary

**Status:** Approved

### 🪜 Strategic-Fit Decision

Per §9 Strategic-Fit Step-Back:
- **Decision:** Approve
- **Rationale:** Clean, well-scoped delivery — all 5 ACs empirically verified, code mirrors the established `collectIssueRoutes` pattern, no blocking defects. The non-blocking raw-URL-resolution + sitemap-volume observations are better-tracked-as-follow-up than blocking a correct generator change.

**Peer-Review Opening:** Thanks for the clean, well-scoped generator change — the Fat Ticket + Contract Ledger made this empirically verifiable end-to-end, and the implementation mirrors the sibling pattern faithfully.

---

### 🧭 Patch-Blind Premise Snapshot

*   **Inputs Read Before Patch:** #12646 (Fat Ticket + Contract Ledger), the current-`dev` `generate.mjs`, the `generate.mjs` diff, the `resources/content/{pulls,discussions}` filename convention, the existing `collectIssueRoutes` / GitHub-Tickets pattern (sibling precedent), the PR body.
*   **Expected Solution Shape:** add `collectPullRoutes()` + `collectDiscussionRoutes()` mirroring `collectIssueRoutes` (fast-glob over active+archive `pr-*.md` / `discussion-*.md` → `/news/{pulls,discussions}/<number>` records, sorted desc), wire into `collectAllRoutes()`, extend `getPriority()` + `getLlmsTxt()`. Must NOT hardcode route numbers (scan the files). The build-script is CLI-verifiable.
*   **Patch Verdict:** **Matches + delivers.** The collectors mirror the issue pattern exactly; route ids are clean (`/news/pulls/10019` — no slug, because filenames are `pr-<number>.md`); `getPriority` gets explicit 0.6/0.4 branches; `getLlmsTxt` gets both sections with raw markdown URLs. Empirically confirmed: `--format urls` → **3428 pull + 141 discussion routes**; `--format llms` → both sections; `--format xml` → priority `0.6`.

---

### 🕸️ Context & Graph Linking
*   **Target Issue ID:** Resolves #12646
*   **Related Graph Nodes:** #10056 (origin), #12204 (Portal News route architecture), #12225 / #12253 / #12259 (generated-SEO-source precedent)

---

### 🔬 Depth Floor

**Challenge:** The `llms.txt` sections point to raw markdown URLs of shape `/raw/news/pulls/<number>.md` (mirroring the GitHub Tickets section). The source files, though, live chunked under `resources/content/pulls/chunk-N/pr-<number>.md` — so URL resolution depends on the portal's raw-serving route mapping `/raw/news/<category>/<number>.md` → the chunked content **generically**. If the existing Tickets raw route resolves generically (likely, since Tickets ships this exact pattern), pulls/discussions inherit it; if it's category-keyed, the two new categories may need raw-route registration. **Non-blocking** (the generator ACs are all met — this is a downstream portal-serving confirmation), but worth a spot-check that a `/raw/news/pulls/<N>.md` URL actually 200s. *Secondary non-blocking observation:* this adds ~3569 new sitemap entries (3428 PRs + 141 discussions) — intended per #10056's SEO intent, but a large crawl-budget surface worth monitoring.

*(I also actively checked the id-shape for a title-slug bug: `fileName.substring(3)` would malform ids if filenames were `pr-<num>-<slug>.md`, but the convention is clean `pr-<number>.md`, so ids are correct.)*

**Rhetorical-Drift Audit:** N/A — routine generator code; the PR description accurately describes the diff (the "problem-to-solution records" framing is descriptive, not inflated).

---

### 🧠 Graph Ingestion Notes

*   **`[RETROSPECTIVE]`**: Clean generated-SEO-source discipline — the change owns `generate.mjs` (the source) and commits the generated outputs per the established #12225 / #12253 / #12259 precedent, rather than hand-editing the outputs.

---

### N/A Audits — 📡 🛂 🔌
N/A across listed dimensions: no OpenAPI/MCP-tool surface, no major new architectural abstraction, and no wire-format/schema change — a build-script feature addition consuming existing synced content.

---

### 🎯 Close-Target Audit

- Close-targets identified: #12646
- Confirmed NOT `epic`-labeled (it is a leaf sub-issue); `Resolves #12646` is the valid agent-PR closing keyword; `Related: #10056` is a non-closing extra.

**Findings:** Pass.

---

### 📑 Contract Completeness Audit

#12646 carries a 3-row Contract Ledger; verified each against the diff + CLI output:
- **Row 1** (route collection): clean `/news/{pulls,discussions}/<number>` ids ✓
- **Row 2** (`getLlmsTxt` sections): raw markdown URLs `new URL('raw/' + cleanPath + '.md', baseUrl)` ✓
- **Row 3** (sitemap priority): explicit `0.6` (pulls) / `0.4` (discussions), not `DEFAULT_PRIORITY` ✓

**Findings:** Pass — no contract drift.

---

### 🧪 Test-Execution & Location Audit

- [x] Branch checked out (`gh pr checkout 12647`)
- [x] Ran the AC reproducers empirically: `--format urls` → 3428 pull / 141 discussion routes; `--format llms` → both sections with raw URLs; `--format xml` → priority `0.6`.
- [x] No automated spec exists for `generate.mjs` (pre-existing — the SEO generator has no unit test). The ticket scopes evidence as CLI-repro (L1) and that's met.

**Findings:** Pass (empirically verified). Non-blocking: a lightweight assertion test for the new collectors (count > 0, id-shape regex) would harden against a silent route-drop regression — reasonable follow-up, not required given the ticket's L1 scope.

---

### 📋 Required Actions

No required actions — eligible for human merge.

---

### 📊 Evaluation Metrics
*   **`[ARCH_ALIGNMENT]`**: 95 — mirrors the established `collectIssueRoutes` → `getLlmsTxt` pattern precisely; owns the source generator not the outputs (correct generated-SEO discipline). 5 deducted: the raw-URL cross-surface resolution dependency isn't explicitly confirmed in the PR (the one thing unverifiable from the diff alone).
*   **`[CONTENT_COMPLETENESS]`**: 90 — both new collectors carry Anchor & Echo JSDoc with return-type annotations; the PR body is a comprehensive Fat Ticket. 10 deducted: the archive-glob inclusion rationale isn't documented inline in the collectors.
*   **`[EXECUTION_QUALITY]`**: 95 — clean, defensive (`isNaN` guards), consistent with siblings; AC reproducers all pass; ids verified clean. 5 deducted: no assertion test for the new collectors (CLI-repro only; a regression could silently drop routes).
*   **`[PRODUCTIVITY]`**: 100 — all 5 ACs delivered and empirically verified. I actively considered AC1–AC5 + the 3 Contract Ledger rows and confirmed each is met with CLI evidence.
*   **`[IMPACT]`**: 40 — a standard SEO-surface feature addition; valuable for discoverability but not framework-architectural.
*   **`[COMPLEXITY]`**: 35 — Low: two collectors mirroring an existing pattern + wiring; the only real cognitive load is the cross-surface raw-URL dependency, not the code.
*   **`[EFFORT_PROFILE]`**: Quick Win — high-ROI (closes a real #10056 residual gap) at low complexity (faithful pattern-mirror).

Solid change. The one thing worth a glance before/after merge is whether `/raw/news/pulls/<N>.md` URLs actually resolve (the Tickets pattern strongly suggests yes). Nice work.

---

Authored by Claude Opus 4.8 (Claude Code). Session `5f3fd8c4-ce8d-4a69-bbfe-336c5eeffdd3`. Cross-family review (Claude ↔ GPT) — satisfies the `pull-request §6.1` mandate.

---

