@vdaluz/astro-og-cards
v1.1.1
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-cardsAlternatively, a pinned https tarball from a tag works too, with no registry involved:
// package.json
"dependencies": {
"@vdaluz/astro-og-cards": "https://github.com/vdaluz/astro-og-cards/archive/refs/tags/v1.1.0.tar.gz"
}Why a tarball, not
github:vdaluz/astro-og-cards#v1.0.0? npm canonicalizes GitHub shorthand (and even an explicitgit+https://URL) togit+ssh://in the lockfile. CI runners (e.g. Cloudflare Pages/Workers) have no SSH key, sonpm ciwould fail to clone it. The/archive/refs/tags/<tag>.tar.gzURL is anonymous https with an integrity hash in the lockfile - it just works in CI. Bump the tag in the URL to upgrade.
Peer 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.
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 src/assets/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 still live insrc/assets/fonts/for provenance/license visibility and to regenerate the base64 if the font is ever updated; they aren't imported by any code path. 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.
License
MIT
