Skip to content

2026-07-31 infrastructure blog formatting latex

Dashboard blog now renders Notion rich-text annotations and LaTeX (mono#295 -> draft PR mono#296).

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/_span beside _plain. _span wraps a span’s trimmed core innermost-first (` -> ** -> _ -> ~~), then in a link if href is set. Equation spans -> $$...$$; equation blocks -> a fenced $$ on its own lines. _plain retained for titles, tagline, image captions, and code-block bodies. underline deliberately dropped (no Markdown equivalent, and no rehype-raw in the pipeline).
  • dashboard-frontend: added remark-math + rehype-katex + katex to features/blog/markdown.tsx; .katex-display gets overflow-x-auto; excerpt() in presentation.ts strips $/~ 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-deploy SOP’s mock-mode advice does not cover the blog: DASHBOARD_MOCK=true has no blog fixture at all (mock_data.py contains nothing blog-related, and routers/notion.py has no settings.mock fast-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:typecheck does not exist — the frontend project.json only defines dev/build/format. Typecheck and lint run as bun run typecheck:check / bun run lint:check from the app directory.
  • Repo-wide bun run lint:check on dashboard-frontend reports ~171 pre-existing biome errors and tsc --noEmit two pre-existing type errors. Scope both to touched files, and stash-and-rerun to confirm a baseline before chasing anything.
  • pixi run needs the sandbox disabled (global rattler cache lock is outside the writable set), same class of problem as the already-recorded op note.
  • ruff format --check flags services/notion.py and tests/test_blog_service.py on main already. 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:

  1. 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.
  2. Add a short “validating rendering changes headlessly” note with the renderToStaticMarkup recipe and the bun-resolves-from-script-dir gotcha.
  3. Add the frontend’s real typecheck/lint invocations (bun run typecheck:check / lint:check from the app dir; no nx targets) plus the pre-existing-failure baselines, so the next agent does not mistake them for their own breakage.
Navigation

Type to search…

↑↓ navigate↵ selectEsc close