Custom Tooling Integration Guide
Many sites don’t use a framework or static site generator. Instead, they’re built with their own tooling, usually one of these:
- a Vite app, with or without a UI library like React or Vue
- a hand-written build script in Node.js, Python or another language that turns content files into HTML pages
Since every setup is different, this guide gives general advice rather than step-by-step instructions. Sveltia CMS only reads and writes files in your Git repository, so it works with any tooling that can read those files.
Examples
See real-world examples of custom tooling in our showcase. Many sites listed under React and Vue are Vite apps as well. Most of the listed sites include links to their source code, so you can explore how they implemented Sveltia CMS.
Vite
Adding the Admin Page
Vite copies the files in the public folder to the build output unaltered. Create public/admin/index.html and public/admin/config.yml as described in the Getting Started guide. The admin page is then available at /admin/ on the built site.
The development server started with vite is different. Its fallback for single-page apps serves your app’s own index.html for /admin/ and /admin, so only /admin/index.html shows the admin page. To fix this, add a small plugin to the Vite configuration that rewrites these paths:
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [
{
name: 'sveltia-cms-admin',
configureServer(server) {
server.middlewares.use((req, res, next) => {
if (req.url === '/admin' || req.url === '/admin/') {
req.url = '/admin/index.html';
}
next();
});
},
},
],
});The built site doesn’t have this problem on common hosting services. On Netlify, Vercel and Cloudflare Pages, a rewrite rule that sends every path to index.html doesn’t apply to files that exist, so /admin/ still gets the admin page.
Linking to the Admin Page
Link to the admin page with a regular <a> element and a trailing slash:
<a href="/admin/">Edit content</a>The trailing slash matters on vite preview and some hosting services, which serve your app instead of the admin page at /admin. If your app uses a client-side router, avoid its link component, such as React Router’s <Link>. The router handles the link itself and shows its own “not found” page.
Configuration
Store content files under src, e.g. in src/content, and import them into your app as described below. Avoid putting them in public, because Vite would publish the raw files as well as bundle them. The following example manages site settings in a single JSON file and blog posts as Markdown files, with images in public/uploads:
media_folder: public/uploads
public_folder: /uploads
collections:
- name: settings
label: Settings
files:
- name: site
label: Site Settings
file: src/content/site.json
fields:
- { name: title, label: Site Title }
- { name: description, label: Description, widget: text }
- name: posts
label: Blog Posts
label_singular: Blog Post
folder: src/content/posts
create: true
fields:
- { name: title, label: Title }
- { name: date, label: Date, widget: datetime, type: date }
- { name: image, label: Cover Image, widget: image, required: false }
- { name: body, label: Body, widget: markdown }The media_folder must be inside public so the images are copied to the build output, while the public_folder is the URL path written to the content. See Internal Media Storage for more options.
If your site is served from a subfolder, e.g. a GitHub Pages project site with the base option set to /repo/, image paths like /uploads/photo.jpg don’t include the subfolder. Prefix them with import.meta.env.BASE_URL when you render them.
Loading Content
Vite bundles the content files with your app at build time, so there’s no need to fetch them on the client side.
JSON Files
Vite can import JSON files directly. This works well for a file in a file collection:
import site from './content/site.json';An entry collection stores each entry in its own file. Vite’s import.meta.glob function imports all the files in a folder. The following example reads products from a collection with the format option set to json:
type Product = { name: string; price: number; image: string };
const files = import.meta.glob<Product>('../content/products/*.json', {
import: 'default',
eager: true,
});
export const products = Object.entries(files).map(([path, product]) => ({
slug: path.match(/([^/]+)\.json$/)![1],
...product,
}));YAML Files
Vite can’t import YAML files by itself. Add the @rollup/plugin-yaml plugin to the plugins option in the Vite configuration, and then import YAML files the same way as JSON files.
Markdown Files
Add query: '?raw' to the import.meta.glob options to import each Markdown file as a string. Then split the front matter from the body and parse it with a YAML library like yaml:
import { parse } from 'yaml';
const files = import.meta.glob<string>('../content/posts/*.md', {
query: '?raw',
import: 'default',
eager: true,
});
export const posts = Object.entries(files).map(([path, raw]) => {
const [, frontMatter = '', body = raw] =
raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/) ?? [];
return { slug: path.match(/([^/]+)\.md$/)![1], ...parse(frontMatter), body };
});Render the body with a Markdown library like Marked. Avoid gray-matter in browser code, because it relies on the Node.js Buffer API and throws an error in the browser.
Custom Build Scripts
Adding the Admin Page
A build script usually copies a folder of static files, like CSS and images, to the output folder. Create admin/index.html and admin/config.yml in that folder as described in the Getting Started guide, or have the script copy the admin folder to the output separately.
Make sure the script copies both files unaltered. A template engine or HTML minifier may change the admin page, e.g. by removing the CMS script.
Link to the admin page with <a href="/admin/">.
Configuration
Set the media_folder to a folder that the script copies to the output, and the public_folder to the URL path of that folder on the site. Content files can be stored anywhere outside the output folder, because the script reads them.
Loading Content
Read the content files with the file system API of your language and parse them with a library for the format:
- JSON files: use the built-in parser, such as
JSON.parse()in JavaScript or thejsonmodule in Python. - YAML files: use a library like yaml or js-yaml for Node.js, or PyYAML for Python.
- Markdown files: split the front matter from the body with gray-matter for Node.js or python-frontmatter for Python, then render the body with a Markdown library.
The DateTime field with type: date saves an unquoted date like 2026-10-01. Parsers don’t agree on how to read it:
- gray-matter, PyYAML and js-yaml 4 return a date object.
- yaml and js-yaml 5 return a string.
Check what your parser returns before formatting or sorting dates.
Publishing Changes
Either way, the content is built into your site, so the site must be rebuilt to reflect content changes made in the CMS. Most hosting services, including Netlify, Vercel and Cloudflare, can run your build command automatically when a commit is pushed to the repository. For GitHub Pages, use a GitHub Actions workflow that runs the build and deploys the output.
If you’d rather skip the build step for content, place the files in a published folder, such as public in a Vite app, and fetch them on the client side, as described in the Vanilla JavaScript Integration Guide.