Collections

Collection Architecture

How collections are defined and managed in the codebase.

Overview

Collections follow a centralized specification pattern that separates data from logic. This allows both nuxt.config.ts and content.config.ts to share the same collection definitions without circular dependencies.

File Structure

collections.ts                # Pure data — single source of truth
shared/collectionUtils.ts     # Types + transforms + utilities
shared/contentUtils.ts        # Content utility functions
content.config.ts             # Builds Nuxt Content collections
nuxt.config.ts                # Builds llms sections

collections.ts

The root collections.ts file contains only data — no imports, no types, no logic. It uses Nuxt Content's native CollectionSource structure:

export const collectionsRegistry = {
  landing: {
    source: { include: '*.md' },  // No prefix = root ('/')
    frontpage: '/',
    navigation: false
  },
  docs: {
    source: { include: 'docs/**', prefix: '/docs' },
    frontpage: '/docs/about',
    navigation: {
      title: 'Docs',
      description: 'Portal documentation.',
      icon: 'i-lucide-book-open'
    }
  },
  engineering: {
    source: {
      repository: 'https://github.com/ontopix/engineering-handbook',
      include: '**/*.md',
      prefix: '/engineering'
    },
    frontpage: '/engineering/readme',
    navigation: {
      title: 'Engineering Handbook',
      description: 'Engineering standards and guidelines.',
      icon: 'i-lucide-wrench'
    }
  },
  schemas: {
    source: {
      repository: 'https://github.com/ontopix/schemas',
      include: 'doc/**/*.md',
      prefix: '/schemas'
    },
    frontpage: '/schemas/readme',
    navigation: {
      title: 'Schemas',
      description: 'Data schemas and type definitions.',
      icon: 'i-lucide-database',
      badge: 'Draft'  // Collection-level badge
    }
  }
} as const

The as const assertion provides full type inference without explicit interfaces.

shared/collectionUtils.ts

The shared collection utilities module provides:

  • Type definitions for collection specs
  • Re-export of the typed registry
  • Badge utilities for normalizing badge props
  • Transform functions like buildLlmsSections()
  • Accessor functions for collection lookup
  • GitHub URL generators for remote collections
import { collectionsRegistry } from '../collections'
import type { CollectionSource } from '@nuxt/content'
import type { BadgeProps } from '@nuxt/ui'

// Extended source type with optional prefix (Nuxt Content native pattern)
// When prefix is omitted, content is served from root ('/')
export type CollectionSourceWithPrefix = CollectionSource & {
  prefix?: string
}

// Types
export interface NavigationConfig {
  title: string
  description?: string
  icon?: string
  badge?: Badge  // String shorthand or full BadgeProps
}

export interface CollectionSpec {
  source: CollectionSourceWithPrefix
  frontpage: string
  navigation: false | NavigationConfig
}

// Get prefix from source (helper)
export function getCollectionPrefix(spec: CollectionSpec): string {
  return spec.source.prefix ?? ''
}

// Re-export typed registry
export { collectionsRegistry }

// Transform for nuxt-llms sections
export function buildLlmsSections(spec: Record<string, CollectionSpec>) {
  return Object.entries(spec)
    .filter(([_, s]) => s.navigation !== false)
    .map(([key, s]) => {
      const nav = s.navigation as NavigationConfig
      return {
        title: nav.title,
        description: nav.description,
        contentCollection: key,
        contentFilters: [
          { field: 'path', operator: 'LIKE' as const, value: `${getCollectionPrefix(s)}/%` }
        ]
      }
    })
}

content.config.ts

Imports the registry and builds Nuxt Content collections:

import { defineContentConfig, defineCollection } from '@nuxt/content'
import { collectionsRegistry } from './shared/collectionUtils'

const collections = Object.fromEntries(
  Object.entries(collectionsRegistry).map(([key, spec]) => {
    return [key, defineCollection({
      type: 'page',
      source: spec.source,  // Pass source directly
      // ... schema definition
    })]
  })
)

export default defineContentConfig({ collections })

nuxt.config.ts

Imports the registry and transform for llms configuration:

import { collectionsRegistry, buildLlmsSections } from './shared/collectionUtils'

export default defineNuxtConfig({
  llms: {
    sections: buildLlmsSections(collectionsRegistry)
  }
})

Why This Pattern?

Problem: Circular Dependencies

Originally, collectionsRegistry was defined in content.config.ts and exported for use in nuxt.config.ts. This caused issues:

  1. nuxt.config.ts imports from content.config.ts
  2. content.config.ts imports z from zod/v4
  3. Zod schema evaluation happens before Nuxt Content's validator context is initialized
  4. Build fails with "Zod is not installed" error

Solution: Separation of Concerns

By extracting the pure data to collections.ts:

  • No circular dependencies — data file has no imports
  • Single source of truth — one place to add/edit collections
  • Type safety — types are defined in shared/collectionUtils.ts and used everywhere
  • Automatic propagation — changes flow to navigation, llms.txt, and MCP

Adding a New Collection

  1. Edit collections.ts:
export const collectionsRegistry = {
  // ... existing
  myDocs: {
    source: {
      repository: 'https://github.com/ontopix/my-repo',
      include: 'docs/**/*.md',
      prefix: '/my-docs'  // URL prefix is part of source
    },
    frontpage: '/my-docs/',
    navigation: {
      title: 'My Documentation',
      description: 'Documentation for my project.',
      icon: 'i-lucide-book',
      badge: 'Beta'  // Optional status indicator
    }
  }
} as const
  1. Run pnpm dev — the collection appears automatically in:
    • Navigation sidebar (with badge if specified)
    • Homepage grid (with badge if specified)
    • /llms.txt sections
    • MCP tool responses

No changes needed to content.config.ts or nuxt.config.ts.

Collection Properties

PropertyTypeRequiredDescription
sourceCollectionSourceWithPrefixYesNuxt Content source config with optional prefix
source.includestringYesGlob pattern for files
source.prefixstringNoURL prefix (e.g., /docs). Omit for root (/)
source.repositorystring | objectRemote onlyGitHub URL or { url, branch }
frontpagestringYesDefault landing page for the collection
navigationfalse | objectYesNavigation config or false to hide
navigation.titlestringWhen visibleDisplay name in navigation and llms
navigation.descriptionstringNoUsed in llms.txt sections
navigation.iconstringNoLucide icon identifier
navigation.badgeBadgeNoStatus badge (see Badges)

Source Configuration

Collections use Nuxt Content's native source structure with prefix as part of source:

Root Collection (No Prefix)

source: { include: '*.md' }  // Served from root ('/')

Bundled Source with Prefix

source: { include: 'docs/**', prefix: '/docs' }

Remote Source (GitHub)

source: {
  repository: 'https://github.com/ontopix/my-repo',
  include: '**/*.md',
  prefix: '/my-docs'
}

Remote with Branch/Tag

source: {
  repository: {
    url: 'https://github.com/ontopix/my-repo',
    branch: 'v2'  // or 'tag': 'v1.0.0'
  },
  include: '**/*.md',
  prefix: '/my-docs'
}

With Exclusions

source: {
  repository: 'https://github.com/ontopix/my-repo',
  include: '**/*.md',
  exclude: ['**/drafts/**', '**/internal/**'],
  prefix: '/my-docs'
}