vaza-content
v0.4.2
Published
Universal SEO layer for content collections. Auto-generates sitemap, RSS, JSON-LD, OG images, and integrity checks for Next.js, SvelteKit, Astro, and Nuxt.
Maintainers
Readme
vaza-content
Universal SEO layer for content collections. Auto-generates sitemap, RSS, JSON-LD, OG images, and integrity checks for Next.js, SvelteKit, Astro, and Nuxt.
Quick Start
npm install vaza-contentCLI Setup
npx vaza-content initThis auto-detects your framework, installs dependencies, and scaffolds a vaza.config.ts.
Manual Setup
Create vaza.config.ts:
import { defineConfig } from 'vaza-content'
export default defineConfig({
site: {
url: 'https://example.com',
name: 'My Site',
description: 'A content-driven site',
language: 'en',
},
collections: {
blog: {
entries: () => getAllPosts(),
map: (post) => ({
slug: post.slug,
title: post.title,
body: post.content,
publishDate: new Date(post.date),
tags: post.tags,
}),
basePath: '/blog',
},
},
})Framework Integration
Next.js
// next.config.ts
import { withVazaContent } from 'vaza-content/next'
import vazaConfig from './vaza.config'
export default withVazaContent({ /* next config */ }, vazaConfig)SvelteKit
// vite.config.ts
import { sveltekit } from '@sveltejs/kit/vite'
import { vazaContent } from 'vaza-content/sveltekit'
import vazaConfig from './vaza.config'
export default { plugins: [sveltekit(), vazaContent(vazaConfig)] }Astro
// astro.config.mjs
import { vazaContent } from 'vaza-content/astro'
import vazaConfig from './vaza.config'
export default defineConfig({
integrations: [vazaContent(vazaConfig)]
})Nuxt
// nuxt.config.ts
import { defineVazaContentModule } from 'vaza-content/nuxt'
import vazaConfig from './vaza.config'
export default defineNuxtConfig({
modules: [defineVazaContentModule(vazaConfig)]
})What It Generates
| Output | Location | Description |
|--------|----------|-------------|
| Sitemap | public/sitemap.xml | XML sitemap with image extensions |
| RSS Feed | public/rss.xml | RSS 2.0 feed |
| JSON-LD | .vaza-content/json-ld/{slug}.json | Structured data per entry |
| OG Images | public/og/{slug}.png | Auto-generated Open Graph images |
| Taxonomy | .vaza-content/taxonomy.json | Tag and category maps |
| Redirects | .vaza-content/redirects.json | Detected slug changes |
| Integrity | .vaza-content/integrity-report.json | Content quality report |
Configuration Reference
site (required)
site: {
url: string // Your site URL (no trailing slash)
name: string // Site name
description?: string
language?: string // Default: "en"
author?: { name: string; url?: string }
}collections (required)
collections: {
[name: string]: {
entries: () => T[] | Promise<T[]> // Function returning raw entries
map: (entry: T) => PartialVazaEntry // Transform to VazaEntry shape
basePath: string // URL prefix (e.g. "/blog")
}
}sitemap
sitemap: {
enabled?: boolean // Default: true
changeFrequency?: string // Default: "weekly"
priority?: number // Default: 0.7
additionalPaths?: string[] // Extra static paths to include
}rss
rss: {
enabled?: boolean // Default: true
path?: string // Default: "/rss.xml"
limit?: number // Default: 50
}jsonLd
jsonLd: {
enabled?: boolean
organization?: { name: string; logo?: string; url?: string }
customSchemas?: Record<string, JsonLdGenerator> // Add custom schema types
}Auto-detected schema types: Article, FAQ, HowTo, Product, Recipe, Event, BreadcrumbList.
ogImages
ogImages: {
enabled?: boolean
template?: "minimal" | "blog" | "product" | "dark" // Default: "blog"
outputDir?: string // Default: "./public/og"
fontPath?: string // Custom .ttf/.woff font (auto-downloads Inter if omitted)
concurrency?: number // Max parallel generations (default: 5)
colors?: { bg?: string; text?: string; accent?: string }
logo?: string
render?: (entry: VazaEntry) => JSX.Element // Custom Satori template
}redirects
redirects: {
enabled?: boolean
strategy?: "git" | "manifest" | "both" // Default: "both"
}Uses git rename history and/or a slug manifest with Levenshtein similarity matching.
integrity
integrity: {
brokenLinks?: Severity
metaTitleLength?: { max?: number; severity?: Severity }
metaDescriptionLength?: { max?: number; severity?: Severity }
altText?: Severity
duplicateSlugs?: Severity
duplicateContent?: Severity
orphanPages?: Severity
freshness?: { maxAge?: string; severity?: Severity } // "90d", "6m", "1y"
}Severity: "error" (fails build), "warn" (prints warning), "off" (disabled).
logLevel
logLevel?: "silent" | "error" | "warn" | "info" | "debug" // Default: "info"CLI
vaza-content init # Scaffold config for detected framework
vaza-content generate # Generate all SEO outputs
vaza-content check # Run all integrity checks
vaza-content check --links # Check broken links only
vaza-content check --meta # Check meta lengths only
vaza-content check --freshness # Check content age only
vaza-content check --alt # Check image alt text only
vaza-content check --duplicates # Check duplicates onlyProgrammatic API
import {
processCollections,
normalize,
generateSitemap,
generateRss,
generateJsonLd,
generateOgImages,
generateTaxonomy,
generateCanonical,
generateBreadcrumbs,
detectRedirects,
checkIntegrity,
} from 'vaza-content'Each function can be used standalone or orchestrated via processCollections.
Entry Shape
The PartialVazaEntry you return from map() only requires slug, title, and body. Everything else is auto-generated:
| Field | Auto-generated From |
|-------|-------------------|
| description | First ~160 chars of body |
| excerpt | First ~160 chars, stripped of markdown |
| readingTime | Word count / 200 WPM |
| wordCount | Body text word count |
| toc | ## and ### headings |
| publishDate | Current date if omitted |
| image.width/height | Read via sharp |
| image.blurDataURL | Tiny blur placeholder via sharp |
License
MIT
