Open Graph & SEO Infrastructure

Complete Open Graph and SEO infrastructure with dynamic image generation, JSON-LD structured data, collection defaults, and messaging-first optimization.

Claude
Claude Claude

Changelog - 2025-12-27 (#2)

Open Graph & SEO Infrastructure

Overview

Implemented a complete Open Graph and SEO system optimized for the messaging-first share economy. When someone shares a link via iMessage, WhatsApp, LinkedIn, or Slack, the link preview is the brand's first impression. This update ensures every page produces rich, accurate, platform-optimized previews.

Key deliverables:

  • SEO Configuration (src/config/seo.ts): Centralized defaults, types, character limits, and collection defaults
  • OG Meta Builder (src/utils/og.ts): Helper functions for building OG and Twitter meta tags
  • JSON-LD Structured Data (src/utils/structured-data.ts): Schema.org builders for rich results
  • Dynamic OG Images (/api/og): Server-side image generation with satori + resvg
  • Layout Integration: Both layouts render meta, canonical, and JSON-LD consistently
  • Collection Defaults: Per-collection image and type defaults for consistent sharing

Architecture

┌─────────────────────────────────────────────────────────────────────────────┐
│                              SEO Data Flow                                   │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│   ┌──────────────┐      ┌─────────────────┐      ┌────────────────────┐    │
│   │   Page       │      │  buildOgMeta()  │      │   <head>           │    │
│   │   (meta      │─────▶│  buildCanonical │─────▶│   OG tags          │    │
│   │    prop)     │      │                 │      │   Twitter cards    │    │
│   └──────────────┘      └─────────────────┘      │   JSON-LD          │    │
│         │                                         └────────────────────┘    │
│         │                                                                    │
│         ▼                                                                    │
│   ┌──────────────┐      ┌─────────────────┐      ┌────────────────────┐    │
│   │  COLLECTION  │      │  /api/og?       │      │   1200x630 PNG     │    │
│   │  _DEFAULTS   │─────▶│  title=...      │─────▶│   (satori+resvg)   │    │
│   │              │      │  description=.. │      │                    │    │
│   └──────────────┘      └─────────────────┘      └────────────────────┘    │
│                                                                              │
└─────────────────────────────────────────────────────────────────────────────┘

Changes by Area

1. SEO Configuration (src/config/seo.ts)

Centralized SEO defaults with TypeScript types:

export const SITE_SEO: SiteSEO = {
  siteName: 'Dark Matter',
  siteUrl: import.meta.env.SITE || 'https://matter-site.vercel.app',
  defaultTitle: 'Dark Matter | Bio Longevity Fund',
  defaultDescription: 'Investing in the science of longevity...',
  defaultImage: '/share-banners/shareBanner__Dark-Matter-Bio_Longevity-Fund-II.webp',
  themeColor: '#0f0f23',
  locale: 'en_US',
};

export const CHAR_LIMITS = {
  title: 60,
  description: 155,
  siteName: 30,
} as const;

2. Collection Defaults

Per-collection OG defaults ensure consistent sharing across page types:

export const COLLECTION_DEFAULTS = {
  thesis:    { image, type: 'website', description },
  strategy:  { image, type: 'website', description },
  portfolio: { image, type: 'website', description },
  pipeline:  { image, type: 'website', description },
  memos:     { image, type: 'article', description },
  slides:    { image: 'Market-Maps.webp', type: 'website', description },
  changelog: { image, type: 'article', description },
  team:      { image, type: 'profile', description },
  dataroom:  { image, type: 'website', noIndex: true },
};

Usage in pages:

import { COLLECTION_DEFAULTS } from '@config/seo';

<BaseThemeLayout
  title="Investment Strategy"
  meta={{
    title: 'Investment Strategy',
    description: COLLECTION_DEFAULTS.strategy.description,
    image: COLLECTION_DEFAULTS.strategy.image,
    type: COLLECTION_DEFAULTS.strategy.type,
  }}
>

3. OG Meta Builder (src/utils/og.ts)

Helper functions with automatic truncation and absolute URL handling:

export function buildOgMeta(input: ShareMetaInput = {}): MetaTag[] {
  const title = truncate(input.title ?? SITE_SEO.defaultTitle, CHAR_LIMITS.title);
  const description = truncate(input.description ?? SITE_SEO.defaultDescription, CHAR_LIMITS.description);
  const image = ensureAbsoluteUrl(input.image ?? SITE_SEO.defaultImage, siteUrl);

  return [
    { property: 'og:type', content: type },
    { property: 'og:title', content: title },
    { property: 'og:description', content: description },
    { property: 'og:image', content: image },
    { property: 'og:image:width', content: '1200' },
    { property: 'og:image:height', content: '630' },
    { name: 'twitter:card', content: 'summary_large_image' },
    // ... full OG + Twitter card support
  ];
}

4. JSON-LD Structured Data (src/utils/structured-data.ts)

Schema.org builders for Google rich results and AI-powered search:

Function Schema Type Use Case
buildWebSiteSchema() WebSite Homepage
buildOrganizationSchema() Organization Company pages
buildInvestmentFundSchema() InvestmentFund Fund pages
buildArticleSchema() Article/BlogPosting Memos, changelog
buildPersonSchema() Person Team members
buildBreadcrumbSchema() BreadcrumbList Navigation context

Example usage on homepage:

const schemas = [
  buildWebSiteSchema(siteUrl),
  buildInvestmentFundSchema({
    siteUrl,
    logo: `${siteUrl}/trademarks/brandMark__Dark-Matter--Light-Mode.svg`,
    focusAreas: ['Longevity Science', 'Healthspan Extension', 'Anti-Aging Therapeutics'],
  }),
];

<BoilerPlateHTML jsonLd={schemas}>

5. Dynamic OG Image Generation (/api/og)

Server-side image generation using satori + resvg:

GET /api/og?title=My+Title&description=...&category=Blog&author=Name&date=2025

Features:

  • 1200x630 PNG output (optimal for all platforms)
  • Dark Matter brand styling with indigo accent
  • Automatic text truncation (80 char title, 120 char description)
  • 24-hour cache headers
  • Inter Bold font bundled locally

Template Layout:

┌─────────────────────────────────────────────────────────────────┐
│   [Logo]                                      darkmatter.bio    │
│                                                                 │
│   ┌─────────────────────────────────────────────────────────┐  │
│   │   [CATEGORY]                                            │  │
│   │   {{TITLE}}                                             │  │
│   │   {{DESCRIPTION}}                                       │  │
│   └─────────────────────────────────────────────────────────┘  │
│                                                                 │
│   {{AUTHOR}} · {{DATE}}                     Bio Longevity Fund  │
└─────────────────────────────────────────────────────────────────┘

6. Layout Integration

Both BoilerPlateHTML.astro and BaseThemeLayout.astro updated with:

interface Props {
  title: string;
  meta?: ShareMetaInput;
  jsonLd?: object | object[];
  noIndex?: boolean;
}

const ogMeta = buildOgMeta({
  ...meta,
  url: meta.url ?? Astro.url.pathname,
});

<head>
  <link rel="canonical" href={canonical} />
  {noIndex && <meta name="robots" content="noindex, nofollow" />}
  {ogMeta.map((m) => (
    m.property
      ? <meta property={m.property} content={m.content} />
      : <meta name={m.name} content={m.content} />
  ))}
  {schemas.map((schema) => (
    <script type="application/ld+json" set:html={serializeSchema(schema)} />
  ))}
</head>

7. Astro Configuration

Added site URL and @config alias:

// astro.config.mjs
export default defineConfig({
  site: process.env.SITE_URL || 'https://matter-site.vercel.app',
  vite: {
    resolve: {
      alias: {
        '@config': path.resolve(__dirname, './src/config'),
      },
    },
  },
});

Dependencies Added

Package Version Purpose
satori ^0.18.3 Convert HTML/CSS-like structures to SVG
@resvg/resvg-js ^2.6.2 Convert SVG to PNG

Fonts Bundled

Local TTF fonts for satori (satori requires TTF/OTF, not woff2):

public/fonts/
├── Inter/           # Primary - used for OG images
├── Work_Sans/       # Alternative
├── Libre_Franklin/  # Alternative
└── Funnel_Sans/     # Alternative

Platform Optimization

The system follows messaging-first priorities:

Platform Key Considerations
WhatsApp Aggressive caching, absolute HTTPS URLs required
iMessage Large preview on iOS 17+, respects OG tags well
LinkedIn 1200x627 image, truncates at ~70 chars title
Slack Recognizes twitter:label1/data1 for extra metadata
Discord Uses theme-color meta for embed accent
Twitter/X twitter:card + fallback to OG tags
Facebook Full OG protocol support

Character limits enforced:

  • Title: 60 characters (safe for all platforms)
  • Description: 155 characters
  • Site name: 30 characters

Validation

Test with platform-specific validators:

Platform Validator URL
Facebook https://developers.facebook.com/tools/debug/
LinkedIn https://www.linkedin.com/post-inspector/
Twitter/X https://cards-dev.twitter.com/validator
Schema.org https://validator.schema.org
Google Rich Results https://search.google.com/test/rich-results

File Structure

src/
├── config/
│   └── seo.ts           # SITE_SEO, COLLECTION_DEFAULTS, types
├── utils/
│   ├── og.ts            # buildOgMeta(), buildCanonical()
│   └── structured-data.ts # JSON-LD schema builders
├── layouts/
│   ├── BoilerPlateHTML.astro  # Updated with meta/jsonLd props
│   └── BaseThemeLayout.astro  # Updated with meta/jsonLd props
└── pages/
    ├── api/
    │   └── og.ts        # Dynamic OG image endpoint
    ├── index.astro      # JSON-LD added
    ├── strategy/index.astro  # Collection defaults
    ├── thesis/index.astro    # Collection defaults
    └── portfolio/index.astro # Collection defaults

public/
├── fonts/               # TTF fonts for satori
│   └── Inter/static/Inter_24pt-Bold.ttf
└── share-banners/       # Static OG images
    ├── shareBanner__Dark-Matter-Bio_Longevity-Fund-II.webp
    └── shareBanner__Dark-Matter-Bio_Market-Maps.webp

Blueprint Reference

See context-v/Maintain-an-Elegant-Open-Graph-System.md for the complete specification including:

  • Platform-specific considerations
  • Character limits reference
  • Image fallback chain
  • Override hierarchy (Site → Collection → Page → Frontmatter)
  • Dynamic image generation patterns
  • Validation checklist

Future Enhancements

  1. Wire dynamic images to pages: Use /api/og?title=... instead of static images
  2. Template variants: Article, Profile, Product templates
  3. Frontmatter schema: Add shareImage field to content collections
  4. CI validation: Playwright tests for OG tag presence
  5. Cache busting: Query param versioning for WhatsApp

Summary

This update establishes a complete SEO foundation:

  1. Centralized Configuration: One source of truth for defaults
  2. Collection Defaults: Consistent OG metadata per page type
  3. Platform Optimization: Messaging-first approach for maximum share impact
  4. JSON-LD: Structured data for rich search results
  5. Dynamic Images: Server-side generation with GitHub-style branded templates
  6. Layout Integration: Consistent rendering across all pages

Every page now automatically gets proper OG tags, Twitter cards, canonical URLs, and optional JSON-LD structured data.