Skip to content

Generate the render stylesheet so both routes share one source - #3905

Open
rossnelson wants to merge 8 commits into
mainfrom
rossnelson/share-markdown-stylesheet
Open

Generate the render stylesheet so both routes share one source#3905
rossnelson wants to merge 8 commits into
mainfrom
rossnelson/share-markdown-stylesheet

Conversation

@rossnelson

Copy link
Copy Markdown
Collaborator

Stacked on #3882 — review that one first.

The problem

/render has two implementations. A packaged ui-server serves it from Go (server/server/route/ui.go); the SvelteKit route at src/routes/(app)/render only runs where there is a SvelteKit server. Both need the same stylesheet, and the Go one has been carrying a copy pasted in when the route was added in #2430.

None of the differences were chosen. Every change to src/markdown.reset.css since #2796 landed on one side only:

Date PR Touched the Go copy?
2025-08-07 #2796 User metadata yes
2026-05-22 #3430 fix(a11y): rem font-size, unitless line-height no
2026-05-28 #3463 fix(markdown): nested lists render as block no
2026-07-14 #3673 Support markdown tables no
2026-08-17 #3785 Workflow Catalog no
2026-09-09 #3849 Io design system no

So a packaged server is missing two shipped bug fixes (#3430, #3463), has no table styles, and renders markdown in the 2024 Holocene palette because #3849 never reached it.

Why it could not just be hand-synced

Two hard constraints:

  • The Io colours only exist as TypeScript (ioThemeToCssVariables), which Go cannot import.
  • server/ is a separate Go module, so //go:embed cannot reach src/.

The change

One pure function composes the stylesheet, called from two places:

  • src/lib/utilities/markdown-stylesheet.tscomposeMarkdownStylesheet(reset), extracted from the route
  • the SvelteKit route calls it per request
  • scripts/generate-markdown-css.ts calls it to write server/server/route/markdown.gen.css, which Go embeds

ui.go loses 251 lines of duplicated CSS.

Drift is now unmergeable

Editing the reset or an Io colour without running pnpm generate:markdown-css fails scripts/generate-markdown-css.test.ts. CI runs that (lint-and-test.yml) and the Go tests (test.ymlmake test).

Verification

  • Dumped the CSS each route actually serves and diffed them: byte-identical apart from the generated-file banner comment
  • pnpm test: 251 files, 3209 passed, 2 skipped
  • pnpm check: 885 files, 0 errors
  • go test ./server/route/: pass, gofmt and go vet clean
  • Confirmed each new test fails without its fix

Deliberately left out

  • The SvelteKit route still reads the reset with fs.readFileSync at request time. A build-time ?raw import would be nicer, but vitest stubs .css imports by default and working around that means changing the shared vitest config — not worth it in this PR.
  • Only the generated file is new; src/markdown.reset.css stays the hand-edited source.

`Preview` renders into an iframe served by `/render`, which pads the page
by 1rem and lays out `main`/`p` as blocks. That is right for a document and
wrong for a short string: a one-line summary arrives in a padded box, and
the padding is counted into the measured frame height.

Adds two props:

- `compact` drops the page padding and lets the block wrappers flow, so a
  short string renders as a line of text. Wrapping stays on, so content of
  a sentence or two still fits its frame rather than being clipped.
- `title` sets the frame's accessible name, which was hardcoded to
  "output". A page with several of these frames announced them all
  identically.

Also sets `border-0` on the iframe, which otherwise picks up the UA
default border in some browsers.

Both props are additive and default to today's behaviour.

The approach here comes from @Tidemand's work in #3865, which needs this
same body-class plumbing for inline event summaries. This is deliberately
narrower: no shrink-to-fit width measurement, so that PR's inline mode can
layer on top rather than conflict with it.
Measuring the first consumer in a browser turned up three things the
initial pass missed.

The reset sizes every element through a `*` selector, which beats
inheritance, so `body.compact { font-size }` never reached `main`, `p` or
the inline elements — content rendered at 12.25px next to 14px text
outside the frame. The metrics are now restated for the elements a short
string can contain.

`main` and `p` go back to blocks with the margins zeroed rather than
`display: inline`. `getRenderedHeight` measures `main`, and an inline box
reports the text box rather than the line box: two pixels short of the
leading, which clips descenders and the background of a code span.

Inline code keeps its horizontal padding and drops the vertical, which
otherwise grows the line box past the height the frame was sized to.

Measured on a one-line string with bold, a code span and a link: the frame
went from 65px to 23px, with nothing clipped and the type matching its
surroundings.
The star selector at the top of the reset sets font-weight on every
element, which beats the UA default for strong. Nothing restored it, so
`**bold**` has been rendering at normal weight on every markdown surface —
event summaries and incident summaries included, not just the compact mode
this PR adds. Found while checking a consumer.

Also merges main for the Io colour system. The reset now resolves code
colours through --color-surface-secondary and --color-content-primary, and
the compact rules here still hold against it: they set layout and metrics,
not colour.
The reset set no overflow-wrap anywhere, so a long URL in a summary ran
past its container - and in compact mode, where the body clips, it simply
disappeared off the edge.

Same bug as the bold one: it affects every markdown surface, not just the
compact mode this PR adds. A consumer hit it, was fixed there, and this is
the half that belongs upstream.
The ui-server serves /render from its own Go handler with the stylesheet
inlined as a template literal, so the compact changes to
src/markdown.reset.css only took effect under the SvelteKit dev server.
A packaged binary kept the 1rem page padding and the opaque box the
compact class exists to remove.

Ports the compact block, reads the compact query param with the same
exact-string parsing the SvelteKit route uses, and adds overflow-wrap so
a long unbroken token in a 255-character description wraps instead of
overflowing the frame.

Also pins strong/b to 600. Bold already worked here via the UA default,
since unlike the SvelteKit reset this star selector sets no font-weight,
but the UA's 700 rendered heavier than the same markdown does in dev.

The two stylesheets cannot be fully unified while this handler has no way
to read the Io theme variables the SvelteKit copy resolves colors
through, so a test holds the colorless compact block to parity and names
any selector that goes missing from it.
The compact mode shipped here painted its own canvas, so an embedder on
any surface other than the default got an opaque box: white on a dark
page. The fix has two halves and needs both. body.compact drops the
background, and the root declares a color-scheme matching the theme on
body, because a mismatch between the document's scheme and the one the
page gave the iframe element makes the browser paint an opaque canvas
regardless of what the document asks for.

Ports both halves to the Go render route as well, for the same reason the
compact styles went there: a packaged ui-server serves /render from Go,
not from SvelteKit.
/render has two implementations. A packaged ui-server serves it from Go,
and the SvelteKit route only runs where there is a SvelteKit server, so
both need the same CSS. The Go one carried a copy pasted in when the
route was added in #2430 and it has been drifting ever since: every
change to src/markdown.reset.css since #2796 landed on one side only.

That is not a difference anyone chose. It means a packaged server is
missing #3430's a11y fix for font metrics, #3463's nested-list fix,
#3673's table styles, and the whole Io palette from #3849, which is why
it still renders markdown in the 2024 Holocene blue.

The colours are the reason it could not simply be kept in sync by hand:
they only exist as TypeScript, and server/ is a separate Go module, so
//go:embed cannot reach src/ either. So the stylesheet is now composed by
one pure function, called from two places. The SvelteKit route calls it
per request; scripts/generate-markdown-css.ts calls it to write the copy
Go embeds. Both routes now serve byte-identical CSS.

Drift is no longer possible to merge: editing the reset or an Io theme
colour without regenerating fails generate-markdown-css.test.ts, and CI
runs both that and the Go tests.
@vercel

vercel Bot commented Sep 11, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
holocene Ready Ready Preview Sep 11, 2026 3:17pm UTC

Request Review

Base automatically changed from rossnelson/inline-markdown-preview to main September 11, 2026 16:13
@rossnelson
rossnelson marked this pull request as ready for review September 11, 2026 19:08
@rossnelson
rossnelson requested a review from a team as a code owner September 11, 2026 19:08
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