On this page

ITSYWEB DOCUMENTATION

Start with HTML. Get it online.

Layouts, partials and clean URLs without a frontend build pipeline. A local dev server, SSH deployment and automatic HTTPS without assembling a second toolchain to run the site.

HTML doesn't need a build pipeline.

You want to write HTML and CSS. Then you need the same navigation on every page, a shared footer and URLs without .html. That shouldn't mean adopting a framework or generating a second copy of your website.

ItsyWeb is a web server that understands HTML with a few extra instructions. Layouts wrap your pages, partials reuse markup, and variables, loops and conditions let you work with settings and JSON data. The server assembles the page when it serves it. You keep writing HTML.

It also includes the tools to run the site: local development with live reload, Ubuntu server setup over SSH, deployment, automatic HTTPS, backups and code rollback. There is no frontend build step or language runtime to install.

Start with the HTML workflow below. You can use the server and deployment tools without setting up an editor or enabling an app. Those optional features are documented separately.

Install ItsyWeb

The command is itsy. Site settings live in itsy.config.json, and downloads are served from itsyweb.dev.

For Apple Silicon Macs and x86-64 Linux. Open Terminal and run:

curl -fsSL https://itsyweb.dev/install.sh | bash

The installer downloads the matching executable, checks its SHA-256 checksum and installs it to ~/.local/bin/itsy. No sudo and no extra runtime such as Go, Node or Python is required. It does not change your shell profile.

Use the installed copy in this terminal and check the version:

export PATH="$HOME/.local/bin:$PATH"
itsy version

If needed, add that PATH line to your shell profile for future terminals. Then follow the quickstart below.

0.20.5 is the current public test release. Downloads cover Apple Silicon macOS and x86-64 Linux. Both CLIs can deploy to x86-64 and ARM64 Linux servers. Intel Mac and Linux ARM64 CLI downloads are not included; previously published downloads remain available at their versioned URLs. The Mac binary is ad-hoc signed, not notarized, and clean-machine testing is still in progress.

Deployment protocol is 28 and the Itsy App Protocol is version 2. Update the local CLI with itsy update. Update an existing server separately with itsy update-server before deployment or host administration; updating the CLI does not upgrade the host. See release notes for changes in this version.

You can read the installer before running it, or download the Mac binary, Linux x86-64 binary and checksums directly.

A site needs no frontend build pipeline or database. The CLI is a single binary, and the server needs nothing installed by hand either. Deployment uses Git and SSH; see Set up the server.

Your first HTML site

Create a project and start the local server:

itsy new my-site
cd my-site
itsy dev

Enter a site name when prompted. The admin email is optional; skip it to start with the HTML workflow. Open the Website URL printed in the terminal.

Edit a page

Replace the contents of site/index.html with your own markup:

<h1>Hello, from our little corner.</h1>
<p>A website made with HTML and CSS.</p>

Save the file and the browser reloads. The default site/main.layout.html supplies the document wrapper, header and footer. Edit those once to change them across the site. Your page markup goes where the layout has {slot}.

Create site/about.html and visit /about. There's no route configuration or generated output directory. See pages, layouts and partials for the template syntax, then put the site online.

Check and save

itsy status

Status validates the project without rewriting it, shows page, asset and editable-area counts, and reports Git state. When the current branch has a saved deployment, it also checks that destination over SSH. The remote report separates website reachability, deployed code and editable content; a previous pull baseline is needed to identify which side changed. Without a saved destination, status finishes locally.

New projects have a Git repository on main with an Initial site commit. Review and commit your changes before deploying. The creation command prints instructions to connect and push to your remote repository; it does not create a remote for you. Use --skip-git to skip Git setup. A failed initial commit leaves the usable site and instructions to retry.

Other ways to start

Already in an empty project folder? Run itsy new .. Relative and absolute directory paths work too. ItsyWeb creates a missing directory when its parent exists. Existing directories must be completely empty, including hidden files and .git; files and directory symlinks are refused. The folder name supplies the default site name. Use itsy new --starter my-site or -s for example pages instead of the clean homepage.

Dev starts at port 3000 and explains occupied ports before trying 3001, 3002 and so on. An explicit --port stays fixed; --port 0 lets the operating system choose. Running itsy dev again replaces this site's running dev server, including when changing ports or LAN mode. Pull and reset are never interrupted. Stop any dev process started with an older binary manually once.

Test on another device

To use a phone, tablet or another computer on the same trusted network, start dev with:

itsy dev --lan

itsy dev -l does the same thing. ItsyWeb prints Network and Hostname URLs for detected private/Tailscale IPv4 addresses and this computer's name/local alias. Open a printed URL, such as http://studio:3000, when that name resolves to the dev computer. Its /admin link opens sign-in. Exact Host/port and peer checks remain required. Without the flag, dev stays accessible only on your computer using the printed Website URL.

This is the full dev site: approved admins can sign in, edit text and collections, upload photos and videos, and save changes. Contact forms, browser reloads and dev diagnostics work over the network too. Each device signs in separately.

Sign-in codes and email previews

With email preview enabled, messages still open on the computer running ItsyWeb. Enter the code on your other device. To share a private preview, replace its loopback IP with the printed network IP, keeping the port and random path. Anyone with that link and network access can read the message, so keep it private. There is no public inbox.

LAN mode uses unencrypted HTTP. Use it only on a trusted network, never forward the development port, and restart dev after changing networks. VPN addresses may also appear. Your firewall must allow the selected port; guest Wi-Fi can block connections between devices. Use --port 3001 if you need a different port.

A folder you can follow

site/
Public HTML, CSS, images and other assets.
site/main.layout.html
The shared page wrapper.
itsy/snips.json
Editable content and page metadata.
itsy/data/
Optional JSON data you maintain in code.
itsy/media/
Managed photos and videos.
mailers/
Private email templates.
itsy.config.json
Site settings and branch deployment destinations.
itsy/runtime/
Private, Git-ignored runtime state.
itsy/skills/
Portable instructions for coding agents.

Public URLs leave off site/. For example, site/images/logo.svg is served at /images/logo.svg. Keep secrets out of configuration, templates and Git.

HTML, with reusable pieces

Keep page content in HTML files. The server handles the shared pieces when it serves a page, so you do not need to run a static site generator after changing your header.

Add a page

Create site/about.html for /about. You can use site/about/index.html instead, but not both. Link to extensionless URLs without a trailing slash. Explicit .html URLs and trailing slashes return 404 unless you configure an exact redirect. Assets keep their paths: site/images/logo.svg is /images/logo.svg. Files and directories beginning with _ or . are private.

Share a layout

The default wrapper is site/main.layout.html. Put {slot} where the page content belongs.

<header>Our studio</header>
<main>{slot}</main>
<footer>Thanks for stopping by.</footer>

For a different wrapper, put this comment at the very start of a page:

<!-- itsy
layout: /custom.layout.html
-->

Preserve the generated layout's app.current_admin conditional blocks, CSRF meta tag and admin toolbar include. They allow the optional editor to work if you enable admin access later; they do not require you to make your page content editable.

Reuse a partial

Put navigation, a card or any other shared markup in a private partial. Include it wherever you need it; editing the partial updates every page that uses it.

Create site/_card.html, then include it with {render card}. Relative names resolve beside the calling template. {render /shared/card} selects site/shared/_card.html.

Settings and structured data

Public settings live under app in itsy.config.json. Use {app.site_name} in your templates. A file named itsy/data/team.json is available as data.team.

{#if app.phone}
  <p>Call {app.phone}</p>
{/if}

{#each data.team.members as member}
  <p>{member.name}</p>
{:else}
  <p>Meet the team soon.</p>
{/each}

Conditions take dotted paths, not JavaScript expressions. Use {#unless app.phone}...{/unless} for a negative condition. Both support {:else} and {:else if dotted.path}.

Page titles and descriptions

Page metadata lives under the route in pages in itsy/snips.json. Update its title and description fields. The layout can use {page.title}, {page.description} and {page.path}. Ordinary page text can stay directly in your HTML.

Your CSS, your design

Generated sites include Prewind 1.3.1. Start with global styles, then use utilities for individual adjustments. There is no CSS build step.

Edit site/css/theme.css for colors, fonts and spacing tokens. Put shared rules in site/css/global.css inside @layer styles. CSS stays in source and is published by normal deployment.

Icons

{icon arrow-right}
{icon search size="20" aria-label="Search"}

Dev downloads only the referenced Lucide icons into itsy/icons/. Commit the cache for offline use. Status checks do not download icons.

Put your site online

ItsyWeb hosts multiple sites on one Ubuntu server. You'll need an Ubuntu host you control, SSH access, a domain pointing to that server, and inbound TCP ports 80 and 443 for HTTPS. ItsyWeb configures the web server on that host; it does not rent a server or create DNS records for you.

Set up the server

itsy setup

Run this from your computer and point it at a blank Ubuntu server over SSH. ItsyWeb installs the bundled server there and prepares the host to serve your sites. There is nothing to install by hand — no Go, Node, Python or database on the host. Use a server with ports 80 and 443 available for HTTPS.

Deploy a branch

Save your files, check the site with itsy status, and review and commit the changes.

itsy deploy

The first run asks for the server, domain and HTTPS preferences. It saves that branch's destination in itsy.config.json and offers to commit your reviewed files. Deployment publishes committed source and requires a clean working tree.

Each branch has its own destination. Use itsy deploy -configure to change it, or itsy https -configure to revisit just the HTTPS settings. Configuring a destination does not create DNS records.

ItsyWeb requests and renews Let's Encrypt certificates when HTTPS is enabled. There is no separate certificate tool to configure.

If you also use the optional on-page editor, see syncing live content before publishing content changes.

Temporary packaging storage

From 0.20.1, local packaging respects TMPDIR. If your Linux computer has a small RAM-backed /tmp, first check that /var/tmp is disk-backed, then use private scratch for one deployment:

(
  scratch=$(mktemp -d /var/tmp/itsy-job.XXXXXXXX) || exit 1
  trap 'rmdir -- "$scratch"' EXIT
  TMPDIR="$scratch" itsy deploy
)

The selected directory must exist and be writable. An invalid explicit setting fails without falling back elsewhere. Keep worktrees and retained screenshots or logs outside disposable scratch.

Preserve old links with 301 redirects

From ItsyWeb 0.20.0, keep visitors and search engines reaching the right content when you migrate a site or move a page. Add exact old paths to a top-level redirects object in itsy.config.json, alongside app:

{
  "app": {"site_name": "Example"},
  "redirects": {
    "/about-us.html": "/about",
    "/services/": "/services",
    "/old-team.php": "/about#team"
  }
}

GET and HEAD requests for a listed path return 301 Moved Permanently. Sources match exact, case-sensitive paths, including explicitly listed old extensions and trailing slashes. Unlisted legacy URLs still return 404. These rules never redirect POST or other mutation requests.

Destinations must be existing public pages or assets on this site, including published Blog posts. They may include a query or fragment. Incoming queries survive unless the destination supplies a query of its own; a destination ending in ? explicitly clears it. ItsyWeb decodes source paths once and rejects duplicate decoded sources, unsafe/private paths, conflicts with active pages, assets or managed app subtrees, and redirect chains or loops. Former Blog slugs can be explicit sources. Update their redirects when removing a destination.

There are no wildcard, regex, query-specific or external-URL rules. An old site that uses different pages at /?p=123 and /?p=456 needs a separate migration decision. Path rules do not configure DNS, certificates or arbitrary hostname redirects. Existing HTTP-to-HTTPS and WWW redirects continue to use 308.

Inventory the original website with an agent

Ask your coding agent:

Use itsy-site to inventory https://example.com and prepare 301 redirects for this Itsy site.

The skill reads public sitemaps and internal links, preserves useful URLs, maps moved content to real replacements and records ambiguous or unsupported URLs. Supply old URL lists or analytics exports when available: a live crawl cannot discover every orphaned page. Full brochure imports include this step too. Existing sites can copy the matching skill from a fresh temporary site; updating the CLI does not replace project instructions.

Validate before deploying

Run itsy status, then use a local dev server to check each old URL without following redirects. Confirm its 301 and Location, then check the destination serves the intended content in one hop. Check query behavior and unchanged URLs too.

Update both CLI and server. This feature requires deployment protocol 28. Run itsy update, then explicitly update the shared server with itsy update-server before deploying rules. Public 0.19.0 and earlier ignore site redirect settings.

Sitemaps, canonical URLs and page exclusions

ItsyWeb 0.20.0 can keep your public page list up to date for search engines. A sitemap lists the page addresses you want discovered. A canonical tag identifies a page's preferred address. Configure these once; ItsyWeb updates the generated sitemap when public content changes.

1. Check your CLI and server

Update your installed CLI and check its version:

itsy update
itsy version

This feature starts in 0.20.0 and requires matching CLI/server deployment protocol 28. If an existing server runs an older version, run the following from a site whose current Git branch has that server saved as its deployment destination:

itsy update-server

It asks before briefly restarting every site on that host. Updating the CLI or deploying website files alone does not update the server executable. A new server set up with the current CLI already receives its bundled runtime. See deployment for destination setup.

2. Set the production address

Add a top-level seo object to itsy.config.json, alongside app. This minimal example shows the placement; keep your existing app, admin, apps and deployment settings:

{
  "app": {"site_name": "Example"},
  "seo": {
    "url": "https://example.com",
    "exclude": ["/campaign/receipt"]
  }
}

Replace https://example.com with your site's production HTTPS address, even while working locally or on staging. Use only the origin: no credentials, subdirectory, query or fragment. ItsyWeb uses this configured address rather than a request Host or forwarding header. Without seo, automatic discovery is disabled. This setting does not configure DNS, register a hostname or enable HTTPS.

exclude is optional. Replace the example path with an existing page you want kept out of search, or omit the list. Entries match one exact path without wildcards, queries or trailing slashes; excluding /campaign does not exclude its descendants. Exclusion does not create a missing page.

3. Check locally, then deploy

itsy status
itsy dev

Open the local address printed by dev. Check these responses:

  • /sitemap.xml: XML containing eligible public page addresses on your production domain, with no localhost or staging URLs.
  • /robots.txt: a Sitemap: https://example.com/sitemap.xml line using your production address, unless you supply a custom file.
  • A public page: use View Source to find its rel="canonical" link. It uses the production address without tracking queries. An HTML fragment without a closing head receives a canonical HTTP Link header instead.
  • An existing excluded page: it still loads, but the browser's Network panel shows X-Robots-Tag: noindex. It has no generated canonical or sitemap entry.

Commit your reviewed configuration and site changes, then deploy from the intended branch:

itsy deploy

Normal deployment publishes the committed configuration and preserves existing live editable content. Repeat the checks on the public domain. If you use curl, check an existing excluded page's headers with:

curl -I https://example.com/campaign/receipt

Replace the address and path with your own; expect X-Robots-Tag: noindex. Excluded pages remain public. Use authentication for private information, and keep excluded pages crawlable so search engines can read the noindex header.

What ItsyWeb includes and preserves

The generated sitemap includes ordinary public pages, enabled Blog listings and published posts, and enabled Contact forms. Drafts, private/admin routes, assets, errors, managed custom apps, redirects and enabled Contact's thank-you page stay out. Contact's thank-you page receives noindex automatically. Pages with robots or googlebot noindex metadata are also omitted. There are no guessed modification dates; beyond 50,000 URLs or 50 MiB, provide a custom sitemap index.

Existing canonical tags are preserved. A page with a custom canonical enters the generated sitemap only if that tag exactly matches its absolute production URL. Relative, empty or different custom canonicals keep the page out of the generated list.

Custom site/sitemap.xml and site/robots.txt routes take precedence independently and are served unchanged. ItsyWeb does not append a Sitemap line to a custom robots.txt or remove excluded URLs from a custom sitemap. Maintain those declarations and exclusions yourself. Generated documents live in memory; you do not need to create or commit them. Redirect rules cannot use either discovery endpoint as a source while automatic SEO is enabled.

The exact-host staging policy remains separate. It still adds noindex/nofollow to staging responses, including discovery documents, while shared sitemaps and canonicals name the production origin. A production URL does not automatically protect other hostnames; configure staging policy explicitly.

Optional: tell Google where the sitemap is

Google Search Console is Google's free reporting tool for website owners. It is optional. ItsyWeb's generated robots.txt already advertises the sitemap, so Google can discover it without an account or integration in ItsyWeb.

To see whether Google has read the sitemap and reported processing errors, first make sure it is reachable on your public production domain. Then:

  1. Open Search Console and verify that you own the production domain or site address.
  2. Select that site and open Sitemaps.
  3. Submit https://example.com/sitemap.xml. If the form already supplies your site's address, enter only sitemap.xml.

You submit the URL, not an uploaded XML file. Keep the same URL as content changes; you do not need to submit it after every edit. Submission does not guarantee indexing, ranking or AI citations. See Google's sitemap guide.

Keep staging out of search

From ItsyWeb 0.19.0, configure indexing for an already registered hostname over SSH. Staging and production can serve the same website files:

itsy server indexing root@server staging.example.com noindex
itsy server list root@server

Type the exact hostname when asked. ItsyWeb sends X-Robots-Tag: noindex, nofollow on that hostname's pages, assets, app responses, errors, downloads and redirects. Aliases can have different policies. The command restarts the shared service, briefly affecting other sites.

Policy belongs to the server, outside site app settings, editable content and release files. Normal deployment, content sync, code rollback and site restore retain the current policy. itsy status and itsy server list show effective settings. Existing hostnames without a policy keep their behavior; ItsyWeb does not infer staging from the hostname.

itsy server indexing root@server staging.example.com allow

allow stops adding the framework restriction. It does not emit index, follow, remove owner-authored headers or meta tags, or guarantee search visibility. Each exact registered hostname is configured separately; wildcards and arbitrary forwarding headers cannot select a policy.

Keep robots.txt crawlable. A site-wide Disallow: / can stop crawlers discovering the noindex header. The host policy does not rewrite robots.txt, sitemaps, canonical tags or templates, and reports detected static site-wide crawl blocks. The separate SEO settings can generate missing discovery documents and canonical tags. This controls cooperative search indexing, not private access.

Removing a site clears only its own hostname policies; redeploying a removed name does not restore them. Site backups exclude host policy. Interrupted policy changes use the existing server-side sudo itsy-server -recover journal, which restores the complete prior host configuration. Upgrade both client and server to protocol 28 before using this feature. Verify the live staging header before removing any existing site-level meta fallback.

Backups & recovery

Make a backup

itsy backup ../before-redesign.itsy-backup

This downloads the active site's code and editable content, including managed media and source CSS. Choose a new file path outside your site and keep a separate copy. Credentials, certificates, deployment mappings, host indexing policies and sessions are excluded.

Roll back code

itsy rollback --list
itsy rollback

Rollback republishes earlier code and CSS while retaining current admin content and host indexing policy. It asks for confirmation before acting.

Restore a backup

itsy restore ../before-redesign.itsy-backup

Restore replaces live code, site configuration and editable content while retaining current host indexing policy. It requires typed confirmation. Take a fresh backup first if newer content matters.

Reset a local project

itsy reset returns the local site to a clean starting point after confirmation. It replaces local files and settings, keeping Git history and a private sibling backup. It does not reset a live site.

Keep ItsyWeb up to date

Update your installed CLI with:

itsy update

ItsyWeb verifies the downloaded checksum and version before replacing the executable. A local update does not update your remote host. From a configured site, explicitly run:

itsy update-server

After confirmation, a terminal spinner shows that the update is running. Redirected output uses plain progress text; animation yields to SSH diagnostics and interactive sudo prompts. Successful updates print a completion message. Server updates affect every site on that host and ask before restarting it. Keep backups and check compatibility first. An interrupted update can be recovered with itsy update-server --recover.

AI-assisted development

ItsyWeb projects include portable instructions so coding agents can follow the site's conventions and preserve existing content. For a focused addition, ask your agent to “add a blog”, “let's create a blog” or “add a contact form”. The project's AGENTS.md directs it to the matching skill.

The two setup skills ship in newly generated sites from 0.15.1. The public installer downloads 0.20.5. Blog requires 0.15.0 or newer; 0.20.5 uses server protocol 28.

  • itsy/skills/add-blog/SKILL.md covers Blog setup, private list/post templates, navigation, post JSON, drafts and publishing. It documents blog.data.posts, blog.data.post and the exact rich-body directive {blog.data.post.body}.
  • itsy/skills/add-contact-form/SKILL.md covers Contact setup, the form and thank-you page, exact field names, {csrf}, contact.data.*, contact.invalid.*, contact.error and contact.flash.notice. It also explains private email templates and local email previews.
  • itsy/skills/import-brochure/SKILL.md guides imports and redesigns through content inventory, two design directions, a sample page, local assets and editable snips.
  • .agents/skills/itsy-site/SKILL.md covers research-first site building, plus old-site URL inventories and verified 301 mappings. Its migration workflow can run on its own without a redesign.

A focused Blog or Contact addition follows your existing design without restarting a full-site consultation. Agents read the Markdown instructions; no plugin, account or runtime keyword listener is required.

Use the skills in an existing project

A CLI update leaves your project's files intact. Generate a disposable site with a matching 0.15.1-or-newer CLI, then copy itsy/skills/add-blog/ and itsy/skills/add-contact-form/ into your project. Add the matching request routes to your own AGENTS.md, preserving its custom rules. You can also point an agent directly at either SKILL.md.

These guidance files stay in Git but are excluded from public serving and deployment. A skill does not grant permission to publish posts, send real test mail or deploy.

Where ItsyWeb fits

Use ItsyWeb for websites you would otherwise build in plain HTML, with shared templates and the tools to run them. It does not turn HTML into a general application framework or provide an ecommerce platform.

  • No general content field schemas or general-purpose form backend. Templates do not execute arbitrary server-side application code; custom logic belongs in a managed app.
  • Custom apps are managed local executables only: no remote services, package registry or public SDK. Each app owns and protects its own storage.
  • No video transcoding or video fields in collections.
  • No content revision-history interface. Use the backup and recovery commands.
  • Editable rich text is sanitized and does not accept unrestricted HTML. This restriction applies to the optional content editor; your source templates remain developer-authored HTML.
  • Downloads currently cover Apple Silicon macOS and x86-64 Linux. Clean-machine testing is in progress.

The source repository is private. The binaries are free to use, without an account or license key.

When you need more than templates

The HTML, development and deployment workflow works without these features. Add on-page editing when someone else needs to change content, or enable an app when the site needs a blog, a contact form or custom server-side behavior.

The following sections cover that extra functionality. You can leave ordinary page content in HTML and come back here when you need it.

Optional on-page editing

Use snips only for content an admin should change in the browser. Layouts, partials and ordinary HTML pages do not need them. Leave developer-managed text in the template.

A snip is a piece of content the admin can change. Mark an element with a bare snip attribute:

<section snip>
  <h2>Made just around the corner.</h2>
  <p>Visit our studio on Saturday.</p>
</section>

Dev extracts the inner HTML into JSON and replaces it with a generated snip directive. Let ItsyWeb generate the ID and keep it unchanged. The JSON entry becomes the source of the editable value.

Classes, layout and other attributes stay in the template. Don't nest snips or put template directives inside a new editable region. Use dedicated image and video snips for media.

Admins can format text and edit page titles and descriptions using Page details. Changes are published with the page's Save button.

Editable photos & videos

Ordinary images and videos use normal HTML and public asset paths. The snip attributes below are for media you want an admin to replace in the browser.

Photos

<img snip src="/images/studio.jpg"
  alt="The studio in the afternoon"
  width="1200" height="800" />

Dev imports a local JPEG or PNG into managed media and creates the source and alt-text bindings. Keep dimensions, classes and links in your HTML. Don't combine a media snip with srcset, picture or competing source elements.

Admins click a photo in Edit mode to choose or upload a replacement. Save publishes it; Cancel discards the draft.

Videos

<video snip src="/videos/studio.mp4" controls></video>

Use a fast-start MP4 with H.264 video and optional AAC-LC audio, up to 25 MiB. ItsyWeb does not transcode videos. Admins can replace the video or its poster from the page editor.

Video storage is limited to 64 MiB, including retained replacements. Managed MP4 files are Git-ignored. Restore missing files on a fresh checkout with itsy pull before validation.

Admin-managed collections

For developer-managed lists, use JSON data and a loop as shown in templates. Use a multisnip when an admin needs to manage the list on the page.

A multisnip lets an admin add, remove and reorder items such as team members. Give the group a name and name its editable fields:

<article multisnip="team">
  <h2 snip="name">Sam Rivera</h2>
  <p snip="title">Studio founder</p>
  <img snip="photo" src="/images/sam.jpg" alt="Sam" />
</article>

Dev turns the seed into a loop. Admins use the on-page Add tile, remove button and drag handles. Arrow keys on a handle can reorder items too.

Keep each group on an individual page, with one root element. Don't nest groups or place them in layouts, partials or other loops. Video fields are not supported in multisnips.

Hand over the editing

Add an approved admin from your site folder:

itsy admin admin@example.com

This adds the address to admin.emails. Admins visit /admin and sign in with an emailed code. All approved admins have the same editing access.

Editing starts enabled. Click text or media, make changes, then use Save or Cancel. Both keep editing on. Save updates the page in place without reloading it; Cancel returns to the latest successful save.

New local sites preview email in the browser without sending it. Production needs HTTPS and real email delivery. Commit and deploy admin-list changes before sharing the live sign-in link.

Sessions have a default 30-day idle window. Set admin.session_idle_days to a value from 1 to 365. An empty admin list disables live sign-in.

Sync optional editable content

Keep admin changes

Normal deployment preserves live editable content and publishes committed CSS. To bring those edits into your local project, stop dev and run:

itsy pull

Pull retrieves live content and referenced media. Review and commit the result.

To deliberately publish local content edits, first establish a pull baseline, then run:

itsy deploy --with-snips

Conflicts require an explicit choice between the live and local changes.

Email & contact forms

New sites set dev.email_preview to true. Local sign-in and contact emails open as private browser previews without Resend credentials. Preview messages expire and disappear when dev stops.

For real delivery, run:

itsy email

Local credentials belong in the private, Git-ignored .env file. For real local delivery, set the preview toggle to false and restart dev. Production ignores the preview toggle and needs server-side mail configuration.

A contact form

Enable the bundled Contact app under apps.contact in itsy.config.json, with a mount and the recipient in email.to. This replaces the removed top-level contact settings and the old /_itsy/contact route: there is no alias or automatic migration, and deploy with a current server; 0.20.0 uses deployment protocol 28. Create site/contact/index.html and a mount-relative site/contact/thank-you.html. The form includes an explicit CSRF control and posts to the mount:

<form method="post" action="/contact">
  {csrf}
  <label>Name <input name="name" required maxlength="100" /></label>
  <label>Email <input type="email" name="email" required /></label>
  <label>Message <textarea name="message" required maxlength="5000"></textarea></label>
  <div hidden><label>Leave empty <input name="website" tabindex="-1" autocomplete="off" /></label></div>
  <button type="submit">Send message</button>
</form>

Success redirects to the mount's thank-you page with a visitor-bound notice. Messages are emailed, not stored, and validation re-renders the form with safe values on error. There is no general-purpose form backend.

Email templates

Customize mailers/*.email.html and *.email.txt. HTML emails share mailers/main.layout.html. Keep its slot and use escaped app.* and email.* values.

itsy email preview sign-in

This renders a sample without sending an email.

Enable a blog

Blog is an optional built-in content application. It adds a public post list and individual post pages, plus a basic admin editor using ItsyWeb’s existing sign-in, rich text and photo library. No database or extra service is required.

Requires ItsyWeb 0.15.0 or newer and a current server. The public installer downloads 0.20.5, which includes Blog, and 0.20.5 uses server protocol 28. Enable it explicitly in each site; a CLI update does not change existing project files.

1. Enable it in your site configuration

Merge this into itsy.config.json, preserving your other settings and apps:

{
  "apps": {
    "blog": {
      "mount": "/blog"
    }
  }
}

The mount owns /blog and its entire URL subtree, so ordinary pages and assets cannot occupy those URLs. Removing the Blog configuration disables its routes without deleting stored posts.

2. Add the templates

Sites created with ItsyWeb 0.15.0 include these private templates. For an existing site, generate a fresh temporary site with the matching CLI and copy these files, preserving your site’s custom layout:

  • site/blog/_index.html for the public list.
  • site/blog/_post.html for individual posts.
  • site/admin/blog.html and site/admin/blog-edit.html for the admin screens.

Copy the Blog rules from the fresh site’s site/css/global.css into your own stylesheet. Keep your existing styles and layout. If you choose another mount, move the two public templates under that directory while keeping their private filenames. Add an admin link to /admin/blog, or visit it directly.

3. Preview, commit and deploy

itsy dev
# Check your site locally, then stop dev and commit the changes.
itsy deploy

Both the local CLI and the destination server need the matching version. An explicit itsy update-server updates the shared server and briefly restarts every site on that host; back up first. Updating a server does not add Blog templates or enable Blog on existing sites.

Create and publish posts

Sign in at /admin, then open /admin/blog. Choose New post, enter a title, slug, excerpt and rich body, and optionally select or upload a cover photo. Save creates a draft. Publish makes it public; Unpublish makes it private again. Saving an already published post updates it immediately. Cancel discards unsaved changes.

Published posts appear at /blog and /blog/<slug>. Draft text stays out of public routes and template data, and a draft URL returns 404. Photo files are public assets, including covers attached to drafts. Covers use the existing JPEG/PNG photo processing; inline body photos are not supported.

Leave the publication date empty to assign it when first publishing, or enter an RFC3339 date such as 2026-10-13T14:00:00Z. A future date publishes immediately, not on a schedule. Lists show the newest publication dates first. Changing a slug changes the URL without creating a redirect automatically. Add an explicit migration redirect for the old slug when needed.

Post JSON for agents and developers

The add-blog agent skill documents this setup and the supported template values.

Posts use the optional top-level blog array in itsy/snips.json. Preserve the existing pages, shared, archive and media values. Here is one draft post:

{
  "blog": [
    {
      "id": "a7Km2pQ9xB",
      "title": "Welcome",
      "slug": "welcome",
      "excerpt": "Our first article.",
      "body": "<p>Welcome to our website.</p>",
      "status": "draft"
    }
  ]
}

Keep the ten-character, case-sensitive alphanumeric ID stable through edits. Slugs must be unique across drafts and published posts, using lowercase letters, digits and separating hyphens, up to 100 characters. Titles are required and limited to 200 characters; excerpts to 500. The limit is 200 posts and 1 MiB per body, within the existing 5 MiB content file. Status is draft or published; published records require published_at. An optional cover references an existing ID in the media map, never a filename or URL.

Template values

The listing template can iterate published posts:

{#each blog.data.posts as post}
  <article>
    <h2><a href="{post.url}">{post.title}</a></h2>
    <p>{post.excerpt}</p>
  </article>
{:else}<p>No posts yet.</p>
{/each}

The post template receives blog.data.post, including title, excerpt, URL, publication date, body and an optional cover object with src, alt, width and height. The listing URL is blog.data.url.

<h1>{blog.data.post.title}</h1>
<time datetime="{blog.data.post.published_at}">
  {blog.data.post.published_label}
</time>
<div>{blog.data.post.body}</div>

Only the exact {blog.data.post.body} directive renders sanitized rich HTML, and only in ordinary element content. Other values remain escaped. Never put the body directive in an attribute, script or textarea. Braces inside a post body are ordinary text, not template source.

Keep live edits when deploying

Normal itsy deploy preserves live posts by ID and seeds new source-only IDs. Before an agent edits existing live posts in Git, run itsy pull. Publish reviewed source changes with itsy deploy --with-snips; this existing flag includes Blog records and cover metadata. Conflicting edits require a whole-post choice, and noninteractive conflicts stop.

Omitting a post from source never deletes its live record. Unpublish it to remove its public page. Backups include live posts and covers; rollback keeps current content while selecting earlier code. Comments, categories, tags, search, scheduling and revision history are deferred.

Apps

Blog is a built-in content application available from 0.15.0; see Blog setup. The service contract below applies to apps such as Contact, rather than Blog’s content storage and admin editor.

An app is a small, separate JSON service mounted at a public URL. ItsyWeb owns the public page, the HTML form, the redirect, the template values and the email; the app owns its own logic and validates its own fields. Values an app returns are request-local and are never written into your site.

Contact ships with ItsyWeb. You can also register your own managed local app package. See Building a custom app.

What you can use

  • Contact sends a visitor's message to one email address. Its configuration, form and templates are in Email & contact forms.
  • Custom apps are managed local executables registered under apps.<name> with a validated package. They can own their own sessions.
  • Remote services, public read-only routes declared in config, databases and uploads are not part of the shipped contract.
  • There is no public SDK. ItsyWeb's Go packages are private, but an app is an ordinary executable that speaks the documented HTTP/JSON contract.

Enable Contact

Merge this into itsy.config.json, keeping your other settings:

{
  "apps": {
    "contact": {
      "mount": "/contact",
      "email": {"to": "owner@example.com"}
    }
  }
}

mount is required: a canonical public path such as /contact or /deep/contact. email.to is required. Optional email.from overrides the server sender and must be authorized by your mail provider. Credentials never belong here. Remove the apps.contact entry to disable the app.

Create site/contact/index.html and a mount-relative site/contact/thank-you.html. Do not also create site/contact.html. The starter includes both pages but leaves the app disabled until you choose a recipient. Restart dev after changing app settings.

How a submission flows

  1. The visitor submits an ordinary HTML form to the mount.
  2. ItsyWeb checks CSRF, exact Origin, method, content type and size, then converts the fields to flat JSON.
  3. ItsyWeb calls the app's mount-relative route with its own private credential. Browser cookies and authorization are never forwarded.
  4. The app returns JSON. ItsyWeb redirects, re-renders with errors, or shows a safe unavailable message.

App values appear under the app name, such as contact.data.name, contact.invalid.email, contact.error and contact.flash.notice. They are escaped values, never template source.

Building a custom app

You can write an independent HTTP service that speaks the current Itsy App Protocol (version 2) in any language, then register it as a managed local executable. It receives flat JSON and returns a small JSON envelope. This section documents the contract so you can build, mount and test one.

Custom apps are available from 0.17.0. Add an apps.<name> entry with a mount, and put a validated package at itsy/apps/<name>/ containing app.json and the executables it names. There is no install command, no package registry, no url or port key and no public SDK. ItsyWeb launches only this confined local executable; remote services are not supported. Deploying an app package requires server protocol 28, so update the server first; a protocol-25 server refuses the package.

Register a package

Build your service for the server platform, then create a package. The manifest's name must match the config key, protocol must be 2, and executable maps a <goos>-<goarch> key to one filename in the same directory. Build for every platform you run (your development host and the server) and list each one:

itsy/apps/session/app.json
{"name":"session","version":"0.1.0","protocol":2,"executable":{"darwin-arm64":"session-darwin-arm64","linux-amd64":"session-linux-amd64"}}
itsy/apps/session/session-darwin-arm64
itsy/apps/session/session-linux-amd64

Enable it in itsy.config.json:

{
  "apps": {
    "session": {"mount": "/session"}
  }
}

Create a page at the mount, such as site/session/index.html. A GET reads from the app; a POST submits a form that includes {csrf}. ItsyWeb strips the mount, so /session/login calls service route /login. Restart itsy dev after changing app settings or rebuilding the executable. At handshake time ItsyWeb verifies the running service, so the manifest is not trusted as proof of compatibility.

Two boundaries

Keep the browser-to-ItsyWeb form boundary separate from the ItsyWeb-to-app call. The form is URL-encoded and bounded to 32 KiB; only POST carries a body. ItsyWeb strips the configured mount and forwards the rest of the path unchanged, so the mount itself becomes /.

Request JSON

There is no request envelope. The body is a flat object of form names:

{"name":"Alex","email":"alex@example.com","message":"Hello"}
  • A name submitted once is a string. A repeated name is an array of strings.
  • An empty field stays ""; a missing field is absent.
  • Names are literal: address.city and items[0] stay those exact keys, with no nesting.
  • Values are always strings or arrays of strings. The app converts types itself.
  • ItsyWeb verifies and removes its own _csrf field before forwarding. An app must not define it.
  • The JSON body is bounded to 192 KiB.

Response envelope

The service replies with one JSON object using only these keys:

  • data (object): app-owned values, rendered as contact.data.*.
  • invalid (object of string to string): validation messages, contact.invalid.*.
  • error (string): one operational failure message, contact.error.
  • flash (object of string to string): temporary messages, contact.flash.*.
  • redirect (string): a mount-relative destination, handled by ItsyWeb and not shown in templates.
  • session (object or null): an app-owned session instruction, not shown in templates. See App-owned sessions.

Only these keys are reserved. Unknown keys, an explicit null (except for session, where null clears the cookie), duplicate keys, a non-object body, a wrong content type or an oversized body are treated as a service failure. invalid and flash values must be strings; values inside data may be any JSON. Flash is limited to 4,096 bytes and responses to 1 MiB.

Statuses and browser outcomes

  • 200 with no invalid and no error: success. With redirect, the browser gets a 303.
  • 422 with at least one invalid and no error: the page re-renders with status 422.
  • 503 with an error and no invalid: the page re-renders with status 503.
  • Any other status or a malformed response becomes a safe 503 with a generic message.

Success:

{"redirect":"thank-you","flash":{"notice":"Your message was sent."}}

Validation:

{"data":{"email":"invalid"},"invalid":{"email":"Enter a valid email address."}}

Failure:

{"error":"We couldn't confirm delivery. Please try again later."}

Launch contract

ItsyWeb launches a managed app itself; a site configures only apps.<name>.mount and never a port, descriptor or token. The child receives three inherited descriptors:

  • fd 3: an already-bound 127.0.0.1:0 TCP listener, so there is no port race and no port in config.
  • fd 4: one JSON object with exactly five string keys: {"Name":"<app>","Token":"<64 hex>","Storage":"<private dir>","CapabilityURL":"","CapabilityToken":""}. A custom app gets empty capability values; the bundled Contact app gets a non-empty capability URL and token. Unknown fields are rejected, so declare all five.
  • fd 5: a parent-liveness pipe; EOF means shut down.

The child environment is only LANG=C, and its standard output and error are discarded so private payloads are never logged. The executable is re-opened through a confined root at launch, checked against the size and SHA-256 recorded when the site loaded, and copied to a private file before it runs; a swap after validation cannot change what runs. One child runs per site and app with bounded backoff; an unavailable app only affects its own routes.

Handshake and transport

Before any app call, ItsyWeb performs a private handshake:

GET /_itsy/handshake
Authorization: Bearer <app-token>

The service must return 200 with JSON like:

{"name":"example","version":"0.1.0","protocol":2}

ItsyWeb checks the name and protocol: 2; version is the app's own release. A manifest alone is not proof. Every call carries the bearer token; browser cookies and authorization are never forwarded. Responses must be application/json, redirects are not followed, and POSTs are never retried. The handshake deadline is 2 seconds and ordinary calls 15 seconds; at most four calls run per app and 64 process-wide, with no waiting queue.

App-owned sessions

A custom app can own its own server-side sessions; ItsyWeb stores no session data. ItsyWeb reads its own per-app cookie and sends the opaque token to the app as the X-Itsy-Session request header, never as a browser cookie or authorization header. The app's reply may add a session key: omit it to leave the cookie unchanged, use null to clear it, or set {"token":"...","max_age":86400} to set or rotate it. Tokens are 16 to 512 cookie-safe printable ASCII bytes and max_age is 1 second to 30 days. A malformed session value is rejected and no cookie changes. ItsyWeb owns the cookie name, Secure, HttpOnly, SameSite=Lax, Path=/ and the __Host- prefix. The cookie is isolated by app, and by site through host-only scoping, so a production cookie is never sent to another site's host; a malformed or duplicate cookie, including a valid cookie with a malformed duplicate of the same name, is ignored. The token never appears in templates, logs, URLs or public JSON. Store session state in the private directory ItsyWeb passes to the app at launch. It is stable per-site runtime state outside every release, excluded from releases and backups, and never pruned by ItsyWeb, so it survives a release change; back it up yourself. An app session never grants admin or editor access.

A complete managed sample

This standard-library program is a whole managed app: it reads the inherited descriptors, answers the handshake, serves a page, and owns a session cookie. Copy it into an empty directory with go.mod, build it for every platform you run, and register the package as shown above. It imports no ItsyWeb package.

// A complete managed Itsy app using only the standard library. Build it for
// every platform you run, place the files under itsy/apps/<name>/, and register
// the package under apps.<name> in itsy.config.json. Itsy launches it and speaks
// the contract below; it never imports a Itsy package.
package main

import (
	"context"
	"crypto/rand"
	"crypto/subtle"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net"
	"net/http"
	"os"
	"os/signal"
	"path/filepath"
	"sync"
	"syscall"
	"time"
)

const (
	protocol      = 2
	version       = "0.1.0"
	sessionHeader = "X-Itsy-Session"
	maxAge        = 3600
	maxSessions   = 1000
)

// fd 4 is one JSON object with exactly these five string keys. A custom app
// receives empty capability values; unknown fields are rejected.
type launchConfig struct {
	Name, Token, Storage, CapabilityURL, CapabilityToken string
}

func main() {
	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
	defer stop()
	if err := run(ctx); err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
}

func run(ctx context.Context) error {
	configFile := os.NewFile(4, "app-credentials")
	var cfg launchConfig
	decoder := json.NewDecoder(io.LimitReader(configFile, 4097))
	decoder.DisallowUnknownFields()
	err := decoder.Decode(&cfg)
	var extra any
	end := decoder.Decode(&extra)
	configFile.Close()
	if err != nil || end != io.EOF || len(cfg.Token) != 64 {
		return errors.New("invalid launch configuration")
	}
	name := cfg.Name
	if name == "" {
		name = "example"
	}

	listenerFile := os.NewFile(3, "app-listener")
	listener, err := net.FileListener(listenerFile)
	listenerFile.Close()
	if err != nil {
		return errors.New("app listener is unavailable")
	}
	defer listener.Close()

	ctx, cancel := context.WithCancel(ctx)
	defer cancel()
	alive := os.NewFile(5, "app-parent")
	defer alive.Close()
	go func() { _, _ = io.Copy(io.Discard, alive); cancel() }()

	app := newHandler(name, cfg.Token, cfg.Storage)
	server := &http.Server{
		ReadHeaderTimeout: 2 * time.Second,
		ReadTimeout:       15 * time.Second,
		WriteTimeout:      15 * time.Second,
		IdleTimeout:       30 * time.Second,
		MaxHeaderBytes:    16 << 10,
		Handler:           app,
	}
	done := make(chan error, 1)
	go func() { done <- server.Serve(listener) }()
	select {
	case err := <-done:
		return err
	case <-ctx.Done():
	}
	shutdown, finish := context.WithTimeout(context.Background(), 2*time.Second)
	defer finish()
	if err := server.Shutdown(shutdown); err != nil {
		server.Close()
	}
	<-done
	return nil
}

// handler is a tiny session service. The "login" action is a demonstration
// only: it checks no credentials and is never real authentication.
type handler struct {
	name    string
	token   string
	path    string
	mu      sync.Mutex
	tokens  map[string]int64
}

func newHandler(name, token, storage string) *handler {
	h := &handler{name: name, token: token, tokens: map[string]int64{}}
	if storage != "" {
		h.path = filepath.Join(storage, "sessions.json")
		if data, err := os.ReadFile(h.path); err == nil {
			_ = json.Unmarshal(data, &h.tokens)
		}
	}
	return h
}

func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	if len(r.Header.Values("Authorization")) != 1 ||
		subtle.ConstantTimeCompare([]byte(r.Header.Get("Authorization")), []byte("Bearer "+h.token)) != 1 {
		http.Error(w, "Unauthorized", http.StatusUnauthorized)
		return
	}
	if r.URL.RawPath != "" || r.URL.RawQuery != "" {
		writeJSON(w, 400, map[string]any{"error": "Unsupported request."})
		return
	}
	if r.URL.Path == "/_itsy/handshake" && r.Method == http.MethodGet {
		writeJSON(w, 200, map[string]any{"name": h.name, "version": version, "protocol": protocol})
		return
	}
	if r.URL.Path != "/" {
		writeJSON(w, 404, map[string]any{"error": "Unsupported request."})
		return
	}
	switch r.Method {
	case http.MethodGet:
		token := r.Header.Get(sessionHeader)
		body := map[string]any{"data": map[string]any{"authenticated": h.valid(token)}}
		if token != "" && !h.valid(token) {
			body["session"] = nil
		}
		writeJSON(w, 200, body)
	case http.MethodPost:
		h.post(w, r)
	default:
		w.Header().Set("Allow", "GET, POST")
		writeJSON(w, 405, map[string]any{"error": "Use GET or POST."})
	}
}

func (h *handler) post(w http.ResponseWriter, r *http.Request) {
	if r.Header.Get("Content-Type") != "application/json" {
		writeJSON(w, 400, map[string]any{"error": "Send JSON."})
		return
	}
	var fields map[string]any
	decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 192<<10))
	if err := decoder.Decode(&fields); err != nil {
		writeJSON(w, 400, map[string]any{"error": "Invalid body."})
		return
	}
	action, _ := fields["action"].(string)
	switch action {
	case "login":
		token, err := h.create()
		if err != nil {
			writeJSON(w, 503, map[string]any{"error": "Could not start a session."})
			return
		}
		writeJSON(w, 200, map[string]any{"data": map[string]any{"authenticated": true}, "session": map[string]any{"token": token, "max_age": maxAge}})
	case "logout":
		if err := h.drop(r.Header.Get(sessionHeader)); err != nil {
			writeJSON(w, 503, map[string]any{"error": "Could not end the session."})
			return
		}
		writeJSON(w, 200, map[string]any{"data": map[string]any{"authenticated": false}, "session": nil})
	default:
		writeJSON(w, 422, map[string]any{"data": fields, "invalid": map[string]string{"action": "Use login or logout."}})
	}
}

func (h *handler) valid(token string) bool {
	if token == "" {
		return false
	}
	h.mu.Lock()
	defer h.mu.Unlock()
	h.expire()
	_, ok := h.tokens[token]
	return ok
}

func (h *handler) expire() {
	now := time.Now().Unix()
	for token, expiry := range h.tokens {
		if expiry <= now {
			delete(h.tokens, token)
		}
	}
}

func (h *handler) create() (string, error) {
	var raw [32]byte
	if _, err := rand.Read(raw[:]); err != nil {
		return "", err
	}
	token := hex.EncodeToString(raw[:])
	h.mu.Lock()
	defer h.mu.Unlock()
	h.expire()
	if len(h.tokens) >= maxSessions {
		return "", errors.New("full")
	}
	h.tokens[token] = time.Now().Add(maxAge * time.Second).Unix()
	if err := h.save(); err != nil {
		delete(h.tokens, token)
		return "", err
	}
	return token, nil
}

func (h *handler) drop(token string) error {
	if token == "" {
		return nil
	}
	h.mu.Lock()
	defer h.mu.Unlock()
	previous, ok := h.tokens[token]
	if !ok {
		return nil
	}
	delete(h.tokens, token)
	if err := h.save(); err != nil {
		h.tokens[token] = previous
		return err
	}
	return nil
}

// save writes durably and returns any failure so the request can fail closed.
func (h *handler) save() error {
	if h.path == "" {
		return nil
	}
	data, err := json.Marshal(h.tokens)
	if err != nil {
		return err
	}
	dir := filepath.Dir(h.path)
	f, err := os.CreateTemp(dir, ".sessions-")
	if err != nil {
		return err
	}
	temp := f.Name()
	defer os.Remove(temp)
	if _, err = f.Write(data); err == nil {
		err = f.Sync()
	}
	if closeErr := f.Close(); err == nil {
		err = closeErr
	}
	if err != nil {
		return err
	}
	if err = os.Rename(temp, h.path); err != nil {
		return err
	}
	directory, err := os.Open(dir)
	if err != nil {
		return err
	}
	defer directory.Close()
	return directory.Sync()
}

func writeJSON(w http.ResponseWriter, status int, body any) {
	w.Header().Set("Content-Type", "application/json")
	w.Header().Set("Cache-Control", "no-store")
	w.WriteHeader(status)
	_ = json.NewEncoder(w).Encode(body)
}

Try the contract locally

A compatible service can be tested without ItsyWeb. The essential flow is: require a bearer token, answer GET /_itsy/handshake with the identity above, and answer POST / with an envelope. For example, a local service could answer a handshake:

curl -s http://127.0.0.1:8787/_itsy/handshake \
  -H "Authorization: Bearer $APP_TOKEN"

and a form submission:

curl -s -X POST http://127.0.0.1:8787/ \
  -H "Authorization: Bearer $APP_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"Alex"}'

This proves the wire contract, authentication and statuses. It does not mount the service in a site.

What remains planned

Remote (non-local) services, public read-only routes declared in configuration, databases, storage helpers and file uploads remain planned. ItsyWeb supervises only a confined local executable; it does not download packages or run arbitrary commands.

Release notes

0.20.5 · November 9, 2026

The approved navy mark now appears on light backgrounds across icons, the website header/footer, sharing artwork and generated sites. The light website header has matching navy navigation and a readable sticky scroll treatment. Existing customer branding and protocols remain unchanged.

0.20.4 · November 9, 2026

Favicons and touch icons now keep the same white mark on a solid navy background as the website header, in both light and dark browser themes. New sites receive the matching icons. Protocols and existing customer branding are unchanged.

0.20.3 · November 9, 2026

Little Skip now uses the approved filled treatment, with three filled body segments and a friendly face. Updated SVG logos, favicons and touch icons ship in new sites and development diagnostics. Existing sites retain their own branding. The Nunito lettering and deployment/app protocols remain unchanged.

0.20.2 · November 2, 2026

Updated SVG logos and favicons appear throughout the website, docs, generated sites and development diagnostics. Existing sites keep their custom branding. Deployment protocol stays 28; app protocol stays 2.

0.20.1 · November 2, 2026

Local deployment, server setup and Mac installer packaging now respect TMPDIR. Each invocation keeps a private scratch directory and removes it on completion, ordinary errors and handled cancellation. Storage errors identify the temporary location and explain how to choose another one. The release client also removes its own smoke-test directory. Deployment protocol stays 28; app protocol stays 2.

0.20.0 · October 30, 2026

Adds exact 301 migration redirects in the top-level redirects config object. List legacy extensions and trailing-slash paths explicitly, keep or replace queries, and point directly to existing local public pages or assets. Validation rejects unsafe paths, route conflicts, missing destinations, chains and loops. The Itsy site skill and brochure import workflow can inventory old URLs, prepare mappings and verify responses. Deployment protocol moves to 28; update the CLI and shared server together. The optional seo settings add snapshot-generated sitemap/robots discovery, missing canonicals and exact noindex page exclusions while preserving custom files and staging host policy. App protocol remains 2.

0.19.0 · October 24, 2026

Adds exact-host indexing control through itsy server indexing. Configure staging once and retain X-Robots-Tag: noindex, nofollow across deploy, sync, code rollback and site restore while serving the same website source on other hosts. List/status show effective policies and static robots.txt crawl-block warnings. No templates or robots.txt files are rewritten. Deployment protocol moves to 27; matching clients and servers are required, with a separate explicit server update. App protocol remains 2.

0.18.0 · October 21, 2026

Create a site in your current empty folder with itsy new ., or pass a relative or absolute directory path. Existing directories must be completely empty, including hidden files and .git; Itsy refuses nonempty directories without overwriting files. Folder-based naming and the initial Git commit are preserved. Linux ARM64 CLI downloads are discontinued; Apple Silicon macOS and x86-64 Linux CLIs retain ARM64 Linux server deployment support. Previously published downloads remain available. Starter agent guidance uses the globally installed Impeccable skill.

0.17.0 · October 18, 2026

Adds custom managed apps: register a validated local executable under apps.<name> and Itsy supervises it with the Itsy App Protocol version 2. Apps can own server-side sessions through Itsy’s isolated __Host- cookie and the X-Itsy-Session header, with state kept in stable per-site runtime storage. App output is data, never template source. Deployment protocol moves to 26 with no fallback: update the server with itsy update-server before deploying a package-bearing site. Existing sites keep their current releases until redeployed.

0.16.1 · October 18, 2026

Ordinary literal brace groups and raw JSON now work in template text and attributes. Recognizable malformed commands retain useful errors, and normal interpolation and contextual escaping remain active. Use &#123;app.site_name&#125; to display exact Itsy syntax literally. Update both the local CLI and production server to use the fix.

0.16.0 · October 13, 2026

LAN dev accepts this computer's hostname and local alias, including connections over Tailscale IPv4. Exact Host/port, peer, Origin and CSRF checks remain in place. Default dev tries the next available port when 3000 is occupied and explains the selected URL. Server updates and recovery show terminal progress after confirmation, with plain output when redirected.

0.15.1 · October 13, 2026

New sites include focused Blog and Contact setup skills, with request routing, template values and content/form conventions. Existing projects can adopt the instructions without changing their layouts. Available through the public installer and itsy update.

0.15.0 · October 13, 2026

Adds optional Blog with drafts, explicit Publish/Unpublish, rich text, cover photos and a JSON format for agents. Public templates receive published posts only. Blog requires explicit site setup and server protocol 25.

0.14.0 · October 13, 2026

Removes browser Theme editing, CSS overlays and theme synchronization. CSS remains in source and is published by normal deployment. The --with-theme, --theme-only and --snips-only flags have been removed. Older servers need reviewed upgrade preparation as described above; saved CSS customizations must be preserved before removing obsolete host mappings.

0.13.0 · October 12, 2026

itsy status now prints local validation, site counts and Git state first, then automatically checks the current branch’s configured deployment. Code, content and theme are reported separately, with change direction when a pull baseline exists. Status remains read-only; no remote flag is needed.

0.12.0 · October 12, 2026

Adds a Linux ARM64 CLI download alongside Apple Silicon Mac and Linux x86-64. The installer detects aarch64 and verifies its checksum and version. All CLIs still bundle both Linux server architectures. Missing-download errors now name the release and selected platform.

0.11.0 · October 12, 2026

itsy new now creates an Initial site commit on main and prints commands to connect and push to a remote repository. It respects your Git identity, hooks and signing settings. A failed commit preserves the site with instructions to retry; --skip-git skips Git setup. Itsy does not create or configure a remote repository automatically.

0.10.0 · October 5, 2026

Itsy renames itsy server sites to itsy server list with no alias. Both itsy server list and itsy server remove now ask for the server login (for example root@itsyweb.dev) when you omit the SSH target, and itsy server remove without a site name lists the managed sites and asks you to choose one by its exact name before the existing confirmation. Listing shows each site's name and branch, then HTTPS domains, then HTTP-only domains, then redirects. Removal is unchanged: the list is read-only, removal only unregisters routing and deletes no files, and it restarts the shared server, so other sites are briefly unavailable. These commands need a current server runtime. Run itsy update for the CLI.

0.9.0 · October 5, 2026

Itsy now bundles the Contact app and adds server-wide site administration with itsy server sites and itsy server remove. Enable a form under apps.contact with a mount and recipient; it posts with an explicit CSRF control to the mount, and success redirects to the mount-relative thank-you page. This replaces the top-level contact settings and the /_itsy/contact route, and deployment requires protocol 23; there is no alias or automatic migration, so update the server and existing sites together. Large static assets and managed photos now stream from disk instead of loading fully into memory. Run itsy update for the CLI, then update the shared server separately with itsy update-server.

0.8.1 · October 3, 2026

Starting Itsy no longer duplicates its runtime and media rules in .gitignore when other entries follow them. Existing file contents are preserved. Run itsy update to install the fix.

0.8.0 · October 3, 2026

The editing row slides beneath the top bar and disappears outside Edit. Theme slides from the right and fills the available height. Both respect reduced motion. Existing sites must update site/admin/_toolbar.html from the current starter alongside the executable; preserve custom layouts. Run itsy update for the CLI, and update the server separately.

0.7.1 · October 3, 2026

Routine HTTPS deployments now say “Checking site health and HTTPS...” instead of repeating certificate setup instructions. First-time HTTPS enablement still shows DNS and port guidance; certificate verification and renewal are unchanged. Run itsy update to install 0.7.1.

0.7.0 · October 3, 2026

Bundled agent skills now guide substantial imports and redesigns through two design directions and a sample-page review before the full build. Brochure imports also cover portfolio and artist sites, with clearer media, gallery and editing guidance. Release downloads are served from a separate disk-backed store, so website deployments no longer carry all previous binaries.

0.6.2 · October 1, 2026

New sites now bundle a second agent skill, itsy-site, for building or redesigning a site for one real local business, alongside the existing import-brochure skill.

0.6.1 · October 1, 2026

Running itsy dev again gracefully replaces the same site’s dev server, including LAN mode and port changes. Pull and reset stay protected. Crashed dev processes no longer leave a blocking control socket.

Stop any dev process started with an older binary once; subsequent starts replace it automatically. Remote servers remain a separate, explicit update.

0.6.0 · October 1, 2026

Test release. On the first itsy dev, Itsy explains the optional email for an admin who can sign in and edit, points to itsy admin if you skip it, and only asks once. User-facing copy now says "admin", and the busy message for dev, pull and reset is clearer.

0.5.0 · September 29, 2026

Test the full dev site on other devices with itsy dev --lan or -l. Local startup links now use localhost. Page saves update in place without reloading fonts or the page, and Cancel returns to the latest successful save. See network testing for sign-in, preview and security details.

0.4.1 · September 29, 2026

Shorter new-site output: itsy new now finishes with the local development instructions.

0.3.0 · September 29, 2026

The first public test release for Apple Silicon macOS and x86-64 Linux. Install with one command, then use itsy update for future CLI releases.

Includes plain-file sites, on-page text and media editing, repeatable collections, theme controls, local email previews, SSH deployment and backup/recovery commands. This is an early release; please test on a small site before relying on it for production work.

Need a hand?

Start with itsy status to check your site and itsy <command> --help for a command's options.

If you're testing ItsyWeb, send feedback to the person who invited you. Include your operating system, processor, ItsyWeb version, the command you ran and the exact error. Leave out credentials, sign-in codes and private runtime files.