Base Layer

Content collections

Write the site config and pages as content files, localize them, and control SEO and hreflang tags.

This page shows the content files a site built on the base layer reads, and how they become pages, SEO tags and language links. It is for app developers. Set up the layer first: Overview.

Collections

The layer defines two Nuxt Content collections in its content.config.ts. Their files live in your app's content/ folder.

CollectionTypeSourceSchema
configdatacontent/config.yamlWebsiteConfigSchema
pagespagecontent/pages/** (Markdown)WebsitePageSchema

Localized collections

Your app's i18n.locales decides, once at build time, whether the collections are flat or one per locale. The two kinds cannot be mixed.

  • No i18n.locales (default): one config and one pages collection, from content/config.yaml and content/pages/**.
  • i18n.locales set: one pair per locale (config_da, pages_da, config_en, pages_en, ...), from content/<locale>/config.yaml and content/<locale>/pages/**. The flat collections are not defined. A locale code with a hyphen becomes an underscore in the name (en-GB gives pages_en_GB).
export default defineNuxtConfig({
  extends: ['@frigear-nu/base'],
  i18n: {
    locales: [
      { code: 'da', name: 'Danish' },
      { code: 'en', name: 'English' },
    ],
    defaultLocale: 'en',
  },
})

useConfig() and useCurrentPage() pick the right collection through useContentLocale(), so app code does not branch on whether locales are configured.

config schema

content/config.yaml follows WebsiteConfigSchema. Every field is optional, so an app can start with an empty file.

FieldTypeDescription
namestring | { long: string, short?: string }Site name.
logostring | { light: string, dark: string, class?: string, alt?: string }One image path, or separate light and dark images.
navigationArray<{ label: string, description?: string, to: string, icon?: string, tags?: string[] }>Navigation entries. useNavigation(tags?) filters by tags.
footer{ sections?, copyright?, right? }Footer content (tables below).
seo{ titleTemplate?, description?, image? }Site-wide SEO defaults (see SEO). The layer ships none.

footer.sections entries:

FieldTypeDescription
labelstringSection heading.
iconstring (optional)Section icon.
linksArray<{ label: string, to: string, icon?: string }>Links under the section.

footer.right entries, rendered as buttons on the right of the footer:

FieldTypeDescription
labelstringLink text.
tostringLink target.
iconstring (optional)Link icon.
variant'link' | 'solid' | 'outline' | 'soft' | 'subtle' | 'ghost' (optional)Button variant.
size'xs' | 'sm' | 'md' | 'lg' | 'xl' (optional)Button size.

footer.copyright is a string. In it, {fullYear} becomes the current year and {url} the request URL. When it is omitted the footer has no left part.

pages schema

The front matter of each page follows WebsitePageSchema. Every field is optional.

FieldTypeDescription
titlestringPage title, used for the document title and og:title.
descriptionstringPage description, used for meta tags.
seo{ title?, description?, image?, noindex? }Overrides for the document title, meta description, social image and indexing (see SEO).
containerbooleanWraps the rendered body in a UContainer. Use it for plain prose pages such as legal; pages built from u-page-* sections manage their own containers.
pageHeader{ title?: string, description?: string }Props for the page header component.

SEO

The layer's app.vue calls usePageSeo(), which sets the title, description, Open Graph and Twitter tags of each page. The layer sets no values of its own, so with nothing configured the title is the page title, with no suffix.

Each value resolves in this order:

ValueOrder
Titlepage seo.title, page title, site name (long, then short)
Descriptionpage seo.description, page description, config.yaml seo.description
Imagepage seo.image, config.yaml seo.image

Set a title template in config.yaml. %s is the page title and %siteName is site.name from nuxt.config.ts:

seo:
  titleTemplate: '%s | %siteName'
  description: Default description for pages that have none.

A title that equals the site name is not wrapped, so the home page does not read "Frigear.nu | Frigear.nu".

  • Set seo.noindex: true on a page to add noindex, nofollow.
  • Canonical URLs and default Open Graph tags come from nuxt-seo-utils.
  • The site url and name come from the site key in nuxt.config.ts.

Languages and hreflang

On a site with more than one locale, app.vue also calls useLocaleAlternates(). It adds a <link rel="alternate" hreflang> for each locale and for x-default (the default locale). It also adds an og:locale:alternate for each other locale. A single-locale site gets none of these.

The tags come from useLocaleHead() of @nuxtjs/i18n. The layer keeps only the alternates, because nuxt-seo-utils already sets the canonical URL, og:url, og:locale and <html lang>.

  • Each locale's language, a BCP 47 tag, decides its hreflang. A locale without one gets no alternate.
  • og:locale:alternate needs a region, as in ll_TT. A locale with a bare language such as en is left out of it.

With the locales of the Frigear website app:

nuxt.config.ts
export default defineNuxtConfig({
  i18n: {
    defaultLocale: 'da',
    locales: [
      { code: 'da', language: 'da-DK', name: 'Dansk', file: 'da.json' },
      { code: 'en', language: 'en-GB', name: 'English', file: 'en.json' },
    ],
  },
})

every page lists da, da-DK, en, en-GB and x-default alternates. On the home page x-default, da and da-DK point to /, and en and en-GB to /en.

Links are absolute, resolved like the canonical URL: from site.url when it is set, otherwise from the origin of the request.

A prerendered route has no request, so it needs site.url at build time. Without it the layer skips the alternate links and warns once, because search engines ignore relative hreflang URLs.

Turn the tags off when your app writes its own hreflang tags or turns on i18n's experimental.strictSeo:

app.config.ts
export default defineAppConfig({
  seo: {
    localeAlternates: false,
  },
})

Next steps

Copyright © 2026