Original post: OpenGraph, Twitter Cards, and article metadata in Astro
Series: Part of How this blog was built — documenting every decision that shaped this site.
When someone shares a link to a blog post, the card that appears in Slack, LinkedIn,
or iMessage is determined by OpenGraph tags in the <head>. Get them wrong and the
shared link is an unformatted URL. Get them right and it shows the post title,
description, and a properly sized cover image.
This is table stakes for any public-facing blog, but the implementation details
matter — specifically, how to handle different content types (articles vs. pages),
where to put canonical URLs, and how to avoid the common mistake of sharing a
relative image path that produces a broken card.
Centralising metadata in BaseLayout
All meta tags are defined once in BaseLayout.astro. Every page passes what it
needs as props; the layout handles the markup. This avoids duplication and ensures
no page accidentally skips essential tags:
interface Props {
pageTitle: string;
description?: string;
ogImage?: string;
ogType?: "website" | "article";
canonicalUrl?: string;
pubDate?: Date;
author?: string;
tags?: string[];
}
const {
pageTitle,
description = "Practical software engineering guidance from Roger Rajaratnam for people breaking into tech, engineers growing in confidence, and teams improving engineering practice.",
ogImage = "/og-image.png",
ogType = "website",
canonicalUrl = Astro.url.href,
pubDate,
author,
tags,
} = Astro.props;
The default description covers general pages. The default ogType is "website".
Both are overridden for posts. The default canonicalUrl is Astro.url.href —
the fully qualified URL including the site's site value from astro.config.mjs.
The OpenGraph block
<!-- OpenGraph -->
<meta property="og:type" content={ogType} />
<meta property="og:site_name" content={siteName} />
<meta property="og:title" content={pageTitle} />
<meta property="og:description" content={description} />
<meta property="og:url" content={canonicalUrl} />
<meta property="og:image" content={new URL(ogImage, Astro.site ?? Astro.url.origin).href} />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:locale" content="en_GB" />
The og:image value deserves attention. A relative path like /og-image.png
won't work in OG tags — social crawlers need an absolute URL. Constructing it
with new URL(ogImage, Astro.site) handles both cases: if ogImage is already
absolute it passes through unchanged; if it's a root-relative path it's resolved
against the site's configured base URL.
siteName is a plain constant — const siteName = "Sourcier" — defined at the
top of BaseLayout.astro. og:locale uses the IETF language tag for British
English, which matches the site's target audience.
1200×630 is the recommended OG image size that renders well across Facebook,
LinkedIn, and Slack.
Article-specific tags
When ogType is "article", the Open Graph protocol defines additional properties
for structured article metadata. These are conditionally rendered:
{pubDate && (
<meta
property="article:published_time"
content={pubDate.toISOString()}
/>
)}
{author && <meta property="article:author" content={author} />}
{tags && tags.map((tag) => (
<meta property="article:tag" content={tag} />
))}
article:published_time uses the ISO 8601 format with timezone — Date.toISOString()
provides this. article:tag can appear multiple times, once per tag. Some scrapers
and indexers use these to understand content type and category.
Twitter Cards
X (formerly Twitter) has its own metadata system that runs parallel to OpenGraph. The summary_large_image
card type displays the image at full width above the title and description:
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={pageTitle} />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content={new URL(ogImage, Astro.site ?? Astro.url.origin).href} />
The property names keep the twitter: prefix — these are a stable protocol standard and won't change regardless of the platform rebrand. X falls back to OpenGraph values for some properties, but it's more reliable to specify them explicitly. The image URL construction is the same as for OG.
Canonical URLs
<link rel="canonical" href={canonicalUrl} />
The canonical tag tells search engines which URL is the authoritative version of a
page, which matters if content appears at multiple URLs or is syndicated elsewhere.
canonicalUrl defaults to Astro.url.href so it's correct without any manual
input, but can be overridden for pages that need a different canonical (for example,
a paginated page that canonicalises to page 1).
Passing metadata from post pages
MarkdownPostLayout.astro extracts the relevant frontmatter fields and passes
them to BaseLayout:
const ogImage = frontmatter.cover?.image?.src ?? undefined;
<BaseLayout
pageTitle={`${frontmatter.title} — Sourcier`}
description={frontmatter.description}
ogImage={ogImage}
ogType="article"
pubDate={frontmatter.pubDate}
author={frontmatter.author}
tags={frontmatter.tags}
>
The ogImage falls back to undefined if no cover is present, which means
BaseLayout will use the default /og-image.png for posts without cover images.
You can browse the rest of the site code in the
web-sourcier.uk repository.
Top comments (0)