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 |
|---|---|
| Aggressive caching, absolute HTTPS URLs required | |
| iMessage | Large preview on iOS 17+, respects OG tags well |
| 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 |
| 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 |
|---|---|
| https://developers.facebook.com/tools/debug/ | |
| 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
- Wire dynamic images to pages: Use
/api/og?title=...instead of static images - Template variants: Article, Profile, Product templates
- Frontmatter schema: Add
shareImagefield to content collections - CI validation: Playwright tests for OG tag presence
- Cache busting: Query param versioning for WhatsApp
Summary
This update establishes a complete SEO foundation:
- Centralized Configuration: One source of truth for defaults
- Collection Defaults: Consistent OG metadata per page type
- Platform Optimization: Messaging-first approach for maximum share impact
- JSON-LD: Structured data for rich search results
- Dynamic Images: Server-side generation with GitHub-style branded templates
- 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.