SEO
Structured SEO metadata with Drizzle schema and form integration for meta tags and Open Graph.
Database schema
import { seoTable, seoRelations } from '@repo/cms/database'
| Column | Type | Description |
|---|---|---|
id | uuid | Primary key |
title | text | Page title (required) |
description | text | Meta description (required) |
keywords | text | Meta keywords (required) |
assetId | uuid | FK to assetsTable for Open Graph image |
The seoRelations defines a one-to-one relation to assetsTable for the OG image.
Zod schemas
Auto-generated schemas:
import {
seoTableInsertSchema,
seoTableSelectSchema,
seoTableUpdateSchema,
} from '@repo/cms'
Form schema
getSeoInsertSchema returns a form-ready schema with an asset picker for the OG image:
import { getSeoInsertSchema } from '@repo/cms'
const seoSchema = getSeoInsertSchema({
assetToUrl: (id) => `/api/assets/${id}`,
assetDataKey: 'asset',
})
This picks title, description, keywords from the insert schema and wraps assetId with a LinkedModel.Root component for selecting an existing asset.
Usage in a page schema
Combine SEO with page content in a single form:
import { RichTextEditor } from '@repo/cms'
import { zaf } from '@repo/form'
import { z } from 'zod'
const seoSchema = getSeoInsertSchema({ assetToUrl, assetDataKey: 'asset' })
const pageSchema = z.object({
title: z.string(),
slug: z.string(),
seo: seoSchema,
blocks: zaf(z.array(z.any()), {
component: FormField.renderComponent(BlockField.Root, { blocks: pageBlocks }),
}),
})
Rendering meta tags
Use the SEO data in <svelte:head>:
<script lang="ts">
type Props = { seo: { title: string; description: string; keywords: string; assetId?: string } }
const { seo }: Props = $props()
</script>
<svelte:head>
<title>{seo.title}</title>
<meta name="description" content={seo.description} />
<meta name="keywords" content={seo.keywords} />
{#if seo.assetId}
<meta property="og:image" content={`/api/assets/${seo.assetId}/url`} />
{/if}
</svelte:head>