Examples
This guide provides real-world examples and common use cases for building sites with Vitto.
Blog with Markdown
A complete blog implementation with posts, categories, and tags.
Project Structure
blog/
├── content/
│ └── posts/
│ ├── first-post.md
│ └── second-post.md
├── src/
│ ├── pages/
│ │ ├── index.vto
│ │ ├── blog.vto
│ │ ├── post.vto
│ │ └── tag.vto
│ ├── layouts/
│ │ └── base.vto
│ └── partials/
│ ├── header.vto
│ └── post-card.vto
├── hooks/
│ ├── posts.ts
│ └── tags.ts
└── vite.config.ts Markdown Post
content/posts/first-post.md:
---
title: Getting Started with Vitto
excerpt: Learn how to build static sites with Vitto
date: 2024-01-15
author: Your Name
tags:
- vitto
- static-site
- tutorial
---
# Getting Started with Vitto
This is my first blog post built with Vitto... Hooks
hooks/posts.ts:
import { defineHooks } from 'vitto';
import fs from 'node:fs/promises';
import path from 'node:path';
import matter from 'gray-matter';
import { marked } from 'marked';
export const postsHook = defineHooks('posts', async () => {
const postsDir = path.join(process.cwd(), 'content/posts');
const files = await fs.readdir(postsDir);
const posts = await Promise.all(
files
.filter((file) => file.endsWith('.md'))
.map(async (file) => {
const filePath = path.join(postsDir, file);
const content = await fs.readFile(filePath, 'utf-8');
const { data, content: markdown } = matter(content);
return {
slug: file.replace('.md', ''),
title: data.title,
excerpt: data.excerpt,
date: data.date,
author: data.author,
tags: data.tags || [],
content: await marked(markdown),
};
})
);
return posts.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
});
export const postHook = defineHooks('post', async (params) => {
if (!params?.slug) return null;
const filePath = path.join(process.cwd(), 'content/posts', `${params.slug}.md`);
try {
const content = await fs.readFile(filePath, 'utf-8');
const { data, content: markdown } = matter(content);
return {
slug: params.slug,
title: data.title,
excerpt: data.excerpt,
date: data.date,
author: data.author,
tags: data.tags || [],
content: await marked(markdown),
};
} catch (error) {
console.error(`Failed to load post: ${params.slug}`, error);
return null;
}
});
export default postsHook; hooks/tags.ts:
import { defineHooks } from 'vitto';
import { postsHook } from './posts';
export const tagsHook = defineHooks('tags', async () => {
const posts = await postsHook();
const tagMap = new Map();
posts.forEach((post) => {
post.tags.forEach((tag) => {
if (!tagMap.has(tag)) {
tagMap.set(tag, []);
}
tagMap.get(tag).push(post);
});
});
return Array.from(tagMap.entries()).map(([name, posts]) => ({
name,
slug: name.toLowerCase().replace(/\s+/g, '-'),
count: posts.length,
posts,
}));
});
export const tagHook = defineHooks('tag', async (params) => {
if (!params?.slug) return null;
const tags = await tagsHook();
return tags.find((t) => t.slug === params.slug);
});
export default tagsHook; Templates
src/pages/blog.vto:
{{ layout "layouts/base.vto" }}
<div class="container">
<h1>{{ metadata.siteName }} Blog</h1>
<div class="posts-grid">
{{ for post of posts }}
{{ include "partials/post-card.vto" {
title: post.title,
excerpt: post.excerpt,
date: post.date,
author: post.author,
url: `/blog/${post.slug}.html`
} }}
{{ /for }}
</div>
</div> src/pages/post.vto:
{{ layout "layouts/base.vto" }}
{{ if post }}
<article class="blog-post">
<header>
<h1>{{ post.title }}</h1>
<div class="meta">
<time datetime="{{ post.date }}">{{ post.date }}</time>
<span>by {{ post.author }}</span>
</div>
{{ if post.tags && post.tags.length > 0 }}
<div class="tags">
{{ for tag of post.tags }}
<a href="/tags/{{ tag |> lowercase |> replace(' ', '-') }}.html">
{{ tag }}
</a>
{{ /for }}
</div>
{{ /if }}
</header>
<div class="content">
{{ post.content |> safe }}
</div>
<footer>
<a href="/blog.html">← Back to Blog</a>
</footer>
</article>
{{ else }}
<div class="error">
<h1>Post Not Found</h1>
<p>The post you're looking for doesn't exist.</p>
<a href="/blog.html">← Back to Blog</a>
</div>
{{ /if }} src/pages/tag.vto:
{{ layout "layouts/base.vto" }}
{{ if tag }}
<div class="container">
<h1>Posts tagged "{{ tag.name }}"</h1>
<p>{{ tag.count }} post{{ tag.count !== 1 ? 's' : '' }}</p>
<div class="posts-grid">
{{ for post of tag.posts }}
{{ include "partials/post-card.vto" {
title: post.title,
excerpt: post.excerpt,
date: post.date,
author: post.author,
url: `/blog/${post.slug}.html`
} }}
{{ /for }}
</div>
</div>
{{ else }}
<div class="error">
<h1>Tag Not Found</h1>
<a href="/blog.html">← Back to Blog</a>
</div>
{{ /if }} Configuration
vite.config.ts:
import { defineConfig } from 'vite';
import vitto from 'vitto';
import { postsHook, postHook } from './hooks/posts';
import { tagsHook, tagHook } from './hooks/tags';
export default defineConfig({
plugins: [
vitto({
metadata: {
siteName: 'My Blog',
title: 'My Awesome Blog',
description: 'A blog about web development',
author: 'Your Name',
keywords: ['blog', 'web development', 'vitto'],
},
hooks: {
posts: postsHook,
post: postHook,
tags: tagsHook,
tag: tagHook,
},
dynamicRoutes: [
{
template: 'post',
dataSource: 'posts',
getParams: (post) => ({ slug: post.slug }),
getPath: (post) => `blog/${post.slug}.html`,
},
{
template: 'tag',
dataSource: 'tags',
getParams: (tag) => ({ slug: tag.slug }),
getPath: (tag) => `tags/${tag.slug}.html`,
},
],
minify: process.env.NODE_ENV === 'production',
}),
],
}); Documentation Site
Build a documentation site with sidebar navigation.
Hooks
hooks/docs.ts:
import { defineHooks } from 'vitto';
import fs from 'node:fs/promises';
import path from 'node:path';
import matter from 'gray-matter';
import { marked } from 'marked';
interface DocItem {
type: 'file' | 'directory';
name?: string;
slug?: string;
title?: string;
order?: number;
children?: DocItem[];
}
export const docsHook = defineHooks('docs', async () => {
const docsDir = path.join(process.cwd(), 'content/docs');
async function readDocs(dir: string): Promise<DocItem[]> {
const entries = await fs.readdir(dir, { withFileTypes: true });
const docs: DocItem[] = [];
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
const children = await readDocs(fullPath);
docs.push({
type: 'directory',
name: entry.name,
children,
});
} else if (entry.name.endsWith('.md')) {
const content = await fs.readFile(fullPath, 'utf-8');
const { data } = matter(content);
docs.push({
type: 'file',
slug: fullPath.replace(docsDir, '').replace('.md', ''),
title: data.title,
order: data.order || 999,
});
}
}
return docs.sort((a, b) => (a.order || 999) - (b.order || 999));
}
return await readDocs(docsDir);
});
export const docHook = defineHooks('doc', async (params) => {
if (!params?.path) return null;
const filePath = path.join(process.cwd(), 'content/docs', `${params.path}.md`);
try {
const content = await fs.readFile(filePath, 'utf-8');
const { data, content: markdown } = matter(content);
return {
title: data.title,
order: data.order,
content: await marked(markdown),
};
} catch (error) {
console.error(`Failed to load doc: ${params.path}`, error);
return null;
}
});
export default docsHook; Sidebar Component
src/partials/docs-sidebar.vto:
<aside class="docs-sidebar">
<nav>
{{ for item of docs }}
{{ if item.type === 'directory' }}
<div class="nav-section">
<h3>{{ item.name }}</h3>
<ul>
{{ for doc of item.children }}
<li>
<a href="/docs{{ doc.slug }}.html">{{ doc.title }}</a>
</li>
{{ /for }}
</ul>
</div>
{{ else }}
<a href="/docs{{ item.slug }}.html">{{ item.title }}</a>
{{ /if }}
{{ /for }}
</nav>
</aside> Configuration
vite.config.ts:
import { defineConfig } from 'vite';
import vitto from 'vitto';
import { docsHook, docHook } from './hooks/docs';
export default defineConfig({
plugins: [
vitto({
metadata: {
siteName: 'My Docs',
title: 'Documentation',
description: 'Project documentation',
},
hooks: {
docs: docsHook,
doc: docHook,
},
dynamicRoutes: [
{
template: 'doc',
dataSource: 'docs',
getParams: (doc) => ({ path: doc.slug }),
getPath: (doc) => `docs${doc.slug}.html`,
},
],
}),
],
}); Portfolio Website
Create a portfolio with project showcases.
Hooks
hooks/projects.ts:
import { defineHooks } from 'vitto';
interface Project {
id: number;
slug: string;
title: string;
description: string;
image: string;
tags: string[];
url: string;
github?: string;
featured: boolean;
}
export const projectsHook = defineHooks<Project[]>('projects', () => {
return [
{
id: 1,
slug: 'ecommerce-platform',
title: 'E-commerce Platform',
description: 'A full-featured e-commerce solution built with modern technologies',
image: '/images/projects/ecommerce.jpg',
tags: ['React', 'Node.js', 'MongoDB', 'TypeScript'],
url: 'https://example.com',
github: 'https://github.com/user/ecommerce',
featured: true,
},
{
id: 2,
slug: 'mobile-app',
title: 'Mobile App',
description: 'Cross-platform mobile application with real-time features',
image: '/images/projects/mobile.jpg',
tags: ['React Native', 'Firebase', 'Redux'],
url: 'https://example.com',
github: 'https://github.com/user/mobile-app',
featured: true,
},
{
id: 3,
slug: 'design-system',
title: 'Design System',
description: 'Comprehensive UI component library',
image: '/images/projects/design-system.jpg',
tags: ['React', 'Storybook', 'CSS'],
url: 'https://example.com',
featured: false,
},
];
});
export const projectHook = defineHooks<Project | null, { slug: string }>(
'project',
async (params) => {
if (!params?.slug) return null;
const projects = await projectsHook();
return projects.find((p) => p.slug === params.slug) || null;
}
);
export default projectsHook; Template
src/pages/index.vto:
{{ layout "layouts/base.vto" }}
<section class="hero">
<h1>Hi, I'm {{ metadata.author }}</h1>
<p>Web Developer & Designer</p>
<p>{{ metadata.description }}</p>
</section>
<section class="projects">
<h2>Featured Projects</h2>
<div class="projects-grid">
{{ for project of projects }}
{{ if project.featured }}
<article class="project-card">
<img src="{{ project.image }}" alt="{{ project.title }}">
<h3>{{ project.title }}</h3>
<p>{{ project.description }}</p>
<div class="tags">
{{ for tag of project.tags }}
<span class="tag">{{ tag }}</span>
{{ /for }}
</div>
<div class="links">
<a href="{{ project.url }}" target="_blank" rel="noopener">View Project</a>
{{ if project.github }}
<a href="{{ project.github }}" target="_blank" rel="noopener">GitHub</a>
{{ /if }}
</div>
</article>
{{ /if }}
{{ /for }}
</div>
</section> Configuration
vite.config.ts:
import { defineConfig } from 'vite';
import vitto from 'vitto';
import { projectsHook, projectHook } from './hooks/projects';
export default defineConfig({
plugins: [
vitto({
metadata: {
siteName: 'Your Name Portfolio',
title: 'Your Name - Web Developer',
description: 'Full-stack web developer specializing in React and Node.js',
author: 'Your Name',
keywords: ['portfolio', 'web developer', 'react', 'nodejs'],
},
hooks: {
projects: projectsHook,
project: projectHook,
},
dynamicRoutes: [
{
template: 'project',
dataSource: 'projects',
getParams: (project) => ({ slug: project.slug }),
getPath: (project) => `projects/${project.slug}.html`,
},
],
}),
],
}); E-commerce Product Catalog
Build a product catalog with categories.
Hooks
hooks/products.ts:
import { defineHooks } from 'vitto';
interface Product {
id: number;
slug: string;
name: string;
description: string;
price: number;
image: string;
category: string;
sku: string;
inStock: boolean;
}
export const productsHook = defineHooks<Product[]>('products', async () => {
try {
const response = await fetch('https://api.example.com/products');
if (!response.ok) throw new Error('Failed to fetch products');
return await response.json();
} catch (error) {
console.error('Failed to fetch products:', error);
return [];
}
});
export const productHook = defineHooks<Product | null, { id: string }>(
'product',
async (params) => {
if (!params?.id) return null;
try {
const response = await fetch(`https://api.example.com/products/${params.id}`);
if (!response.ok) return null;
return await response.json();
} catch (error) {
console.error(`Failed to fetch product ${params.id}:`, error);
return null;
}
}
);
export const categoriesHook = defineHooks('categories', async () => {
const products = await productsHook();
const categoryMap = new Map();
products.forEach((product) => {
if (!categoryMap.has(product.category)) {
categoryMap.set(product.category, []);
}
categoryMap.get(product.category).push(product);
});
return Array.from(categoryMap.entries()).map(([name, products]) => ({
name,
slug: name.toLowerCase().replace(/\s+/g, '-'),
count: products.length,
products,
}));
});
export default productsHook; Product Page
src/pages/product.vto:
{{ layout "layouts/base.vto" }}
{{ if product }}
<div class="product">
<div class="product-images">
<img src="{{ product.image }}" alt="{{ product.name }}">
</div>
<div class="product-info">
<h1>{{ product.name }}</h1>
<p class="price">${{ product.price }}</p>
<p>{{ product.description }}</p>
<div class="product-meta">
<span>Category: {{ product.category }}</span>
<span>SKU: {{ product.sku }}</span>
<span class="{{ product.inStock ? 'in-stock' : 'out-of-stock' }}">
{{ product.inStock ? 'In Stock' : 'Out of Stock' }}
</span>
</div>
{{ if product.inStock }}
<button class="add-to-cart">Add to Cart</button>
{{ /if }}
</div>
</div>
{{ else }}
<div class="error">
<h1>Product Not Found</h1>
<a href="/products.html">← Back to Products</a>
</div>
{{ /if }} Configuration
vite.config.ts:
import { defineConfig } from 'vite';
import vitto from 'vitto';
import { productsHook, productHook, categoriesHook } from './hooks/products';
export default defineConfig({
plugins: [
vitto({
metadata: {
siteName: 'My Shop',
title: 'My Shop - Quality Products',
description: 'Shop quality products at great prices',
keywords: ['shop', 'ecommerce', 'products'],
},
hooks: {
products: productsHook,
product: productHook,
categories: categoriesHook,
},
dynamicRoutes: [
{
template: 'product',
dataSource: 'products',
getParams: (product) => ({ id: product.id }),
getPath: (product) => `products/${product.slug}.html`,
},
],
}),
],
}); Multi-language Site
Create a multi-language site with i18n support.
Hooks
hooks/i18n.ts:
import { defineHooks } from 'vitto';
const translations = {
en: {
home: 'Home',
about: 'About',
contact: 'Contact',
readMore: 'Read More',
welcome: 'Welcome to our site',
},
es: {
home: 'Inicio',
about: 'Acerca de',
contact: 'Contacto',
readMore: 'Leer Más',
welcome: 'Bienvenido a nuestro sitio',
},
fr: {
home: 'Accueil',
about: 'À propos',
contact: 'Contact',
readMore: 'Lire la suite',
welcome: 'Bienvenue sur notre site',
},
};
export default defineHooks('i18n', (params) => {
const lang = params?.lang || 'en';
return translations[lang] || translations.en;
}); Template
src/pages/index.vto:
{{ layout "layouts/base.vto" }}
<nav>
<a href="/">{{ i18n.home }}</a>
<a href="/about.html">{{ i18n.about }}</a>
<a href="/contact.html">{{ i18n.contact }}</a>
</nav>
<div class="language-switcher">
<a href="/">English</a>
<a href="/es/">Español</a>
<a href="/fr/">Français</a>
</div>
<main>
<h1>{{ i18n.welcome }}</h1>
<h2>{{ metadata.siteName }}</h2>
<p>{{ metadata.description }}</p>
</main> Configuration
vite.config.ts:
import { defineConfig } from 'vite';
import vitto from 'vitto';
import i18nHook from './hooks/i18n';
export default defineConfig({
plugins: [
vitto({
metadata: {
siteName: 'My Multilingual Site',
title: 'Welcome',
description: 'A site available in multiple languages',
},
hooks: {
i18n: i18nHook,
},
}),
],
}); RSS Feed
Generate an RSS feed for your blog.
Hook
hooks/rss.ts:
import { defineHooks } from 'vitto';
import { postsHook } from './posts';
export default defineHooks('rss', async () => {
const posts = await postsHook();
const baseUrl = 'https://example.com';
const items = posts
.slice(0, 20)
.map(
(post) => `
<item>
<title><![CDATA[${post.title}]]></title>
<link>${baseUrl}/blog/${post.slug}.html</link>
<description><![CDATA[${post.excerpt}]]></description>
<pubDate>${new Date(post.date).toUTCString()}</pubDate>
<guid>${baseUrl}/blog/${post.slug}.html</guid>
</item>
`
)
.join('');
return `<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>My Blog</title>
<link>${baseUrl}</link>
<description>Latest blog posts</description>
<language>en</language>
<atom:link href="${baseUrl}/feed.xml" rel="self" type="application/rss+xml"/>
${items}
</channel>
</rss>`;
}); Template
src/pages/feed.xml.vto:
{{ rss |> safe }} Configuration
vite.config.ts:
import { defineConfig } from 'vite';
import vitto from 'vitto';
import rssHook from './hooks/rss';
import { postsHook, postHook } from './hooks/posts';
export default defineConfig({
plugins: [
vitto({
metadata: {
siteName: 'My Blog',
title: 'My Blog',
description: 'A blog about web development',
},
hooks: {
posts: postsHook,
post: postHook,
rss: rssHook,
},
}),
],
}); Sitemap Generation
Generate a sitemap.xml file.
Hook
hooks/sitemap.ts:
import { defineHooks } from 'vitto';
import { postsHook } from './posts';
export default defineHooks('sitemap', async () => {
const posts = await postsHook();
const baseUrl = 'https://example.com';
const urls = [
{ loc: `${baseUrl}/`, priority: '1.0', changefreq: 'daily' },
{ loc: `${baseUrl}/about.html`, priority: '0.8', changefreq: 'monthly' },
{ loc: `${baseUrl}/blog.html`, priority: '0.9', changefreq: 'daily' },
...posts.map((post) => ({
loc: `${baseUrl}/blog/${post.slug}.html`,
lastmod: post.date,
priority: '0.7',
changefreq: 'weekly',
})),
];
const urlElements = urls
.map(
(url) => `
<url>
<loc>${url.loc}</loc>
${url.lastmod ? `<lastmod>${url.lastmod}</lastmod>` : ''}
<changefreq>${url.changefreq}</changefreq>
<priority>${url.priority}</priority>
</url>`
)
.join('');
return `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${urlElements}
</urlset>`;
}); Template
src/pages/sitemap.xml.vto:
{{ sitemap |> safe }} Configuration
vite.config.ts:
import { defineConfig } from 'vite';
import vitto from 'vitto';
import sitemapHook from './hooks/sitemap';
import { postsHook, postHook } from './hooks/posts';
export default defineConfig({
plugins: [
vitto({
metadata: {
siteName: 'My Site',
title: 'My Site',
description: 'A website built with Vitto',
},
hooks: {
posts: postsHook,
post: postHook,
sitemap: sitemapHook,
},
}),
],
}); JSON API
Generate JSON files for API consumption.
Hook
hooks/api.ts:
import { defineHooks } from 'vitto';
import { postsHook } from './posts';
export default defineHooks('api', async () => {
const posts = await postsHook();
return JSON.stringify(
{
version: '1.0',
generated: new Date().toISOString(),
posts: posts.map((post) => ({
slug: post.slug,
title: post.title,
excerpt: post.excerpt,
date: post.date,
author: post.author,
tags: post.tags,
url: `/blog/${post.slug}.html`,
})),
},
null,
2
);
}); Template
src/pages/api/posts.json.vto:
{{ api |> safe }} Configuration
vite.config.ts:
import { defineConfig } from 'vite';
import vitto from 'vitto';
import apiHook from './hooks/api';
import { postsHook, postHook } from './hooks/posts';
export default defineConfig({
plugins: [
vitto({
metadata: {
siteName: 'My Blog',
title: 'My Blog',
description: 'A blog with JSON API',
},
hooks: {
posts: postsHook,
post: postHook,
api: apiHook,
},
}),
],
}); Tip
Feel free to modify these examples to fit your project structure and data sources.
Note
These examples assume file-based data sources (Markdown, JSON). For API-driven sites, replace the file reading logic with fetch calls.
Next Steps
- Troubleshooting - Common issues and solutions
- API Reference - Complete API documentation
- Contributing - Contribute to Vitto