Read the sections affected by a change. Authored data is the source of truth; generated modules, exports, manifests, and deployment files are disposable.
Generation and publication
scripts/build-posts.mts validates configuration/CV, discovers articles, renders
changed MDX and Markdown, builds summaries/search/feeds, then publishes dedicated
output trees. scripts/lib/generated-files.mts compares bytes, stages every
changed tree before replacement, journals rollback, and prunes stale output only
after successful staging. A failure preserves the last successful generation.
This is a single-writer transaction with rollback, not filesystem-wide atomic
visibility to arbitrary concurrent readers.
content-cache.mts hashes article MDX/BibTeX/assets, transitive literal local
imports, and compiler/configuration inputs. Cached HTML, Markdown, and search
text have integrity checks. A fast no-op requires both matching inputs and the
exact output manifest; missing/corrupt/extra outputs are repaired. Dynamic import
paths assembled at runtime are outside dependency discovery: keep authored
component imports literal and local. The cache lives in ignored
.generated/content-cache; removing it forces recompilation safely.
The output trees are src/app/data, .generated/agent-content, and the private
cache. Do not publish the cache or edit generated data directly.
- Site settings live in
configs/{site,blog,comments,redirects}.yaml. The data generator validates them before emitting typed modules undersrc/app/data. The development watcher regenerates YAML changes automatically.
Article payload and lifecycle
The shared shell uses one fixed, responsive toolbar for home/blog navigation,
search, printing, and theme switching. Desktop controls include text; mobile
controls keep 44px touch targets. Article directory controls join this toolbar
below 1024px, while wider screens use the persistent article outline. Context
labels and actions come from ToolbarExtensionService and reset on navigation.
--toolbar-safe-area-top, --toolbar-height and --toolbar-clearance in src/styles.css reserve space for
the header and device safe area in paper layout, sticky sidebars, and heading
anchor positions. Disclosure summaries use the same clearance when scrolled into
view. Keep those consumers aligned when changing header dimensions.
The toolbar is excluded from print output and embedded PDF readers.
The reading design retains the original warm paper, subtle shadow, centered
ruled titles, compact lists and Morandi colors. The paper is capped at 900px
(1150px for documentation). From 1024px, articles with an outline use a transparent
1140px layout containing a 200px external outline, a 40px gutter, and a separate
900px article paper. The article paper owns the warm surface, shadow, breadcrumbs,
title and body; the outline remains on the surrounding page background. Its
original padding preserves the 795px text column when space permits. Center the
article itself on wide screens, smoothly yielding to a 4% left gutter when both
columns need the space. Narrower laptop widths can shrink the article paper.
Use a row subgrid to align the external outline with the title below the
breadcrumbs, including wrapped breadcrumbs; do not hard-code that vertical offset.
The ordinary shell paper remains the only surface below 1024px and on articles
without an outline. Screen-only surface rules leave print with one continuous,
unframed article. Once the title row
passes the toolbar, pin the outline to the viewport rather than the grid's bottom
boundary, keeping its header reachable at the page end. An intersection observer
tracks a one-pixel row marker; no scroll handler or scroll-time geometry reads are
needed. Reset and dispose the observers on viewport/layout changes and navigation.
Body text uses the original 15px desktop / 14px
mobile baseline, with Space Grotesk headings. The toolbar matches that typography
and paper surface. Its background spans the viewport and sits flush with the
top edge, separated from the page by a subtle bottom rule and shadow. Navigation
and article controls sit at the left edge; search, print, and theme controls sit
at the right. Responsive 12–32px gutters respect device safe areas. The toolbar
does not inherit the paper's width limit, so controls stay in place when moving
between different paper widths. Article/documentation styles set
--paper-max-width on the shell only while their matching route is present. Desktop navigation combines
the author avatar / Blog icon and text; mobile keeps
the avatar and compact icons. The current location uses a subtle full-button tint with a small
corner radius, without a tab underline. It is 56px tall on desktop and 52px on
mobile, plus the device safe area inside the header surface; reduced-motion settings
must not change its dimensions. Theme tokens live in
src/styles/morandi.css. Preserve visible keyboard focus and readable colors in
both themes. The borderless article outline shows numbered chapters with separate
subsection disclosure buttons. Its labeled header can collapse the whole outline
while retaining the current section and reopen control. Keep the text column and
scroll position unchanged when doing so; new articles start expanded.
The current chapter opens automatically unless the
reader explicitly overrides it; preserve these choices and keyboard focus until
the article changes. Sidebar and dialog disclosures must use distinct IDs. Render
all section links in static HTML and enable folding only after hydration.
Chapter and whole-outline disclosures animate their grid rows, with closed
content made inert and hidden from assistive technology immediately. Keep the
bounded flex/grid chain intact so long lists still scroll independently. Selection
uses a tinted row and inset marker without changing font weight or text geometry.
The current section supplies the toolbar context; the outline footer describes
position in the article rather than claiming reading completion. Never auto-scroll
the outline while its links are hovered or keyboard-focused.
On narrower screens, a fixed bottom contents dock always shows its label and the
current section (or chapter count before reading). It replaces the former inline
entry that scrolled away. The dock and toolbar action open the same native
bottom-sheet dialog. Reserve its height, including the device bottom safe area,
at the end of the document and below the back-to-top control. Remove that space
for other routes and print. Preserve 44px touch targets, Escape dismissal,
focus restoration, section-link focus and independent scrolling. Resizing into the
wide layout closes the dialog without focusing its now-hidden trigger.
Opening the sheet reveals the current section again, even when it changed while
the dialog was closed. Its surface and backdrop enter/exit together; reduced
motion disables those transitions and disclosure motion.
Keep the native dialog open and modal through its explicit exit state; then close
it before positioning/focusing a selected heading. This avoids depending on
cross-browser discrete top-layer/display transitions. Cancel the exit listener
and fallback timer on route cleanup or a resize into the wide layout.
Keep a small end inset in the scrollable list so fractional scroll offsets cannot
clip its final link. Hide the navigation component's host for print, rather than
letting encapsulated wide-screen selectors override a global sidebar rule.
The outline's initial grid-row position bounds its scroll viewport, measured on
layout/viewport changes rather than every scroll; dispose the resize observer and
listener on navigation. Print removes shell spacing,
shadows and navigation while preserving article and CV content.
Keep the app-post host block-level: an inline host spanning block content can
intercept disclosure clicks in mobile Chromium after scrolling within the flex
shell. The code-disclosure browser regression covers that interaction.
Article styles.css is embedded in that article's rendered HTML under
style[data-post-style]. Prefix selectors, preserve semantic theme variables,
and keep same-document SVG fragment URLs intact. Styles exist before JavaScript
and leave with the article DOM. Shared presentation remains in PostComponent.
Build-only styles and scripts are excluded by Angular's article asset rules.
Reusable build-time visual primitives live in content/shared; post components
import them directly, and the content cache follows those imports so a shared
change regenerates only its consumers. Their prefixed presentation styles are
global because the same generated markup appears in more than one article.
The generator discovers scripts/*.post-client.ts and generates per-slug lazy
loaders. Each module exports enhancePost(container): () => void.
bindPostEnhancements owns async attachment and cleanup, including imports that
finish after navigation. Share numerical models with build-time TSX via literal
local imports. Adding an article must not require editing the application shell.
Mark a static example with data-post-enhancement="forward" to defer
forward.post-client.ts until its example is within 600px of the viewport or
receives focus/pointer interaction. Multiple names are space-separated. Modules
without matching markers retain immediate attachment. Comments use the same
near-viewport boundary; navigation disposes pending observers and imports.
Generated
POSTScontains summaries only. Load article HTML/TOC throughloadPostand the generated per-slug loaders. Load the search engine, serialized index and plaintext snippets through a separate dynamic import after the deferred search dialog opens; neither the dialog nor the eager shell should statically import that corpus. Idle prefetch may load the small dialog without initializing the engine. Discard outdated query results after edits or dismissal. Offer a page reload after loading fails: Chromium can cache failed module imports, so callingimport()again need not retry the network request. Assess payloads on served production pages, since CLI initial totals exclude lazy chunks and external scripts. Measure cold search opening separately from warm queries and result rendering. Include worker requests when measuring search transfers; window resource timing omits those requests and can substantially undercount a worker-based index. On a direct article visit, transfer only the TOC and adopt the existing prerendered body identified by its slug; do not import or rewrite that body. This initial-only adoption is not cached as a full post. Subsequent SPA visits still use the article loader, including returning to the initially opened post.Keep article presentation in
PostComponent, DOM enhancement and cleanup inPostContentDirective, and TOC interaction inPostNavigationComponent. Use render hooks and cleanup callbacks rather than retrying DOM queries. Keep a shrinkable flex chain from the bounded TOC host to its scrolling list; percentage heights do not inherit a sidebar'smax-heightand can leave long lists clipped. Verify the final entry is reachable in both sidebar and drawer.PostContentDirectivemarks.post-bodywithdata-rendered="true"after client DOM enhancements attach; browser tests asserting on client elements (such as embedded PDF readers) should wait for this attribute before querying or scrolling iframes to ensure hydration has settled.Angular-authored content images use
app-image-lightbox, which wrapsNgOptimizedImageandmedium-zoom. The toolbar avatar is a decorativeNgOptimizedImageinside the Home link, not zoomable content: keep its alt text empty and let the link own its accessible name, focus and navigation. Do not nest the lightbox's button semantics or extra tab stop inside that link.Article formulas are compiled by
scripts/lib/static-math.mtsusing pinned MathJax CHTML, per-article adaptive CSS, assistive MathML and first-party fonts. Formula-bearing heading labels in the table of contents reuse that build-rendered output; their plain-text labels remain available for navigation metadata. Static containers express MathJax's font-height matching in CSSexunits, using the output font's x-height and minimum scale. This lets the browser use the actual surrounding font, including after webfont loading, instead of freezing Node's fallback metrics into a percentage. Keep runtime MathJax's normal font-height matching; verify visual size across fonts and font sizes. Serialize rendering and create a fresh TeX input parser per article so macros and counters cannot leak between articles or incremental rebuilds. Search and Markdown use the pre-typeset source, not expanded formula markup. Formulas in closed disclosures are also static: opening them needs no engine. Copying a selection containing formulas uses retained TeX with inline or display delimiters; right-click keeps the browser's native behavior and must not replace or resize formula DOM. Printing opens disclosures temporarily and restores their previous state afterward. Keep the complete article DOM.MathJax loads on demand through
src/app/utils/mathjax.ts, with automatic typesetting disabled. Angular-authored views outside generated post content must calltypesetMathafter rendering when they contain TeX delimiters. The runtime uses only the TeX input and disables MathJax's custom context menu. Use MathJax'sdisplayOverflow: 'scroll'for wide equations. Display wrappers useflow-rootto contain margins without another scroll container: outeroverflow-x: autoalso enables vertical scrolling for small glyph overhangs. Keep raw display TeX wrappable before MathJax initializes, without introducing another scroll container around the finished formula. Keep raw TeX wrappable while the renderer is loading or unavailable.The heading spy caches document coordinates and uses binary search on scroll. Refresh after article/body resize, disclosure changes, media loads and font changes; never rescan every heading on every scroll frame. Disconnect all observers/listeners and ignore late async completion when leaving an article.
Enhanced TOC clicks commit their fragment through the router with manual scroll ownership before moving. Keep ordinary anchor URLs, modifier-click behavior and the no-JavaScript fallback. History/reload restoration and non-TOC links retain their existing scroll behavior; a repeated target must still work without adding a duplicate history entry. Nearby targets use a bounded eased scroll; jumps beyond three viewport heights go directly to the destination instead of racing through a long article. Each animation frame uses an explicit instant scroll so global CSS smooth scrolling cannot start a second animation. Hold the selected chapter during travel, then focus its heading without another scroll and briefly tint that heading on arrival. The deliberate selection remains until the reader moves or the layout changes, including targets too close to the document end to align with the activation line. Wheel, touch, pointer, scrolling/focus keys, history, resize, a newer jump and navigation cleanup cancel pending work without a late focus transfer. Reduced-motion readers get direct positioning and no arrival animation. Do not add scroll-frame heading measurement or a second history stack. Reveal a target's enclosing disclosures before measuring its position, as native fragment links do, including nested
<details>.
Keep the pinned real-rendering test packages synchronized with the MathJax CDN version. Its worker compatibility adapter and removal criteria are documented in mathjax-webkit.md.
Routes, metadata, and reading interaction
The shared toolbar always links to the homepage and
/blogon desktop and mobile. Router link state highlights the active destination, including blog articles and directory pages.The root application shows an indeterminate top bar while client router navigation waits for lazy routes or resolved content. Completion, cancellation, and skipped navigation clear the status; failures offer a normal document link to reopen the destination, avoiding cached failed module imports. A missing lazy chunk usually means an open tab crossed a deployment, so that specific failure performs one full-document navigation to the destination. Session storage suppresses another automatic attempt for the same URL for 30 seconds; storage failures and repeated import failures retain the manual link instead of entering a reload loop. New navigation clears previous failures. The failure notice uses a live status region. Feedback does not block reading or other navigation, respects reduced motion, and starts after hydration so prerendered pages do not acquire an initial loading overlay.
Blog pagination uses
/blogand/blog/page/:page. Keep the resolver, prerendered page count, metadata, and navigation on the same pagination helpers and configured page size. Keep search over all posts and avoid list removal animations that interfere with the router's saved scroll positions.Blog categories follow the nested directories under
content/posts.blog.routes.tslazily loads the archive, category pages, and full article paths; prerender every article and ancestor category with the same data./blog/contentsis the root directory, with category pages under/blog/contents/<category>; article URLs remain/blog/<slug>. Directory pages list immediate children, folders first, with dates equal to the latest descendant publicationdate, ignoringupdated. Keep/blogas the paginated archive and preserve the contents root even with no posts. Derive browser-test directory counts from generated post paths so adding a post does not require updating unrelated navigation assertions. Posts markedprivate: trueretain a direct prerendered route for host-level access control, but are excluded from public archives, directories, search, feeds, sitemaps, Markdown exports, and discovery metadata. This visibility split also keeps their content loaders out of the browser build; the server build loads them only while prerendering the protected HTML route. Protect or isolate article assets and source repositories separately. Use full path strings for router navigation; encoding an entire slug or passing it as a single noninitial router command turns/into%2F.PageMetadataStrategyhandles browser and prerendered metadata. It also owns articleBlogPostingJSON-LD: reuse authored dates and article covers, omit unknown modification dates and unrelated fallback images, and escape<when serializing JSON into HTML script elements. Homepage/CVProfilePagedata and matching article bylines share the/#personID. Additional front-matter authors remain named author nodes without being misclassified as the profile owner. Build the compact generated person data from rendered CV content and its external profile links; avoid importing the full CV into the eager metadata service. Use the configured author avatar for its image, independently of the visible introduction portrait. Clear page-specific structured data when navigating away.PageScrollServiceconsumes router scroll events and corrects saved positions or fragments after fonts/formulas settle, unless the reader has scrolled. It saves one viewport snapshot in tab-local session storage onpagehideand when the tab becomes hidden, without writing on every scroll. Only a browser reload restores this snapshot, including when the URL still contains an older fragment. The snapshot must match the full URL and applies only to initial navigation; new article/fragment navigation and router history restoration keep their own behavior. Hydrated pages restore after the first render and again when content settles. Storage failures must not prevent navigation. Avoid saving into history state duringpagehide: Chromium has already captured the state that the replacement document will receive.The shared 404 experience lives in
src/app/pages/not-foundand is also used for missing blog slugs. Keep its recovery and peer-review interactions covered by component tests. Preserve its academic copy during style-only changes; simplify framing and decoration first. The review dialog must capture keyboard focus and restore it when dismissed.Keep the search listbox explicitly
tabindex="-1": Chromium otherwise adds an overflowing results container to the Tab order. Its combobox owns arrow-key navigation; cover overflow explicitly in browser tests.Keep search-result animations local: AutoAnimate's removal animation adjusts window scrolling and can overwrite the page's saved reading position.
Markdown, feeds, and deployment
The same generator writes
/llms.txt,/profile.md,/blog/index.md, and/blog/<slug>.mdto the ignored.generated/agent-contentasset directory. Directory indexes are exported at/blog/contents/index.mdand/blog/contents/<category>/index.md. Export expanded MDX before syntax highlighting so code annotations remain intact; retain math, citation links, diagram descriptions, and direct PDF URLs. Profile exports reuse rendered CV data. Article exports carry the authored byline, falling back to the site author when no byline was supplied. Never maintain a second copy of prose. Angular copies these files at the site root; keep Markdown discovery links inPageMetadataStrategyand Markdown MIME types in hosting/preview configuration.Original page URLs negotiate Markdown only for an explicit, preferred
Accept: text/markdown. Postbuild derives the Worker route map from prerendered alternate links and validates the corresponding exports; do not duplicate it by hand. The generated Worker entry and map stay outside public assets. Keepwrangler.jsoncworker-first patterns covering these page routes, with anASSETSbinding for separate HTML/Markdown asset caching. Negotiated responses needVary: Acceptand must not enter a shared URL-only cache.pnpm previewshares the handler;pnpm preview:cloudflareand Playwright's Cloudflare project also exercise actual Worker/static-asset routing locally. Cloudflare applies_headersto HTML 404 fallbacks using the requested URL; keep/blog/*.mdwithin worker-first routing so missing exports receive the Worker's MIME correction, and retain its Cloudflare regression test. One-to-one article Markdown exports also send an HTTP canonical pointing to the HTML article, including direct GET/HEAD and conditional 304 responses. Do not canonicalize the full archive or profile exports to partial pages.Production builds promote the prerendered
/404route to404.htmlfor Cloudflare's404-pagehandling and generate_redirectsfromconfigs/redirects.yaml; do not edit either file underdistmanually. Postbuild also appends immutable caching rules for Angular's fingerprinted JS, CSS and media, validating the reserved output namespaces. See deployment for their scope; negotiated HTML/Markdown caching remains separate.The postbuild step generates
sitemap.xmlandrobots.txtfrom Angular's prerender manifest and page canonicals. It excludes noindex and redirect pages; do not maintain a separate URL list or use build time aslastmod.Keep Cloudflare HTML handling aligned with the canonical URL format. Check that sitemap and canonical URLs return 200 directly; asset redirects must not add trailing slashes while page metadata declares the slashless URL canonical. Hosting uses
assets.html_handling: drop-trailing-slash; verify this in the Cloudflare Playwright project, since the lightweight Node preview does not reproduce every static asset redirect.The generator writes summary feeds at
/feed.xml(RSS) and/atom.xmlalongside Markdown exports. Both use stable article URLs and sort by the latest known authored date (updated ?? date), never the build clock. Entries carry each article's authored byline or the site-author fallback. Feed discovery lives in page metadata, the footer, andllms.txt; keep MIME types correct in both Cloudflare_headersand the Node preview.
Embedded PDF readers
- Post PDF iframes targeting
/posts/...pdfor/assets/...pdfare rewritten at build time to the lazy/pdf-viewerroute usingngx-extended-pdf-viewer. Keep one reader per iframe: the library uses global DOM IDs and cannot share one document between simultaneous instances. Preserve direct PDF links. Prerender this shell for static hosting, keep it noindex, and copy the viewer's assets throughangular.json. KeeppdfDefaultOptions.assetsFolderrelative to the base href to avoid doubled slashes. Preview must serve.mjsas JavaScript and.wasmas WebAssembly. Version 30 omits PDF.js's CMYK ICC profile;public/assets/web/iccssupplies it at the engine's default path. Verify actual canvases and asset requests in desktop/mobile browser tests. Start embedded readers with the sidebar closed: a PDF's authored outline preference can otherwise squeeze slide pages into thumbnails on mobile. Slide decks can repeat PDF page labels across overlays. The page-number input uses those labels;.page[data-page-number]uses physical page indexes.
Documentation pages
The handbook is authored once in docs/. Its navigation manifest groups pages for authors and developers. Agent working records live separately in .agents/docs/ and are not published. The generator validates and compiles Markdown, adds heading IDs and highlighted code, rewrites repository/document links, and publishes page summaries, lazy body modules, and Markdown exports together with other generated files. Documentation changes participate in the generation fingerprint and the development watcher.
The lazy /docs route uses generated summaries for page routes and navigation. Server routes use the same summaries for prerendering. Each page resolves only its own body; documentation is excluded from blog summaries, feeds, and full-text search. Shared typography/code/table styles preserve the blog theme, while the documentation component owns its navigation and content enhancement lifecycle. Page metadata advertises the one-to-one Markdown alternative, allowing the existing postbuild sitemap and Worker route discovery to include docs without a second URL list. See documentation maintenance for the source format, link validation boundaries, and synchronization workflow.