Maintain an Elegant Open Graph System
Objectives
- Centralize defaults while allowing clean per-page overrides.
- Keep metadata rendering consistent in one place (layout), not scattered.
- Support dynamic routes, content collections, and future multi-brand/multi-locale needs.
- Enable optional dynamic OG image generation without complicating most pages.
Guiding Principles
- One source of truth for defaults and types.
- Small, composable helpers that return ready-to-render meta tags.
- Pages provide only the minimum context (title/description/image/url); everything else is inferred.
- Layout owns actual
<meta>
and canonical rendering for consistency. - Prefer absolute URLs in production; allow dev-friendly relative paths in dev.
Recommended Structure
- Project-level defaults and helpers
src/config/seo.ts
— site defaults, typessrc/utils/og.ts
— helper(s) to build OG/Twitter tagssrc/layouts/BaseLayout.astro
— renders meta, canonical
- Monorepo shared package (optional, recommended as sites grow)
packages/seo/
— export types, defaults builder, helpers- Sites import from
@lossless/seo
(or similar alias) for consistency
Site Defaults (Config)
Define one config object to drive defaults across pages.
ts
// src/config/seo.ts
export interface SiteSEO {
siteName: string;
site?: string; // set in astro.config.mjs for absolute URL resolution
twitterHandle?: string;
defaultTitle: string;
defaultDescription: string;
defaultImage: string; // path under /public or absolute URL
}
export type ShareMetaInput = {
title?: string;
description?: string;
image?: string;
url?: string;
type?: 'website' | 'article' | 'profile' | string;
};
export const SITE_SEO: SiteSEO = {
siteName: 'Parslee',
defaultTitle: 'Parslee',
defaultDescription: 'Enabling better use of AI through contextual understanding of documents.',
defaultImage: '/shareBanner__Parslee-Zinger.webp',
twitterHandle: '@parslee_ai',
};
Helper API (Meta Composition)
Keep helpers small and predictable.
ts
// src/utils/og.ts
import { SITE_SEO } from '../config/seo';
import type { ShareMetaInput } from '../config/seo';
type MetaTag = { name?: string; content: string; property?: string };
export function buildOgMeta(input: ShareMetaInput = {}): MetaTag[] {
const title = input.title ?? SITE_SEO.defaultTitle;
const description = input.description ?? SITE_SEO.defaultDescription;
const image = input.image ?? SITE_SEO.defaultImage;
const url = input.url; // optional; pass absolute in production
const type = input.type ?? 'website';
const meta: MetaTag[] = [
{ name: 'description', content: description },
{ property: 'og:type', content: type },
{ property: 'og:site_name', content: SITE_SEO.siteName },
{ property: 'og:title', content: title },
{ property: 'og:description', content: description },
{ property: 'og:image', content: image },
];
if (url) meta.push({ property: 'og:url', content: url });
meta.push({ name: 'twitter:card', content: 'summary_large_image' });
if (SITE_SEO.twitterHandle) meta.push({ name: 'twitter:site', content: SITE_SEO.twitterHandle });
meta.push({ name: 'twitter:title', content: title });
meta.push({ name: 'twitter:description', content: description });
meta.push({ name: 'twitter:image', content: image });
return meta;
}
Optional additions:
og:image:width
,og:image:height
,og:image:type
for completeness.twitter:image:alt
to describe the banner.- A
buildCanonical(url)
helper to emit a<link rel="canonical">
.
Layout Responsibilities
- Render the
<meta>
array and title. - Render canonical link when absolute URL is available.
- Optionally preload hero/share image if above-the-fold.
astro
<!-- src/layouts/BaseLayout.astro -->
{meta.map((m) => <meta {...m} />)}
{canonical && <link rel="canonical" href={canonical} />}
<title>{title}</title>
Page Usage Patterns
- Static pages
- Provide
title
, optionallydescription
,image
,url
. - Use site defaults for anything omitted.
- Dynamic routes (
/posts/[slug]
)- Derive metadata from content frontmatter.
- Compute absolute
url
vianew URL(Astro.url.pathname, Astro.site).toString()
.
- Content collections
- Standardize frontmatter:
title
,description
,shareImage
,shareType
. - Build metadata at render using those fields.
- i18n / multi-brand
- Parameterize
SITE_SEO
via brand and locale. - Provide brand-aware defaults and locale-specific descriptions.
Absolute URLs and Canonical
- Set
site
inastro.config.mjs
:
js
// astro.config.mjs
export default defineConfig({
site: 'https://parslee.ai',
});
- Compute canonical:
ts
const canonical = new URL(Astro.url.pathname, Astro.site).toString();
Dynamic OG Image Generation (Optional)
When you need runtime banners (e.g., post title + brand):
- Approaches
- HTML/CSS to image via server route (
/api/og
). - Satori/Vercel OG Images in an endpoint.
- Pre-generate at build-time for known content.
- Requirements
- Cache aggressively (CDN, short TTL with revalidation).
- Deterministic templates; avoid heavy client bundles.
- Fallback to static default image if generation fails.
Asset Guidance
- Dimensions:
1200x630
(or1200x628
) preferred. - Format:
webp
orjpeg
; ensure social scrapers can fetch it. - Files under
public/
for stable paths. - Consider
og:image:width/height/type
for strict parsers.
Validation & Tooling
- Internal checks
- Add lint rules/CI checks to ensure pages include minimum metadata.
- Snapshots for
buildOgMeta()
output for common cases.
- External validators
- Use social validators (Facebook/LinkedIn/Twitter) during QA.
- Maintain a small script or doc listing validation endpoints.
Performance & Caching
- Static images: long
Cache-Control
with fingerprinted filenames. - Dynamic endpoints: short TTL + revalidation, server-side caching layer.
- Avoid computing metadata on client; keep it server-rendered.
Governance & Maintenance
- Ownership: one team/component owns
SITE_SEO
and helpers. - Versioning: changes in shared
packages/seo
must be semver’d. - Documentation: keep this blueprint updated alongside releases.
Migration Plan (from current state)
- Introduce
SITE_SEO
andbuildOgMeta()
in each site. - Move shared logic into
packages/seo
and update imports. - Expand
BaseLayout.astro
to add canonical and optional width/height meta. - Set
site
inastro.config.mjs
for absolute URL generation. - Optionally add
/api/og
for dynamic banners with caching. - Add unit tests for helpers and a QA checklist.
Usage Examples
Page-level wiring (static page):
astro
---
import BaseLayout from '../layouts/BaseLayout.astro';
import { buildOgMeta } from '../utils/og';
import { SITE_SEO } from '../config/seo';
const pageTitle = 'Parslee: Enabling better use of AI through contextual understanding of documents.';
const pageDescription = SITE_SEO.defaultDescription;
const pageImage = SITE_SEO.defaultImage;
const pageUrl = Astro.site ? new URL(Astro.url.pathname, Astro.site).toString() : Astro.url.pathname;
---
<BaseLayout title={pageTitle} meta={buildOgMeta({ title: pageTitle, description: pageDescription, image: pageImage, url: pageUrl })}>
<!-- page content -->
</BaseLayout>
Dynamic route (content collection):
astro
---
import { getEntry } from 'astro:content';
import BaseLayout from '../../layouts/BaseLayout.astro';
import { buildOgMeta } from '../../utils/og';
const { slug } = Astro.params;
const post = await getEntry('posts', slug);
const title = post.data.title;
const description = post.data.description;
const image = post.data.shareImage ?? '/default-share.webp';
const url = Astro.site ? new URL(Astro.url.pathname, Astro.site).toString() : Astro.url.pathname;
---
<BaseLayout title={title} meta={buildOgMeta({ title, description, image, url, type: 'article' })}>
<!-- post content -->
</BaseLayout>
Checklist
- Site-wide defaults defined and documented.
- Layout renders meta + canonical consistently.
- Pages pass minimal overrides only.
- Absolute URLs configured via
astro.config.mjs
. - OG image dimensions and format validated.
- Optional dynamic OG endpoint designed with caching.
- Tests and QA checklist in place.