Introduction
Klar is a visual CMS for static websites. Edit text, images, links, and icons directly on your live webpage, then save the changes back to GitHub.
What You Can Do#
- Click-to-edit — hover to highlight, click to edit any text, heading, button, or link
- Structured panel — edit text, attributes (href, src, alt, class, style), and raw HTML in a sidebar form
- Image and media management — upload, browse, and swap images from your GitHub-backed media library
- Icon picker — search and insert icons from Heroicons, Material Design, and Lucide libraries
- SVG editing — edit SVG attributes like stroke, fill, and viewBox, or replace SVGs entirely
- Link editing — edit href on any anchor element, even when the link wraps images or other content
- List/container editing — add, remove, reorder, and duplicate items in repeating containers like lists and grids
- Block editor — split paragraphs, reorder headings and list items, save reusable templates, and duplicate blocks with a floating toolbar
- Rich text and tables — a rich text inline editor for paragraphs and content blocks, with full table cell, row, and column editing
- Code editor — edit HTML, CSS, and JavaScript directly with syntax highlighting and live preview
- AI assistant — ask AI to make changes to your page with natural language
- Undo and redo — step backwards and forwards through your pending edits with Cmd+Z / Cmd+Shift+Z
- Multi-page management — create, duplicate, rename, and delete pages in your repo
- Shared sections — edit a header or footer once and sync it across all pages
- Save and publish — commit your edits to GitHub and publish to GitHub Pages with one click
- Version history — browse previous versions of your page and restore any commit
- Members and invites — invite collaborators to a project as admin or editor via email
- Form submissions — accept contact-form posts from your live site straight into a Klar inbox
- Visitor accounts and apps — let visitors sign in and build interactive apps (bookings, comments, RSVPs) with the Users SDK: per-user data, shared collections with server-enforced rules, an owner admin view, and email/SMS automations — no backend to run
- Responsive preview — test your page at desktop, tablet, and phone sizes, or freely resize the viewport
- Light and dark themes — toggle the editor chrome to match your preference
How It Works#
Go to klar.website, sign in with Google, and
connect your GitHub repository. Klar loads your page in a live
preview. Click on any element to start editing. When you're
done, hit Save — your changes are committed directly to your
repo as clean HTML.
<!-- Your original HTML -->
<section class="hero">
<h1>Welcome to My Site</h1>
<p>This is the description.</p>
</section>
<!-- After editing with Klar — still clean! -->
<section class="hero">
<h1>Welcome to My Updated Site</h1>
<p>A better description here.</p>
</section>
Setup & Login
Sign in to Klar, connect your GitHub repository, and create your first project.
Sign In#
Go to klar.website and sign in with your Google account. This gives you access to the project dashboard where you can create and manage multiple websites.
What You Need#
- A GitHub repository with your static website HTML files
-
A GitHub personal access token with
repoandpagesscope (needed to save and publish changes)
repo and
pages scope. Your token is encrypted before
being stored.
Create a Project#
From the dashboard, click New Project. Fill in the following:
| Field | Description |
|---|---|
| Website URL | The live URL of your site (used to load your page and resolve relative paths) |
| GitHub Repository |
In owner/repo format, e.g.
myname/mysite
|
| GitHub Token | Your personal access token for reading and writing files |
| HTML File |
Path to the HTML file in your repo, e.g.
index.html
|
You can also optionally set an Images Folder (for the media library), CSS File and JS File paths (for the code editor), and more. See Project Settings for all options.
The Dashboard#
Your dashboard shows all your projects. You can search projects by repository name or website URL, sort by recent or alphabetically, and click any project to open it in the editor. Each project card shows the repository, website URL, and last updated time. Use the gear icon to edit settings, or the delete button to remove a project.
Quick Start
Go from sign-in to your first edit in under a minute.
1. Sign In & Create a Project#
Go to klar.website, sign in with Google, and create a new project with your GitHub repository details (repo, token, and HTML file path).
2. Start Editing#
Klar loads your page in a live preview. Hover over any element to see it highlighted with a blue outline. Click to select it — text elements become editable right on the page, while images and SVGs open the structured panel on the right.
Use the sections panel on the left to navigate between page sections like header, content areas, and footer. Click a section name to scroll directly to it.
3. Make Changes#
Edit text by typing directly in the preview. Use the structured panel to change attributes like links, image sources, alt text, CSS classes, and styles. You can also open the code editor for direct HTML, CSS, and JavaScript editing, or use the AI assistant to make changes with natural language.
The toolbar shows a badge with your pending edit count. Each change is tracked and can be individually reverted from the structured panel.
4. Save & Publish#
Press Cmd+Shift+S (or click Save) to commit your changes to GitHub. Your HTML stays clean — no wrappers or data attributes are injected.
Press Cmd+Shift+P (or click Publish) to also deploy your site to GitHub Pages. Klar creates a dedicated publish branch and enables Pages automatically.
Visual Editing
Click directly on your page to edit text, images, links, icons, and more.
How It Works#
Your website loads in a live preview inside the editor. Hover over any element to see it highlighted with a blue outline. Click to select it — the element becomes editable directly on the page.
Editable Elements#
By default, Klar makes the following element types editable:
- Text elements — headings (h1–h6), paragraphs, spans, links, buttons, list items, table cells, labels, blockquotes, and more
- Images — click to change the source, alt text, or pick a new image from the media library
- SVGs & Icons — edit SVG attributes (stroke, fill, viewBox, width, height) or replace the icon entirely using the icon picker
- Links — edit the href on any anchor element, even when the link wraps images or other content
-
Background images — swap background images
on elements with the
:style(background-image)selector
Text Editing#
Click any text element to start typing directly on the page.
Your changes are tracked and shown in the structured panel on
the right. Line breaks are preserved using
<br> tags. Press Enter to
confirm and deselect, or Shift+Enter to insert a
line break inside the field.
Paragraphs and content blocks open a rich text editor for bold, italic, headings, lists, and links. Pasted content is sanitized so styling from the source is stripped out and the result stays clean.
Block Toolbar#
When you hover a block inside a list, grid, or content group, a small floating toolbar appears next to it. From the toolbar you can:
- Add — open a menu of block types and your saved templates, then insert the one you pick
- Duplicate — copy the current block, including any connected partner element
- Move up / Move down — reorder blocks inside their container
- Save as template — store the block's HTML as a reusable template for this project
- Delete — remove the block from the page
The toolbar follows the hovered block and adapts its position for both vertical lists and horizontal rows.
Table Editing#
Click inside any table cell to edit its contents. A cell toolbar lets you add or remove rows and columns, while text inside each cell is edited with the same rich text controls as paragraphs.
Undo & Redo#
Press Cmd+Z to step backwards through your pending edits and Cmd+Shift+Z (or Cmd+Y) to step forwards again. Undo also covers structural changes like add, duplicate, reorder, and delete inside the block toolbar — the iframe is kept in sync so the preview always matches the underlying edit state.
Container & List Editing#
For repeating elements like lists, feature grids, or card layouts, Klar provides a dedicated container editor. Hover over the container and click the "Edit list" button that appears. This opens a modal where you can:
- Edit text fields within each item
- Add new items
- Remove items
- Reorder items
- Duplicate items
Containers are detected automatically, or you can mark them
with the [] selector suffix or the
data-klar-list attribute. See
Custom Selectors.
Sections Navigation#
The left panel shows your page structure organized by sections (header, content areas, footer, etc.). Click any section name to scroll the preview directly to it. Sections are auto-detected from your HTML structure, or you can configure a custom sections selector in Project Settings.
Script Toggle#
By default, JavaScript in your page is disabled in the editor to prevent interference with editing. You can enable it from the Editor tab in project settings to test dynamic content like animations or interactive elements.
Structured Panel
The sidebar form for precise editing of text, attributes, and HTML.
Overview#
The structured panel is the right sidebar. When you select an element in the visual editor, the panel shows editable fields for that element. You can toggle the panel on or off from the toolbar.
What You Can Edit#
Depending on the element type, the panel shows different fields:
| Field | Available On | Description |
|---|---|---|
| Text | All text elements | Edit the text content with live preview |
| Href | Links (<a>) |
Edit the link URL or anchor reference |
| Src | Images | Change image source via URL or media picker |
| Alt | Images | Edit alt text for accessibility |
| Classes | All elements | Edit CSS class names |
| Style | All elements | Edit inline CSS styles |
| Background Image | Elements with background-image | Swap background images via media picker |
| SVG attributes | SVG elements | Edit stroke, fill, viewBox, width, height, stroke-width, stroke-linecap |
| Aria-label | Headings (h1–h6) | Edit accessibility labels |
Element List#
When no element is selected, the panel shows a scrollable list of all editable elements on the page. Each element card shows:
- A color-coded tag badge (green for headings, blue for links, etc.)
- The CSS selector (truncated)
- A text preview
- Edit status indicators for changed fields
Click the locate icon on any element to highlight and scroll to it in the preview. Click the revert button to undo changes for that element.
Edit Tracking#
Edited fields are highlighted with a yellow border, and the original value is shown below for comparison. Each field has an individual revert button to undo that specific change. The toolbar shows a badge with the total number of pending edits.
Raw HTML Editing#
Every selected element exposes a raw HTML view powered by
CodeMirror. Open it to edit the element's
outerHTML directly, with HTML syntax
highlighting. Useful for replacing an SVG icon with custom
markup or for surgical fixes that don't fit the structured
fields.
Pick Mode#
Some settings (like the sections selector) accept a CSS selector. Click the Pick button next to the field and then click any element on the page — Klar copies the element's selector into the field for you, so you don't have to write it by hand.
Blocks Tab#
When the selected element is a section or container, the blocks tab lists every child block as a draggable row. Expand a row to edit its text fields, drag the handle to reorder, duplicate or delete from the inline action icons, or save the block as a reusable template for this project.
Link Bubble#
Editing a link href shows a small bubble above the input with
a
Pick link button to choose another page or
anchor in your repo, plus a Choose file button
to point the link at a file from your media library. The picker
lists the HTML files in your project and any in-page
id
targets; picked pages are inserted as root-relative paths
(e.g.
/blog/post.html) so internal links keep working
after you rename or move pages.
Code Editor
Edit HTML, CSS, and JavaScript directly with syntax highlighting and live preview.
Opening the Code Editor#
Toggle the code editor from the toolbar. The editor panel can be positioned on the left, right, or bottom of the screen, and you can resize it by dragging the edge.
HTML, CSS & JS Tabs#
The code editor has separate tabs depending on your project configuration:
- HTML — always available. Edit the full HTML source of your page.
- CSS — available when you've configured a CSS file path in project settings. Changes are previewed live in the visual editor.
- JS — available when you've configured a JS file path in project settings. Changes are previewed live.
CSS and JS changes are saved alongside HTML when you click Save or Publish.
Features#
- Syntax highlighting for HTML, CSS, and JavaScript
- Line numbers
- Code folding (fold state is saved per project)
- Click a section in the left panel to jump to the relevant HTML
- Apply button to load your modified HTML into the visual preview
AI Assistant
Use natural language to make changes to your page with AI-powered editing.
Overview#
The AI chat panel lets you describe changes in plain English and have them applied to your page. Toggle it from the toolbar; like the code editor it can dock on the left, right, or bottom. You bring your own AI provider key (added in settings), so you pick the model and stay in control of cost.
Modes#
Choose a mode under the message box — it tells the AI what kind of change you want:
| Mode | What it does |
|---|---|
| Page | Generate a complete new page from a prompt. |
| Modify page | Change the current page; returns the full updated document so you can review a diff before applying. |
| Section | Generate a self-contained block or section to insert into the page. |
| Edit | Change the element you selected in the editor. |
| Chat | Ask questions about your site without generating code. |
| Site / Site Strict | Generate a complete multi-page website from a brief (Strict also builds a reusable design system). |
Editing a selected element#
In Edit mode, click any element in the visual editor and the AI gets its context — ask "make this heading larger" or "change this button to blue" and it knows which element you mean. This works on normal HTML and on elements rendered at runtime by JavaScript or React: for those there's no static HTML to swap, so the AI finds the code that generates the element and edits that instead.
Reviewing & applying changes#
Nothing touches your page until you apply it. When the AI produces code you'll see apply buttons — and for whole-page changes (Modify page, and Edit on a runtime element) you can open View diff to see a before/after of the entire page first.
- Apply / Replace Element — apply the change
- View diff — review the full before/after first
- Apply CSS / JS — apply style or script changes
What the AI can see#
Chips under the message box control the context sent with your prompt — the page HTML, your project CSS and JS, other Site files, and your design system. Modes that need the page (Modify page, Edit) attach it automatically; the chips are a manual override for the other modes. A lit chip means that context is being included.
Design system mode#
Turn on DS to have the AI reuse your
project's existing classes and tokens instead of inventing new
styles. In Strict it only reuses what already
exists; otherwise it can extend your
style.css when a needed style is genuinely
missing — keeping generated markup consistent with the rest of
your site.
Media & Icons
Upload, browse, and swap images from your GitHub repo, and search icons from multiple libraries.
Media Library#
When you click an image element, the structured panel shows the current source and alt text, plus a button to open the media library. The media library has three tabs:
- Images — browse existing images from your configured media folder in GitHub
- Upload — drag and drop or click to upload new images directly to your repo
- URL — enter a direct image URL
You can also delete unused images and copy file URLs. File sizes are displayed for each image.
Assets Folder#
If you configure an assets folder in project settings, you also get a file browser for non-image files (fonts, documents, etc.) with upload and delete capabilities.
Icon Picker#
For SVG icons, click the icon picker button when an SVG element is selected. The icon picker includes:
- Heroicons, Material Design Icons, and Lucide libraries
- Real-time search across all icon sets
- Visual grid preview
- Click any icon to insert it as inline SVG, replacing the current icon
The picker remembers your last selected library and search query.
Background Images#
Elements with CSS background images can be edited through the
media library too. Use the
:style(background-image) selector modifier or the
data-klar-style-bg HTML attribute to enable this
on specific elements.
Page Management
Create, duplicate, rename, and delete pages in your repository.
Page Switcher#
The page switcher dropdown in the toolbar shows the current file path and lets you switch between all HTML files in your repository. Click any page to load it in the editor.
Page Actions#
- Create page — add a new HTML file to your repository
- Duplicate page — copy an existing page with a new filename
- Rename page — change a page's filename
- Delete page — remove a page from your repo (requires confirmation)
All page operations are committed directly to your GitHub repository.
Page Types#
A page type is a reusable template for pages
that share a structure and the same
content fields —
blog posts, products, team members. A page type is itself an
editable page (for example
page-types/blog-post.html); when you create or
duplicate a page from it, the layout and field definitions
come along, and each new page stores its own field values.
Those values feed
listing templates
and the content API.
Per-Page Settings#
Editor settings can be overridden for a single page without touching the rest of the site — useful when one page needs a different editable scope or shared-section behavior. See Per-Page Overrides.
Open Website#
Use the "Open website" link in the toolbar to open the live version of your site in a new tab, using your configured website URL and current file path.
Save & Publish
Commit your edits to GitHub and deploy to GitHub Pages.
Saving#
Click Save in the toolbar (or press Cmd+Shift+S) to commit your changes to GitHub. Klar applies your edits to the original HTML file in your repo and creates a new commit. Your HTML output is always clean — no editor artifacts, wrappers, or data attributes are added.
If you have CSS or JS files configured, any changes made in the code editor are saved alongside the HTML in the same operation.
Publishing#
Click Publish (or press Cmd+Shift+P) to save your changes and deploy to GitHub Pages. Publishing does everything Save does, plus:
-
Creates or updates a dedicated
klar-publishbranch from the latest commit - Enables GitHub Pages on that branch
- Returns the live Pages URL so you can see your site immediately
Pages that contain listing templates are re-rendered at publish, so blog indexes and card grids always reflect the latest content you've saved.
Shared Section Sync#
If you have shared sections configured and auto-sync enabled, any edits to those sections are automatically pushed to all other pages when you save. The success toast shows how many pages were synced.
Version History
Browse previous versions of your page and restore any past commit.
Viewing History#
Open the version history tab in the structured panel to see a list of commits for the current HTML file. Each entry shows:
- Commit message
- Author name
- Date and time
- Commit SHA
The list is paginated — use the First / Prev / Next / Last controls to page through history (Next and Last load older commits).
Restoring a Version#
Click any commit to load that version of the page into the editor. You can review the content, then either save it as the new current version or click Back to current version to return to the latest version.
Members & Invites
Invite collaborators to a project and control what they can change.
Roles#
Each member of a project has one of two roles:
- Admin — can edit content, manage members, rotate the form token, and change project settings. The project owner is always an admin and can't be removed.
- Editor — can edit and save content but can't manage members or change project-level settings.
Inviting a Member#
Open project settings and switch to the Members tab. Type the invitee's email address, choose a role, and click Invite. Klar generates a tokenized invite URL for that email address only.
If SMTP is configured, the invite link is sent automatically from the Klar mail address. If SMTP isn't configured, the invite URL is shown in the panel so you can copy and share it manually.
Accepting an Invite#
The recipient opens the invite URL and signs in with the Google account that matches the invited email address. After accepting, the project shows up in their dashboard.
Managing Members#
Admins can remove a member from the Members tab. Two safety checks apply automatically:
- You can't remove yourself.
- You can't remove the last remaining admin — promote someone else first if you want to leave.
Form Submissions
Accept contact-form posts from your live site straight into a Klar inbox.
Overview#
Every project gets a unique, tokenized submission URL of the
form
POST /api/forms/<form-token>/submit. Point
an HTML <form> on your customer site at
that URL and the submitted fields land in your Klar inbox.
Connecting a Form#
The Forms tab in project settings shows the submission URL and a ready-made HTML snippet you can paste into your site:
<form action="https://klar.website/api/forms/YOUR_TOKEN/submit" method="POST">
<input name="name" placeholder="Your name" required>
<input type="email" name="email" placeholder="Your email" required>
<textarea name="message" placeholder="Message" required></textarea>
<!-- Honeypot: hidden from users, bots fill it in and get silently dropped. -->
<input type="text" name="_gotcha" style="display:none" tabindex="-1" autocomplete="off">
<button type="submit">Send</button>
</form>
Anti-Abuse Defenses#
-
Origin allowlist — submissions are only
accepted from the project's
websiteUrl, the matching GitHub Pages host, and any extra origins you list in Additional Origins. -
Honeypot fields — any input whose
namestarts with an underscore (for example_gotcha) is treated as a honeypot. If a bot fills it in, the submission is silently dropped. - Rate limit — capped at 20 submissions per hour per IP and per project.
- Body size cap — request bodies larger than 256 KB are rejected.
Inbox#
Submissions are stored in Klar and listed in the Forms tab. Click a row to expand it, mark it as read, or delete it. Unread submissions are highlighted. If SMTP is configured, the project owner also receives an email for each submission with the sender's email set as Reply-To.
Rotating the Token#
If the submission URL leaks or starts receiving abuse you can't filter, click Regenerate in the Forms tab. The old URL stops accepting submissions immediately — update the form on your site with the new URL afterwards.
Project Settings
Configure your project with repository details, editing scope, media paths, and more.
Opening Settings#
Click the gear icon in the toolbar to open the settings modal. Settings are organized into tabs: General, GitHub, Editor, Members, Forms, Collections, SMS, Tags, Publishing, and Danger Zone. Tabs that depend on a saved project (Members, Forms, Collections, SMS, Tags) are disabled until you create the project for the first time.
General Tab#
Page metadata that's written into the
<head> of your saved HTML:
| Setting | Description |
|---|---|
| Title |
The browser-tab and search-result title
(<title>)
|
| Meta description |
The search-result snippet (meta name="description") — aim for about 150–160 characters
|
| Favicon |
URL or path to the icon shown in browser tabs (link rel="icon")
|
Use the Project / Page toggle to set a default for the whole site or override the metadata just for the page you have open.
GitHub Tab#
| Setting | Description |
|---|---|
| Website URL | The live URL of your site (used to load the page and resolve relative paths like images and CSS) |
| GitHub Token | Your personal access token for reading and writing files. Encrypted before storage. |
| GitHub Repository | Your repo in owner/repo format |
| HTML File |
Path to the HTML file in your repo (e.g.,
index.html or
pages/about.html)
|
| CSS File | Optional path to a CSS file for live editing in the code editor |
| JS File | Optional path to a JS file for live editing in the code editor |
| Images Folder |
Path to the folder where media library images are
stored (e.g., images)
|
| Assets Folder | Optional path for non-image files (fonts, documents, etc.) |
Editor Tab#
| Setting | Description |
|---|---|
| Editable Elements | CSS selectors that define which areas of your page are editable. See Custom Selectors. |
| Sections Selector | CSS selector that defines how the left panel discovers page sections. Leave empty for auto-detection. |
| Shared Sections |
Selectors for sections that appear on all pages (e.g.,
nav, footer). See
Shared Sections.
|
| Sync Shared Sections on Save | Toggle to automatically sync shared sections to all pages when saving |
| Show "Sync to all pages" in left panel | Toggle to show sync icons on shared sections in the sections panel |
| JavaScript | Toggle to enable or disable JavaScript execution in the editor preview |
Members Tab#
Invite collaborators to a project by email. Each member is either an admin (can manage settings and other members) or an editor (can edit and save content). See Members & Invites.
Forms Tab#
Each project gets a tokenized form-submission URL you can
point HTML <form> elements at. Manage
allowed origins, regenerate the token, and read inbound
submissions here. See
Form Submissions.
Collections Tab#
Define the shared data collections your site's
apps use — bookings, comments, RSVPs — and the integrity rules
Klar enforces on every write: unique,
no-overlap (makes double-booking impossible),
and capacity. Apps read and write these through
the Users SDK
(user.collection()), and because the rules live on
the server a client can't get around them. (Available once the
project has been saved.)
SMS Tab#
Connect your own 46elks account — a developer-friendly SMS API that sends worldwide — so your site's automations can text people (booking confirmations, reminders). Paste the API username + password and pick a sender name (your brand); the credentials are stored encrypted and you pay 46elks directly. (Available once the project has been saved.)
Tags Tab#
Define the vocabularies — reusable lists of
allowed values like categories, tags, or authors — that back
your tags and select
content fields.
Editors then pick from these lists instead of free-typing,
which keeps values consistent and powers faceted filtering in
listings.
(Available once the project has been saved.)
Publishing Tab#
| Setting | Description |
|---|---|
| Pages Branch |
The branch Klar copies your published files onto when
you click Publish. Defaults to
klar-publish.
|
| Production Branch | The branch Klar treats as your site's production source. Leave blank to use the repository's default branch. |
Per-Page Overrides#
Editor settings (Editable Elements, Sections Selector, Shared Sections, Sync Shared Sections on Save, Show "Sync to all pages" in left panel, and JavaScript) inherit in this order: Klar default → project value → page override. When you have a specific page open, each setting shows a "Set for this page" control so you can override the project default for that page only without touching the others.
Danger Zone#
The danger zone tab lets you delete the project entirely. This removes the project from your Klar account but does not affect your GitHub repository.
Custom Selectors
Control which elements are editable using scope selectors, container syntax, and data attributes.
Editable Selectors#
By default, Klar makes all standard text elements editable across the entire page. The Editable Elements setting in the Editor tab lets you restrict which areas are editable by providing CSS selectors.
# Only elements inside .hero and .content are editable
.hero, .content
# Everything in .hero except links and images
.hero:not(a, img), .content
# Mark a container for list editing
.feature-list[]
# Enable background-image editing
.hero__bg:style(background-image)
Selector Syntax#
| Syntax | Meaning |
|---|---|
.hero |
All editable elements inside .hero |
.hero:not(a, img) |
Everything in .hero except links and
images
|
.feature-list[] |
Treat .feature-list as a container for
list editing
|
.bg:style(background-image) |
Enable background-image editing on .bg
|
HTML Data Attributes#
You can also mark elements directly in your HTML without configuring selectors:
-
data-klar-editordata-klar— mark any element as editable regardless of tag name -
data-klar-listordata-list— mark a parent as a list container for container editing -
data-klar-style-bg— enable background-image editing on that element
Content Fields#
Editable selectors do more than mark text editable — adding a
:field(...) rule turns a matched element into
named, structured content (a title, image, tag list, date, and
more) that's saved per page. This is how you model blog posts,
products, and other page types. See
Content Fields for
the full syntax, types, and options.
Quick reference#
| Modifier | Effect |
|---|---|
:not(…) |
Exclude matching elements from the scope |
[] |
Treat the element as a list/container for container editing |
:blocks |
Make the element a drag-and-drop block container — its children get the block toolbar (add, duplicate, reorder, delete) |
:readonly |
Mark matching elements read-only so they're never editable, even inside an editable scope |
:snapshot |
Capture the element's content at save time; read-only
inline (implies :readonly)
|
:style(background-image) |
Make the element's
background-image swappable from the media
library (background images only)
|
:field(…) |
Capture the element as a named content field |
Content Fields
Turn parts of a page into named, structured data — titles, images, tags, dates, rich blocks — saved per page and reused across your site.
Overview#
A field is a named piece of content attached
to a page. You declare fields by adding
:field(...) rules to your
Editable Elements (Editor tab). Each field's
value is saved with the page, rendered back into the page on
save, and made available to
listing templates
and the
content API. Fields are what
turn a page into a "blog post", a "product", or a "team
member" rather than just markup.
Declaring a field#
Write a CSS selector, then :field(...) with at
least a name (the storage key) and a
type:
# bind a heading's text to a "title" field
h1.post-title :field(title: Title, name: title, type: string)
# bind an image's src to an "image" field
img.hero :field(name: image, type: image)
# a tag picker backed by a project vocabulary
.tags :field(name: tags, type: tags, options(vocab: tags))
The selector decides which element shows the value; the
name is how the value is stored and referenced
everywhere else.
Field types#
| Type | Use for |
|---|---|
string |
Short single-line text (headings, labels) |
text |
Longer multi-line text (excerpts, descriptions) |
number / integer |
Numeric values |
boolean |
On/off toggle |
image |
An image path, picked from the media library |
link |
A URL with an optional label |
tags / select |
Multiple (tags) or single (select) choice, optionally from a vocabulary |
array |
Repeatable blocks or list items (rich content) |
slug / location |
URL slug / geographic point |
A string can be narrowed with a
format — e.g. date,
email, url, markdown,
or html.
Options#
Extra settings tune how a field looks and behaves. Top-level
keys control layout; an options(...) group holds
the rest:
| Option | Meaning |
|---|---|
title |
Label shown in the editor |
order |
Position of the field in the panel |
col |
Width in the form grid (e.g. col: 6 =
half)
|
section / sectionOrder
|
Group fields under a labelled section |
default |
Starting value for new pages |
options(vocab: …) |
Back a tags/select field with a project vocabulary |
options(layout: …) |
Editor layout — e.g. blocks,
list, editor,
chip,
radio
|
options(dateFormat: …) |
Display format for a date field |
Tags & vocabularies#
A vocabulary is a reusable list of allowed
values (categories, tags, authors) configured in
Project Settings → Tags. Bind a field to one with
options(vocab: name) so editors pick from the
list instead of free-typing. Use type: tags for
multiple values and type: select for one — these
power faceted filtering in listings.
Data-only fields#
A field doesn't need an element on the page. Omit the selector to store a value with no on-page target — handy for an excerpt, SEO description, or other metadata:
:field(name: lead, type: text, default: 'Short summary shown in listings.')
Editing fields#
Element-bound fields can be edited inline on the page, and the right panel's Edit all fields view lists every field for the page in one form — including data-only fields that have no place on the page. Values are saved per page when you Save.
Listing Templates
Render dynamic lists — blog indexes, card grids, archives — from your page content, with no manual copy-paste.
Overview#
A <template data-for data-query> block
renders a list of pages into a container on your page. Every
time you save, Klar runs the query and replaces the
container's children with the result. The
<template> itself stays in your HTML as the
source of truth, so you can keep editing the design.
Example#
<div id="post-list"></div>
<template data-for="post-list"
data-query="list('blog-post', { published: true, order: 'published_at:desc', limit: 10 })">
{% for item in items %}
<a href="{{ item.url }}">
<img src="{{ item.content.image.src }}" alt="{{ item.content.image.alt }}">
<h3>{{ item.content.title }}</h3>
</a>
{% endfor %}
</template>
The body is a Liquid template with the
matched pages available as items. You get the
full loop toolkit — {% for %},
forloop.index/first/last,
{% cycle %}, and nested loops.
Query verbs#
| Verb | Returns |
|---|---|
list('type', { … }) |
Pages of a page-type — supports order,
limit, offset,
published, where
|
facets('type', 'field') |
Distinct values and counts for a field (for filter menus) |
vocabularies() |
Every project-configured vocabulary (the full tag map) — takes no argument |
Filtering#
Narrow a list by field values with where — for
example only posts tagged css in the
fashion category:
list('blog-post', { where: { category: 'fashion', tags: 'css' } })
When listings update#
Listings re-render every time you save the page they live on, and Klar refreshes all listing pages at publish so they always show your latest content.
Content API & SDK
Read your published content and add visitor sign-in with per-user data on any site, using small drop-in JavaScript modules.
Overview#
Two ES modules are served straight from your Klar host — no
install, no build step. The Content SDK reads
the content you manage in Klar; the
Users SDK lets visitors of your site sign in
and store their own data. Both are scoped by your
projectId (found in Project Settings).
Reading content#
// served from your Klar host — no build step
import { createKlarClient } from 'https://<your-klar-host>/sdk/v1/content.js';
const klar = createKlarClient({ projectId: 42 });
const posts = await klar.list('blog-post', { published: true, order: 'published_at:desc' });
document.querySelector('#list').innerHTML =
klar.render(posts, `<a href="${url}"><h3>${content.title}</h3></a>`);
list(), facets() and
vocabularies() mirror the
listing query verbs, and render() fills a template with each item.
A content-static.js variant reads a pre-built
JSON file for fully static sites.
Sign-in & per-user data#
Let visitors sign in with Google — through a Klar-hosted popup, so there's no Google setup on your side — and read or write their own data:
import { createKlarUser } from 'https://<your-klar-host>/sdk/v1/users.js';
const user = createKlarUser({ projectId: 42 });
await user.signIn();
await user.data.set('favorites', ['post-1', 'post-7']);
const favs = await user.data.get('favorites');
Each visitor only ever reads and writes their own data, scoped to your project. Sign-in identifies visitors and gives them personal storage — it does not lock down the page source. The Users SDK also does a lot more — shared collections for app data (bookings, comments), email & SMS automations, and an admin namespace for the project owner — see the Users SDK reference.
Full reference#
The complete API — every option, return shape, and the static variant — has its own reference page: Content SDK and Users SDK.
Content SDK
Read your published content from any site — list pages, filter
by field, resolve asset paths. Full reference for
createKlarClient (/sdk/v1/content.js).
Quick start#
import { createKlarClient } from 'https://<your-klar-host>/sdk/v1/content.js';
const klar = createKlarClient({ projectId: 42 });
const posts = await klar.list('blog-post', { published: true, order: 'published_at:desc', limit: 10 });
const fashion = await klar.list('blog-post', { where: { category: 'fashion', tags: 'css' } });
const tags = await klar.facets('blog-post', 'tags');
const vocab = await klar.vocabularies('blog-post');
Served from your Klar host — no install, no build step.
Everything is scoped by your projectId (Project
Settings). These endpoints are public and read-only.
createKlarClient(config)#
| Config | Required | Description |
|---|---|---|
projectId |
Yes | Numeric project ID |
baseUrl |
— | Klar host; defaults to where the SDK was loaded from |
cdn |
— |
{ repo, branch } — enables
cdn() rewriting to jsDelivr
|
Returns a client with list, facets,
vocabularies, cdn,
render, and insert.
list(pageType, options)#
| Option | Default | Notes |
|---|---|---|
published |
both |
true = published only,
false = drafts only
|
order |
created_at:desc |
column ∈ published_at / created_at / updated_at /
file_path; dir asc|desc
|
limit |
50 | 1–200 |
offset |
0 | skip N |
where |
— | filter by field values (see below) |
signal |
— | AbortSignal to cancel the request |
Returns ContentItem[]. A
where clause matches scalar fields by equality
and array (tags) fields by membership; multiple
keys are AND'd:
await klar.list('blog-post', { where: { category: 'fashion', tags: 'css' } });
// explicit operators when you need them:
await klar.list('blog-post', { where: { tags: { has: 'css' }, status: { eq: 'live' } } });
facets & vocabularies#
facets(pageType, field, options) returns distinct
values and counts for one field (for filter menus), sorted by
count. vocabularies(pageType) returns the
project's tag vocabularies for that page-type. Both accept
published, where, and
signal.
ContentItem shape#
{
filePath: "blog/post-1.html", // repo-relative
url: "/blog/post-1.html", // gets /<repo> prefix on GitHub Pages project sites
pageType: "blog-post",
content: { title: "…", image: { src: "…", alt: "…" }, tags: ["css"] }, // your :field() values
pathFields: ["image"], // names of src/href fields
createdAt: "2026-05-01T…",
publishedAt: "2026-05-02T…", // null if unpublished
updatedAt: "2026-05-03T…"
}
render & insert#
render(data, template) fills
${dotted.path} placeholders from an item (or
array), walking into content — values are
inserted un-escaped.
insert(selector, position, html) places HTML
relative to an element; positions are before,
after, prepend, append,
replace.
const posts = await klar.list('blog-post', { published: true });
klar.insert('#list', 'replace', klar.render(posts, `
<a href="${url}">
<img src="${content.image.src}" alt="${content.image.alt}">
<h3>${content.title}</h3>
</a>`));
Asset & URL resolution#
Path-field values on content are rewritten
automatically by list() per environment: in the
Klar editor → jsDelivr; on a GitHub Pages project site →
prefixed with /<repo>; on a custom domain →
unchanged. Use client.cdn(path) for ad-hoc paths.
Absolute URLs and data: URIs always pass through.
One-shot, errors & static#
-
fetchContent({ projectId, pageType, … })— a one-shot list without keeping a client around. -
Failed requests throw
KlarApiError({ status, message }). -
content-static.js— the same API, backed by a pre-built JSON file for fully static sites.
Raw endpoints#
GET /api/content?projectId&page_type&published&order&limit&offset&where → { items }
GET /api/facets?projectId&page_type&field&published&where → { items }
GET /api/vocabularies?projectId&page_type → { vocabularies }
Users SDK
Sign visitors in, give each one their own data store, share
data across users with collections, send
email & SMS automations, and read it all
back as the project admin. Full reference for
createKlarUser (/sdk/v1/users.js).
Quick start#
<button id="login">Sign in</button>
<button id="logout">Sign out</button>
<script type="module">
import { createKlarUser } from 'https://<your-klar-host>/sdk/v1/users.js';
const user = createKlarUser({ projectId: 123 });
user.onChange(profile => {
document.querySelector('#login').hidden = !!profile;
document.querySelector('#logout').hidden = !profile;
});
document.querySelector('#login').onclick = () => user.signIn();
document.querySelector('#logout').onclick = () => user.signOut();
</script>
createKlarUser(opts)#
| Option | Required | Description |
|---|---|---|
projectId |
Yes | Your Klar project id |
host |
— | Klar host origin (defaults to the SDK's own host) |
storageKey |
— | Override the localStorage key |
| Method | Description |
|---|---|
getUser() |
Current signed-in user, or null.
Synchronous.
|
signIn() |
Opens the Google popup; resolves with the user once signed in |
signOut() |
Clears the local session |
onChange(fn) |
Subscribe to sign-in / sign-out / expiry; returns an unsubscribe fn |
decodeToken() |
Decoded JWT claims of the current user, or
null
|
The user object contains sub,
projectId, provider,
email, name, picture,
exp, plus the raw token. Sessions
persist in localStorage and auto-expire on the
JWT's exp.
Per-user data — user.data#
Key-value storage scoped to (project, signed-in user). All methods require a signed-in user (they throw otherwise).
await user.data.set('favorites', ['post-1', 'post-7']); // any JSON value
const favs = await user.data.get('favorites'); // undefined if absent
const keys = await user.data.list(); // [{ key, updatedAt }]
await user.data.delete('favorites');
Constraints: keys 1–200 chars
([A-Za-z0-9._:-]); values anything
JSON.stringify handles, ≤ 64 KB.
Sharing a project across apps?
user.data is one flat namespace per
(project, user), so if a to-do app and a
booking app live in the same project they'd collide on a bare
settings key. Prefix your keys per app —
booking:settings, todo:items — so
each app reads only its own. The : is just a
convention (any allowed char works); the admin list can then
count and filter by that prefix — see
Admin access.
Shared collections — user.collection()#
Where user.data is private to each user, a
collection is shared across everyone on the
project — the backend for bookings, comments, RSVPs, anything
multiple people read and write. Each record remembers its
owner, so edits and deletes stay
owner-scoped.
const bookings = user.collection('bookings');
await bookings.create({ resourceId, start, end, status: 'confirmed' }); // you own this row
const mine = await bookings.list({ where: { resourceId }, order: 'start:asc' });
await bookings.update(id, { status: 'cancelled' }); // only your own
await bookings.remove(id); // only your own
Records come back flattened — your fields plus
id, ownerId, createdAt,
updatedAt. list() takes
where, order, limit and
offset.
You set each collection's access and integrity rules in Settings → Collections, and Klar enforces them on the server — a client can't get around them:
- Read access — public, signed-in (the default), or owner-only.
- Unique — a field or combination must be unique (one RSVP per person, one seat per show).
-
No overlap — no two records share an
overlapping time range within a group. A
bookingscollection grouped byresourceId(only confirmed) makes double-booking impossible. - Capacity — at most N records matching a filter (cap tickets per event).
A blocked write rejects with
err.status === 409 (err.conflict
describes the rule) — catch it and ask the visitor to pick
another slot. A client-side check is fine for instant
feedback, but the server rule is what keeps bookings correct
when people act at the same time.
Admin access — user.admin#
The project owner can read and manage everything their site has collected — and build their own admin dashboard from this SDK. You're recognised as the admin automatically when you sign in with the same Google account that owns the Klar project (no extra setup, no separate login). Everyone else gets a 403.
if (user.isAdmin()) { // gate your admin UI
const { users } = await user.admin.listUsers({ limit: 50 });
const detail = await user.admin.getUser(users[0].id); // { profile, data, records }
const bookings = await user.admin.listRecords('bookings'); // all records, any owner
await user.admin.deleteUser(users[0].id); // GDPR delete
}
isAdmin() is a cosmetic check (reads your token)
— use it to decide whether to render the admin UI, but it's
never the security boundary: every
user.admin.* call is re-verified on the server,
so a non-admin just gets a 403.
getUser(id) returns their
user.data values and the records they own;
listRecords()
returns every record in a collection regardless of owner; and
deleteUser() removes the user and their private
data while leaving shared records ownerless. Write methods let
you edit too: setUserData() /
deleteUserData() change a user's stored data, and
updateRecord() / deleteRecord() edit
or cancel any record.
The only requirement is the shared Google account — your Klar login and your site sign-in must use the same one.
Several apps in one project?
listUsers() takes optional scoping so one app's
admin shows only its own people and counts — no need to spin
up a separate project per app. Have each app write a
membership marker to user.data at
login, then filter on it:
// at login — tag this user as a member of this app (write once)
await user.data.set('app:booking', { joinedAt: Date.now() });
// in admin — list only this app's users, with app-scoped counts
const { users } = await user.admin.listUsers({
hasKey: 'app:booking', // only users who have that marker key
keyPrefix: 'booking:', // count only data keys with this prefix
collection: 'appointments', // count only records in this collection
});
hasKey filters the list to users holding a given
user.data key; keyPrefix and
collection scope each row's
dataKeys / records counts to one app.
All three run server-side in a single indexed query, so it
stays flat whether you have 30 users or 30,000 — no per-user
round-trips. (Why server-side? The list returns only summary
counts, not each user's keys/records, so filtering in the
client would mean an N-times getUser() fan-out.)
Collections are already isolated by name, and
getUser() returns one user's full data + records —
so scope that detail view in your own UI by key prefix
/ collection.
Email automations#
Send a templated email on a data event (a booking is created → confirmation) or on a schedule (daily → remind tomorrow's bookers). Like collections, an app declares its automations and self-provisions them.
await user.admin.ensureAutomations({
confirm: {
collection: 'bookings', on: 'create', to: 'owner',
subject: 'Confirmed — {{ data.resourceName }}',
body: '<p>See you {{ data.start | date: "%b %-d" }}.</p>',
},
remind: {
collection: 'bookings',
schedule: { every: 'day', atHour: 9 }, // hourly | daily | weekly (UTC)
window: { dateField: 'start', withinHours: 24 }, // next 24h
where: { status: 'confirmed' }, to: 'owner',
subject: 'Reminder', body: '…',
},
});
Trigger (on):
create/update/delete. Schedule: presets
(every hour/day/week + atHour), with
an optional window to target records by a date
field. Recipients are owner (the
record's owner) or projectOwner only — never an
arbitrary address. subject/body are
Liquid; a scheduled reminder fires once per record. Manage
with listAutomations(),
setAutomationEnabled(),
deleteAutomation(). Requires SMTP on the Klar
host.
Set channel: 'sms' to send a text instead. SMS
has no address from sign-in, so the recipient is a phone field
on the record (to: 'field:phone') and the message
is a single Liquid text. Klar sends via
46elks
(sends worldwide): set your own credentials in
Settings → SMS (each project owns its sender id, e.g.
Klar). With no credentials it's a no-op.
Raw endpoints#
# sign-in flow (the SDK drives these)
GET /api/sdk/auth/popup?projectId&origin&code
GET /api/sdk/auth/poll?code → { token, user }
POST /api/sdk/auth/google → mints the JWT
# per-user data — header: Authorization: Bearer <token>
GET /api/sdk/data → { keys }
GET /api/sdk/data/:key → { value } (404 if absent)
PUT /api/sdk/data/:key body { value }
DELETE /api/sdk/data/:key
# shared collections — Bearer token (read can be public via ?projectId)
GET /api/sdk/collections/:name → { items } (?where &order &limit &offset)
GET /api/sdk/collections/:name/:id → { item }
POST /api/sdk/collections/:name body { data } → { item } (409 on a rule)
PATCH /api/sdk/collections/:name/:id body { data } → { item } (owner-only)
DELETE /api/sdk/collections/:name/:id → { deleted } (owner-only)
# admin — project owner only (same Google account as the Klar login); 403 otherwise
GET /api/sdk/admin/users → { users, total } (?search &limit &offset &hasKey &keyPrefix &collection)
GET /api/sdk/admin/users/:id → { profile, data, records }
DELETE /api/sdk/admin/users/:id → { deleted }
PUT /api/sdk/admin/users/:id/data/:key body { value } → { ok }
DELETE /api/sdk/admin/users/:id/data/:key → { deleted }
GET /api/sdk/admin/collections/:name → { items } (all owners)
PATCH /api/sdk/admin/collections/:name/:id body { data } → { item }
DELETE /api/sdk/admin/collections/:name/:id → { deleted }
GET /api/sdk/admin/automations → { automations }
POST /api/sdk/admin/automations body { automations } → { name:{created} }
PATCH /api/sdk/admin/automations/:name body { enabled } → { ok }
DELETE /api/sdk/admin/automations/:name → { deleted }
Keyboard Shortcuts
All keyboard shortcuts available in the Klar editor.
Global#
| Shortcut | Action |
|---|---|
| Cmd+Shift+S | Save changes to GitHub |
| Cmd+Shift+P | Publish to GitHub Pages |
| Cmd+Z | Undo the last edit (text or structural) |
| Cmd+Shift+Z / Cmd+Y | Redo the last undone edit |
| Cmd+B | Toggle the left (sections) panel |
| Cmd+I | Toggle the right (structured) panel |
| Cmd+J | Toggle the AI assistant panel |
| Cmd+Shift+F | Toggle both side panels |
| Cmd+E | Toggle zen mode (hide editor chrome) |
| Cmd+Shift+E | Open the code editor |
| Cmd+Enter | Toggle preview mode |
| Cmd+, | Open project settings |
| Cmd+Shift+, | Go to the dashboard |
| Cmd+1–Cmd+9 | Jump to page 1–9 |
| Escape | Close modals, dialogs, and dropdowns |
Visual Editor#
| Shortcut | Action |
|---|---|
| Hover | Highlight editable element with blue outline |
| Click | Select element for editing |
| Cmd+Click | Deactivate current element and re-discover editable elements |
| Enter | Confirm text edit and deselect |
| Shift+Enter | Insert line break in text field |
Viewport & Preview
Test your page at different screen sizes and preview changes before saving.
Viewport Sizes#
The toolbar provides preset viewport sizes to test responsive design:
| Preset | Width |
|---|---|
| Desktop | 1024px |
| Tablet | 768px |
| Phone | 375px |
| Full width | No constraint (responsive) |
Responsive Mode#
Toggle responsive mode to freely resize the preview by dragging the left, right, bottom, or corner handles. The current dimensions are shown below the preview (e.g., "375 × 667"). Minimum size is 280 × 200 pixels.
Preview Mode#
Click the eye icon to toggle preview mode. In preview mode:
- Editing interactions are disabled — you see the page as visitors would
-
Anchor links (
#fragment) work with smooth scrolling - External links show a "Preview mode — links are disabled" toast
- Scroll position is preserved when switching between edit and preview modes
Maximize#
Click the maximize button to toggle the preview between windowed and fullscreen mode, hiding the browser chrome simulation.