Skip to main content

Quickstart

Go from an empty Nuxt 4 app to Markdown rendering at its own URL in about five minutes.

Start with an empty Nuxt 4 app. In about five minutes, you will render content/index.md at / and a second Markdown file at /guide.

Install

Add Ginko with the Nuxt CLI:

terminal
npx nuxi module add @lupinum/ginko-content

Or install and register it yourself:

terminal
pnpm add @lupinum/ginko-content zod
nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@lupinum/ginko-content']
})

The current package supports Nuxt 4.5.1 through Nuxt 4.x, Vue 3.5.35 through Vue 3.x, and Node.js 22.18–22.x, 24.11–24.x, or 26+.

Declare a collection

Create content.config.ts at the app root. Export the pages handle so components can import the collection instead of referring to it by a string name.

content.config.ts
import { defineCollection, defineContentConfig } from '@lupinum/ginko-content/config'
import { z } from 'zod'

export const pages = defineCollection({
  type: 'page',
  source: '**/*.md',
  schema: z.object({
    title: z.string(),
    description: z.string().optional()
  })
})

export default defineContentConfig({
  collections: { pages }
})

Add two Markdown files

content/index.md
---
title: Welcome
description: The home page for this content site.
---

# Welcome

This page comes from `content/index.md`.

[Read the guide](/guide)
content/guide.md
---
title: Guide
description: A second route backed by Markdown.
---

# Guide

Both pages belong to the same typed collection.

index.md resolves to / and guide.md resolves to /guide.

Render them at their URL

Use one catch-all route for the collection. useContentPage resolves the current URL; the page component owns the 404, metadata, and rendering policy.

app/pages/[...slug].vue
<script setup lang="ts">
import { createError, useSeoMeta } from '#imports'
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
  })
}

useSeoMeta({
  title: () => page.value?.title,
  description: () => page.value?.description
})
</script>

<template>
  <main v-if="page">
    <ContentRenderer :value="page" />
  </main>
</template>
Pass the whole document to <ContentRenderer>, not page.body. The renderer uses the document's collection, locale, and resolved references while rendering Markdown. Passing only the body drops that context.

Run it

terminal
pnpm dev

Open http://localhost:3000/ and /guide to see rendered Markdown, and /missing to see your 404.

Add another Markdown file matched by the pages collection and Ginko exposes it at the corresponding route.