Skip to content

Serve one docs corpus to both the public Help Center and the product - #1

Merged
joshparolin merged 4 commits into
mainfrom
feat/help-center-in-product
Aug 13, 2026
Merged

Serve one docs corpus to both the public Help Center and the product#1
joshparolin merged 4 commits into
mainfrom
feat/help-center-in-product

Conversation

@joshparolin

Copy link
Copy Markdown
Member

Docs half of the in-product Help Center work. The app half is in
web-ui-v2 #440 and
web-api #3778.

This is already deployed. learn.sessionboard.com has been serving these
commits since tonight (worker version d8950ccd), so this PR is not a request to
ship — it is getting production under CI and review, which it has not been.

What it does

The Help Center stays the single source of truth, and the product renders the
same articles personalized to an event's features and settings. Rather than copy
articles into the app — forking the corpus on day one — personalization lives in
the MDX:

  • IfFeature / IfSetting wrap conditional content and app: links point at
    product routes. All compile to inert data-sb-* attributes, so the public page
    renders every branch and stays fully crawlable. Nothing about SEO changes.
  • Frontmatter gains features / audience / jtbd, which is what lets
    retrieval filter by entitlement instead of inferring from prose.
  • Marker names are validated against a product contract exported from web-api and
    web-ui-v2, so a marker naming something that no longer exists fails the build.
  • npm run build also emits dist/_internal/help-index.json (226 articles,
    1,395 chunks, sanitized body HTML, per-article contentHash), which web-api
    pulls nightly to embed for Team Lead. The Worker gates /_internal/ on a
    bearer token and answers a bad one with the same 404 an unknown path gets.

Hydration is specified rather than reimplemented: MARKER_SPEC.md plus one
implementation, vendored into web-ui-v2 against a shared fixture corpus with a
checksum. Two copies drifting would show up as the product hiding content the
public page shows — nothing in either repo would catch that alone.

Worth a reviewer's attention

  • Taxonomy coverage is partial by design — 64 of 226 articles name features.
    A wrong tag hides an article from someone entitled to it, so the tagger only
    emits what it can infer confidently.
  • Only one article uses a marker so far (sessions/accept-decline). The
    machinery is complete; the corpus is not. In-product reading looks like the
    public site until authors tag more.
  • The build previously never wrote the index despite LAUNCH.md claiming it
    did. Because the Worker answers a bad token with a 404, a missing file and a
    wrong secret look identical — Team Lead would have retrieved nothing, silently.
    Now a postbuild hook, and CI fails if any article stops producing chunks.

Test plan

  • Full CI gate sequence locally: images, style, markers, hydrator (63 pass),
    vendored-copy checksum, breadcrumbs, build with link validation, redirect
    map, index check
  • Legacy redirect audit against the live host: 541/541 URLs reach a live
    page, 0 failures
    (11 expected fall-throughs for deleted HubSpot articles)
  • Post-deploy smoke: no X-Robots-Tag on canonical host, robots.txt
    sitemap line, GPTBot 403, help. 301s to learn., article and sitemap 200
  • Marker traced source → build → live HTML → index
  • Index/consumer handshake: indexVersion: 1 matches web-api's
    SUPPORTED_INDEX_VERSION
  • Sitemap submitted to GSC (204); gsc_health_check.py clean
  • Authenticated GET /_internal/help-index.json returns 200 — needs the
    HELP_INDEX_TOKEN value, which only exists in web-api's env

Made with Cursor

joshparolin and others added 4 commits August 12, 2026 21:17
Nine miniflare sqlite files under .wrangler/state were committed at some point
and churn on every `wrangler dev`, so every branch carries binary noise that
has nothing to do with its change. They are per-machine caches; nothing reads
them but the local dev server.

Co-authored-by: Cursor <cursoragent@cursor.com>
The Help Center is the source of truth, and the product needs the same articles
personalized to an event's features and settings. Copying them into the app
would fork the corpus on day one, so the personalization lives in the MDX and
resolves differently per surface.

Authoring:

- IfFeature / IfSetting wrap content that only applies when a feature or setting
  is on, and `app:` links point at product routes. All three compile to inert
  data-sb-* attributes, so the public page renders every branch and stays fully
  crawlable — the markers only mean something once something hydrates them.
- Article frontmatter gains features / audience / jtbd, which is what lets
  retrieval filter by entitlement instead of guessing from prose.
- Names are checked against a product contract exported from web-api and
  web-ui-v2, so a marker naming a feature, setting, or route that no longer
  exists fails the build rather than silently rendering nothing.

Hydration is specified rather than reimplemented: MARKER_SPEC.md defines the
rules, src/lib/marker-hydration.mjs is the one implementation, and it is
vendored into web-ui-v2 against a shared fixture corpus with a checksum. Two
copies drifting would show up as the product hiding content the public page
shows, which no test in either repo would catch on its own.

Retrieval: `npm run build` now also emits dist/_internal/help-index.json —
every article chunked by heading with a per-article contentHash and sanitized
body HTML — which web-api pulls nightly to embed for Team Lead. It is the whole
corpus in one GET, so the Worker gates /_internal/ on a bearer token and
answers a wrong or missing one with the same 404 an unknown path gets.

Co-authored-by: Cursor <cursoragent@cursor.com>
Written by `npm run taxonomy:tag`, which infers from folder structure and
existing frontmatter, so this is mechanical apart from one article.

The tags are what make retrieval entitlement-aware: an event without awards
should not be handed an awards article, and an organizer should not get the
participant-portal instructions written for speakers. 64 articles now name the
features they assume; the participant guides carry an explicit audience because
their folder alone does not distinguish who is reading.

Coverage is deliberately partial. A wrong feature tag hides an article from
someone entitled to it, which is worse than an untagged article ranking a little
too broadly, so the script only tags what it can infer confidently and the rest
stay untagged until an author knows better.

sessions/accept-decline.mdx also gets the first real marker — the speaker
confirmation step only exists when enable_speaker_acceptance is on — as the
end-to-end proof that a marker survives authoring, build, the public page, and
the index.

Co-authored-by: Cursor <cursoragent@cursor.com>
CI gains three checks, each for a failure that is invisible until it reaches
the product: markers naming a feature or setting that no longer exists, the
vendored hydrator drifting from the spec, and an article that stops producing
retrievable chunks — which reads as Team Lead simply not knowing something.

The redirect audit also greps app source for Help Center URLs, and it counted
unit-test fixtures and a doc-comment placeholder as broken production
redirects. Four of them, indistinguishable in the output from a real 404 after
cutover. It now skips test and fixture files, ignores non-ASCII placeholders,
and follows the Worker's own trailing-slash 307 instead of failing it. An audit
that reports failures nobody can act on is one people stop reading.

Verified after deploying: 541/541 URLs reach a live page.

Co-authored-by: Cursor <cursoragent@cursor.com>
@joshparolin
joshparolin merged commit 3c957f0 into main Aug 13, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant