ContentBox.js · Guide
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
Step 1
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-runtimeThe 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 PreviewStep 2
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 = falseRun it:
npm run devOpen 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 editor after step 2. Click the text to edit it, add sections with + and blocks from the Blocks panel.
Step 3
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/nodeCreate 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
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.upload is one function for all media. It sends the file to the upload route, which returns { url }.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.
A saved page, with its styles, loads back into the editor the next time you open it.
Step 4
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
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 viewing page, rendered on the server, shows the saved page to visitors.
src/lib/server/content.ts with your database or CMS. Save all three parts: the content, mainCss and sectionCss.{ 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.save and upload routes, so only your editors can save and upload.docs/ folder (docs/README.md to get started, docs/reference.md for all options)