Adding Frameworks

The Adapter Contract

Every adapter is a pure mapping from SEO to a framework's native metadata type:

import type { SEOAdapter, SEO, TagDescriptor } from "@better-seo/core"

interface MyFrameworkOutput { /* your framework's metadata type */ }

const myAdapter: SEOAdapter<MyFrameworkOutput> = {
  id: "my-framework",
  toFramework(seo: SEO): MyFrameworkOutput {
    return {
      title: seo.meta.title,
      meta: [
        { name: "description", content: seo.meta.description },
        // ... map all fields
      ],
      links: seo.meta.canonical ? [{ rel: "canonical", href: seo.meta.canonical }] : [],
    }
  },
}

// Register
import { registerAdapter } from "@better-seo/core"
registerAdapter("my-framework", myAdapter)

Adapter Responsibilities

  1. Map all fields from SEO to framework output — don't skip optional fields
  2. Don't re-serialize JSON-LD — use serializeJSONLD from core
  3. Stay typed — no any in adapter output type
  4. Test with golden fixtures — snapshot SEO input → expected output

Example: Remix Adapter

// @better-seo/remix
import type { SEOAdapter, SEO } from "@better-seo/core"

export const remixAdapter: SEOAdapter<{ meta: object[]; links: object[] }> = {
  id: "remix",
  toFramework(seo: SEO) {
    const meta: object[] = [
      { title: seo.meta.title },
      { name: "description", content: seo.meta.description },
      ...(seo.openGraph?.images?.map(img => ({ property: "og:image", content: img.url })) ?? []),
    ]
    const links: object[] = seo.meta.canonical
      ? [{ rel: "canonical", href: seo.meta.canonical }]
      : []
    return { meta, links }
  },
}

Then in a Remix route:

import { remixAdapter } from "@better-seo/remix"
import { createSEO } from "@better-seo/core"

export const meta = () => {
  const seo = createSEO({ title: "Home" }, config)
  return remixAdapter.toFramework(seo).meta
}

export const links = () => {
  const seo = createSEO({ title: "Home" }, config)
  return remixAdapter.toFramework(seo).links
}

Packaging

Published as @better-seo/<framework> with the framework as a peerDependency. The adapter package depends on @better-seo/core only.

{
  "name": "@better-seo/remix",
  "peerDependencies": {
    "@remix-run/node": ">= 2.0",
    "@better-seo/core": ">= 0.0.2"
  }
}

Next