Hwaro::Content::Seo::OgImage
Auto-generate OG (Open Graph) preview images as SVG files. Produces 1200x630 SVG images with the page title, description, site name, and optional logo — all without external dependencies.
Constants
band: a full-width solid band behind the (knocked-out) title,
echoed by a thin secondary band above it.
bauhaus: flat geometric art composition on the right side.
Brand row: a small accent tick + the site name in text color.
brutalist: thick framed panel with a hard offset shadow block.
diagonal: stripe wedge in the bottom-right corner triangle
(DIAG_WEDGE_X0, HEIGHT) - (WIDTH, HEIGHT) - (WIDTH, DIAG_WEDGE_Y1).
dots: corner-weighted halftone fade; text sits lower-left.
editorial ("magazine front"): hairline rules + kicker.
framed ("invitation card"): a neutral hairline frame + accent
corner brackets; the only centered composition.
grid ("blueprint"): focal crosshair + registration marks.
halftone: print-style dot field growing toward the right edge.
Stand-in output directory for callers that don't know the real one (assign_lazy_urls receives only the config). Containment is decided lexically — File.expand_path resolves "."/".." without touching the filesystem — so any sufficiently deep absolute root gives the same verdict a real output directory would.
--- Shared typographic design system (SVG + PNG renderers) --- One margin grid, one line-height model, one description hierarchy, and one brand treatment — so the styles differ in composition, not in sloppy metrics.
default ("masthead"): eyebrow on top, title anchored high.
The OG image slug for a page. Uses the URL-based slug to avoid collisions between pages with the same title in different sections (e.g. /posts/hello/ and /guides/hello/).
The gsub("/", "-") collapses distinct URLs that differ only in
slash-vs-hyphen placement (e.g. /posts/foo/ and /posts-foo/) onto
the same slug. Left unresolved, the second page overwrites the
first's manifest entry and both Pass-2 workers issue a concurrent
File.write to the SAME .png/.svg path (torn file under -Dpreview_mt),
and one page advertises an OG image rendered for the other. Append
a short stable hash of the URL so each distinct URL owns a path.
seen_slugs (slug => owning page.url) carries that disambiguation
state across calls within one pass.
Longest bare slug: + ".svg"/".png" (4) + "-<8 hex>" collision suffix (9)
stays under the 255-byte NAME_MAX.
--- Style families (single source of truth, shared by SVG + PNG renderers) ---
"Modern" styles are typography/panel-driven and reuse the classic background patterns. "Geometric" styles paint bold, distinctive background shapes (color blocks, bands, frames). "Signature" styles render a complete, self-contained composition (terminal window, bauhaus shapes, halftone fade) and ignore the soft text panel.
monument: accent rule ABOVE the title, brand row bottom-right.
Yield frequency inside the heavy OG PNG (and fallback SVG) rendering worker loop (see generate()).
Yielding after every single render adds scheduler overhead on
CPU-bound work. Yielding too rarely can starve the HTTP accept
fiber during serve --fast-start background OG generation.
8 is a pragmatic balance for current workloads.
Bump to invalidate incremental OG caches when the renderer's design changes without any config change (it feeds compute_config_hash). rev 2: 2026-07 typography + style redesign. rev 3: 2026-09 logo keeps its aspect ratio inside the LOGO_SIZE box. rev 4: 2026-10 PNG titles hard-break segments wider than the line. rev 5: 2026-10 untitled pages headline the site title; the masthead and editorial kickers are XML-escaped; JPEG backgrounds and logos honour their EXIF orientation.
--- Geometric style layout (shared by SVG + PNG so both stay in sync) ---
split: a diagonal color block on the left; text lives on the right.
Font stacks for the SVG fallback renderer. They lead with the bundled brand faces so environments that have them render in parity with the PNG output, then degrade gracefully.
terminal: a code-editor window with a title bar + traffic lights.
waves: text centers over the calm region above the tide bands.
Class methods
Lighten (positive delta) or darken (negative) a hex color in HSL space.
Content digest of an on-disk asset (logo / background image). Replacing the file at the same path must invalidate cached OG images — hashing only the path string left stale logos baked into cached images forever.
Lazy serve mode ([og.auto_image] lazy_generate = true): assign
each eligible page the URL its OG image WILL live at without
rendering anything, so page HTML carries the same og:image meta a
full build would emit. The dev server's OgLazyImageHandler
generates the file on first request. Returns the number of pages
assigned.
The extension assumes PNG rendering will be available when the format asks for it; if font initialization fails at request time the handler serves the SVG fallback bytes under the advertised path (dev-only pragmatism).
True when image was assigned by auto-OG generation — it points
into the auto-OG output directory — rather than set by the user.
Used by the serve incremental re-parse to preserve the assignment
(front matter alone can't restore it) and by Pass 1 above so a
preserved URL doesn't read as a "custom image" that blocks
regeneration.
Balanced title wrap (character-count analog of the PNG renderer's measured version): greedy wrap first; when the last line is an orphan (much shorter than the longest line), re-wrap against a tighter target so line lengths even out. Only accepted when it does not add lines.
How many title lines fit inside the fixed-height color band used by
the band style. Beyond this the title overflows the band and, being
drawn in the background color, renders invisibly off-band.
Cap lines to max, marking the last kept line with an ellipsis
so the truncation is visible rather than silent.
Compute a hash of OG-relevant config properties. RENDER_REVISION is folded in so a renderer design change regenerates cached images on existing sites even though no config value changed.
Compute a hash of page content that affects OG image rendering.
True if the text contains CJK ideographs, kana, hangul, or fullwidth
forms — glyphs the bundled/system Latin fonts cannot render, so PNG
OG images would show blank "tofu" boxes unless a CJK-capable
font_path is configured.
Derive a punchy complementary color from a hex string via HSL.
The headline of a card: the page title, or the site title for an
untitled page (the blog scaffold's home page has title = "") —
the fallback og:title, feeds and llms.txt already apply.
Pages auto-OG generation applies to (mirrors the Pass-1 filters, minus the custom-image skip which callers evaluate themselves).
A page that publishes no file of its own — it lost an output-path
collision, or its URL traverses out of the output directory — has
nothing to advertise a card for, and its page.url-derived slug
produced names like posts-..-..-escape.png. An unpublished
page (future/expired kept by a preview flag) follows the draft
rule: it renders for preview but stays out of generated assets.
Convert a file to a data URI with base64 encoding
Generate OG images for all pages that lack a custom image. Sets page.image to the generated SVG path so that og:image meta tags pick it up automatically.
partial signals that the caller is only handing in a subset
of the site's pages (e.g. --fast-start runs this once for
the priority subset and again for the deferred remainder).
In partial mode we accumulate manifest entries across calls
rather than overwriting; in full mode (default) we still
truncate so manifest entries — and the disk files they
describe — for deleted pages get pruned naturally.
forced_slug is for single-page callers (the dev server's
OgLazyImageHandler) whose page already advertises a slug computed
by assign_lazy_urls against the FULL page set: recomputing the
slug here with a fresh seen_slugs would collapse slug-colliding
pages (/posts/foo/ vs /posts-foo/) onto one file. Only meaningful
when pages holds exactly one page.
Convert "#RRGGBB" to {hue(0-360), saturation(0-1), lightness(0-1)}.
Convert HSL back to a "#rrggbb" hex string.
Load the OG manifest file. Returns {config_hash, entries}.
Compute logo (x, y) for a given position string. Shared by both SVG and PNG renderers.
Mix two hex colors in RGB space (t toward other). Mirrors the
PNG renderer's lerp_color so gradient stops match across formats.
A quiet hairline color derived from the background: slightly lighter on dark backgrounds, slightly darker on light ones.
Styles that drop the classic thin top/bottom accent bars for a cleaner, more modern composition.
Normalize a user-supplied hex color to a bare 6-digit "rrggbb" form. Accepts "#rgb" shorthand (expanded to "rrggbb"), "#rrggbb", and "#rrggbbaa" (alpha is dropped). Returns nil for anything that isn't a valid hex color so callers can fall back deterministically.
The files the last generate call left published: every image its
manifest records (PNG or the SVG fallback, whichever is on disk) and
the manifest itself. The hook claims them with the builder, so the
Finalize prune removes them once a build stops generating — the
generator's own manifest prune never runs when auto images are
switched off or --skip-og-image is passed, and a warm --cache
build then kept publishing every image a cold build no longer has.
Render each style's signature background: bold geometric shapes (split / band / brutalist) plus generated backdrops for the modern styles (artistic gradient, hero glow, surreal aurora, framed frame). The generated modern backdrops are skipped when a background image is present so a user photo shows through. Returns "" for plain styles.
Render a style/pattern SVG snippet based on the configured style.
Every pattern is a composition with a focal point rather than
uniform wallpaper; opacity acts as the peak alpha with internal
falloff. Mirrors the PNG renderer's render_pattern.
Render an SVG image for a page
Resolve the second color for two-tone geometric styles. Falls back to a complementary tone auto-derived from the accent color.
Save the OG manifest file.
Rotate a hex color's hue by degrees, optionally forcing a minimum
saturation so derived tones stay vivid on flat compositions.