Simplify the 404 page, and add the social preview card generator - #3
Conversation
The 404 page said the same thing three times: a lede, a bulleted list of reasons, and six diagram cards below the fold. Keep the joke and the diagram, drop the rest, and make the primary action a way back to the homepage rather than deeper into the site. The diagram loses its annotations, which the shape already conveys, and is capped narrower than the text column so it stops dominating the page. The cards themselves are not wired into any page yet: this is the generator and a contact sheet (scripts/og-preview.mjs) to review 16 designs before they ship. Each diagram page gets its own card showing its actual diagram, which is why the diagram renderer moved out of generate-seo-pages.mjs into diagram-svg.mjs. That file is a script and writes dist/ when imported, so nothing could reuse a function from it. The move is verified output-identical across all 20 pages.
Name the error in the heading, since "This page has no supply" alone is a joke before it is an explanation. Replace "the market clears at nothing at all" with "nothing is traded", which is the same point without the jargon, and label the intersection "equilibrium at Q = 0" rather than "404", which the heading now carries. The three sentences were one paragraph and read as a single long apology. Split them: the joke, the translation, then the advice.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughRelease 1.1.2 adds reusable SVG and Open Graph card generation, Chrome-based PNG rendering, route-specific social metadata, simplified 404 output, build-time card validation, and updated release metadata. ChangesSocial Preview and SEO Pipeline
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant OG_CARDS
participant renderOgSvg
participant og-render.mjs
participant Chrome
participant generate-seo-pages.mjs
OG_CARDS->>renderOgSvg: build card SVG
renderOgSvg->>og-render.mjs: provide SVG markup
og-render.mjs->>Chrome: capture 1200x630 PNG
Chrome-->>og-render.mjs: write card image
generate-seo-pages.mjs->>OG_CARDS: resolve route card mapping
generate-seo-pages.mjs->>generate-seo-pages.mjs: validate referenced assets
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Reviewer's GuideSimplifies the 404 page content/layout and introduces shared SVG diagram rendering plus a full Open Graph/Twitter social card pipeline (templates, card definitions, rasterisation, build guards, and meta tag wiring). Sequence diagram for build-time social card existence guardsequenceDiagram
participant Build as build
participant SeoScript as generate_seo_pages_mjs
participant Guard as assertEverySocialCardExists
participant FS as filesystem
Build->>SeoScript: node scripts/generate-seo-pages.mjs
SeoScript->>Guard: assertEverySocialCardExists()
Guard->>FS: existsSync(public/og/<card>.png) for each OG_CARDS
Guard->>FS: existsSync(dist/og/<card>.png) when dist is built
FS-->>Guard: presence/missing results
alt [all card images present]
Guard-->>SeoScript: return OG_CARDS.length
SeoScript-->>Build: log "All social cards are present."
else [one or more images missing]
Guard-->>Build: console.error(missing list)
Guard-->>Build: process.exit(1) (fail build)
end
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
The site set twitter:card to summary_large_image and then supplied no image, so every link shared to Discord, WhatsApp, Reddit or Teams rendered as a bare text stub. All 16 routes now point at a card of their own, each diagram page showing its actual diagram. The PNGs are rendered by scripts/og-render.mjs and committed rather than built on Vercel. The cards are set in Inter, which is installed here but not on the build image, so generating during the build would silently ship a different typeface than the one reviewed, and nothing would report it. Headless Chrome does the rasterising, so there is no new dependency and fonts resolve exactly as they did in the preview. Two things that only showed up by looking at the output: the diagram labels came out in serif, because the diagram SVG sets no font of its own and had been inheriting one from the page it was embedded in, fixed by setting the family on the card root; and the route shells needed their own substitutions, since they are built by rewriting index.html rather than through pageShell. Guarded both ways: a referenced card missing from public/ fails the build, as does one that never reached dist/.
There was a problem hiding this comment.
Hey - I've left some high level feedback:
- The
OG_CARDS.pathsarray hard‑codes route strings independently ofCLIENT_ROUTES, which makes future path changes easy to miss; consider deriving these paths from the existing route definitions to keep them in sync. - The OG image selection (
OG_IMAGE_FOR_PATH/ogImagePath) is defined ingenerate-seo-pages.mjswhile the card metadata lives inog-pages.mjs; consolidating this mapping logic alongsideOG_CARDSwould make the social card routing easier to maintain as the set of cards grows.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- The `OG_CARDS.paths` array hard‑codes route strings independently of `CLIENT_ROUTES`, which makes future path changes easy to miss; consider deriving these paths from the existing route definitions to keep them in sync.
- The OG image selection (`OG_IMAGE_FOR_PATH`/`ogImagePath`) is defined in `generate-seo-pages.mjs` while the card metadata lives in `og-pages.mjs`; consolidating this mapping logic alongside `OG_CARDS` would make the social card routing easier to maintain as the set of cards grows.Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
There was a problem hiding this comment.
Pull request overview
This PR refactors diagram SVG rendering into a reusable module, simplifies the generated 404 page output, and adds infrastructure (templates/scripts + metadata wiring + build guard) to support Open Graph/Twitter social preview images across the site.
Changes:
- Extract diagram SVG rendering into
scripts/diagram-svg.mjsand reuse it for both SEO page generation and OG card generation. - Add OG card definitions + SVG template + preview/rasterization scripts, and wire
og:image/twitter:imagetags into generated HTML (with a guard that fails if referenced PNGs are missing). - Simplify the generated 404 page copy/layout and update versioning/changelog entries.
Reviewed changes
Copilot reviewed 9 out of 26 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| scripts/og-template.mjs | New SVG template renderer for 1200×630 social preview cards. |
| scripts/og-render.mjs | New script to rasterize OG SVGs to committed PNGs in public/og/. |
| scripts/og-preview.mjs | New script to generate an HTML contact sheet for reviewing all card designs. |
| scripts/og-pages.mjs | Central registry of OG cards (names, routes, specs), including per-diagram cards. |
| scripts/generate-seo-pages.mjs | Uses shared diagram renderer, simplifies 404 output, wires OG/Twitter meta tags, and adds build guard for missing cards. |
| scripts/diagram-svg.mjs | New shared diagram SVG renderer extracted from the SEO generator script. |
| index.html | Adds default OG/Twitter image meta tags for the SPA entry HTML. |
| package.json | Bumps version to 1.1.2. |
| package-lock.json | Updates lockfile version field to 1.1.2. |
| CHANGELOG.md | Adds 1.1.2 entry describing the new social previews + 404/build guard changes. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| <title>OG card previews (${OG_CARDS.length})</title> | ||
| <link rel="preconnect" href="https://fonts.googleapis.com"> | ||
| <style> |
| <meta property="og:image" content="${SITE_URL}${ogImagePath(canonicalPath)}"/> | ||
| <meta property="og:image:width" content="1200"/> | ||
| <meta property="og:image:height" content="630"/> | ||
| <meta property="og:image:alt" content="${esc(title)}"/> | ||
| <meta name="twitter:card" content="summary_large_image"/> |
| return found; | ||
| } | ||
|
|
||
| const chrome = process.env.CHROME_PATH ?? findChrome(); |
PR Summary by QodoAdd OG/Twitter social preview cards + simplify 404 page copy/layout
AI Description
Diagram
High-Level Assessment
Files changed (26)
|
Code Review by Qodo
1. Dist OG guard can skip
|
| function assertEverySocialCardExists() { | ||
| const publicDir = join(__dirname, '..', 'public', 'og'); | ||
| const distDir = join(__dirname, '..', 'dist', 'og'); | ||
| // Only check dist when a real build produced it; this script also runs on | ||
| // its own, before vite has copied public/ across. | ||
| const distBuilt = existsSync(join(__dirname, '..', 'dist', 'index.html')) && existsSync(distDir); | ||
|
|
||
| const missing = []; | ||
| for (const card of OG_CARDS) { | ||
| const file = `${card.name}.png`; | ||
| if (!existsSync(join(publicDir, file))) { | ||
| missing.push(`public/og/${file}`); | ||
| } else if (distBuilt && !existsSync(join(distDir, file))) { | ||
| missing.push(`dist/og/${file} (present in public/, not copied)`); | ||
| } |
There was a problem hiding this comment.
1. Dist og guard can skip 🐞 Bug ☼ Reliability
assertEverySocialCardExists() disables the dist/og validation when dist/og is missing, so a build that fails to copy public/og into dist can still pass while generated og:image/twitter:image URLs point at non-existent assets in production.
Agent Prompt
### Issue description
`scripts/generate-seo-pages.mjs` adds a guard meant to fail the build when referenced social preview PNGs are missing. However, it only checks `dist/og/*` if `dist/og` already exists:
```js
const distBuilt = existsSync(dist/index.html) && existsSync(dist/og)
```
If the build stops copying `public/og` into `dist/` (or `dist/og` is missing for any reason), `distBuilt` becomes false and the guard *skips* validating the deployed output, allowing broken `og:image`/`twitter:image` URLs to ship.
### Issue Context
This script writes pages directly into `dist/`, and those pages reference `/og/*.png` via `ogImagePath()`.
### Fix Focus Areas
- scripts/generate-seo-pages.mjs[622-636]
### Suggested change
1. Determine “dist is a real build output” using only the presence of `dist/index.html` (or equivalent).
2. If `dist` is built, explicitly require `dist/og` to exist; if it does not, fail the build (or record a missing entry).
3. Then check each expected `${card.name}.png` under `dist/og/`.
Example shape:
- `const distBuilt = existsSync(join(__dirname, '..', 'dist', 'index.html'));`
- If `distBuilt && !existsSync(distDir)`, treat as missing (`dist/og/ (directory missing, public/og present)`), and fail.
- Otherwise validate each file in `dist/og/` as you already do.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (4)
scripts/generate-seo-pages.mjs (1)
147-154: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winCard dimensions (1200x630) are duplicated as literals instead of deriving from
OG_SIZE.scripts/og-template.mjsexportsOG_SIZEas the single source of truth for card dimensions, andscripts/og-render.mjsalready imports it correctly — but these two locations hardcode the same numbers separately, risking silent drift if the card size ever changes.
scripts/generate-seo-pages.mjs#L147-L154: importOG_SIZEfrom./og-template.mjsand interpolateOG_SIZE.width/OG_SIZE.heightinstead of the literals"1200"/"630".index.html#L30-L42: since this is static markup with no build-time templating, keep the1200/630values in sync by convention, or haveassertEverySocialCardExists()(or a similar guard inscripts/generate-seo-pages.mjs) additionally assert thatdist/index.html's width/height matchOG_SIZEbefore the build succeeds.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@scripts/generate-seo-pages.mjs` around lines 147 - 154, Use OG_SIZE as the single source of truth for card dimensions: in scripts/generate-seo-pages.mjs lines 147-154, import OG_SIZE from ./og-template.mjs and interpolate its width and height in the generated metadata. In index.html lines 30-42, retain the static 1200/630 values by convention or extend assertEverySocialCardExists() to validate them against OG_SIZE before the build succeeds.scripts/og-render.mjs (1)
73-107: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low valueConsider a timeout guard and try/finally cleanup for the render loop.
execFileAsynchas notimeout, so a hung Chrome process (crash, lock contention, etc.) would block the script indefinitely with no automatic recovery. Also,rmSynccleanup forstaging/profileonly runs if the loop completes; an unexpected throw (e.g.writeFileSyncfailing) would leak both temp dirs.♻️ Suggested tweaks
- await execFileAsync(chrome, [ + await execFileAsync(chrome, [ '--headless', ... pathToFileURL(htmlPath).href, - ]); + ], { timeout: 15_000 });Wrap the loop body (or the whole render step) in
try { ... } finally { rmSync(staging, ...); rmSync(profile, ...); }.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@scripts/og-render.mjs` around lines 73 - 107, Update the OG_CARDS render loop to pass a finite timeout to execFileAsync so hung Chrome processes fail automatically, and wrap the render operation in try/finally so staging and profile are always removed via rmSync, including when writeFileSync or another unexpected operation throws.scripts/og-template.mjs (2)
19-25: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winDuplicate
esc()instead of importing the shared one.
diagram-svg.mjsalready exportsescspecifically so it can be reused (per its own header comment). This file redefines an identical implementation locally rather than importing it.♻️ Proposed fix
+import { esc } from './diagram-svg.mjs'; + const W = 1200; const H = 630; const FONT = "Inter,'Segoe UI',system-ui,-apple-system,'Helvetica Neue',Arial,sans-serif"; -function esc(s) { - return String(s) - .replace(/&/g, '&') - .replace(/</g, '<') - .replace(/>/g, '>') - .replace(/"/g, '"'); -}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@scripts/og-template.mjs` around lines 19 - 25, Remove the local esc function from og-template.mjs and import the shared esc export from diagram-svg.mjs instead. Update the import usage so all existing escaping calls continue to use the shared implementation without changing behavior.
69-77: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueBadge width formula duplicated between
badge()and its caller.
22 + label.length * 9.1is computed both insidebadge()(line 70) and again inline when spacing badges out (line 107). Consider a smallbadgeWidth(label)helper so the two stay in sync if the formula ever changes.Also applies to: 104-108
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@scripts/og-template.mjs` around lines 69 - 77, Extract the duplicated 22 + label.length * 9.1 calculation into a shared badgeWidth(label) helper in scripts/og-template.mjs. Update badge() and the caller’s badge-spacing logic around the existing badge invocation to use this helper, keeping badge rendering and spacing behavior unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@CHANGELOG.md`:
- Around line 12-25: Align the 1.1.2 changelog entry with the shipped
functionality by removing or revising claims about social preview images,
generated assets, route wiring, and build guards that are not implemented. Keep
only the 404-page changes that this release actually includes, unless the
missing preview and build-guard work is completed and its assets are added
before release.
In `@scripts/diagram-svg.mjs`:
- Around line 21-52: Update the sub/superscript handling loop so each
consecutive token applies the pending baseline reset before emitting the next
token, preventing SVG sibling tspan dy values from accumulating. Adjust the
token branch around pendingReset and preserve the existing plain-text and
braced-token behavior.
---
Nitpick comments:
In `@scripts/generate-seo-pages.mjs`:
- Around line 147-154: Use OG_SIZE as the single source of truth for card
dimensions: in scripts/generate-seo-pages.mjs lines 147-154, import OG_SIZE from
./og-template.mjs and interpolate its width and height in the generated
metadata. In index.html lines 30-42, retain the static 1200/630 values by
convention or extend assertEverySocialCardExists() to validate them against
OG_SIZE before the build succeeds.
In `@scripts/og-render.mjs`:
- Around line 73-107: Update the OG_CARDS render loop to pass a finite timeout
to execFileAsync so hung Chrome processes fail automatically, and wrap the
render operation in try/finally so staging and profile are always removed via
rmSync, including when writeFileSync or another unexpected operation throws.
In `@scripts/og-template.mjs`:
- Around line 19-25: Remove the local esc function from og-template.mjs and
import the shared esc export from diagram-svg.mjs instead. Update the import
usage so all existing escaping calls continue to use the shared implementation
without changing behavior.
- Around line 69-77: Extract the duplicated 22 + label.length * 9.1 calculation
into a shared badgeWidth(label) helper in scripts/og-template.mjs. Update
badge() and the caller’s badge-spacing logic around the existing badge
invocation to use this helper, keeping badge rendering and spacing behavior
unchanged.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 268ec67a-d1e3-46c5-9213-b95b3af0d14a
⛔ Files ignored due to path filters (17)
package-lock.jsonis excluded by!**/package-lock.jsonpublic/og/og-ad-as-diagram.pngis excluded by!**/*.pngpublic/og/og-compare.pngis excluded by!**/*.pngpublic/og/og-default.pngis excluded by!**/*.pngpublic/og/og-diagrams.pngis excluded by!**/*.pngpublic/og/og-exchange-rate-diagram.pngis excluded by!**/*.pngpublic/og/og-monopoly-diagram.pngis excluded by!**/*.pngpublic/og/og-negative-externalities.pngis excluded by!**/*.pngpublic/og/og-perfect-competition.pngis excluded by!**/*.pngpublic/og/og-positive-externalities.pngis excluded by!**/*.pngpublic/og/og-ppc-diagram.pngis excluded by!**/*.pngpublic/og/og-price-ceilings-and-floors.pngis excluded by!**/*.pngpublic/og/og-pricing.pngis excluded by!**/*.pngpublic/og/og-subsidy-diagram.pngis excluded by!**/*.pngpublic/og/og-supply-and-demand.pngis excluded by!**/*.pngpublic/og/og-tariff-diagram.pngis excluded by!**/*.pngpublic/og/og-tax-incidence.pngis excluded by!**/*.png
📒 Files selected for processing (9)
CHANGELOG.mdindex.htmlpackage.jsonscripts/diagram-svg.mjsscripts/generate-seo-pages.mjsscripts/og-pages.mjsscripts/og-preview.mjsscripts/og-render.mjsscripts/og-template.mjs
| - **Social preview images** for every page. The site asked for a large preview | ||
| card and supplied no image, so a link posted to Discord, WhatsApp, Reddit or | ||
| Teams rendered as a bare text stub. Each of the 12 diagram pages now has its | ||
| own card showing that diagram, with separate cards for the homepage, the | ||
| guides hub, pricing and comparison | ||
|
|
||
| ### Changed | ||
|
|
||
| - **The 404 page is simpler.** The heading names the error, three short lines | ||
| replace a paragraph followed by a list of reasons and six links, and the | ||
| main button goes back to the homepage rather than further into the site | ||
| - The build now fails if a page references a social card that is not there, and | ||
| if a view can be navigated to but has no route (the latter would 404 only | ||
| after a reload) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Align the release notes with what 1.1.2 actually ships.
This entry presents social images, route wiring, generated assets, and build guards as released, but the PR objectives state that the preview infrastructure is not wired into pages, generated images are absent, and this work remains follow-up. Update the implementation/assets before release or narrow these notes to the functionality actually shipped.
As per PR objectives: preview infrastructure is not wired into pages, generated images are not included, and related build-guard work remains planned follow-up.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@CHANGELOG.md` around lines 12 - 25, Align the 1.1.2 changelog entry with the
shipped functionality by removing or revising claims about social preview
images, generated assets, route wiring, and build guards that are not
implemented. Keep only the 404-page changes that this release actually includes,
unless the missing preview and build-guard work is completed and its assets are
added before release.
| while (i < text.length) { | ||
| const ch = text[i]; | ||
| if ((ch === '_' || ch === '^') && i + 1 < text.length) { | ||
| let token = text[i + 1]; | ||
| let consumed = 2; | ||
| if (text[i + 1] === '{') { | ||
| const close = text.indexOf('}', i + 2); | ||
| if (close !== -1) { | ||
| token = text.slice(i + 2, close); | ||
| consumed = close - i + 1; | ||
| } | ||
| } | ||
| const dy = ch === '_' ? '3' : '-4'; | ||
| out += `<tspan dy="${dy}" font-size="9">${esc(token)}</tspan>`; | ||
| pendingReset = ch === '_' ? '-3' : '4'; | ||
| i += consumed; | ||
| } else { | ||
| // Gather the whole plain-text run and emit it once, applying any | ||
| // pending baseline reset to it. A trailing '_' or '^' has nothing | ||
| // to mark up and lands here, so always consume the character at i | ||
| // to guarantee the outer loop makes progress. | ||
| let j = i + 1; | ||
| while (j < text.length && text[j] !== '_' && text[j] !== '^') j += 1; | ||
| const run = text.slice(i, j); | ||
| out += pendingReset !== null | ||
| ? `<tspan dy="${pendingReset}">${esc(run)}</tspan>` | ||
| : esc(run); | ||
| pendingReset = null; | ||
| i = j; | ||
| continue; | ||
| } | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Baseline dy isn't reset between consecutive sub/superscript tokens.
pendingReset is only applied when the loop lands in the plain-text branch (line 45-47). If a _x is immediately followed by ^y with no plain text between them (e.g. "x_a^b"), the second tspan's dy stacks on the first one's un-reset baseline instead of resetting first, since SVG dy is cumulative across sibling tspans. Net effect: the superscript ends up offset by the sum of both deltas rather than just its own.
🐛 Proposed fix
const dy = ch === '_' ? '3' : '-4';
- out += `<tspan dy="${dy}" font-size="9">${esc(token)}</tspan>`;
+ const effectiveDy = pendingReset !== null ? String(Number(dy) + Number(pendingReset)) : dy;
+ out += `<tspan dy="${effectiveDy}" font-size="9">${esc(token)}</tspan>`;
pendingReset = ch === '_' ? '-3' : '4';📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| while (i < text.length) { | |
| const ch = text[i]; | |
| if ((ch === '_' || ch === '^') && i + 1 < text.length) { | |
| let token = text[i + 1]; | |
| let consumed = 2; | |
| if (text[i + 1] === '{') { | |
| const close = text.indexOf('}', i + 2); | |
| if (close !== -1) { | |
| token = text.slice(i + 2, close); | |
| consumed = close - i + 1; | |
| } | |
| } | |
| const dy = ch === '_' ? '3' : '-4'; | |
| out += `<tspan dy="${dy}" font-size="9">${esc(token)}</tspan>`; | |
| pendingReset = ch === '_' ? '-3' : '4'; | |
| i += consumed; | |
| } else { | |
| // Gather the whole plain-text run and emit it once, applying any | |
| // pending baseline reset to it. A trailing '_' or '^' has nothing | |
| // to mark up and lands here, so always consume the character at i | |
| // to guarantee the outer loop makes progress. | |
| let j = i + 1; | |
| while (j < text.length && text[j] !== '_' && text[j] !== '^') j += 1; | |
| const run = text.slice(i, j); | |
| out += pendingReset !== null | |
| ? `<tspan dy="${pendingReset}">${esc(run)}</tspan>` | |
| : esc(run); | |
| pendingReset = null; | |
| i = j; | |
| continue; | |
| } | |
| } | |
| while (i < text.length) { | |
| const ch = text[i]; | |
| if ((ch === '_' || ch === '^') && i + 1 < text.length) { | |
| let token = text[i + 1]; | |
| let consumed = 2; | |
| if (text[i + 1] === '{') { | |
| const close = text.indexOf('}', i + 2); | |
| if (close !== -1) { | |
| token = text.slice(i + 2, close); | |
| consumed = close - i + 1; | |
| } | |
| } | |
| const dy = ch === '_' ? '3' : '-4'; | |
| const effectiveDy = pendingReset !== null ? String(Number(dy) + Number(pendingReset)) : dy; | |
| out += `<tspan dy="${effectiveDy}" font-size="9">${esc(token)}</tspan>`; | |
| pendingReset = ch === '_' ? '-3' : '4'; | |
| i += consumed; | |
| } else { | |
| // Gather the whole plain-text run and emit it once, applying any | |
| // pending baseline reset to it. A trailing '_' or '^' has nothing | |
| // to mark up and lands here, so always consume the character at i | |
| // to guarantee the outer loop makes progress. | |
| let j = i + 1; | |
| while (j < text.length && text[j] !== '_' && text[j] !== '^') j += 1; | |
| const run = text.slice(i, j); | |
| out += pendingReset !== null | |
| ? `<tspan dy="${pendingReset}">${esc(run)}</tspan>` | |
| : esc(run); | |
| pendingReset = null; | |
| i = j; | |
| continue; | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/diagram-svg.mjs` around lines 21 - 52, Update the sub/superscript
handling loop so each consecutive token applies the pending baseline reset
before emitting the next token, preventing SVG sibling tspan dy values from
accumulating. Adjust the token branch around pendingReset and preserve the
existing plain-text and braced-token behavior.
404 page
The page said the same thing three times: a lede, a bulleted list of reasons, and six diagram cards below the fold. It now keeps the joke and the diagram and drops the rest, and the primary action goes back to the homepage rather than deeper into the site.
Social preview cards
og:imageandtwitter:imagewere missing site-wide whiletwitter:cardwas set tosummary_large_image, so every link shared to Discord, WhatsApp or Reddit rendered as a bare text stub.All 16 routes now point at a card of their own, each diagram page showing its actual diagram. That reuse is why the diagram renderer moved out of
generate-seo-pages.mjsintoscripts/diagram-svg.mjs: that file is a script and writesdist/when imported, so nothing could pull a function from it. The move is verified output-identical across all 20 generated pages.The PNGs are rendered by
scripts/og-render.mjs(headless Chrome, no dependency to install) and committed rather than built on Vercel. The cards are set in Inter, which is not on the build image, so generating during the build would silently ship a different typeface. The build fails if a referenced card is missing frompublic/, or if it never reacheddist/.