Skip to main content

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'
ColumnTypeDescription
iduuidPrimary key
titletextPage title (required)
descriptiontextMeta description (required)
keywordstextMeta keywords (required)
assetIduuidFK 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>