Content collections
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.
| Collection | Type | Source | Schema |
|---|---|---|---|
config | data | content/config.yaml | WebsiteConfigSchema |
pages | page | content/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): oneconfigand onepagescollection, fromcontent/config.yamlandcontent/pages/**. i18n.localesset: one pair per locale (config_da,pages_da,config_en,pages_en, ...), fromcontent/<locale>/config.yamlandcontent/<locale>/pages/**. The flat collections are not defined. A locale code with a hyphen becomes an underscore in the name (en-GBgivespages_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.
| Field | Type | Description |
|---|---|---|
name | string | { long: string, short?: string } | Site name. |
logo | string | { light: string, dark: string, class?: string, alt?: string } | One image path, or separate light and dark images. |
navigation | Array<{ 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:
| Field | Type | Description |
|---|---|---|
label | string | Section heading. |
icon | string (optional) | Section icon. |
links | Array<{ label: string, to: string, icon?: string }> | Links under the section. |
footer.right entries, rendered as buttons on the right of the footer:
| Field | Type | Description |
|---|---|---|
label | string | Link text. |
to | string | Link target. |
icon | string (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.
| Field | Type | Description |
|---|---|---|
title | string | Page title, used for the document title and og:title. |
description | string | Page description, used for meta tags. |
seo | { title?, description?, image?, noindex? } | Overrides for the document title, meta description, social image and indexing (see SEO). |
container | boolean | Wraps 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:
| Value | Order |
|---|---|
| Title | page seo.title, page title, site name (long, then short) |
| Description | page seo.description, page description, config.yaml seo.description |
| Image | page 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: trueon a page to addnoindex, nofollow. - Canonical URLs and default Open Graph tags come from
nuxt-seo-utils. - The site
urlandnamecome from thesitekey innuxt.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:alternateneeds a region, as inll_TT. A locale with a bare language such asenis left out of it.
With the locales of the Frigear website app:
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.
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:
export default defineAppConfig({
seo: {
localeAlternates: false,
},
})
Next steps
- Layouts and routing – see how a page file becomes a route.
- Composables and utils – read the config and current page in your own components.