Tip
If internationalization is not a requirement for your project, you can check out the π§± branch without Nuxt i18n.
If this is your first time building an application with Nuxt, I recommend taking a look at the π Kirby Nuxt Starterkit first to get a basic understanding of this tech stack. It is a Nuxt and KQL port of the Kirby starter kit.
This repository is a minimal but feature-rich Nuxt starter kit. It is the evolved version of the Kirby Nuxt Starterkit and my best practice solution to build a Nuxt based frontend on top of a headless Kirby CMS. The π« Cacao Kit backend is the counterpart to this frontend.
Note
If i18n is not a requirement for your project, you can check out the π§± branch without i18n.
- π Internationalization with
@nuxtjs/i18n - π Motto: βEverything is a blockβ β Kirby blocks define what to render for each page
- π£οΈ All pages are rendered by the catch-all route by default (you can still create Nuxt pages)
- π Use Kirby's page structure as the source of truth
- π« Kirby Query Language with
nuxt-kirby - π Global site data similar to Kirby's
$site - π SSR generated SEO data
- π Prettier & ESLint
- π’ Pre-configured VSCode settings
A block-first approach is one of the core design decisions for this Nuxt template. This means that you can use Kirby's page structure as the source of truth, without having to replicate it in Nuxt. All pages are rendered by the catch-all route. However, you are not obliged to stick with the block-first architecture.
If you find it unsuitable or if you require custom Kirby page blueprints with custom fields, you can always create Nuxt pages and query the content using KQL. See the pages/about.vue page for an example.
- Enable Corepack using
corepack enable - Install dependencies using
pnpm install - Adapt the relevant environment variables:
# Base URL of the Kirby backend
KIRBY_BASE_URL=
# Token for bearer authentication
# See https://github.com/johannschopplich/cacao-kit-backend#bearer-token
KIRBY_API_TOKEN=- Start the development server using
pnpm run dev - Visit localhost:3000
Build the application for production with pnpm run build.
Check out the deployment documentation.
The Cacao Kit follows a clear architectural pattern designed around its block-first approach:
app/
βββ components/
β βββ Kirby/
β βββ Block/ # Individual block components
β βββ Blocks.vue # Block renderer
β βββ Layouts.vue # Layout renderer
βββ composables/
β βββ links.ts # Internal link handling
β βββ proxy.ts # Development proxy utilities
βββ pages/
β βββ [...slug].vue # Universal page renderer
β βββ about.vue # Custom page example
βββ plugins/
β βββ site.ts # Global site data management
βββ queries/ # KQL query definitions
βββ index.ts
βββ page.ts
βββ site.ts
βββ prefetch.ts
Every page is rendered through the catch-all route [...slug].vue, which dynamically renders either:
- Layouts: Column-based content using
KirbyLayouts - Blocks: Linear content using
KirbyBlocks
<template>
<div>
<KirbyLayouts v-if="page?.layouts?.length" :layouts="page.layouts" />
<KirbyBlocks v-else-if="page?.blocks" :blocks="page.blocks" />
</div>
</template>- Create the block component in
app/components/Kirby/Block/:
<!-- app/components/Kirby/Block/MyCustomBlock.vue -->
<script setup lang="ts">
import type { KirbyBlock } from '#nuxt-kirby'
defineProps<{
block: KirbyBlock<'my-custom-block'>
}>()
</script>
<template>
<section class="my-custom-block">
<h2>{{ block.content.title }}</h2>
<div v-html="block.content.text" />
</section>
</template>- Register the block in
app/components/Kirby/Blocks.vue:
import { LazyKirbyBlockMyCustomBlock } from '#components'
const blockComponents: Record<string, Component> = {
// Custom blocks
'my-custom-block': LazyKirbyBlockMyCustomBlock,
}Define reusable queries in the queries/ directory:
// app/queries/blog.ts
import type { KirbyQuerySchema } from 'kirby-types'
export const blogQuery: KirbyQuerySchema = {
query: 'page("blog")',
select: {
title: true,
children: {
query: 'page.children.listed',
select: {
title: true,
date: true,
excerpt: 'page.text.excerpt(300)',
cover: {
query: 'page.cover.toFile?.resize(600)',
select: ['url', 'alt'],
},
},
},
},
}Use them in components:
<script setup lang="ts">
import { blogQuery } from '~/queries/blog'
const { locale } = useI18n()
const { data } = await useKql(blogQuery, {
language: locale.value,
})
</script>The kit includes full i18n support with @nuxtjs/i18n.
This kit uses semantic HTML with minimal styling via new.css for demonstration. To implement your own styling, remove the import in app.vue and add your custom styles.
For maximum performance and CDN compatibility, generate a static site:
pnpm run generateThis creates a fully static version in the dist/ directory that can be hosted on any static hosting service.
Deploy with full SSR capabilities:
pnpm run buildEnsure these environment variables are set in production:
# Required: Your Kirby backend URL
KIRBY_BASE_URL=https://your-kirby-backend.com
# Required: Authentication token for KQL queries
KIRBY_API_TOKEN=your-secret-token
# Optional: Public site URL for SEO and social sharing
NUXT_PUBLIC_SITE_URL=https://your-frontend.com- Production site: cacao-kit.byjohann.dev
- getkirby.com β Get to know the CMS.
- Try it β Take a test ride with our online demo. Or download one of our kits to get started.
- Documentation β Read the official guide, reference and cookbook recipes.
- Issues β Report bugs and other problems.
- Feedback β You have an idea for Kirby? Share it.
- Forum β Whenever you get stuck, don't hesitate to reach out for questions and support.
- Discord β Hang out and meet the community.
- YouTube - Watch the latest video tutorials visually with Bastian.
- Mastodon β Spread the word.
- Instagram β Share your creations: #madewithkirby.
MIT License Β© 2023-PRESENT Johann Schopplich
