---
url: /en/docs/frameworks/vanilla.md
description: >-
  Learn how to integrate Sveltia CMS with vanilla JavaScript sites, from storing
  content as JSON to fetching and rendering it without a build step.
---

# Vanilla JavaScript Integration Guide

Sveltia CMS works with plain HTML, CSS and JavaScript sites, with no framework or build tools required. It’s a popular choice for small business sites, portfolios and event pages that need editable content without the overhead of a framework.

## How It Works

Your repository holds the HTML pages, scripts and content files, and the hosting service publishes them as is:

1. Editors change content in Sveltia CMS, which commits JSON files to the repository.
2. The hosting service publishes the new commit, with no build step.
3. When a visitor opens a page, its JavaScript fetches the JSON files and renders the content.

Sveltia CMS itself is a [single-page application served from a CDN](/en/docs/architecture#how-sveltia-cms-works), so the admin page doesn’t need a build step either.

## Examples

See real-world examples of vanilla JavaScript sites in our [showcase](/en/showcase?framework=vanilla). Most of the listed sites include links to their source code, so you can explore how they implemented Sveltia CMS.

## Setup

### Adding the Admin Page

Create `admin/index.html` and `admin/config.yml` in the folder your site is published from, usually the repository root, as described in the [Getting Started](/en/docs/start#manual-installation) guide. The admin page is then available at `/admin/`.

### Linking to the Admin Page

Link to the admin page with a regular `<a>` element:

```html
<a href="/admin/">Edit content</a>
```

## Configuration

### Storing Content as JSON

Store content in JSON files, which browsers can parse without a library. The following example manages the home page in one file and a list of team members in another:

```yaml [admin/config.yml]
media_folder: images/uploads
public_folder: /images/uploads
collections:
  - name: pages
    label: Pages
    files:
      - name: home
        label: Home Page
        file: data/home.json
        fields:
          - { name: title, label: Title }
          - { name: body, label: Body, widget: markdown }
  - name: members
    label: Team Members
    label_singular: Team Member
    file: data/members.json
    identifier_field: name
    fields:
      - { name: id, label: ID }
      - { name: name, label: Name }
      - { name: role, label: Role, required: false }
      - { name: photo, label: Photo, widget: image, required: false }
```

The two collections work differently:

* The `pages` collection is a [file collection](/en/docs/collections/files). Each file holds the content of one page or section, and the format is determined by the `.json` extension.
* The `members` collection is a [single-file collection](/en/docs/collections/entries/single-file). Editors add, edit, delete and reorder members one by one, and all of them are stored in `data/members.json` as an array of objects.

### Avoiding One File per Entry

An entry collection with the `folder` option stores each entry in a file of its own. Avoid it on a vanilla JavaScript site. A browser can’t list the files in a folder, so your script can’t find the entries without an index.

Use a single-file collection instead, as in the example above. If you need one file per entry, e.g. for long blog posts, you have to generate an index file at deploy time, such as with a small script in a GitHub Actions workflow. At that point, a build step that renders the pages may be simpler. See the [Custom Tooling Integration Guide](/en/docs/frameworks/custom).

### Images

The `media_folder` is where uploaded images are stored in the repository, and the `public_folder` is the URL path written to the content. Start the `public_folder` with a slash, so the image paths work on every page, including pages in subfolders.

If your site is served from a subfolder, e.g. a GitHub Pages project site at `https://username.github.io/repo/`, include the subfolder in the `public_folder`, such as `/repo/images/uploads`.

## Loading Content

### Fetching JSON Files

Fetch the JSON files with the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch). The following script renders the home page title and the list of team members from the example above:

```js [js/main.js]
const loadJSON = async (path) => {
  const response = await fetch(path, { cache: 'no-cache' });

  if (!response.ok) {
    throw new Error(`Failed to load ${path}: ${response.status}`);
  }

  return response.json();
};

const home = await loadJSON('/data/home.json');

document.querySelector('#title').textContent = home.title;

const members = await loadJSON('/data/members.json');

document.querySelector('#members').replaceChildren(
  ...members.map(({ name, role }) => {
    const item = document.createElement('li');

    item.textContent = role ? `${name} (${role})` : name;

    return item;
  }),
);
```

Load the script as a module, which allows top-level `await`:

```html
<script type="module" src="/js/main.js"></script>
```

The `cache: 'no-cache'` option makes the browser check for a newer version of the file on every visit. Without it, visitors may see outdated content for a while after an update. GitHub Pages, for example, lets browsers cache files for 10 minutes.

### Rendering Text Safely

Set plain text with the `textContent` property, as in the example above, rather than `innerHTML`. A value like `<b>Jane</b>` is then shown as is instead of being interpreted as HTML. This also protects the page from cross-site scripting (XSS) if the content contains a malicious script.

### Rendering Markdown

The [RichText field](/en/docs/fields/richtext), set with `widget: markdown` in the example above, saves Markdown text. Convert it to HTML with a library like [Marked](https://marked.js.org/), and sanitize the HTML with [DOMPurify](https://github.com/cure53/DOMPurify) before inserting it into the page. Add the imports to the top of the script, and render the body after loading the home page data:

```js [js/main.js]
import { marked } from 'https://cdn.jsdelivr.net/npm/marked@18/lib/marked.esm.js';
import DOMPurify from 'https://cdn.jsdelivr.net/npm/dompurify@3/dist/purify.es.mjs';

// ...

document.querySelector('#body').innerHTML = DOMPurify.sanitize(marked.parse(home.body));
```

Both libraries are loaded from a CDN as JavaScript modules, so no installation is needed.

## Publishing Changes

Since there’s no build step, the hosting service only has to publish the files in the repository. GitHub Pages with the “Deploy from a branch” option, as well as Netlify and Cloudflare Pages without a build command, publish every new commit automatically. Content changes made in the CMS usually go live within a minute or two.

## Limitations

Content rendered by JavaScript isn’t part of the HTML that the server sends. Google can render JavaScript, but some search engines and most social media link previews read only the HTML, so they don’t see the content.

If search engine optimization matters for your site, render the pages at build time with a static site generator or your own build script. See the [Framework Guides](/en/docs/frameworks) and the [Custom Tooling Integration Guide](/en/docs/frameworks/custom).

## Support for Vanilla JavaScript

We have implemented specific features to enhance the integration of Sveltia CMS with vanilla JavaScript sites:

* [Single-file collections](/en/docs/collections/entries/single-file): Store all the entries of an entry collection in a single JSON file as an array of objects, which your script can fetch with one request. Editors still add, edit, delete and reorder the entries one by one.
* [Top-level List fields](/en/docs/fields/list#top-level-list): Edit a JSON file whose root is an array, such as a navigation menu, as a single entry of a file collection, using the `root` option.
