MonoStack
Concepts

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

Schemas derived from Drizzle tables with drizzle-zod:

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>

On this page