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.

plugins/blog/src/content/article.ts
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

plugins/blog/src/routes.ts
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

plugins/blog/src/pages/article-page.tsx
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 ArticlePage

Keep 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

OptionTypeDefaultDescription
enabledtrueOpts into the delivery layer. Requires publicApi: { enabled: true }.
redirects.enabledbooleanfalseStores historical public slugs in core_content_slug_history and serves 301 redirects when slugs change. Requires editorial: { enabled: true }.
seo.titleFieldstringExposed field name used to populate page titles.
seo.fallbackTitleFieldstringFallback field name if titleField is empty.
seo.descriptionFieldstringExposed field name used for page meta description.
seo.fallbackDescriptionFieldstringFallback field name if descriptionField is empty.
seo.noIndexFieldstringShared boolean field that excludes record from sitemap and marks robots: { index: false }.
seo.openGraph{ titleField?, descriptionField? }Optional Open Graph title and description fields.
sitemap.enabledbooleanfalseIncludes published records in the XML sitemap.
sitemap.changeFrequency"always" | "hourly" | "daily" | "weekly" | "monthly" | "yearly" | "never"Sitemap <changefreq> hint.
sitemap.prioritynumberSitemap 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.

Learn More