Content Engine
Content Delivery and SEO
Deliver Content Engine records from a plugin with canonical metadata, slug redirects, hreflang, and XML sitemap support.
Content delivery starts in the plugin that owns the content type. Opt into the public API first, then let the same plugin claim the page URL. Search engines get a stable story; future you gets fewer scattered files.
Enable public delivery on the content type
publicApi explicitly chooses exposed fields. Delivery then projects only those fields into metadata, slug redirect history, and sitemap entries.
import { defineContentType, field } from "@vitnode/core/content"
export const articleContentType = defineContentType({
id: "blog.article",
tableName: "blog_articles",
publication: { enabled: true },
editorial: { enabled: true },
fields: {
excerpt: field.textarea({ nullable: true }),
slug: field.slug({ source: "title" }),
title: field.text({ required: true }),
},
publicApi: {
enabled: true,
fields: ["id", "title", "slug", "excerpt", "publishedAt"],
path: "articles",
},
delivery: {
enabled: true,
redirects: { enabled: true },
seo: {
descriptionField: "excerpt",
titleField: "title",
},
sitemap: { enabled: true, changeFrequency: "weekly", priority: 0.8 },
},
})All fields referenced in delivery.seo must exist in publicApi.fields.
Claim the public URL in the plugin
import { definePluginRoutes, lazy, page } from "@vitnode/core/routing"
export const routes = definePluginRoutes([
page("/articles/:slug", {
component: lazy(() => import("./pages/article-page")),
}),
])Render data and metadata from the plugin route
import type { PluginRoutePageProps } from "@vitnode/core/routing"
import { definePluginRoute } from "@vitnode/core/routing"
interface Article {
excerpt: string | null
title: string
}
export const route = definePluginRoute({
load: async ({ params }) => await fetchArticle(params.slug),
head: ({ loaderData }) => ({
description: loaderData?.excerpt,
title: loaderData?.title,
}),
})
const ArticlePage = ({ loaderData }: PluginRoutePageProps<Article>) => (
<article className="container mx-auto max-w-3xl p-4">
<h2 className="text-3xl font-semibold">{loaderData.title}</h2>
<p>{loaderData.excerpt}</p>
</article>
)
export default ArticlePageKeep delivery with its plugin
Do not recreate the article page in the host app. The content type, slug rules, public API, and public route evolve together, so they belong together.
Delivery Configuration Reference
| Option | Type | Default | Description |
|---|---|---|---|
enabled | true | — | Opts into the delivery layer. Requires publicApi: { enabled: true }. |
redirects.enabled | boolean | false | Stores historical public slugs in core_content_slug_history and serves 301 redirects when slugs change. Requires editorial: { enabled: true }. |
seo.titleField | string | — | Exposed field name used to populate page titles. |
seo.fallbackTitleField | string | — | Fallback field name if titleField is empty. |
seo.descriptionField | string | — | Exposed field name used for page meta description. |
seo.fallbackDescriptionField | string | — | Fallback field name if descriptionField is empty. |
seo.noIndexField | string | — | Shared boolean field that excludes record from sitemap and marks robots: { index: false }. |
seo.openGraph | { titleField?, descriptionField? } | — | Optional Open Graph title and description fields. |
sitemap.enabled | boolean | false | Includes published records in the XML sitemap. |
sitemap.changeFrequency | "always" | "hourly" | "daily" | "weekly" | "monthly" | "yearly" | "never" | — | Sitemap <changefreq> hint. |
sitemap.priority | number | — | Sitemap priority rating from 0.0 to 1.0. |
hreflang.xDefault | "defaultLocale" | — | Emits x-default alternate link pointing to the default locale URL for localized content. |