Skip to main content

Ginko version upgrades

Apply Ginko's version-specific hard cuts, remove superseded APIs, and verify the exact installed or packed artifact.

Ginko may make hard public-contract cutovers before 1.0. Upgrade directly to the documented end state; do not preserve old and new paths in parallel.

From 0.3 to 0.4

The 0.4.0 release candidates use npm's next channel. Certify the exact prerelease before adopting it; stable 0.4.0 follows the RC soak.

bash
pnpm add @lupinum/ginko-content@0.4.0-rc.2

1. Make the Markdown profile explicit

content.markdown.plugins now has a literal empty default. Nothing is enabled implicitly, and omitting the option is identical to plugins: []. Add only the syntax the application owns:

ts
export default defineNuxtConfig({
  content: {
    markdown: {
      plugins: [
        'toc',
        'shiki',
        'summary',
      ],
    },
  },
})

The zero-option shiki entry uses Ginko's bundled light/dark Material themes. Replace the deprecated highlight plugin name with shiki. highlight remains a warning-only alias during 0.4.x, but configuring both names is an error. Custom registrations use imported Shiki objects under themes: { light, dark } and languages; replace the invalid singular theme and langs spellings, which now fail module setup instead of falling back silently. Add Shiki directly to the application when its config imports custom registration objects.

Ginko and @comark/vue now use the matched Comark 0.6.2 line. Custom Comark plugin modules still run only in the build/server filesystem profile, and their normalized AST must satisfy Ginko's closed inert render policy. Parser support does not authorize Vue directives, bindings, event handlers, arbitrary active HTML, or undeclared component props and slots.

2. Treat inline Markdown as its own safe API

ContentRendererInline intentionally uses a fixed client-safe profile for SSR, hydration, and reactive updates. It supports standard Markdown, tables, task lists, and normalized GFM alerts. It discards comments and does not inherit build-time plugins, Shiki, footnotes, Math, Mermaid, custom MDC components, or markdown.tags mappings.

Use ContentRenderer with an ingested document when output needs configured plugins or application components. The inline components prop may replace a renderer for an already authorized tag; it cannot authorize new syntax or expand the safety policy.

3. Install optional render integrations deliberately

Math and Mermaid are emitted through build-generated literal imports so they work in production browser chunks and packed consumers. Install only the peer for the enabled plugin:

bash
pnpm add katex
pnpm add beautiful-mermaid

Math also needs its stylesheet in Nuxt:

ts
export default defineNuxtConfig({
  css: ['katex/dist/katex.min.css'],
  content: {
    markdown: {
      plugins: ['math'],
    },
  },
})

Do not add Comark-specific build.transpile, Vite ssr.noExternal, or Nitro externals.inline overrides. Ginko and Comark package their ESM boundaries without those blanket settings.

4. Use the universal Nuxt 404 recipe

Catch-all content pages must handle both direct requests and reused-page client navigation. Key the page by route path and throw after the awaited composable; do not guard the missing-page check with import.meta.server:

vue
<script setup lang="ts">
import { pages } from '~~/content.config'

definePageMeta({ key: route => route.path })

const { page } = await useContentPage(pages)

if (!page.value) {
  throw createError({
    statusCode: 404,
    statusMessage: 'Page not found',
    fatal: true,
  })
}
</script>

<template>
  <ContentRenderer v-if="page" :value="page" />
</template>

ContentRenderer now forwards inherited Vue attributes exactly once, including its declared class prop. Remove wrapper workarounds that existed only because the renderer dropped that class.

5. Keep server configuration private

The public runtime projection now contains only client-owned query, search, locale, render, and integrity facts. Code must not read provider module specifiers, filesystem source/exclude patterns, CMS settings, agent definitions, schema inventories, or other server/build metadata from runtimeConfig.public.content. Server integrations read the private runtimeConfig.content boundary or use a documented package API.

This is a removal of accidental payload exposure, not a new compatibility bridge. Do not copy the removed server objects into another public config key.

6. Rebuild generated output

Canonical Markdown normalization now closes over Comark task lists, table alignment, fenced-code metadata, named component slots, GFM alerts, and comment tuples. Comments never enter renderer, search, summary, portable, or agent text. Typed YAML values on MDC components now survive portable asset rewrite and reparse.

The content cache format changed. Delete deployment/build caches and rebuild content snapshots and search indexes after upgrading. Portable document format v1 and the CMS wire format are unchanged; no portable-file migration is required.

7. Verify the exact candidate

Run application type checks, production build/static generation, and browser navigation tests against the installed prerelease. Exercise every enabled optional Markdown plugin in a production browser, not only in Node SSR. Provider, data-source, and portability integrations should run their exported conformance contracts against the same packed tarball.

From 0.2 to 0.3

Upgrade directly from 0.2.1 to 0.3.0; there is no intermediate stable release.

1. Update the runtime

Ginko Content now requires Node.js 22.18–22.x, 24.11–24.x, or 26+. Update development, CI, build, and production runtimes together before changing the package version.

Install the stable release:

bash
pnpm add @lupinum/ginko-content@0.3.0

Pin the exact version when certifying a deployment, provider, or CMS consumer.

2. Replace deleted composables

The app-facing composable surface is intentionally small:

  • useContentPage owns route-page loading and optional surroundings.
  • useContentSearch owns headless search state and results.

Replace the deleted list, navigation, pagination, backlink, surroundings, and direct-read composables with the equivalent one-shot async query function from @lupinum/ginko-content/client, paired with Nuxt's useAsyncData:

ts
import { many, navigation } from '@lupinum/ginko-content/client'
import { docs } from '~~/content.config'

const { data: pages } = await useAsyncData(
  'docs:list',
  () => many(docs, { sort: { order: 'asc' } }),
)

const { data: tree } = await useAsyncData(
  'docs:navigation',
  () => navigation(docs),
)

The current helpers and their return types are in the composable API reference.

3. Read canonical route facts

Documents now expose route and localization facts through route and resolution. Replace reads of the former top-level path, localePaths, and variants fields with the corresponding canonical facts. Do not reconstruct localized public paths in consumer UI or provider adapters.

Application query semantics in 0.3.0

0.3.0 makes the path vocabulary in application queries canonical: mount-agnostic and locale-prefix-free. Paths remain locale-scoped when translated slugs are enabled. Both by: { path } and where: { path } now exclude the collection's route mount. Remove the mount from every such value:

ts
// content.config.ts
export const docs = defineCollection({
  type: 'page',
  source: '*/1.*/**/*.md',
  i18n: true,
  route: { en: '/guide', de: '/leitfaden' },
})
ts
// 0.3.0-rc.4
await one(docs, { by: { path: '/guide/getting-started' } })
await one(docs, { by: { path: '/leitfaden/erste-schritte' }, locale: 'de' })
await many(docs, { where: { path: { $prefix: '/guide/deep' } } })

// 0.3.0
await one(docs, { by: { path: '/getting-started' } })
await one(docs, { by: { path: '/erste-schritte' }, locale: 'de' })
await many(docs, { where: { path: { $prefix: '/deep' } } })

A collection's index document is canonical / in every locale, not the mounted directory name.

Stale mounted values do not raise an error. They select no document. one() returns null, and many() returns []. Offset pagination returns an empty page with total: 0. Cursor pagination returns an empty page without a total field. In development and during prerender, a by: { path } miss on a value shaped like the collection mount logs an advisory hint naming the canonical form; production is silent.

by: { route } is unchanged. It still takes the public application URL including the collection mount and the locale prefix, so by: { route: '/de/leitfaden/erste-schritte' } keeps working exactly as before. Prefer it whenever you already hold a browser URL.

Filesystem canonicalKey values follow the same rule and are generated after removing the configured source mount, so a key such as 1/1 becomes 1. Rebuild stored snapshots and any prerelease fixtures that persisted filesystem-generated keys.

4. Migrate providers and data sources

Existing H3-facing providers remain behind @lupinum/ginko-content/provider. Backend-neutral CMS or remote sources can implement the bounded ContentDataSource contract from @lupinum/ginko-content/data-source; the Ginko binder owns timeouts, bounds, validation, cache hints, and public projection.

Run the exported provider or data-source conformance contract against the real adapter fixtures before release. Capabilities remain runtime truth: advertise only operators and pagination modes the adapter implements.

Provider wire v3 in 0.3.0-rc.4

0.3.0-rc.4 hard-cuts the prerelease provider query wire to v3. Providers must recompile against the rc.4 types; Ginko does not negotiate or dispatch v2.

  • Advertise and execute $nin directly. It is no longer represented as a structural negation of $in.
  • Return complete raw ProviderDocumentInput identity and route facts even when the query carries only or without. Core validates the raw document before applying the public projection.
  • Return { result: undefined } for a missing in-process first result. { result: null } is invalid at the provider seam; the public application and HTTP boundary maps a missing single result to { result: null }. The HTTP response is always 200 application/json with the full envelope; an empty body is an invalid transport response rather than a missing result.
  • Use the closed, non-generic provider query and site-data envelopes and closed capability declarations.
  • Return { data, updatedAt? } from provider siteData() implementations. Remove provider response echoes of key and locale; Ginko derives them from the request. Data sources continue to echo both values for binder validation.
  • Read ordinary slice pagination from top-level plan.skip and plan.limit. Explicit offset/cursor pagination is carried separately in plan.paging.
  • Route/reference queries retain raw plan.resolveVariant and also carry the closed provider selector in plan.variantSelector.
  • Preserve provider order when no sort is supplied and reject unsupported or non-JSON query shapes, including unsafe regular-expression flags.

There is no compatibility adapter. Delete v2-specific provider fixtures and update conformance probes so each advertised operator matches a nonempty exact subset rather than merely returning successfully.

Provider wire v4 in 0.3.0

0.3.0 hard-cuts provider wire v3 to v4. There is no v3 dispatcher or compatibility adapter.

  • Import ContentProviderQueryPlan instead of ContentQueryPlan.
  • Read all pagination from plan.pagination; top-level skip/limit and plan.paging are removed.
  • Pattern-match plan.variant.by. Path selectors carry by: 'path'; route selectors carry ordered mounted candidates; reference selectors carry the resolved locale chain. Raw resolveVariant and variantSelector fields no longer cross the provider boundary.
  • Use toContentProviderQuery() only for context-free filters, explicit fallback chains, and mounted providerPath selectors. Application path/route/reference requests are closed by runtime dispatch because the public helper has no collection route policy.
  • Return canonicalKey on every provider document. Core no longer derives canonical identity from the mounted contentPath.
  • Read navigation locale and fallback only from query.plan.resolveLocale; the separate navigation options argument is removed.
  • Treat every provider contentPath as collection-mounted and locale-specific, but never application-locale-prefixed.
  • Stop importing longestMountForPath from the CMS contract. Route lowering validates the resolved locale's configured mount and no longer guesses a mount from another locale.
  • Localized route objects must contain exactly one entry for every configured collection locale and no unknown keys. Use a route string when every locale shares one mount. Unlocalized collections use a string or { default: ... }. Every mount must be a leading-slash, site-relative path without traversal, query, fragment, or URL syntax.
  • getCollectionPath() remains a pure collection-handle helper and cannot see module-level locale policy inherited through i18n: true. Use the explicit collection-local i18n: { locales, defaultLocale } form for collections passed to this helper.
  • Filesystem canonicalKey values are now generated after removing the configured source mount, matching the mount-agnostic internal path. This is the same rule application queries follow; see "Application query semantics in 0.3.0" above.
  • Navigation queries require a named collection. .navigation.yml files stay collection-neutral during ingestion and are joined to actual collection pages while building navigation.
  • Provider wire plans and graph-executor plans are separate phases. Providers continue importing only ContentProviderQueryPlan; no internal canonical plan type is public.

Every v4 query must survive an exact JSON round trip. Recompile providers against 0.3.0 and update conformance fixtures to construct already-closed route/reference selectors.

Reference metadata in 0.3.0

CONTENT_REFERENCE_PREFIX and description-based reference detection are removed. A schema manually authored as .describe("__nuxt_content_ref__:authors") is now an ordinary human description. Replace it with reference('authors') or withContentReferenceMetadata(schema, 'authors').

CONTENT_REFERENCE_METADATA_KEY also changes value, from "__nuxt_content_ref__:" to "ginko:contentReference". This only affects schemas that wrote the metadata key literally; reference() and withContentReferenceMetadata() are unaffected. Descriptions never carry reference semantics under either spelling.

Public query vocabulary in 0.3.0-rc.4

  • Replace a bare locale fallback such as fallback: 'en' with fallback: ['en']. Keep 'default' when the collection default is intended.
  • Replace field-level negation such as { status: { $not: { $eq: 'draft' } } } with logical negation: { $not: { status: { $eq: 'draft' } } }.
  • Replace public numeric sort directions with asc and desc. Numeric directions remain an internal provider-wire detail.
  • Import LocalizedContentDocument instead of the removed LocalizedDoc alias. Use ContentCollectionName or ContentCollectionTarget instead of the removed ContentCollectionStringName alias.

5. Replace the removed CMS importer

@lupinum/ginko-content/cms-import is removed without a compatibility shim. It duplicated mapping decisions that now belong to the portable contract.

  • Use @lupinum/ginko-content/portability for portable documents, codecs, validation, references, assets, manifests, and semantic comparison.
  • Use @lupinum/ginko-content/portability/node for bounded directory reads and writes.
  • Keep authorization, workflow, transactions, receipts, and cleanup in Ginko CMS or another consumer-owned adapter.

Certify the CMS integration against the exact candidate tarball and record its SHA-256. A source-workspace import does not prove package compatibility.

6. Rebuild search and enable validation deliberately

Rebuild generated search output after upgrading. Pagefind now emits a locale manifest and locale-specific indexes; MiniSearch uses explicit immutable index ownership. Both return plain-text excerpts so highlighting remains consumer-owned.

Content validation defaults to report-only:

ts
export default defineNuxtConfig({
  content: {
    validation: 'report',
  },
})

Inspect the generated report with pnpm exec ginko-content validate. Switch to validation: 'error' only after missing links, anchors, route references, and assets are resolved; strict mode prevents publishing an invalid snapshot.

7. Build Windows release artifacts outside Windows

With Nuxt 4.4.7–4.4.8, a production build can fail during Nitro prerendering on Windows because Nuxt's cache-driver file URL is emitted as a raw drive-letter import. This is a build-host limitation, not a content portability or production runtime limitation. Build release artifacts on Linux or macOS until the upstream Nuxt/Nitro path handling is corrected.

8. Verify the exact package

Before adopting the stable release:

  1. run application unit, type, build, static-generation, and browser checks;
  2. run provider/data-source and portability conformance tests;
  3. run Ginko CMS against the exact packed tarball;
  4. verify localized routes, selected-locale search, and all-language search;
  5. confirm no code imports the removed cms-import subpath or deleted composables.

Adopt 0.3.0 only after the exact tarball passes every required consumer.