aboutsummaryrefslogtreecommitdiffstats
path: root/AGENTS.md
diff options
context:
space:
mode:
authorgrm <grm@eyesin.space>2026-09-16 18:37:20 +0300
committergrm <grm@eyesin.space>2026-09-16 18:37:20 +0300
commit6b6b5d2e35ff182a0732da245f4eb37c8afa0564 (patch)
treef56960ae86c3c289f9a68e38ec01f1e3ff997466 /AGENTS.md
parent5119018feeaa22c47c0e91e15d3b9414dd6e0772 (diff)
downloadblogspace-6b6b5d2e35ff182a0732da245f4eb37c8afa0564.tar.gz
blogspace-6b6b5d2e35ff182a0732da245f4eb37c8afa0564.tar.bz2
blogspace-6b6b5d2e35ff182a0732da245f4eb37c8afa0564.zip
Add an HTML mode to posts, page intros and announcements
Markdown is the default and unchanged; the editor's new Format switch stores the text as raw HTML instead, put on the blog exactly as written. Like the custom HTML module it is unsanitised on purpose: the escape hatch for embeds, scripts and inline styles that Markdown cannot express. The source column keeps holding the text in both modes and a `format` column says how to read it, so the public templates and the feed still print the stored `*_html`. The dashboard preview of HTML goes into a sandboxed iframe rather than the page, because a superadmin edits other people's blogs and their markup must never run on the dashboard origin. Search snippets of HTML posts are cut from a tag-stripped copy. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A
Diffstat (limited to 'AGENTS.md')
-rw-r--r--AGENTS.md52
1 files changed, 35 insertions, 17 deletions
diff --git a/AGENTS.md b/AGENTS.md
index 3959498..1ea5ef9 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -28,10 +28,14 @@ table and deployment notes.
**Each blog is its own database** (`blog_<sub>`, see Key mechanics) so a
blog is backed up and restored with plain `pg_dump`/`psql`; the control
database (`DATABASE_URL`) holds only `users` and the `blogs` registry.
-- Posts are Markdown (goldmark → bluemonday). No WYSIWYG. The editor's
- toolbar, preview and "Insert file" are scripted conveniences over a plain
- textarea (`partials/editor.html`); the no-JS fallback (bare textarea, file
- appended on save) must keep working.
+- Posts, page intros and announcements are Markdown (goldmark → bluemonday)
+ **or raw HTML**: each has a `format` column (`markdown` | `html`), the
+ editor's Format radios. HTML is stored and output **unsanitised** — the
+ owner's decision, same as the custom HTML module below, same blast radius.
+ No WYSIWYG. The editor's toolbar, preview and "Insert file" are scripted
+ conveniences over a plain textarea (`partials/editor.html`); the no-JS
+ fallback (bare textarea, format radios, file appended on save) must keep
+ working.
- Changes are live immediately — there is no draft/preview system. The UX
is "save, then refresh your blog tab"; keep the "View blog ↗" links.
- Theme customisation is a structured form only; **no custom CSS input**.
@@ -212,7 +216,7 @@ internal/web/ server.go (host router, middleware, render helpers)
`![name](…)` for images and `[name](…)` for the rest. Deleting a file
clears theme references to it. `POST /b/{sub}/files/upload` takes several
`file` parts (the no-JS `<input multiple>`), or answers JSON
- (`{id, filename, kind, size, markdown}` / `{error}`) for one file when the
+ (`{id, filename, kind, size, markdown, html}` / `{error}`) for one file when the
request has `Accept: application/json` — what the editor and the Files
page scripts call. `dashboard/files.html` lists by `?kind=&q=&p=`
(`ListFiles`, 50 per page, `pageBounds` clamps), shows `FileUsage` and the
@@ -220,17 +224,31 @@ internal/web/ server.go (host router, middleware, render helpers)
is `blogs.max_upload_bytes` in the control DB (NULL = `MAX_UPLOAD_MB`, now
10), set at `POST /admin/blogs/{id}/upload-limit` from `admin/index.html`;
`Blog.UploadLimit(cfg)` resolves it.
-- **Editor** (`partials/editor.html`, args via `dict`: name, value, rows,
- tall, upload, preview, csrf): a textarea with a Markdown toolbar (bold,
- italic, strike, heading cycle, quote, code, lists, rule, link box, Insert
- file), Ctrl+B/I/K, list continuation on Enter, and a Write/Preview toggle
- that POSTs the text to `/b/{sub}/preview` (`handlePreview`: `markdown.Render`
- → `{html}`, nothing stored). Every edit goes through one `replace()` built on
- `execCommand("insertText")` so browser undo works. The toolbar is `hidden`
- until the script runs; without JS it is a bare textarea + "Insert file" +
- cheat-sheet, and paste/drop take any file. Forms using it must be
- `multipart/form-data` and their save handler must call
- `s.readUpload(r, "inline_file", false)` + `appendFileMD` (the no-JS path).
+- **Editor** (`partials/editor.html`, args via `dict`: name, value, format,
+ rows, tall, upload, preview, csrf): a Format row (radios `format` =
+ `markdown`|`html`, always rendered) and a textarea with a Markdown toolbar
+ (bold, italic, strike, heading cycle, quote, code, lists, rule, link box,
+ Insert file), Ctrl+B/I/K, list continuation on Enter, and a Write/Preview
+ toggle that POSTs the text to `/b/{sub}/preview` (`handlePreview`:
+ `markdown.Render` → `{html}`, nothing stored). Every edit goes through one
+ `replace()` built on `execCommand("insertText")` so browser undo works. The
+ toolbar is `hidden` until the script runs; without JS it is a bare textarea
+ + "Insert file" + cheat-sheet, and paste/drop take any file. Forms using it
+ must be `multipart/form-data` and their save handler must set `Format` with
+ `pick(r.FormValue("format"), store.FormatMarkdown, store.FormatHTML)`, call
+ `s.readUpload(r, "inline_file", false)` + `appendFile(body, f, format)`
+ (the no-JS path: `fileMarkdown` or `fileHTML`, i.e. `<img>`/`<a>`), and
+ store `renderBody(format, src)` (`markdown.Render`, or the source untouched
+ for HTML) in the `*_html` column — the public templates only ever print
+ that column.
+ **HTML mode** (`.editor.html`, toggled by the radios): the Markdown toolbar
+ groups (`.ed-md`), link box, shortcuts, list continuation and cheat-sheet
+ are off, the textarea is monospace, Insert file inserts the upload JSON's
+ `html` field. Preview does not touch the server: the text goes into
+ `<iframe sandbox srcdoc>` (`.ed-preview-frame`) — an opaque origin with no
+ scripts — because the text may not be the viewer's own (a superadmin edits
+ other people's blogs) and must never run on the dashboard origin, where the
+ session lives. Markdown preview keeps using `.ed-preview` in the page.
- **Announcements** (`sections` table, `store/sections.go`,
`handlers_sections.go`, `/b/{sub}/announcements…`): per-blog notices with
`placement` (`<column>-<position>`: left|main|right × top|bottom, split by
@@ -342,7 +360,7 @@ superadmin password to `admin`. Production refuses both.
`internal/db/migrations/control/` for users and the registry; goose
`-- +goose Up/Down` sections; they run automatically at startup. Never
edit an applied migration. Blog chain so far: `00001_init`, `00002_language`,
- `00003_files`, `00004_tags`, `00005_search`; control: `00001_init`, `00002_upload_limit`. Both chains were re-baselined at 00001 after
+ `00003_files`, `00004_tags`, `00005_search`, `00006_format`; control: `00001_init`, `00002_upload_limit`. Both chains were re-baselined at 00001 after
the move to per-blog databases; deployments from before it have
`goose_db_version` rows 2–7 in the control DB that must be deleted once
(README "Upgrading from a single database") or the next control migration