npm package: @karaoke-cms/module-blog
Description: Blog module for karaoke-cms — posts, tags, RSS
Latest version: 0.20.10

# @karaoke-cms/module-blog

Blog module for karaoke-cms — paginated post listing, individual post pages, tag filtering, pagination, and an RSS link in the footer nav.

## Installation

```bash
npm install @karaoke-cms/module-blog
```

## Usage

```ts
// karaoke.config.ts
import { defineConfig } from '@karaoke-cms/astro';
import { loadEnv } from '@karaoke-cms/astro/env';
import { blog } from '@karaoke-cms/module-blog';
import { themeDefault } from '@karaoke-cms/theme-default';

const env = loadEnv(new URL('.', import.meta.url));

export default defineConfig({
  vault: env.KARAOKE_VAULT,
  theme: themeDefault(),
  modules: [blog({ mount: '/blog' })],
});
```

## Configuration

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `mount` | `string` | `'/blog'` | URL prefix for all blog routes |
| `enabled` | `boolean` | `true` | Set `false` to disable the module entirely |
| `comments` | `boolean` | `true` | Default comments state for all posts. Per-post frontmatter overrides this. |

## Routes

All routes are relative to `mount`:

| Pattern | Description |
|---------|-------------|
| `{mount}` | Paginated post list |
| `{mount}/[slug]` | Single post page |
| `{mount}/page/[page]` | Overflow pagination pages |

## Frontmatter

```yaml
---
title: "My Post"                      # required
publish: true                         # required to appear on site
date: 2026-01-15                      # optional — YYYY-MM-DD
author: "Name"                        # optional — string or array
featured_image: "img/hero.jpg"        # optional — relative path, absolute path, URL,
                                      #   or Obsidian wiki link: "[[hero.jpg]]"
description: "..."                    # optional — OG tags, RSS, AI-enriched
tags: [writing, tutorial]             # optional — powers /tags pages
reading_time: 5                       # optional — AI-enriched, minutes
related: [slug-a, slug-b]             # optional — AI-enriched, slugs
comments: true                        # optional — per-post Giscus override
---
```

### `featured_image` formats

All of the following work in `featured_image`:

```yaml
featured_image: "./images/hero.jpg"          # relative to the note
featured_image: "/blog/hero.jpg"             # vault-absolute path
featured_image: "https://example.com/x.jpg"  # remote URL
featured_image: "[[hero.jpg]]"               # Obsidian wiki link — resolved via vault lookup
```

When using a wiki link, the image is served from `/media/` during dev and copied to `dist/media/` at build.

## Components

The module ships reusable components you can import in your own pages:

```ts
import FeaturedPost      from '@karaoke-cms/module-blog/components/FeaturedPost';
import RecentPosts       from '@karaoke-cms/module-blog/components/RecentPosts';
import PaginatedPostList from '@karaoke-cms/module-blog/components/PaginatedPostList';
```

All three components call `resolveWikiImage()` internally, so wiki-linked featured images render correctly in list views without any extra work.

## Scaffold

On first `npm run dev`, the module copies starter page files into your project's `src/pages/{mountDir}/`. You can edit these files to customise the layout without modifying the npm package.

## What's new in 0.18.2

- **Variant-aware layouts** -- the blog module now declares three page variants (`root`, `list`, `post`) with default layouts. `blog.post` renders body content in `main` with a recent-posts sidebar in `right`. `blog.root` and `blog.list` render a full-width paginated post list. Themes and users can override these layouts without modifying the module.
- **`componentExport` on region declarations** -- `RecentPosts` and `PaginatedPostList` now declare `componentExport` pointing to their npm subpath exports (`@karaoke-cms/module-blog/components/RecentPosts` and `@karaoke-cms/module-blog/components/PaginatedPostList`). The `virtual:karaoke-cms/region-registry` resolves these automatically, so themes no longer need to wire up component imports manually.
- **`RecentPosts` self-detects `blogMount`** -- the `RecentPosts` component reads `blogMount` from the virtual config module instead of requiring it as a prop, so it works correctly regardless of where the blog is mounted.

## What's new in 0.18.0

No user-facing changes in this release.

## What's new in 0.17.0

- **`comments` option** — `blog({ comments: false })` disables Giscus comments by default on all posts. Defaults to `true`. Individual posts can override with `comments: false` or `comments: true` in frontmatter.
- **Two-column post list** — the post list page uses a two-column grid layout with a `PostListItem` card component.
- **Featured post hero** — the first pinned post (or most recent when none is pinned) renders as a full-width `PostListFeatured` hero card at the top of the list.

## What's new in 0.13.1

No user-facing changes in this release.

## What's new in 0.10.3

No user-facing changes in this release.

## What's new in 0.10.0

- **`[[wiki link]]` in `featured_image`** — `featured_image: "[[hero.jpg]]"` now works in post cards, the featured hero card, and the individual post page; images are resolved via vault path lookup and served from `/media/`
- **Blog list page** — `/blog` now has a dedicated list page separate from the post template, fixing a route conflict with the theme-default fallback

## What's new in 0.9.5

- **Full page scaffold** — on first dev run, `list.astro`, `[slug].astro`, and `page/[page].astro` are copied to your `src/pages/blog/` so you can customise them
- **`FeaturedPost`, `RecentPosts`, `PaginatedPostList` components** exported for use in custom pages
- **30 demo posts** included in new project vaults so the site looks populated on first visit
- **Default CSS** (`src/styles/blog.css`) copied on first dev run and auto-imported into `global.css`
- **`enabled` flag** — `blog({ mount: '/blog', enabled: false })` disables the module without removing it from config
- **Fixed zod import** — `zod` now correctly declared as a dependency (was missing from lockfile)