ContentBox.js · Guide

Using ContentBox.js in a SvelteKit Project

ContentBox.js is a JavaScript library for designing and building complete web pages with simple drag and drop. In this guide we add it to a SvelteKit app. First we embed the editor, then we add saving, uploads and a page that shows the saved page to visitors.

ContentBox comes in two parts:

ContentBox

The editor, used on the page where pages are built.

ContentBox Runtime

Renders the saved page on your site. It makes interactive blocks (sliders, counters, charts, accordions and more) work, both while editing and when viewing.

In this guide

  1. Set up the project
  2. Embed the editor
  3. Add saving and uploads
  4. Show the page to visitors
  5. Try it

Step 1

Set up the project

Create a SvelteKit project and install ContentBox and the Runtime:

npx sv create my-app --template minimal --types ts --no-add-ons --install npm
cd my-app
npm install @innovastudio/contentbox @innovastudio/contentbox-runtime

The editor needs its assets (sections, snippets, icons, fonts, plugins). They are in the ContentBox.js ZIP package, which you can download from your user area. Copy these into your project's static/ folder:

static/
├── assets/      # sections, snippets, fonts, plugins
├── runtime/     # runtime files, used by blank.html and preview.html
├── blank.html   # the page you edit, shown inside the editor
└── preview.html # Page Preview

Step 2

Embed the editor

Create src/routes/edit/+page.svelte:

<script lang="ts">
    import { onMount } from 'svelte'
    import ContentBox from '@innovastudio/contentbox'
    import '@innovastudio/contentbuilder/public/contentbuilder/contentbuilder.css'
    import '@innovastudio/contentbox/public/contentbox/contentbox.css'

    onMount(() => {
        // Create the editor
        const builder = new ContentBox({
            controlPanel: true,
            iframeSrc: '/blank.html', // the page you edit
            snippetUrl: '/assets/minimalist-blocks/content-plugins.js', // the blocks users can drag in
        })

        // Load the page to edit
        builder.loadHtml(`
            <div class="is-section is-box type-system-ui">
                <div class="is-overlay"></div>
                <div class="is-container v2 leading-14 size-18">
                    <div class="row">
                        <div class="column">
                            <h1>Hello World</h1>
                            <p>Start editing your page here.</p>
                        </div>
                    </div>
                </div>
            </div>`)

        return () => builder.destroy()
    })
</script>

<!-- ContentBox adds its own interface to the page -->

Keep the two stylesheets in this order: contentbuilder.css first, then contentbox.css.

The page you edit is shown inside the editor from blank.html. It already starts the runtime, so interactive blocks work while you edit.

The editor runs in the browser only, so turn off server rendering for this page. Create src/routes/edit/+page.ts:

export const ssr = false

Run it:

npm run dev

Open http://localhost:5173/edit. You have a working editor: click the text to edit it, add sections with the + button on the left, and drag in blocks from the Blocks button below it, including interactive ones like a countdown or a chart.

The ContentBox.js editor in a SvelteKit app after step 2, with Hello World

The editor after step 2. Click the text to edit it, add sections with + and blocks from the Blocks panel.

Step 3

Add saving and uploads

Now let's add saving and uploads. In this guide the page is kept in a JSON file, data/content.json, so you can run it without a database.

A page has three parts: the content (html), the styles used on the page (mainCss) and the styles of each section (sectionCss). All three are saved and loaded together.

The server code uses Node's file functions. Install their types:

npm install -D @types/node

Create src/lib/server/content.ts, which reads and writes the page:

import { mkdir, readFile, writeFile } from 'fs/promises'

export type Page = { html: string; mainCss: string; sectionCss: string }

export async function loadPage(): Promise<Page> {
    try {
        return JSON.parse(await readFile('data/content.json', 'utf8'))
    } catch {
        return { html: '', mainCss: '', sectionCss: '' } // nothing saved yet
    }
}

export async function savePage(page: Page) {
    await mkdir('data', { recursive: true })
    await writeFile('data/content.json', JSON.stringify(page))
}

Create the save route, src/routes/api/save/+server.ts:

import { json } from '@sveltejs/kit'
import { savePage } from '#lib/server/content.ts'
import type { RequestHandler } from './$types'

export const POST: RequestHandler = async ({ request }) => {
    const { html, mainCss, sectionCss } = await request.json()
    await savePage({ html, mainCss, sectionCss })
    return json({ ok: true })
}

Create the upload route, src/routes/api/upload/+server.ts, and an empty static/uploads/ folder for the files:

import path from 'path'
import { writeFile } from 'fs/promises'
import { error, json } from '@sveltejs/kit'
import type { RequestHandler } from './$types'

export const POST: RequestHandler = async ({ request }) => {
    const file = (await request.formData()).get('file')
    if (!(file instanceof File)) error(400, 'No file received.')

    const filename = path.basename(file.name).replaceAll(' ', '_')
    await writeFile(path.join('static/uploads', filename), Buffer.from(await file.arrayBuffer()))

    return json({ url: '/uploads/' + filename }) // ContentBox needs { url }
}

The editor page gets the saved page from the server. Create src/routes/edit/+page.server.ts:

import { loadPage } from '#lib/server/content.ts'
import type { PageServerLoad } from './$types'

export const load: PageServerLoad = async () => {
    return { page: await loadPage() }
}

Then replace src/routes/edit/+page.svelte:

<script lang="ts">
    import { onMount } from 'svelte'
    import type { PageProps } from './$types'
    import ContentBox from '@innovastudio/contentbox'
    import '@innovastudio/contentbuilder/public/contentbuilder/contentbuilder.css'
    import '@innovastudio/contentbox/public/contentbox/contentbox.css'

    let { data }: PageProps = $props()

    const DEFAULT_CONTENT = `
    <div class="is-section is-box type-system-ui">
        <div class="is-overlay"></div>
        <div class="is-container v2 leading-14 size-18">
            <div class="row">
                <div class="column">
                    <h1>Hello World</h1>
                    <p>Start editing your page here.</p>
                </div>
            </div>
        </div>
    </div>`

    // Sends the file to the upload route, which returns { url }
    async function upload(file: File) {
        const formData = new FormData()
        formData.append('file', file)
        const response = await fetch('/api/upload', { method: 'POST', body: formData })
        return response.json()
    }

    let builder: ContentBox | null = null

    onMount(() => {
        // Create the editor
        builder = new ContentBox({
            controlPanel: true,
            iframeSrc: '/blank.html', // the page you edit
            snippetUrl: '/assets/minimalist-blocks/content-plugins.js', // the blocks users can drag in
            topSpace: true,           // leaves room at the top for our toolbar
            upload,                   // handles image, video, audio and file uploads
        })

        // Load the saved page, or the default the first time
        builder.loadHtml(data.page.html || DEFAULT_CONTENT)
        builder.loadStyles(data.page.mainCss || '', data.page.sectionCss || '')

        return () => {
            builder?.destroy()
            builder = null
        }
    })

    // Sends the page to the save route
    async function save() {
        if (!builder) return
        await fetch('/api/save', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({
                html: builder.html(),
                mainCss: builder.mainCss(),       // styles used on the page
                sectionCss: builder.sectionCss(), // styles of each section
            }),
        })
    }
</script>

<div style="position: fixed; top: 0; left: 0; right: 0; height: 55px; z-index: 10;
            display: flex; align-items: center; justify-content: flex-end; gap: 8px;
            padding: 0 20px; background: #fff; border-bottom: 1px solid #eee">
    <button onclick={() => builder?.openPagePreview()}>Preview</button>
    <button onclick={async () => { await save(); window.open('/view', '_blank') }}>View page</button>
    <button onclick={async () => { await save(); alert('Page saved!') }}>Save</button>
</div>

What's new

  • Saving: html(), mainCss() and sectionCss() return the three parts of the page, which are sent to the save route. The next time, +page.server.ts gets the saved page from the server, and loadHtml() and loadStyles() load it into the editor.
  • Uploads: upload is one function for all media. It sends the file to the upload route, which returns { url }.
  • Toolbar: topSpace leaves room at the top for it. Preview shows the page at any screen width (using static/preview.html), Save saves, and View page saves and opens the viewing page, which we add next.
The ContentBox.js editor in a SvelteKit app, with a saved page loaded back in

A saved page, with its styles, loads back into the editor the next time you open it.

Step 4

Show the page to visitors

The viewing page is rendered on the server, so the page is already in the HTML that browsers and search engines receive. This is good for SEO. The page needs only the runtime, not the editor.

Create src/routes/view/+page.server.ts:

import { loadPage } from '#lib/server/content.ts'
import type { PageServerLoad } from './$types'

export const load: PageServerLoad = async () => {
    return { page: await loadPage() }
}

Create src/routes/view/+page.svelte:

<script lang="ts">
    import { onMount } from 'svelte'
    import type { PageProps } from './$types'
    import ContentBoxRuntime from '@innovastudio/contentbox-runtime'
    import '@innovastudio/contentbox-runtime/dist/contentbox-runtime.css'

    let { data }: PageProps = $props()

    // Activate interactive blocks in the page rendered by the server
    onMount(() => {
        const runtime = new ContentBoxRuntime({
            pluginFolder: window.location.origin + '/assets/plugins/',
        })
        runtime.init()
        return () => runtime.destroy()
    })
</script>

<svelte:head>
    <!-- The saved styles: used on the page, and of each section -->
    {@html data.page.mainCss + data.page.sectionCss}
</svelte:head>

<!-- is-wrapper is required for page styling -->
<div class="is-wrapper">
    {@html data.page.html || '<p>No content available.</p>'}
</div>

Important: the viewing container must have the is-wrapper class.

pluginFolder tells the runtime where the plugins are. A plugin's files are loaded only when the page uses it.

Step 5

Try it

With npm run dev running, open http://localhost:5173/edit, add some sections and blocks, and click Save. Click View page to see the saved page.

The SvelteKit viewing page showing a page saved with ContentBox.js

The viewing page, rendered on the server, shows the saved page to visitors.

Going to production

  • Saving: replace the JSON file in src/lib/server/content.ts with your database or CMS. Save all three parts: the content, mainCss and sectionCss.
  • Uploads: send files to your storage (S3 or any other) and return { url }. After npm run build, SvelteKit serves only the files that were in static/ at build time, so new uploads saved there don't show.
  • Security: add authentication to the save and upload routes, so only your editors can save and upload.

Resources

  • ContentBox.js innovastudio.com/contentbox
  • Documentation on GitHub github.com/innovastudio-dev/contentbox
  • In the package: the same documentation is in its docs/ folder (docs/README.md to get started, docs/reference.md for all options)

© 2026 InnovaStudio

This site is built with Site Builder Kit