nuxt-x-blog
nuxt-x-blog
Blog layer for Nuxt 4. Powered by Nuxt Content v3 — provides 9 auto-imported XBL-prefixed components, a useBlog composable, and 2 pre-built pages. The consuming app writes Markdown posts in content/blog/; the layer handles all UI, routing, pagination, tag filtering, and related-posts logic.
What the consumer writes
The layer ships all UI, routing, and data access. A minimal consumer provides four things: the package, an extends entry, a content.config.ts defining the blog collection, and Markdown posts.
1. Install the layer and its peers (nuxt, @nuxt/ui, and @nuxt/content v3 are peer dependencies):
npm install @xenterprises/nuxt-x-blog nuxt @nuxt/ui @nuxt/content
2. Extend the layer in nuxt.config.ts:
// nuxt.config.ts
export default defineNuxtConfig({
extends: [['@xenterprises/nuxt-x-blog', { install: true }]],
})
3. Define the blog collection. The collection is owned by the consumer, not the layer — create content.config.ts at the project root. This location is load-bearing: Nuxt Content v3 resolves a collection's source against the rootDir of the project whose content.config.ts defines it, so defining the collection in your own app binds blog/*.md to your own content/blog/ directory:
// content.config.ts
import { defineContentConfig, defineCollection } from '@nuxt/content'
import { z } from 'zod'
export default defineContentConfig({
collections: {
blog: defineCollection({
type: 'page',
source: 'blog/*.md',
schema: z.object({
title: z.string(),
description: z.string().optional(),
date: z.date(),
author: z.string().optional(),
image: z.string().optional(),
tags: z.array(z.string()).optional(),
published: z.boolean().default(true),
readingTime: z.number().optional(),
}),
}),
},
})
4. Write posts in content/blog/*.md (frontmatter below), and optionally override defaults in app/app.config.ts — note it must live in the app/ directory, not the project root:
// app/app.config.ts
export default defineAppConfig({
xBlog: {
title: 'Blog',
description: 'Latest articles and updates',
postsPerPage: 9,
dateFormat: 'MMM d, yyyy',
showAuthor: true,
showReadingTime: true,
showTags: true,
showTableOfContents: true,
showShareButtons: true,
},
})
No environment variables are required. /blog (listing) and /blog/[...slug] (post) work out of the box; the layer's / page 301-redirects to /blog. Every default route can be disabled via xBlog.pages (home / list / post — disabled routes return 404) or replaced with your own page at the same path (standard Nuxt page overriding).
Content Structure
Create blog posts in content/blog/:
content/
blog/
my-first-post.md
getting-started.md
Post Frontmatter
---
title: My First Post
description: A brief description of the post
date: 2025-01-15
author: Jane Doe
image: /images/blog/cover.jpg
tags:
- nuxt
- vue
published: true
---
Your post content here...
| Field | Type | Required | Description |
|---|---|---|---|
title | string | Yes | Post title |
description | string | No | Short excerpt |
date | string | Yes | ISO date string (used for sorting) |
author | string | No | Author display name |
image | string | No | Cover image URL |
tags | string | No | Tag list for filtering |
published | boolean | No | Set false to draft (default: true) |
What the Layer Provides
Components (auto-imported, XBL prefix):
XBLPostCard— Blog post preview card with image, title, date, tags, and reading timeXBLPostList— Responsive grid ofXBLPostCarditemsXBLPostHeader— Post page header with title, meta, author, and cover imageXBLPagination— Page navigation controlsXBLSearchInput—v-modelsearch input for filtering posts by title/descriptionXBLTagList— Tag filter list with countsXBLRecentPosts— Sidebar widget showing the most recent postsXBLTableOfContents— Table of contents for post pages, frombody.toc.linksXBLShareButtons— Social share buttons (X/Twitter, LinkedIn, Facebook) plus copy-link
Composable:
useBlog()— All blog data access methods backed byqueryCollection('blog')
Pages:
/— 301 redirect to/blog/blog— Blog listing page with search, tag filtering, and pagination/blog/[...slug]— Individual post page with TOC, share buttons, and related posts
App Config Options
Configure under the xBlog key in app/app.config.ts (must live in the consumer's app/ directory, not the project root):
| Option | Type | Default | Description |
|---|---|---|---|
title | string | 'Blog' | Blog section title |
description | string | 'Latest articles and updates' | Blog section description |
postsPerPage | number | 9 | Posts per page for pagination |
dateFormat | string | 'MMM d, yyyy' | Display date format. Note: currently reserved — the shipped formatDate() renders a fixed localized short date (toLocaleDateString('en-US')) and does not read this key yet |
showAuthor | boolean | true | Show author on post cards and pages |
showReadingTime | boolean | true | Show estimated reading time |
showTags | boolean | true | Show tags on post cards and pages |
showTableOfContents | boolean | true | Show TOC on post pages |
showShareButtons | boolean | true | Show social share buttons on post pages |
pages.home | boolean | true | Enable the / → /blog redirect (false → 404) |
pages.list | boolean | true | Enable the /blog listing page (false → 404) |
pages.post | boolean | true | Enable the /blog/[...slug] post page (false → 404) |
useBlog() Composable
const {
config, // BlogConfig from app.config.ts
getPosts, // (options?) => Promise<{ posts, total, page, totalPages, hasMore }>
getPostByPath, // (path) => Promise<BlogPost | null>
getAllTags, // () => Promise<{ tag, count }[]>
getRecentPosts, // (limit?) => Promise<BlogPost[]>
getRelatedPosts, // (post, limit?) => Promise<BlogPost[]>
formatDate, // (dateStr) => string
estimateReadingTime, // (text) => number — minutes
} = useBlog()
getPosts(options?)
| Option | Type | Default | Description |
|---|---|---|---|
page | number | 1 | Page number |
tag | string | — | Filter by tag |
limit | number | postsPerPage | Posts per page override |
Returns: { posts: BlogPost[], total: number, page: number, totalPages: number, hasMore: boolean }
BlogPost Type
interface BlogPost {
id: string
path: string
title: string
description?: string
date: string
author?: string
image?: string
tags?: string[]
published?: boolean
readingTime?: number
body?: unknown
}
Minimal Usage Example
<script setup>
const { getPosts } = useBlog()
const { posts } = await getPosts({ page: 1 })
</script>
<template>
<XBLPostList :posts="posts" />
</template>
Layer Architecture
| Path | Purpose |
|---|---|
nuxt.config.ts | Registers @nuxt/ui, @nuxt/content, Tailwind CSS |
app.config.ts | All configurable options under xBlog namespace |
app/composables/useBlog.ts | Blog data access via queryCollection('blog') |
app/components/X/BL/ | 9 auto-imported XBL-prefixed components |
app/pages/blog/ | Listing page and catch-all post page |
app/types/index.ts | TypeScript interfaces: BlogPost, BlogAuthor, BlogConfig |
Environment Variables
This layer reads no environment variables. All configuration is through app/app.config.ts.
Gotchas (Nuxt Content v3)
- The
blogcollection is consumer-defined. Without acontent.config.tsdeclaring ablogcollection (type: 'page',source: 'blog/*.md'),queryCollection('blog')has nothing to query. See the quickstart above. - Collection source binds to the defining project's rootDir. Nuxt Content v3 resolves a collection's
sourceagainst the rootDir of the project whosecontent.config.tsdefines it — which is why the layer deliberately does not ship one. Define the collection in your own project root; yourcontent/blog/*.mdfiles are then picked up. (If a layer defined it instead, the source would resolve insidenode_modulesand the blog would silently render empty.) - Content v3 query syntax. The layer and any custom consumer code must use
queryCollection('blog')with SQL operators — e.g..where('published', '<>', false)(<>for not-equal;!=is not a validSQLOperator), not the v2queryContent(...)API. - Code highlighting config lives at
content.build.markdown.highlightinnuxt.config.ts(not the v2 top-levelcontent.highlight). The layer's playground uses thegithub-light/github-darkthemes.
AI Context
package: "@xenterprises/nuxt-x-blog"
use-when: >
Adding a complete blog to a Nuxt 4 app backed by Nuxt Content v3.
The consumer defines a 'blog' collection in content.config.ts and writes
posts as Markdown in content/blog/ with title, date, and published
frontmatter. Use useBlog() for data access — getPosts() for paginated
listings, getPostByPath() for post pages, getAllTags() for tag filters.
All XBL* components are auto-imported; the pre-built /blog and
/blog/[...slug] pages work out of the box. Configure display options
(showAuthor, showReadingTime, postsPerPage, etc.) under xBlog in
app/app.config.ts.
