Static-HTML-site CMS with visual editor, AI edits, and surge.sh publishing
Project description
Scopuli
Turn a folder of static HTML/CSS/JS sites into a lightweight CMS. Edit
visually in the browser, push live to surge.sh (with optional custom
domain), and use AI for both per-element tweaks ("make this button red and
bigger") and whole-site work ("add about.html and contact.html matching
the home page style, then link to them from the hero").
The UI is themed with Radix Colors + Radix Themes design primitives, with a
vanilla-JS port of the Radix Select component (RxSelect) for searchable
dropdowns.
Install
Pick whichever you have on hand:
uvx scopuli # zero-install — `uv` will fetch and run
pipx install scopuli # isolated, persistent install
pip install scopuli # plain pip into your current env
Optional AI provider SDKs (install only the ones you'll use):
pip install "scopuli[anthropic]" # or .[openai], .[gemini], .[all]
You'll also need the surge CLI for publishing (Node-based):
npm install -g surge
surge login # one time
Quickstart
cd ~/Sites # or any folder you'd like to host sites from
scopuli # http://127.0.0.1:5001 opens in your browser
On first run Scopuli creates ./websites/example/ with a small starter
site so you can immediately click Edit and try the editor. Drop your own
folders next to it later — anything containing an index.html (or any
*.html) shows up as a site card.
Other CLI options:
scopuli --port 8080 --host 0.0.0.0 # bind elsewhere (see Security)
scopuli --root /path/to/sites # use a specific websites folder
scopuli --no-seed # skip auto-seed (empty folder)
scopuli --no-browser # don't auto-open the browser
python -m scopuli # alternative if the script isn't on PATH
A typical workspace ends up looking like:
my-workspace/
└── websites/
├── eros/
│ ├── index.html
│ ├── about.html
│ ├── style.css
│ └── img/
│ └── logo.svg
└── portfolio/
└── index.html
The home page
Each site card shows:
- Primary domain — the custom domain if you've set one, otherwise
<sub>.surge.sh. When both are set, the custom domain shows on top with a smallvia <sub>.surge.shcaption underneath. - Edit → opens the visual editor.
- Publish → runs
surgeagainst the site folder. - Open live → opens the primary domain in a new tab.
Click set / change in the surge row to open the modal:
- Surge subdomain (required):
your-name.surge.sh. - Custom domain (optional): something like
myportfolio.com. When set, Scopuli writes<site>/CNAMEwith a single line containing this domain; surge picks it up on publish and serves your site from it (you still need to point DNS at surge per their docs). Save with this field empty to remove an existing custom domain.
The editor
The toolbar at the top: ← Scopuli (back to home), the site name, the surge domain pill, a Preview ↔ Edit toggle, an unsaved-changes pill, Discard, Save, Publish.
Preview / Edit toggle
Click Preview to disable selection so links navigate, forms submit, and the iframe behaves like the live site. Click Edit to come back. The mode persists across in-iframe link navigations.
Side panel (Edit mode)
- Hover any element in the page → dashed outline.
- Click → solid outline, the Style tab populates.
The side panel has three tabs:
- Style: tweak text, color, background, font size, padding, margin, border radius. Changes apply live in the iframe.
- Images: lists every image found under
img/,imgs/,images/,assets/img/,assets/imgs/,assets/images/inside the site directory. Click a thumbnail with an<img>selected to replace its source; click with any other element selected to insert the image as the last child. Newly inserted images become selectable after Save. - AI: two scopes via a radio at the top.
- Selected element: type "make this button red and twice as big", click Send. The active provider returns a patch; it's applied to the iframe and buffered for Save.
- Whole site (works with no selection): type something like
"I added 3 new pages — add thumbnail cards and links to them on the
home page" or "Create a new
contact.htmlthat matches the style ofabout.html". The AI reads every.htmlin the site folder plus the image asset list and returns a plan of file writes (create or modify). Review the plan in the side panel and click Apply changes to write the files — nothing happens to disk until you do.
Save / Publish
- Save rewrites the source HTML on disk (inline
style="…"on the element, replacedtextContentfor plain-text edits, child node insertion for image inserts). - Publish runs
surge <site_path> <surge_or_custom_domain>.
AI providers
Scopuli ships with four pluggable providers. Pick one from /settings:
| Provider | Default model | Auth |
|---|---|---|
| Anthropic | claude-sonnet-4-6 |
API key |
| OpenAI | gpt-4o-mini |
API key |
| Gemini | gemini-2.0-flash |
API key |
| Ollama | auto-detected | none (local only) |
Anthropic is the default. Switch any time from /settings — the active
provider is picked via a Radix segmented control.
You can set ANTHROPIC_API_KEY / OPENAI_API_KEY / GEMINI_API_KEY as
env vars; if the matching API key field in /settings is blank, the
provider falls back to the env var.
For Ollama, Scopuli queries the configured base URL's /api/tags
endpoint and presents installed models in a RxSelect dropdown. Install
new models with ollama pull <name> and they show up automatically; if
Ollama isn't reachable you get a friendly error and the previously-saved
model stays selectable.
How edits persist
Edits rewrite source HTML directly in websites/<site>/. No overlay,
no build step. Element-scoped edits touch the entry HTML; site-scoped AI
edits can create or overwrite any .html file within the site folder.
There is no automatic undo — commit websites/ to git before any large
site-scoped prompt so you can git restore if the AI takes the design
somewhere you don't want.
Configuration
Created automatically on first launch at ~/.scopuli/config.toml:
[ai]
provider = "anthropic"
[providers.anthropic]
model = "claude-sonnet-4-6"
api_key = "" # blank → falls back to ANTHROPIC_API_KEY
[providers.openai]
model = "gpt-4o-mini"
api_key = ""
base_url = "" # blank → default OpenAI
[providers.gemini]
model = "gemini-2.0-flash"
api_key = ""
[providers.ollama]
model = "llama3.2"
base_url = "http://localhost:11434" # local only — no API key
[surge.sites]
eros = "eros.surge.sh"
Per-site custom domains live next to the site, not in this config:
websites/<site>/CNAME (single line, the custom domain).
CLI
scopuli [--host 127.0.0.1] [--port 5001]
[--root websites] [--no-seed]
[--no-browser] [--debug]
No subcommands. scopuli --help prints the full options list.
Security
- Default bind is
127.0.0.1. The editor accepts arbitrary HTML/CSS patches and the AI endpoint will spend whatever API credit your key has — do not expose Scopuli to the open internet. If you bind0.0.0.0, put it behind a firewall or VPN. - API keys live in
~/.scopuli/config.tomlin plaintext.chmod 600is recommended. - Path-traversal is guarded on every file route; only
./websites/<site>/…paths are reachable. - The site-AI Apply step writes only
.htmlfiles within the site folder (CSS / JS / image overwrites are refused per-entry on the server).
Project layout
scopuli/
├── app.py # Flask app factory
├── cli.py # `scopuli` entry point
├── __main__.py # `python -m scopuli` entry point
├── config.py # ~/.scopuli/config.toml (tomli / tomllib)
├── routes.py # HTML + /api/* blueprint
├── sites.py # per-site discovery, traversal guards
├── pages.py # per-page (.html) discovery + read/write
├── assets.py # image asset discovery
├── editor.py # HTML annotation + patch application
├── surge.py # surge CLI wrapper + CNAME helpers
├── ai/
│ ├── base.py # AIProvider + edit/compose contracts
│ ├── anthropic.py
│ ├── openai.py
│ ├── gemini.py
│ └── ollama.py
├── templates/
│ ├── base.html
│ ├── home.html
│ ├── editor_shell.html
│ └── settings.html
└── static/
├── radix-theme.css # Radix Colors + Themes design tokens
├── base.css # global components (buttons, fields, modals)
├── home.css # site cards + surge modal
├── shell.css # editor toolbar + side panel
├── shell.js # parent-frame editor controller
├── settings.css
├── settings.js # provider config + Ollama auto-detect
├── rx-select.css # vanilla Radix Select component styles
├── rx-select.js # vanilla Radix Select component controller
├── editor.css # injected into edited pages (outlines)
└── editor.js # injected into edited pages (selection, postMessage)
See PRD.md for the design rationale and CHANGELOG.md for release notes.
License
MIT.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file scopuli-0.1.0.tar.gz.
File metadata
- Download URL: scopuli-0.1.0.tar.gz
- Upload date:
- Size: 60.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ebf46788c42794920deb273bc14173b1569bc3f66191030f5ad64aba224dba0f
|
|
| MD5 |
4667c6657a4f020511383acf2074fdb2
|
|
| BLAKE2b-256 |
32f67647dc212983c0d5fe54f071dd8a33f643a77700ca47fc00d2837939c0b9
|
File details
Details for the file scopuli-0.1.0-py3-none-any.whl.
File metadata
- Download URL: scopuli-0.1.0-py3-none-any.whl
- Upload date:
- Size: 56.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
db7d8114743423abe8e757427a66fcbf48861fa55d35c0871afbf72053d10f64
|
|
| MD5 |
2eabbba5cdd782fa69350344e6b08fa0
|
|
| BLAKE2b-256 |
7d2e9a4014a0e4c142fddc2dd496d8892c31222dd7798e80575023143d216b78
|