X Enterprises

nuxt-x-blog

Nuxt 4 blog layer powered by Nuxt Content v3 — 9 XBL-prefixed components, a useBlog composable, 2 pre-built pages, and configurable blog settings via app.config.ts.

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...
FieldTypeRequiredDescription
titlestringYesPost title
descriptionstringNoShort excerpt
datestringYesISO date string (used for sorting)
authorstringNoAuthor display name
imagestringNoCover image URL
tagsstringNoTag list for filtering
publishedbooleanNoSet 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 time
  • XBLPostList — Responsive grid of XBLPostCard items
  • XBLPostHeader — Post page header with title, meta, author, and cover image
  • XBLPagination — Page navigation controls
  • XBLSearchInputv-model search input for filtering posts by title/description
  • XBLTagList — Tag filter list with counts
  • XBLRecentPosts — Sidebar widget showing the most recent posts
  • XBLTableOfContents — Table of contents for post pages, from body.toc.links
  • XBLShareButtons — Social share buttons (X/Twitter, LinkedIn, Facebook) plus copy-link

Composable:

  • useBlog() — All blog data access methods backed by queryCollection('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):

OptionTypeDefaultDescription
titlestring'Blog'Blog section title
descriptionstring'Latest articles and updates'Blog section description
postsPerPagenumber9Posts per page for pagination
dateFormatstring'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
showAuthorbooleantrueShow author on post cards and pages
showReadingTimebooleantrueShow estimated reading time
showTagsbooleantrueShow tags on post cards and pages
showTableOfContentsbooleantrueShow TOC on post pages
showShareButtonsbooleantrueShow social share buttons on post pages
pages.homebooleantrueEnable the //blog redirect (false → 404)
pages.listbooleantrueEnable the /blog listing page (false → 404)
pages.postbooleantrueEnable 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?)

OptionTypeDefaultDescription
pagenumber1Page number
tagstringFilter by tag
limitnumberpostsPerPagePosts 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

PathPurpose
nuxt.config.tsRegisters @nuxt/ui, @nuxt/content, Tailwind CSS
app.config.tsAll configurable options under xBlog namespace
app/composables/useBlog.tsBlog 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.tsTypeScript 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 blog collection is consumer-defined. Without a content.config.ts declaring a blog collection (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 source against the rootDir of the project whose content.config.ts defines it — which is why the layer deliberately does not ship one. Define the collection in your own project root; your content/blog/*.md files are then picked up. (If a layer defined it instead, the source would resolve inside node_modules and 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 valid SQLOperator), not the v2 queryContent(...) API.
  • Code highlighting config lives at content.build.markdown.highlight in nuxt.config.ts (not the v2 top-level content.highlight). The layer's playground uses the github-light / github-dark themes.

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.
Copyright © 2026