Troubleshooting
This guide covers common issues and their solutions when working with Vitto.
Installation Issues
Node Version Mismatch
Problem: Error about incompatible Node.js version
Solution: Ensure you’re using Node.js 20.19+ or 22.12+
# Check your Node version
node --version
# Install correct version using nvm
nvm install 20
nvm use 20 Package Installation Fails
Problem: npm install fails with errors
Solution:
# Clear npm cache
npm cache clean --force
# Delete node_modules and package-lock.json
rm -rf node_modules package-lock.json
# Reinstall
npm install PNPM Issues
Problem: PNPM installation errors
Solution:
# Update PNPM
npm install -g pnpm@latest
# Clear PNPM cache
pnpm store prune
# Reinstall
pnpm install PNPM Ignored Build Scripts
Problem: pnpm install succeeds but warns about ignored build scripts:
[ERR_PNPM_IGNORED_BUILDS] Ignored build scripts: esbuild@0.28.1, workerd@1.20260722.1
Run "pnpm approve-builds" to pick which dependencies should be allowed to run scripts. This happens because pnpm v10+ blocks dependency lifecycle scripts by default. In pnpm v11, the old onlyBuiltDependencies setting in package.json is deprecated — all build settings must go in pnpm-workspace.yaml.
Solution: Create or update pnpm-workspace.yaml in your project root:
# pnpm-workspace.yaml
allowBuilds:
'*': true This allows all packages to run their build scripts. For a more restrictive approach, list only the specific packages that need builds:
allowBuilds:
esbuild: true
workerd: true Alternatively, run the interactive approval command after install:
pnpm approve-builds
# or approve all at once (pnpm >= 10.32)
pnpm approve-builds --all When using create-vitto with the --start or -s flag and pnpm as the package manager, this configuration is automatically generated in the scaffolded project.
Build Errors
“Cannot find module ‘vitto’”
Problem: Module not found error
Solution:
- Verify installation:
npm list vitto - Reinstall if missing:
npm install -D vitto - Check
vite.config.ts:
import vitto from 'vitto'; // Correct
// not: import vitto from '@vitto/core' Missing Required Metadata
Problem: Error about missing required metadata configuration
Error: metadata is required in VittoOptions Solution: Ensure metadata is provided with required fields:
// Wrong - missing metadata
vitto({
pagesDir: 'src/pages',
});
// Correct - metadata is required
vitto({
metadata: {
siteName: 'My Site', // Required
title: 'My Site', // Required
},
pagesDir: 'src/pages',
}); Minimum required fields:
siteName(string)title(string)
Optional fields:
description(string)keywords(string[] or string)- Any custom metadata fields
Build Hangs or Takes Too Long
Problem: Build process never completes
Solution:
- Check for infinite loops in hooks:
// Bad - infinite recursion
export default defineHooks('data', async () => {
const data = await defineHooks('data', ...)() // Don't call hooks recursively
return data
})
// Good - proper hook implementation
export default defineHooks('data', async () => {
const response = await fetch(url)
return await response.json()
}) - Add timeouts to external API calls:
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000);
try {
const response = await fetch(url, { signal: controller.signal });
return await response.json();
} catch (error) {
if (error.name === 'AbortError') {
console.error('Request timed out');
}
return [];
} finally {
clearTimeout(timeout);
} - Enable verbose logging:
vitto({
metadata: {
siteName: 'My Site',
title: 'My Site',
},
pagefindOptions: {
verbose: true,
},
}); Out of Memory Errors
Problem: JavaScript heap out of memory
Solution:
- Increase Node memory limit:
NODE_OPTIONS="--max-old-space-size=4096" npm run build - Add to
package.json:
{
"scripts": {
"build": "NODE_OPTIONS='--max-old-space-size=4096' vite build"
}
} - Reduce data in hooks:
// Only return necessary fields
export default defineHooks('posts', async () => {
const allPosts = await fetchPosts();
return allPosts.map((post) => ({
slug: post.slug,
title: post.title,
excerpt: post.excerpt,
date: post.date,
// Don't include full content here for list pages
}));
}); Template Errors
“Layout not found”
Problem: Template cannot find layout file
Solution:
- Check the path:
{{ layout "layouts/base.vto" }} - Verify
layoutsDirconfiguration:
vitto({
metadata: {
siteName: 'My Site',
title: 'My Site',
},
layoutsDir: 'src/layouts', // Default
}); - Ensure layout file exists:
ls src/layouts/base.vto “Include not found”
Problem: Cannot find partial/include file
Solution:
- Use correct path relative to
partialsDir:
{{ include "partials/header.vto" }} # Correct
{{ include "header.vto" }} # Also correct if in partialsDir - Check
partialsDirsetting:
vitto({
metadata: {
siteName: 'My Site',
title: 'My Site',
},
partialsDir: 'src/partials',
}); Variable Undefined Errors
Problem: Template variables show as undefined
Solution:
- Check hook is registered:
import postsHook from './hooks/posts';
vitto({
metadata: {
siteName: 'My Site',
title: 'My Site',
},
hooks: {
posts: postsHook, // Make sure this is included
},
}); - Verify hook returns data:
export default defineHooks('posts', async () => {
const data = await fetchData();
console.log('Posts data:', data); // Debug
return data; // Must return something (array, object, etc.)
}); - Use conditional checks in templates:
{{ if posts && posts.length > 0 }}
{{ for post of posts }}
<h2>{{ post.title }}</h2>
{{ /for }}
{{ else }}
<p>No posts found</p>
{{ /if }} - Check metadata is available:
{{# Metadata is always available #}}
<h1>{{ metadata.siteName }}</h1>
<meta name="description" content="{{ metadata.description }}"> HTML Not Rendering (Escaped)
Problem: HTML shows as text instead of rendering
Solution: Use the safe filter:
{{# Wrong - HTML is escaped #}}
{{ content }}
{{# Correct - HTML is rendered #}}
{{ content |> safe }}
{{# Also use safe for renderAssets #}}
{{ renderAssets() |> safe }} Dynamic Routes Issues
Pages Not Generated
Problem: Dynamic route pages aren’t created
Solution:
- Verify configuration:
vitto({
metadata: {
siteName: 'My Site',
title: 'My Site',
},
hooks: {
posts: postsHook,
post: postHook,
},
dynamicRoutes: [
{
template: 'post', // Must match template name (without .vto)
dataSource: 'posts', // Must match hook name
getParams: (post) => ({ slug: post.slug }),
getPath: (post) => `blog/${post.slug}.html`, // Must include .html
},
],
}); - Check template exists:
ls src/pages/post.vto - Ensure hook returns array:
export default defineHooks('posts', async () => {
try {
const posts = await fetchPosts();
return posts; // Must return array
} catch (error) {
console.error('Failed to fetch posts:', error);
return []; // Return empty array on error
}
}); Wrong URLs Generated
Problem: Generated URLs don’t match expected structure
Solution:
- Check
getPathfunction:
{
// For /blog/my-post.html
getPath: (post) => `blog/${post.slug}.html`;
} - For pretty URLs, use
outputStrategy:
vitto({
metadata: {
siteName: 'My Site',
title: 'My Site',
},
outputStrategy: 'directory', // Generates: /blog/my-post/index.html
dynamicRoutes: [
{
template: 'post',
dataSource: 'posts',
getParams: (post) => ({ slug: post.slug }),
getPath: (post) => `blog/${post.slug}.html`,
},
],
}); Parameters Not Available in Template
Problem: Parameters from getParams not accessible
Solution:
- Ensure hook accepts params:
export const postHook = defineHooks('post', async (params) => {
if (!params?.slug) {
console.error('Slug parameter missing');
return null;
}
console.log('Received params:', params);
return await fetchPost(params.slug);
}); - Register parameterized hook:
import { postsHook, postHook } from './hooks/posts';
vitto({
metadata: {
siteName: 'My Site',
title: 'My Site',
},
hooks: {
posts: postsHook, // List of all posts
post: postHook, // Individual post with params
},
dynamicRoutes: [
{
template: 'post',
dataSource: 'posts',
getParams: (post) => ({ slug: post.slug }), // These params go to postHook
getPath: (post) => `blog/${post.slug}.html`,
},
],
}); Hook Errors
Hook Data Not Available
Problem: Hook data doesn’t appear in templates
Solution:
- Check hook is imported and registered:
import myHook from './hooks/myHook';
vitto({
metadata: {
siteName: 'My Site',
title: 'My Site',
},
hooks: {
myData: myHook, // Use this name in templates: {{ myData }}
},
}); - Verify hook exports correctly:
// Named export (good for multiple hooks)
export const myHook = defineHooks('myData', async () => {
return await fetchData();
});
// Default export (preferred for single hook per file)
export default defineHooks('myData', async () => {
return await fetchData();
});
// Both (best practice)
export const myHook = defineHooks('myData', async () => {
return await fetchData();
});
export default myHook; Async Hook Errors
Problem: Hook fails with async/await errors
Solution:
- Always use async/await:
// Wrong - don't use .then()
export default defineHooks('data', () => {
return fetch(url).then((r) => r.json());
});
// Correct - use async/await
export default defineHooks('data', async () => {
const response = await fetch(url);
return await response.json();
}); - Handle errors properly:
export default defineHooks('data', async () => {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
return await response.json();
} catch (error) {
console.error('Hook failed:', error);
return []; // Return fallback data
}
}); File System Hook Issues
Problem: Can’t read files in hooks
Solution:
- Use absolute paths:
import path from 'node:path';
import fs from 'node:fs/promises';
export default defineHooks('data', async () => {
const filePath = path.join(process.cwd(), 'content', 'data.json');
try {
const content = await fs.readFile(filePath, 'utf-8');
return JSON.parse(content);
} catch (error) {
console.error('Failed to read file:', filePath, error);
return {};
}
}); - Check file exists before reading:
import fs from 'node:fs/promises';
try {
await fs.access(filePath);
const content = await fs.readFile(filePath, 'utf-8');
return JSON.parse(content);
} catch (error) {
if (error.code === 'ENOENT') {
console.error('File not found:', filePath);
}
return {};
} Search Problems
Search Not Working
Problem: Pagefind search doesn’t work
Solution:
- Ensure search is enabled:
vitto({
metadata: {
siteName: 'My Site',
title: 'My Site',
},
enableSearchIndex: true, // Default is true
}); - Build in production mode:
npm run build
npm run preview Important: Search doesn’t work in development mode (npm run dev)!
- Check search files exist:
ls dist/_pagefind/ No Search Results
Problem: Search returns no results
Solution:
- Mark content as searchable:
<main data-pagefind-body>
{{ content |> safe }}
</main> - Check
rootSelectorconfiguration:
vitto({
metadata: {
siteName: 'My Site',
title: 'My Site',
},
pagefindOptions: {
rootSelector: 'html', // Default
verbose: true, // Enable for debugging
},
}); - Remove
data-pagefind-ignoreif mistakenly added:
{{# Wrong - excludes from search #}}
<article data-pagefind-ignore>
{{ content |> safe }}
</article>
{{# Correct - includes in search #}}
<article data-pagefind-body>
{{ content |> safe }}
</article> Search Index Too Large
Problem: Search index files are very large
Solution:
- Configure exclusions:
vitto({
metadata: {
siteName: 'My Site',
title: 'My Site',
},
pagefindOptions: {
rootSelector: 'main',
excludeSelectors: ['nav', 'footer', 'aside', '.sidebar', '.comments'],
},
}); - Use
data-pagefind-ignorefor large sections:
<article data-pagefind-body>
<h1>{{ post.title }}</h1>
<div class="content">{{ post.content |> safe }}</div>
<div data-pagefind-ignore>
<div class="comments">
{{# Comments are excluded from search #}}
</div>
</div>
</article> Asset Loading Issues
CSS Not Loading
Problem: Styles don’t apply
Solution:
- Ensure
renderAssets()is in template:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
{{ renderAssets() |> safe }}
</head>
<body>
{{ content |> safe }}
</body>
</html> - Check CSS file is imported:
// src/main.ts or main.js
import './style.css'; - Verify Vite config:
import { defineConfig } from 'vite';
import vitto from 'vitto';
export default defineConfig({
plugins: [
vitto({
metadata: {
siteName: 'My Site',
title: 'My Site',
},
}),
],
// No need to configure CSS, Vite handles it automatically
}); JavaScript Not Executing
Problem: JavaScript doesn’t run
Solution:
- Check script is loaded via
renderAssets():
<head>
{{ renderAssets() |> safe }}
</head> - Verify entry point exists:
ls src/main.ts # or main.js -
Check for JavaScript errors in browser console
-
Ensure module type is correct:
<!-- renderAssets() generates this automatically -->
<script type="module" src="/src/main.js"></script> Images Not Found (404)
Problem: Images return 404 errors
Solution:
- Put static images in
public/directory:
public/
└── images/
└── photo.jpg - Reference without
public/prefix:
<img src="/images/photo.jpg" alt="Photo"> - For processed/optimized images, import them:
// In your script file
import logo from './assets/logo.png';
// logo will be the processed URL Performance Issues
Slow Build Times
Problem: Build takes too long
Solution:
- Cache hook results:
let cache = null;
let cacheTime = 0;
const CACHE_DURATION = 60000; // 1 minute
export default defineHooks('data', async () => {
const now = Date.now();
if (cache && now - cacheTime < CACHE_DURATION) {
console.log('Using cached data');
return cache;
}
console.log('Fetching fresh data');
cache = await fetchExpensiveData();
cacheTime = now;
return cache;
}); - Use
Promise.allfor parallel operations:
// Slow - sequential (10 seconds for 10 files)
const results = [];
for (const file of files) {
results.push(await processFile(file));
}
// Fast - parallel (1 second for 10 files)
const results = await Promise.all(files.map((file) => processFile(file))); - Disable minification in development:
vitto({
metadata: {
siteName: 'My Site',
title: 'My Site',
},
minify: process.env.NODE_ENV === 'production',
}); - Reduce Pagefind verbosity:
vitto({
metadata: {
siteName: 'My Site',
title: 'My Site',
},
pagefindOptions: {
verbose: false,
},
}); Slow Page Loads
Problem: Pages load slowly in production
Solution:
- Enable minification:
vitto({
metadata: {
siteName: 'My Site',
title: 'My Site',
},
minify: true,
}); -
Optimize images before adding to
public/ -
Enable compression in your hosting provider
-
Use CDN for static assets
See Performance Guide for more details.
Deployment Problems
Build Succeeds Locally but Fails on CI/CD
Problem: Production build fails in CI/CD pipeline
Solution:
- Match Node versions:
# .github/workflows/deploy.yml
- uses: actions/setup-node@v4
with:
node-version: '20' # Match your local version - Use
npm ciinstead ofnpm install:
- run: npm ci
- run: npm run build - Set environment variables:
env:
NODE_ENV: production
NODE_OPTIONS: '--max-old-space-size=4096' - Check for missing dependencies:
# Ensure all dependencies are in package.json
npm install <missing-package> --save 404 on Deployed Site
Problem: Pages work locally but show 404 on production
Solution:
- Configure base path for subdirectory deployment:
// vite.config.ts
import { defineConfig } from 'vite';
import vitto from 'vitto';
export default defineConfig({
base: '/repo-name/', // For GitHub Pages or subdirectory
plugins: [
vitto({
metadata: {
siteName: 'My Site',
title: 'My Site',
},
}),
],
}); - Configure hosting for clean URLs (if using
outputStrategy: 'directory'):
Netlify (netlify.toml):
[[redirects]]
from = "/*"
to = "/index.html"
status = 200
Vercel (vercel.json):
{
"cleanUrls": true
} Nginx:
location / {
try_files $uri $uri.html $uri/ =404;
}
Assets Not Loading on Production
Problem: CSS/JS 404 errors on deployed site
Solution:
- Check base path matches deployment:
export default defineConfig({
base: '/my-site/', // Must match your hosting path
plugins: [
vitto({
metadata: {
siteName: 'My Site',
title: 'My Site',
},
}),
],
}); - Verify files exist in
dist/after build:
npm run build
ls dist/assets/ - Ensure hosting serves from correct directory:
- GitHub Pages: Set source to
dist/ordocs/ - Netlify: Set publish directory to
dist - Vercel: Set output directory to
dist
Getting Help
Tip
When debugging, start with the simplest reproduction possible. Check the browser console and build logs for error messages.
Warning
Always backup your vite.config.ts before making major changes. Keep your dist/ in .gitignore to avoid committing build artifacts.
Caution
Be aware that rm -rf node_modules and re-installing will lose any uncommitted dependency changes. Commit or stash before cleaning.
If you’re still stuck after trying these solutions: