Skip to main content

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 small via <sub>.surge.sh caption underneath.
  • Edit → opens the visual editor.
  • Publish → runs surge against the site folder.
  • Open live → opens the primary domain in a new tab.

Click set / change in the surge row to open the modal:

  1. Surge subdomain (required): your-name.surge.sh.
  2. Custom domain (optional): something like myportfolio.com. When set, Scopuli writes <site>/CNAME with 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 PreviewEdit 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.html that matches the style of about.html". The AI reads every .html in 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, replaced textContent for 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 bind 0.0.0.0, put it behind a firewall or VPN.
  • API keys live in ~/.scopuli/config.toml in plaintext. chmod 600 is recommended.
  • Path-traversal is guarded on every file route; only ./websites/<site>/… paths are reachable.
  • The site-AI Apply step writes only .html files 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

scopuli-0.1.0.tar.gz (60.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

scopuli-0.1.0-py3-none-any.whl (56.3 kB view details)

Uploaded Python 3

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

Hashes for scopuli-0.1.0.tar.gz
Algorithm Hash digest
SHA256 ebf46788c42794920deb273bc14173b1569bc3f66191030f5ad64aba224dba0f
MD5 4667c6657a4f020511383acf2074fdb2
BLAKE2b-256 32f67647dc212983c0d5fe54f071dd8a33f643a77700ca47fc00d2837939c0b9

See more details on using hashes here.

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

Hashes for scopuli-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 db7d8114743423abe8e757427a66fcbf48861fa55d35c0871afbf72053d10f64
MD5 2eabbba5cdd782fa69350344e6b08fa0
BLAKE2b-256 7d2e9a4014a0e4c142fddc2dd496d8892c31222dd7798e80575023143d216b78

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page