@vdaluz/astro-og-cards
v1.1.4
Published
Build-time OG/social-card generation (satori + sharp) and meta-tag emission for Astro - proven in production on vdaluz.com and imperfectsystems.com.
Maintainers
Readme
@vdaluz/astro-og-cards
Social shares need an OG image, and generating one at build time from real post data (not a hand-designed static fallback) usually means wiring up a headless browser or a from-scratch Satori setup yourself. @vdaluz/astro-og-cards packages both halves: a meta-tag emission component for the og:*/twitter:* tags, and a build-time card generation harness (satori + sharp) that renders styled HTML to a real PNG. Ships raw .astro/.ts - the consuming app's Astro/Vite compiles them (no prebuild step), same as @vdaluz/astro-blog. Proven in production on vdaluz.com and imperfectsystems.com.
Install
npm install @vdaluz/astro-og-cardsPeer dependency: astro >= 6.
Installing this pulls in native
sharp.satori,satori-html, andsharpare regulardependencies, not optional ones, so every consumer installs all three even if it only usesOgMeta(pure meta tags, no image generation). That's an intentional tradeoff, not an oversight: card generation is this package's actual point, and splittingOgMetainto its own zero-dependency package for the rare meta-tags-only consumer isn't worth the maintenance overhead of a second package for one component. If that changes, revisit an optional peerDependency split.
Meta tags
---
import OgMeta from '@vdaluz/astro-og-cards/OgMeta.astro';
---
<OgMeta
title="Page title"
description="Page description"
image="https://example.com/og/page.png"
url="https://example.com/page"
siteName="Example Site"
/>Emits the full og:*/twitter:* tag set (title, description, image, url, type, site name, twitter card, including twitter:image:alt). imageWidth/imageHeight default to 1200/630 (matching generateCard's defaults) but are optional props if your image is a different size.
image and url must be absolute http(s):// URLs - Open Graph scrapers don't fetch relative or protocol-relative values, so resolve them against your site's origin (new URL(image, Astro.site)) before passing them in. OgMeta throws at build time if either isn't absolute, rather than silently shipping a card with a missing image.
For a blog post, pass type="article" plus publishedTime/modifiedTime (ISO 8601) to emit article:published_time/article:modified_time:
<OgMeta
title={post.title}
description={post.description}
image={ogImageUrl}
url={postUrl}
siteName="Example Site"
type="article"
publishedTime={post.pubDate.toISOString()}
modifiedTime={post.updatedDate?.toISOString()}
/>Card generation
import { generateCard } from '@vdaluz/astro-og-cards';
const png = await generateCard(inlineStyledHtml); // Buffer, 1200x630 PNGmarkup must use inline styles, not <style> blocks or CSS classes - satori-html (the HTML-to-Satori adapter) only reads inline styles. A hand-designed card with <style> + classes needs manual translation to inline styles first; see fixtures/imperfectSystemsCard.ts for a worked example (imperfectsystems.com's real default card, translated).
Example
Real output from generateCard(imperfectSystemsCard), the fixture above, unmodified:

Bundles a default static (non-variable) font, Space Mono (OFL-licensed). Pass fonts in the options to override.
Known gotchas
- Use
sharpfor SVG->PNG, not@resvg/resvg-js. resvg-js 2.6.2 (latest stable as of writing) native-panics (uncatchable Rust abort, not a JS exception) on Satori'sfeDropShadow/feGaussianBlurfilter output - i.e. anybox-shadowortext-shadowin the source markup. Confirmed via binary search against a real shadow-bearing design; independent of shadow color format (hex vsrgba()). - Variable fonts fail to parse. Satori's bundled font parser (
@shuding/opentype.js) can't read variable-font files (e.g. macOS's system SF Mono, which has anfvartable). Always pass a static font weight, never a system font reference. - The bundled default font is embedded as base64 in
src/lib/spaceMonoData.ts, not read from a sibling.ttffile at runtime. Confirmed via a realastro build: Vite/Rollup bundles this package's source into a new chunk file at a different physical location in the consumer's build output, so anyimport.meta.url-relative disk read breaks there (regardless of whether it's written asnew URL(...)or a plainpath.join- both are equally broken, since the problem is the module's code being relocated, not a specific path-construction pattern Vite's static analyzer happens to intercept). The raw.ttffiles live in the repo's root-levelassets/fonts/(kept for provenance/license visibility and to regenerate the base64 if the font is ever updated, but outsidesrc/so the npm package doesn't publish them - they aren't imported by any code path). The OFL license text ships alongside the base64 module itself atsrc/lib/OFL.txt, since that module is the actual redistribution of the font data. satori-htmlis stale (last published Dec 2022) but works correctly against the current Satori API as of this writing - the smoke test (npm test) is the tripwire for a future break.satori-htmlunconditionally trims every text node's value, so a literal space in a text node right before an inline-styled<span>is silently dropped (and a non-breaking space doesn't survive either - JStrim()treats it as whitespace too). Give the following span an explicitmargin-left(or the preceding element amargin-right) instead of relying on a whitespace character between elements.
Contributing
Issues welcome. PRs by discussion - open an issue first for anything beyond a typo or docs fix.
Releasing
Maintainer-only. Releases are tag-triggered and published to npm via GitHub Actions (Trusted Publishing / OIDC, no token secret):
- Test before tagging:
npm pack, install the tarball into a scratch Astro app (or a consumer locally),astro check && astro build. - Bump
versioninpackage.json, commit. - Tag
vX.Y.Zand push the tag. Pushing the tag runs.github/workflows/publish.yml, which type-checks, tests, verifies the tag matchespackage.json's version, and only then runsnpm publish. - Confirm the version is live:
npm view @vdaluz/astro-og-cards version. Consumers bump their own semver pin once it's confirmed live - see this package's CHANGELOG.md for what changed.
Consumers
- vdaluz.com
- imperfectsystems.com
- freetoolbox.net
- wq1k.com
- vicstradamus.com (devDependency, build-time card generation only)
License
MIT
