---
url: /en/docs/frameworks/custom.md
description: >-
  Learn how to integrate Sveltia CMS with custom tooling, such as a Vite app or
  a hand-written build script, from the admin page to loading content.
---

# 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](https://vite.dev/) 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](/en/showcase?framework=custom). Many sites listed under [React](/en/showcase?framework=react) and [Vue](/en/showcase?framework=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](https://vite.dev/guide/assets#the-public-directory) to the build output unaltered. Create `public/admin/index.html` and `public/admin/config.yml` as described in the [Getting Started](/en/docs/start#manual-installation) 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:

```js [vite.config.js]
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:

```html
<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](#loading-content). 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`:

```yaml [public/admin/config.yml]
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](/en/docs/media/internal) for more options.

If your site is served from a subfolder, e.g. a GitHub Pages project site with the [`base`](https://vite.dev/config/shared-options#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](/en/docs/collections/files):

```js
import site from './content/site.json';
```

An [entry collection](/en/docs/collections/entries) stores each entry in its own file. Vite’s [`import.meta.glob`](https://vite.dev/guide/features#glob-import) function imports all the files in a folder. The following example reads products from a collection with the [`format`](/en/docs/collections/entries/formats#format) option set to `json`:

```ts [src/lib/products.ts]
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`](https://github.com/rollup/plugins/tree/master/packages/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](https://eemeli.org/yaml/):

```ts [src/lib/posts.ts]
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](https://marked.js.org/). Avoid [gray-matter](https://github.com/jonschlinkert/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](/en/docs/start#manual-installation) 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 the `json` module in Python.
* YAML files: use a library like [yaml](https://eemeli.org/yaml/) or [js-yaml](https://github.com/nodeca/js-yaml) for Node.js, or [PyYAML](https://pyyaml.org/) for Python.
* Markdown files: split the front matter from the body with [gray-matter](https://github.com/jonschlinkert/gray-matter) for Node.js or [python-frontmatter](https://github.com/eyeseast/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](/en/docs/frameworks/vanilla).
