Work Done
Worked mono issue #295 (“support
formatting and latex in dashboard-blog”) through to draft PR
#296 on branch
feat/blog-formatting-latex.
Diagnosis: the loss was entirely on the API side, not the frontend.
apps/dashboard-api/dashboard_api/services/notion.py flattened every Notion
rich-text array through _plain, which concatenates plain_text and discards
annotations, href, and equation spans. Block-level equation blocks carry
no rich_text at all, so to_markdown’s elif text: fallback dropped them
silently. The frontend was already running react-markdown, so it would have
rendered anything the API actually emitted.
Changes:
- dashboard-api: new
_markup/_spanbeside_plain._spanwraps a span’s trimmed core innermost-first (`->**->_->~~), then in a link ifhrefis set. Equation spans ->$$...$$;equationblocks -> a fenced$$on its own lines._plainretained for titles, tagline, image captions, and code-block bodies.underlinedeliberately dropped (no Markdown equivalent, and norehype-rawin the pipeline). - dashboard-frontend: added
remark-math+rehype-katex+katextofeatures/blog/markdown.tsx;.katex-displaygetsoverflow-x-auto;excerpt()inpresentation.tsstrips$/~too.
Validation was local only (no browser): 152 pytest cases pass (5 new),
nx build dashboard-frontend emits the KaTeX font assets, biome clean on
src/features/blog/, check-boundaries passes. Two pre-existing tsc
errors (DataSourcePicker.tsx, useBenchmarkSeries.ts) reproduce unchanged
on main.
Lessons Learned: Pitfalls
ntn(Notion CLI) is installed but not logged in, and the claude.ai Notion connector needs an interactive OAuth run. Listing 1Password items to recover the dashboard’s Notion token was blocked by the permission classifier. Net effect: there is no way to look at real Notion block JSON from a headless session. I proceeded from the documented Notion rich-text schema and a hand-built fixture instead, which was fine — but a future agent should not burn turns trying to reach live Notion. Worth recording in the dashboard SOP.- The
dashboard-app-dev-and-deploySOP’s mock-mode advice does not cover the blog:DASHBOARD_MOCK=truehas no blog fixture at all (mock_data.pycontains nothing blog-related, androuters/notion.pyhas nosettings.mockfast-path, unlike every other router). So “just use mock mode” is not an option for blog work. The SOP reads as if mock mode covers every endpoint; it should be narrowed. bunx nx run dashboard-frontend:typecheckdoes not exist — the frontendproject.jsononly definesdev/build/format. Typecheck and lint run asbun run typecheck:check/bun run lint:checkfrom the app directory.- Repo-wide
bun run lint:checkon dashboard-frontend reports ~171 pre-existing biome errors andtsc --noEmittwo pre-existing type errors. Scope both to touched files, and stash-and-rerun to confirm a baseline before chasing anything. pixi runneeds the sandbox disabled (global rattler cache lock is outside the writable set), same class of problem as the already-recordedopnote.ruff format --checkflagsservices/notion.pyandtests/test_blog_service.pyonmainalready. Do not “fix” it in a feature PR — the diff lands in untouched code.
Lessons Learned: Improvements
The single most valuable missing fact, which cost the most time to establish
empirically: remark-math only treats $$ as display math when the fence
sits on its own line. Emitting $$expr$$ inline produced inline math
(verified: katex-display: 0). Related and non-obvious: remark-math’s default
singleDollarTextMath: true would turn “storage runs $5 per scan and $12 per
cohort” into math, so this PR emits inline equations doubled ($$x$$) and sets
singleDollarTextMath: false. Both facts belong wherever the blog rendering
pipeline is documented.
Second: the headless render harness is the validation technique to reuse.
With no browser tooling and no live Notion, the way to actually prove the
change works end-to-end was to pipe a Notion-block fixture through the real
Python to_markdown, then the resulting Markdown through the real React plugin
chain via renderToStaticMarkup under bun. Two gotchas: the script must live
inside apps/dashboard-frontend/ (bun resolves react/jsx-dev-runtime from
the script’s own directory, so $TMPDIR fails), and assert on counts in the
emitted HTML (class="katex", katex-display, katex-error, <strong>,
literal $ survival) rather than eyeballing it.
Suggested edits to wiki/workstreams/infrastructure/sops/dashboard-app-dev-and-deploy.md:
- Narrow the mock-mode claim — name the blog as the endpoint mock mode does not cover, and say that blog work therefore needs either a Notion token or a fixture-driven headless render.
- Add a short “validating rendering changes headlessly” note with the
renderToStaticMarkuprecipe and the bun-resolves-from-script-dir gotcha. - Add the frontend’s real typecheck/lint invocations (
bun run typecheck:check/lint:checkfrom the app dir; no nx targets) plus the pre-existing-failure baselines, so the next agent does not mistake them for their own breakage.