npm package: @karaoke-cms/astro
Description: Core Astro integration for karaoke-cms — virtual config, wikilinks, handbook routes
Latest version: 0.20.10

# @karaoke-cms/astro

Core Astro integration for karaoke-cms. Wires up themes, modules, collections, menus, Obsidian embed handling, and the virtual config module at build time. Every karaoke-cms project depends on this package.

## Installation

```bash
npm install @karaoke-cms/astro
```

## Usage

```js
// astro.config.mjs
import { defineConfig } from 'astro/config';
import karaoke from '@karaoke-cms/astro';
import karaokeConfig from './karaoke.config.ts';

export default defineConfig({
  site: 'https://your-site.pages.dev',
  integrations: [karaoke(karaokeConfig)],
});
```

```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 { docs } from '@karaoke-cms/module-docs';
import { themeDefault } from '@karaoke-cms/theme-default';

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

export default defineConfig({
  vault: env.KARAOKE_VAULT,
  title: 'My Site',
  description: 'What this site is about.',
  theme: themeDefault(),
  modules: [blog({ mount: '/blog' }), docs({ mount: '/docs' })],
});
```

## Configuration

All fields are optional. Defaults produce a working site with no content.

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `vault` | `string` | project root | Path to the Obsidian vault directory (absolute or relative) |
| `title` | `string` | `'Karaoke'` | Site title for browser tab and nav |
| `description` | `string` | `''` | Site description for RSS and OG tags |
| `theme` | `ThemeInstance` | — | Theme from `themeDefault()` or similar |
| `modules` | `ModuleInstance[]` | `[]` | Additional modules not covered by the theme |
| `comments` | `CommentsConfig` | — | Giscus comments config |
| `collections` | `Record<string, CollectionConfig>` | — | Per-collection mode overrides |
| `layout.regions` | `{ top, left, right, bottom }` | — | Region component lists |

## Obsidian embed syntax

Markdown files can use Obsidian's `![[file]]` embed syntax. The integration handles it automatically:

### Image embeds

```markdown
![[photo.jpg]]           — embedded image (Astro-optimized)
![[photo.jpg|300]]       — image with width hint → <img width="300">
![[photo.jpg|300x200]]   — image with width and height → <img width="300" height="200">
![[photo.jpg|alt text]]  — image with alt text
```

Image files are resolved in this order:
1. Same folder as the note
2. Vault `attachmentFolderPath` from `.obsidian/app.json`
3. Vault root

Resolved images go through Astro's image optimisation pipeline.

### Audio and video embeds

```markdown
![[recording.mp3]]   — → <audio controls>
![[clip.mp4]]        — → <video controls>
```

Audio and video files are served from `/media/` in dev and copied to `dist/media/` at build time.

### Wiki links in `featured_image` frontmatter

```yaml
featured_image: "[[hero.jpg]]"
```

The wiki link is resolved using the same vault path lookup as inline embeds and rewritten to `/media/hero.jpg`. Use `resolveWikiImage()` in your templates — see below.

## `resolveWikiImage(value)`

A utility for templates that render `featured_image`. Call it on the raw frontmatter value — it rewrites `[[file.jpg]]` to `/media/file.jpg` and passes regular paths and URLs through unchanged.

```ts
import { resolveWikiImage } from '@karaoke-cms/astro';

// In an Astro template:
<img src={resolveWikiImage(entry.data.featured_image)} alt={entry.data.title} />
```

This is necessary because Astro sets `entry.data` from validated frontmatter at collection-load time, before the remark plugin runs. Templates must call `resolveWikiImage()` to get the resolved URL.

## Exports

### `@karaoke-cms/astro` — main integration

- `karaoke(config)` — Astro integration (default export)
- `defineConfig(config)` — identity wrapper for type inference
- `defineModule(def)` — create a module factory
- `defineTheme(def)` — create a theme factory
- `resolveWikiImage(value)` — resolve `[[wiki link]]` to `/media/` URL for use in templates

### `@karaoke-cms/astro/env`

- `loadEnv(dir)` — reads `.env.default` and `.env` from `dir`, returns merged record

### `@karaoke-cms/astro/collections`

- `makeCollections(root, vault?, collections?)` — creates Astro content collections from the vault

### `virtual:karaoke-cms/config`

Available in any page or component at build time:

```ts
import {
  siteTitle,           // string
  siteDescription,     // string
  resolvedCollections, // Record<string, { modes, label, enabled }>
  resolvedMenus,       // Record<string, { name, orientation, entries }>
  resolvedLayout,      // { regions: { top, left, right, bottom } }
  resolvedModules,     // { comments: { enabled, repo, repoId, category, categoryId } }
  blogMount,           // string — the blog module's mount path
} from 'virtual:karaoke-cms/config';
```

Add to `src/env.d.ts` for TypeScript types:

```ts
/// <reference types="@karaoke-cms/astro/client" />
```

## Menus

Define menus in `{vault}/_website/menus.yaml`. When absent, defaults are generated: a `main` menu with Blog/Docs/Tags and a `footer` menu with the RSS link. Entries support `when: collection:name` to hide automatically when a collection is empty.

## Layout regions

Four regions (top, left, right, bottom) accept component lists from `karaoke.config.ts`:

```ts
layout: {
  regions: {
    top:    { components: ['header', 'main-menu'] },
    right:  { components: ['recent-posts'] },
    bottom: { components: ['footer'] },
  }
}
```

Available components: `'header'`, `'main-menu'`, `'search'`, `'recent-posts'`, `'footer'`.

## What's new in 0.19.2

- **Clean docs URLs for index files** — `index.md` files inside docs sections now resolve to their parent directory URL instead of `/section/index`. Applies to routes, sidebar links, wikilinks, and inline markdown links.
- **Semantic header and footer** — `SiteHeader.astro` now wraps content in a `<header>` element with the main navigation menu. `SiteFooter.astro` now wraps content in a `<footer>` element.
- **Cleaner page titles** — emoji prefix removed from `<title>` tags.
- **Staging robots.txt** — staging deploys (main branch) now serve a deny-all `robots.txt` to prevent search engine indexing.

## What's new in 0.18.2

- **Variant-aware layout system** -- modules declare per-variant layouts specifying which components appear in main/left/right for each page type (e.g. `blog.post` gets body + sidebar, `blog.list` gets a full-width list). Themes override via `layoutOverrides` on their `ThemeInstance`. Users override in `karaoke.config.ts` via `variantLayouts`. The three layers merge in order: module defaults, theme overrides, user overrides. Pages activate a variant by passing `variantKey` to `DefaultPage`.

  ```ts
  // karaoke.config.ts — user-level variant override
  export default defineConfig({
    // ...
    variantLayouts: {
      'karaoke.blog.post': {
        main:  [{ component: 'body', weight: 20 }],
        right: [],  // remove the sidebar on blog posts
      },
    },
  });
  ```

- **Mount-aware link normalization** -- the rehype link normalization plugin now receives a folder-to-mount map built from all active modules. Relative Obsidian links in docs and blog markdown are resolved against the correct mount path, so cross-page links work regardless of URL prefix.

- **Virtual region registry for module components** -- modules declare `componentExport` on their `regionComponentDeclarations`, pointing to an npm subpath export (e.g. `@karaoke-cms/module-blog/components/RecentPosts`). The new `virtual:karaoke-cms/region-registry` Vite module auto-generates imports for all declared components, so `RegionRenderer` can resolve component IDs to actual Astro components at build time without manual wiring.

## What's new in 0.18.1

- **`menus.yaml` location changed** — the navigation config file moved from `{vault}/karaoke-cms/config/menus.yaml` to `{vault}/_website/menus.yaml`. If you have an existing `menus.yaml`, move it to `_website/menus.yaml` in your vault root.

## What's new in 0.18.0

- **Nested docs routes** — docs pages now use a `[...slug]` rest parameter instead of `[slug]`, so subdirectories within a docs section resolve correctly. No config changes needed.
- **Section switcher everywhere** — the styled nav showing all active docs sections now appears on doc pages, section home pages, and section list pages (previously doc pages only).
- **DocsTree sidebar cleanup** — `index.md` leaf nodes are hidden from the sidebar and hoisted to their parent directory node, reducing visual clutter.
- **Wikilink resolution fix** — relative links inside docs sections are now resolved against the module's mount path, so cross-page links work correctly in any docs section regardless of URL prefix.
- **Blog folder config in link normalization** — relative link normalization now respects the blog module's configured folder, fixing broken links when the blog folder differs from the default.
- **`modes` on module instances** — modules can now declare `modes: ['dev']` or `modes: ['dev', 'prod']` directly on the `ModuleInstance`, replacing the need for `collections.yaml` overrides.

## What's new in 0.17.0

- **Submenu support** — `menus.yaml` entries now support `parent: <id>` to nest under a root entry as a hover/click submenu. Use `static: "Label"` for a non-interactive parent label. The `Menu.astro` component renders a toggle button for keyboard accessibility.
- **`themeDefault()` takes no arguments** — modules are passed via `modules: []` in `defineConfig()`, not via `implements: []` inside the theme call. Update `karaoke.config.ts` if you were using the old pattern.
- **Per-module comments default** — `blog()` and `docs()` accept `comments?: boolean`. Blog defaults to `true`, docs defaults to `false`. Individual pages can still override via frontmatter `comments: true/false`.
- **Docs array form** — `docs([...])` now accepts a `DocsSection[]` array, returning one `ModuleInstance` per section. Each section can specify `weight` and `parent` for menu ordering and nesting.

## What's new in 0.10.3

- **Vault path warning** — if `KARAOKE_VAULT` points to a directory that doesn't exist, a clear warning is printed at startup instead of silently building a site with empty collections. The message shows the resolved path and points to `.env` / `.env.default`.
- **`menus.yaml` live reload** — editing `{vault}/_website/menus.yaml` while the dev server is running now triggers an immediate full-page reload. No more restarting `npm run dev` after menu changes.
- **Security: path traversal guard hardened** — the docs codegen path-traversal check now uses `path.normalize()` on both sides of the comparison, so the guard holds correctly on Windows and with symlinked paths.

## What's new in 0.10.2

- **Multiple named docs sections** — run `docs({ mount: '/api-docs', folder: 'api-reference', label: 'API Reference' })` alongside `docs({ mount: '/docs' })`. Each section gets its own sidebar, URL space, and Astro content collection. Routes are code-generated per instance at build time.
- **Auto-registered docs collections** — pass your full config to `makeCollections({ config })` in `content.config.ts` and all docs instances register automatically. No manual sync required when adding a second section.
- **`virtual:karaoke-cms/docs-sections`** — new Vite virtual module exporting all active docs section specs. Import it in any page to build section switchers or cross-section links.
- **Section switcher in doc pages** — when two or more docs sections are active, a nav row lists them with the current one highlighted.
- **Tags and RSS are section-aware** — `/tags`, `/tags/[tag]`, and `/rss.xml` now aggregate entries from every active docs section.
- **Fix: dev-only collection menu entries** — entries with `when: collection:name` now correctly show in dev mode when the collection exists but has no published entries.

## What's new in 0.10.0

- **Obsidian `![[image]]` embed support** — `![[photo.jpg]]` in any markdown note is resolved via vault path lookup and goes through Astro's image optimisation pipeline
- **Size hints** — `![[photo.jpg|300]]` sets `width="300"`; `![[photo.jpg|300x200]]` sets both width and height
- **Audio and video embeds** — `![[recording.mp3]]` → `<audio controls>`, `![[clip.mp4]]` → `<video controls>`; files served from `/media/` in dev, emitted to `dist/media/` at build
- **`featured_image` wiki link support** — `featured_image: "[[hero.jpg]]"` in frontmatter is resolved to `/media/hero.jpg` via vault path lookup
- **`resolveWikiImage(value)` export** — utility for templates to rewrite `[[file.jpg]]` frontmatter values to the correct `/media/` URL at render time

## What's new in 0.9.5

- **`isThemeInstance` type-guard** added — fixes a runtime crash in the `astro:config:setup` hook when a `ThemeInstance` was passed
- **Module array API** — `modules` in config is now `ModuleInstance[]` (was a legacy object shape in some docs)
- **Tighter `astro.config.mjs` error handling** — missing `karaoke.config.ts` is now distinguished from missing dependencies of that file; the latter now surfaces the real error instead of silently falling back to empty config