Skip to content

Simplify the 404 page, and add the social preview card generator - #3

Merged
Sukarth merged 3 commits into
mainfrom
feat/social-preview-cards
Jul 29, 2026
Merged

Simplify the 404 page, and add the social preview card generator#3
Sukarth merged 3 commits into
mainfrom
feat/social-preview-cards

Conversation

@Sukarth

@Sukarth Sukarth commented Jul 29, 2026

Copy link
Copy Markdown
Owner

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.

  • Heading names the error: "Error 404: This page has no supply"
  • "the market clears at nothing at all" becomes "nothing is traded"
  • The intersection is labelled "equilibrium at Q = 0" now that the heading carries the number
  • The three sentences are separate paragraphs instead of one block
  • The diagram lost its annotations, which the shape already conveys, and is capped narrower than the text column so it stops dominating the page

Social preview cards

og:image and twitter:image were missing site-wide while twitter:card was set to summary_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.mjs into scripts/diagram-svg.mjs: that file is a script and writes dist/ 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 from public/, or if it never reached dist/.

Note: this description originally described the card work as inert, with no images and nothing wired up. That was true of the first commit and went stale as the branch continued. The text above reflects what actually merged.

Sukarth added 2 commits July 29, 2026 13:03
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.
@vercel

vercel Bot commented Jul 29, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
ib-econgraph-ai Ready Ready Preview, Comment Jul 29, 2026 10:46am

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Release 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.

Changes

Social Preview and SEO Pipeline

Layer / File(s) Summary
SVG rendering and card definitions
scripts/diagram-svg.mjs, scripts/og-template.mjs, scripts/og-pages.mjs
Adds shared diagram/SVG helpers and defines fixed and diagram-specific Open Graph card specifications.
Preview and PNG generation
scripts/og-preview.mjs, scripts/og-render.mjs
Adds contact-sheet preview generation and headless Chrome rasterization into validated public/og/*.png files.
Route metadata and build validation
scripts/generate-seo-pages.mjs, index.html
Maps canonical routes to social cards, updates static and SPA metadata, simplifies the 404 page, and validates required card assets during builds.
Release metadata
package.json, CHANGELOG.md
Bumps the package version and documents the 1.1.2 release.

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
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 62.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the two main changes: simplifying the 404 page and adding social preview card generation infrastructure.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/social-preview-cards

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@sourcery-ai

sourcery-ai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Reviewer's Guide

Simplifies 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 guard

sequenceDiagram
  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
Loading

File-Level Changes

Change Details Files
Refactor diagram SVG rendering into a reusable module used by SEO pages and social preview cards.
  • Extract esc, svgLabel, and renderDiagramSvg helpers from the SEO generation script into a dedicated module.
  • Update SEO page generation to import and use the shared diagram renderer instead of inline logic.
scripts/generate-seo-pages.mjs
scripts/diagram-svg.mjs
Add Open Graph/Twitter social card configuration, templates, and rendering scripts, including a preview sheet and PNG rasterisation via headless Chrome.
  • Define OG_CARDS metadata linking card names, specs, and site routes, including per-diagram cards.
  • Implement an SVG card template with text wrapping, branding row, badges, and optional nested diagram art.
  • Add a preview script that outputs an HTML contact sheet of all cards for review at half/actual size.
  • Add a rasteriser script that renders cards to public/og/*.png using a standalone Chrome profile and filesystem guards.
scripts/og-pages.mjs
scripts/og-template.mjs
scripts/og-preview.mjs
scripts/og-render.mjs
Wire social preview images into static SEO pages and SPA route shells, and add a build guard to enforce card presence.
  • Import OG_CARDS into the SEO generator and derive a path→image map for OG/Twitter meta tags.
  • Extend pageShell and SPA route shell rendering to set og:image, og:image:width/height, og:image:alt, and twitter:image per canonical path.
  • Add assertEverySocialCardExists to fail the build when referenced cards are missing from public/ or not copied into dist/.
  • Update build logging to report that all social cards are present.
scripts/generate-seo-pages.mjs
Simplify and tighten the 404 page content and diagram while adjusting primary/secondary actions.
  • Redesign the inline SVG diagram to be narrower, less annotated, and more focused on the Q=0 equilibrium joke.
  • Update heading to include the error code and rewrite lede into three short paragraphs with clearer explanation and guidance.
  • Change primary CTA to return to the homepage and secondary CTA to open the editor; remove previous list of reasons and guide cards section.
scripts/generate-seo-pages.mjs
Introduce a default site-wide OG/Twitter image in index.html and bump version/changelog for the new release.
  • Add absolute og:image and twitter:image meta tags for the default card, including dimensions and alt text, to index.html.
  • Update package.json version to 1.1.2 and add a 1.1.2 entry in CHANGELOG describing social previews, 404 simplification, and build guards, plus link reference.
index.html
package.json
CHANGELOG.md

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

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/.
@Sukarth
Sukarth marked this pull request as ready for review July 29, 2026 10:45
Copilot AI review requested due to automatic review settings July 29, 2026 10:45

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've left some high level feedback:

  • 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.
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.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@Sukarth
Sukarth merged commit 486078f into main Jul 29, 2026
5 of 6 checks passed

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.mjs and 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:image tags 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.

Comment thread scripts/og-preview.mjs
Comment on lines +30 to +32
<title>OG card previews (${OG_CARDS.length})</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<style>
Comment on lines +147 to +151
<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"/>
Comment thread scripts/og-render.mjs
return found;
}

const chrome = process.env.CHROME_PATH ?? findChrome();
@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Add OG/Twitter social preview cards + simplify 404 page copy/layout

✨ Enhancement ⚙️ Configuration changes 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Simplify the 404 page copy, diagram styling, and primary CTA to return home.
• Add Open Graph/Twitter image meta tags per route with a fallback default card.
• Introduce scripts + committed PNG assets for generating and validating social preview cards.
Diagram

graph TD
  A["scripts/diagram-svg.mjs"] --> B["scripts/og-pages.mjs"] --> C["scripts/og-template.mjs"] --> D["scripts/og-render.mjs"] --> E[("public/og/*.png")]
  B --> F["scripts/og-preview.mjs"]
  B --> G["scripts/generate-seo-pages.mjs"] --> H[("dist/*.html")]
  I["index.html"] --> H
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Rasterize via resvg (no headless Chrome)
  • ➕ More deterministic/CI-friendly than locating a system Chrome binary
  • ➕ Often faster and simpler to run in automation
  • ➖ Font availability still needs explicit bundling/registration to avoid typeface drift
  • ➖ Adds/locks a native-ish rendering dependency, potentially increasing install friction
2. Generate images at build time on CI/Vercel with bundled fonts
  • ➕ No need to commit PNGs; assets always match code
  • ➕ Easy to enforce missing-card failures automatically
  • ➖ Requires reliable font bundling/installation; current constraint explicitly warns Vercel builders may differ
  • ➖ Risk of silently shipping different typography if the font step fails or differs by platform
3. Use Playwright instead of direct Chrome exec
  • ➕ More robust browser discovery/management across OSes
  • ➕ Better error reporting and isolation
  • ➖ Introduces a heavier dependency footprint than the current “use existing Chrome” approach

Recommendation: Given the stated typography constraint, committing pre-rendered PNGs and adding a build-time guard is a pragmatic approach that prevents broken social previews. If this becomes cumbersome to maintain, the next step would be switching rasterization to a deterministic renderer (e.g., resvg) with explicit font bundling, which would enable CI/Vercel generation without typeface drift.

Files changed (26) +660 / -141 · 16 not counted

Enhancement (2) +104 / -138
index.htmlAdd default OG/Twitter image meta tags +7/-0

Add default OG/Twitter image meta tags

• Adds og:image and twitter:image tags pointing at the default social card PNG, including required width/height/alt metadata for crawlers.

index.html

generate-seo-pages.mjsInject per-route OG images, add card guard, simplify 404 +97/-138

Inject per-route OG images, add card guard, simplify 404

• Wires Open Graph and Twitter image meta tags into generated pages and SPA route shells, using a route→card mapping derived from the card registry. Adds a build-time guard that fails if any referenced OG PNG is missing (or not copied into dist/). Simplifies the 404 page copy/CTAs and reduces diagram annotations and dominance.

scripts/generate-seo-pages.mjs

Refactor (1) +102 / -0
diagram-svg.mjsExtract reusable diagram-spec → SVG renderer +102/-0

Extract reusable diagram-spec → SVG renderer

• Introduces a standalone module exporting HTML/SVG escaping, label formatting with sub/superscripts, and a declarative diagram renderer. This is extracted so other scripts can reuse it without importing a build-writing script.

scripts/diagram-svg.mjs

Documentation (1) +20 / -0
CHANGELOG.mdDocument 1.1.2 release: OG cards + simpler 404 + build guard +20/-0

Document 1.1.2 release: OG cards + simpler 404 + build guard

• Adds a 1.1.2 changelog entry describing new social preview images, the simplified 404 page, and the new build-time validation that referenced cards exist.

CHANGELOG.md

Other (22) +434 / -3
package.jsonBump version to 1.1.2 +1/-1

Bump version to 1.1.2

• Updates the package version to 1.1.2 to match the new release entry.

package.json

package-lock.jsonSync lockfile version bump to 1.1.2 +2/-2

Sync lockfile version bump to 1.1.2

• Updates lockfile metadata to reflect the 1.1.2 version bump.

package-lock.json

og-pages.mjsDefine the canonical set of social preview cards per route +77/-0

Define the canonical set of social preview cards per route

• Adds a single source of truth for which OG card images exist, which routes use them, and what each card contains. Generates per-diagram card specs by embedding each page’s diagram SVG.

scripts/og-pages.mjs

og-preview.mjsAdd OG card contact-sheet generator +65/-0

Add OG card contact-sheet generator

• Adds a script that writes an HTML preview grid of all card designs with a half-size/full-size toggle, for visual review before rasterizing.

scripts/og-preview.mjs

og-render.mjsAdd headless-Chrome rasterizer for OG cards +114/-0

Add headless-Chrome rasterizer for OG cards

• Adds a standalone script that renders each card SVG into a 1200×630 PNG via headless Chrome and writes outputs to public/og/. Includes Chrome discovery, temporary profile isolation, and validation that a usable PNG was produced.

scripts/og-render.mjs

og-template.mjsAdd pure-SVG 1200×630 OG card template renderer +175/-0

Add pure-SVG 1200×630 OG card template renderer

• Implements the shared SVG layout for social cards (branding, title/subtitle wrapping, badges, and optional embedded diagram). Exports the renderer and fixed OG dimensions for use by preview and rasterization scripts.

scripts/og-template.mjs

og-ad-as-diagram.pngAdd OG card PNG: ad-as-diagram not counted

Add OG card PNG: ad-as-diagram

• Adds the committed PNG asset for the ad-as-diagram social preview card.

public/og/og-ad-as-diagram.png

og-compare.pngAdd OG card PNG: compare not counted

Add OG card PNG: compare

• Adds the committed PNG asset for the compare page social preview card.

public/og/og-compare.png

og-default.pngAdd OG card PNG: default not counted

Add OG card PNG: default

• Adds the committed PNG asset for the default/fallback social preview card used site-wide.

public/og/og-default.png

og-diagrams.pngAdd OG card PNG: diagrams hub not counted

Add OG card PNG: diagrams hub

• Adds the committed PNG asset for the diagrams hub social preview card.

public/og/og-diagrams.png

og-exchange-rate-diagram.pngAdd OG card PNG: exchange-rate-diagram not counted

Add OG card PNG: exchange-rate-diagram

• Adds the committed PNG asset for the exchange-rate-diagram social preview card.

public/og/og-exchange-rate-diagram.png

og-monopoly-diagram.pngAdd OG card PNG: monopoly-diagram not counted

Add OG card PNG: monopoly-diagram

• Adds the committed PNG asset for the monopoly-diagram social preview card.

public/og/og-monopoly-diagram.png

og-negative-externalities.pngAdd OG card PNG: negative-externalities not counted

Add OG card PNG: negative-externalities

• Adds the committed PNG asset for the negative-externalities social preview card.

public/og/og-negative-externalities.png

og-perfect-competition.pngAdd OG card PNG: perfect-competition not counted

Add OG card PNG: perfect-competition

• Adds the committed PNG asset for the perfect-competition social preview card.

public/og/og-perfect-competition.png

og-positive-externalities.pngAdd OG card PNG: positive-externalities not counted

Add OG card PNG: positive-externalities

• Adds the committed PNG asset for the positive-externalities social preview card.

public/og/og-positive-externalities.png

og-ppc-diagram.pngAdd OG card PNG: ppc-diagram not counted

Add OG card PNG: ppc-diagram

• Adds the committed PNG asset for the ppc-diagram social preview card.

public/og/og-ppc-diagram.png

og-price-ceilings-and-floors.pngAdd OG card PNG: price-ceilings-and-floors not counted

Add OG card PNG: price-ceilings-and-floors

• Adds the committed PNG asset for the price-ceilings-and-floors social preview card.

public/og/og-price-ceilings-and-floors.png

og-pricing.pngAdd OG card PNG: pricing not counted

Add OG card PNG: pricing

• Adds the committed PNG asset for the pricing page social preview card.

public/og/og-pricing.png

og-subsidy-diagram.pngAdd OG card PNG: subsidy-diagram not counted

Add OG card PNG: subsidy-diagram

• Adds the committed PNG asset for the subsidy-diagram social preview card.

public/og/og-subsidy-diagram.png

og-supply-and-demand.pngAdd OG card PNG: supply-and-demand not counted

Add OG card PNG: supply-and-demand

• Adds the committed PNG asset for the supply-and-demand social preview card.

public/og/og-supply-and-demand.png

og-tariff-diagram.pngAdd OG card PNG: tariff-diagram not counted

Add OG card PNG: tariff-diagram

• Adds the committed PNG asset for the tariff-diagram social preview card.

public/og/og-tariff-diagram.png

og-tax-incidence.pngAdd OG card PNG: tax-incidence not counted

Add OG card PNG: tax-incidence

• Adds the committed PNG asset for the tax-incidence social preview card.

public/og/og-tax-incidence.png

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Dist OG guard can skip 🐞 Bug ☼ Reliability
Description
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.
Code

scripts/generate-seo-pages.mjs[R622-636]

+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)`);
+        }
Evidence
The generator writes directly into dist/ and emits absolute OG/Twitter image tags pointing at
/og/*.png, so missing dist/og assets break social previews in the deployed artifact. The current
guard sets distBuilt false unless dist/og already exists, which prevents detecting the exact
failure case where dist/og was not created/copied.

scripts/generate-seo-pages.mjs[1-6]
scripts/generate-seo-pages.mjs[118-154]
scripts/generate-seo-pages.mjs[622-636]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### 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


Grey Divider

To customize comments, go to the Qodo configuration screen, or learn more in the docs.

Qodo Logo

Comment on lines +622 to +636
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)`);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (4)
scripts/generate-seo-pages.mjs (1)

147-154: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Card dimensions (1200x630) are duplicated as literals instead of deriving from OG_SIZE. scripts/og-template.mjs exports OG_SIZE as the single source of truth for card dimensions, and scripts/og-render.mjs already 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: import OG_SIZE from ./og-template.mjs and interpolate OG_SIZE.width/OG_SIZE.height instead of the literals "1200"/"630".
  • index.html#L30-L42: since this is static markup with no build-time templating, keep the 1200/630 values in sync by convention, or have assertEverySocialCardExists() (or a similar guard in scripts/generate-seo-pages.mjs) additionally assert that dist/index.html's width/height match OG_SIZE before 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 value

Consider a timeout guard and try/finally cleanup for the render loop.

execFileAsync has no timeout, so a hung Chrome process (crash, lock contention, etc.) would block the script indefinitely with no automatic recovery. Also, rmSync cleanup for staging/profile only runs if the loop completes; an unexpected throw (e.g. writeFileSync failing) 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 win

Duplicate esc() instead of importing the shared one.

diagram-svg.mjs already exports esc specifically 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, '&amp;')
-        .replace(/</g, '&lt;')
-        .replace(/>/g, '&gt;')
-        .replace(/"/g, '&quot;');
-}
🤖 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 value

Badge width formula duplicated between badge() and its caller.

22 + label.length * 9.1 is computed both inside badge() (line 70) and again inline when spacing badges out (line 107). Consider a small badgeWidth(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

📥 Commits

Reviewing files that changed from the base of the PR and between 6c7d490 and d54b352.

⛔ Files ignored due to path filters (17)
  • package-lock.json is excluded by !**/package-lock.json
  • public/og/og-ad-as-diagram.png is excluded by !**/*.png
  • public/og/og-compare.png is excluded by !**/*.png
  • public/og/og-default.png is excluded by !**/*.png
  • public/og/og-diagrams.png is excluded by !**/*.png
  • public/og/og-exchange-rate-diagram.png is excluded by !**/*.png
  • public/og/og-monopoly-diagram.png is excluded by !**/*.png
  • public/og/og-negative-externalities.png is excluded by !**/*.png
  • public/og/og-perfect-competition.png is excluded by !**/*.png
  • public/og/og-positive-externalities.png is excluded by !**/*.png
  • public/og/og-ppc-diagram.png is excluded by !**/*.png
  • public/og/og-price-ceilings-and-floors.png is excluded by !**/*.png
  • public/og/og-pricing.png is excluded by !**/*.png
  • public/og/og-subsidy-diagram.png is excluded by !**/*.png
  • public/og/og-supply-and-demand.png is excluded by !**/*.png
  • public/og/og-tariff-diagram.png is excluded by !**/*.png
  • public/og/og-tax-incidence.png is excluded by !**/*.png
📒 Files selected for processing (9)
  • CHANGELOG.md
  • index.html
  • package.json
  • scripts/diagram-svg.mjs
  • scripts/generate-seo-pages.mjs
  • scripts/og-pages.mjs
  • scripts/og-preview.mjs
  • scripts/og-render.mjs
  • scripts/og-template.mjs

Comment thread CHANGELOG.md
Comment on lines +12 to +25
- **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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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.

Comment thread scripts/diagram-svg.mjs
Comment on lines +21 to +52
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;
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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.

Suggested change
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.

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.

2 participants