<feed xmlns='http://www.w3.org/2005/Atom'>
<title>blogspace/internal/web/templates/dashboard/post_form.html, branch master</title>
<subtitle>blogspace</subtitle>
<link rel='alternate' type='text/html' href='https://git.eyesin.space/blogspace/'/>
<entry>
<title>Revert "Special pages: a gallery page of the blog's images"</title>
<updated>2026-09-18T11:36:17+00:00</updated>
<author>
<name>grm</name>
<email>grm@eyesin.space</email>
</author>
<published>2026-09-18T11:35:59+00:00</published>
<link rel='alternate' type='text/html' href='https://git.eyesin.space/blogspace/commit/?id=7ac90f8e99dbf763b0fe04a17d7eef19dd22cae8'/>
<id>7ac90f8e99dbf763b0fe04a17d7eef19dd22cae8</id>
<content type='text'>
This reverts commit 0fc470a. The feature was not wanted. Migration
00013 stays on disk because databases have already run it (goose refuses
a missing applied migration); 00014 drops the column and table it added.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
This reverts commit 0fc470a. The feature was not wanted. Migration
00013 stays on disk because databases have already run it (goose refuses
a missing applied migration); 00014 drops the column and table it added.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A
</pre>
</div>
</content>
</entry>
<entry>
<title>Special pages: a gallery page of the blog's images</title>
<updated>2026-09-18T11:32:22+00:00</updated>
<author>
<name>grm</name>
<email>grm@eyesin.space</email>
</author>
<published>2026-09-18T11:32:22+00:00</published>
<link rel='alternate' type='text/html' href='https://git.eyesin.space/blogspace/commit/?id=0fc470a8d588cac1a51cc4b3e1a2237dc56041c9'/>
<id>0fc470a8d588cac1a51cc4b3e1a2237dc56041c9</id>
<content type='text'>
A page's kind says what it shows between its intro and outro: posts,
as before, or something special. The first special kind is a gallery —
every image in the files library as a masonry wall (CSS columns, no
script), newest first, each linked to its full-size file. The page form
lists the library's images with a Hide tick per picture; exclusions are
stored (page_hidden_files) rather than inclusions so a new upload shows
up without editing the page, and the design's own pictures — logo,
favicon, header and background image — are never shown. Special pages
keep the page chrome (menu, announcements, home, ordering) but hold no
posts: the post forms skip them and a forced page id is refused.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
A page's kind says what it shows between its intro and outro: posts,
as before, or something special. The first special kind is a gallery —
every image in the files library as a masonry wall (CSS columns, no
script), newest first, each linked to its full-size file. The page form
lists the library's images with a Hide tick per picture; exclusions are
stored (page_hidden_files) rather than inclusions so a new upload shows
up without editing the page, and the design's own pictures — logo,
favicon, header and background image — are never shown. Special pages
keep the page chrome (menu, announcements, home, ordering) but hold no
posts: the post forms skip them and a forced page id is refused.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A
</pre>
</div>
</content>
</entry>
<entry>
<title>Let a post hide its date</title>
<updated>2026-09-17T18:42:02+00:00</updated>
<author>
<name>grm</name>
<email>grm@eyesin.space</email>
</author>
<published>2026-09-17T18:42:02+00:00</published>
<link rel='alternate' type='text/html' href='https://git.eyesin.space/blogspace/commit/?id=2a347ced1b4ea8c24440f646df35207c3a3597ac'/>
<id>2a347ced1b4ea8c24440f646df35207c3a3597ac</id>
<content type='text'>
A checkbox on the post form (posts.hide_date) overriding the theme's
"show the date on posts" for that post alone: listings, the post page and
search results leave the date out; the feed keeps its pubDate.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
A checkbox on the post form (posts.hide_date) overriding the theme's
"show the date on posts" for that post alone: listings, the post page and
search results leave the date out; the feed keeps its pubDate.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A
</pre>
</div>
</content>
</entry>
<entry>
<title>Add post summaries, featured images and a page outro</title>
<updated>2026-09-17T13:42:15+00:00</updated>
<author>
<name>grm</name>
<email>grm@eyesin.space</email>
</author>
<published>2026-09-17T13:42:15+00:00</published>
<link rel='alternate' type='text/html' href='https://git.eyesin.space/blogspace/commit/?id=d0601f06c080a8eb8d781fdd0e3338cd7ed42690'/>
<id>d0601f06c080a8eb8d781fdd0e3338cd7ed42690</id>
<content type='text'>
Recreating gizmolab.net as a blog showed three things the design system
could not express, all of them ordinary blog features:

- Listings can show a summary and a "Read more" link instead of the whole
  post (theme.list_style). The summary is the source up to &lt;!--more--&gt;;
  without the marker a Markdown post is cut at a block boundary after 70
  words (never inside a code fence) and an HTML post is shown whole, since
  cutting hand-written markup blind would leave tags open. Computed at
  render time (Post.Excerpt/HasMore), nothing new is stored.
- A post can carry a featured image (posts.image, a library file that is
  unset when the file is deleted): a thumbnail floated beside the entry on
  listings, and on the post page above or below the text, or not at all,
  as theme.post_image says. The picker is the design page's imagepick; its
  library panel moves out of design.html into partials/imagelib.html so
  both forms share it — which also removes a stale pickAction call the
  design form's click handler would have hit.
- A page can have a closing text after its posts (pages.outro_md/html), in
  the intro's format: the per-page counterpart of the blog-wide "after the
  posts" module, for a home page that ends in a site map or a widget. The
  editor partial takes formatof to share another editor's Format row, and
  its upload field is now &lt;name&gt;_file so two editors fit in one form.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
Recreating gizmolab.net as a blog showed three things the design system
could not express, all of them ordinary blog features:

- Listings can show a summary and a "Read more" link instead of the whole
  post (theme.list_style). The summary is the source up to &lt;!--more--&gt;;
  without the marker a Markdown post is cut at a block boundary after 70
  words (never inside a code fence) and an HTML post is shown whole, since
  cutting hand-written markup blind would leave tags open. Computed at
  render time (Post.Excerpt/HasMore), nothing new is stored.
- A post can carry a featured image (posts.image, a library file that is
  unset when the file is deleted): a thumbnail floated beside the entry on
  listings, and on the post page above or below the text, or not at all,
  as theme.post_image says. The picker is the design page's imagepick; its
  library panel moves out of design.html into partials/imagelib.html so
  both forms share it — which also removes a stale pickAction call the
  design form's click handler would have hit.
- A page can have a closing text after its posts (pages.outro_md/html), in
  the intro's format: the per-page counterpart of the blog-wide "after the
  posts" module, for a home page that ends in a site map or a widget. The
  editor partial takes formatof to share another editor's Format row, and
  its upload field is now &lt;name&gt;_file so two editors fit in one form.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A
</pre>
</div>
</content>
</entry>
<entry>
<title>Add an HTML mode to posts, page intros and announcements</title>
<updated>2026-09-16T15:37:20+00:00</updated>
<author>
<name>grm</name>
<email>grm@eyesin.space</email>
</author>
<published>2026-09-16T15:37:20+00:00</published>
<link rel='alternate' type='text/html' href='https://git.eyesin.space/blogspace/commit/?id=6b6b5d2e35ff182a0732da245f4eb37c8afa0564'/>
<id>6b6b5d2e35ff182a0732da245f4eb37c8afa0564</id>
<content type='text'>
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 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
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 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A
</pre>
</div>
</content>
</entry>
<entry>
<title>Make the post form's tag input a box with a folded list of chips</title>
<updated>2026-09-15T20:17:17+00:00</updated>
<author>
<name>grm</name>
<email>grm@eyesin.space</email>
</author>
<published>2026-09-15T20:17:17+00:00</published>
<link rel='alternate' type='text/html' href='https://git.eyesin.space/blogspace/commit/?id=628dcb814ece6335667e2b719049728aa161d8c9'/>
<id>628dcb814ece6335667e2b719049728aa161d8c9</id>
<content type='text'>
A checkbox per existing tag stops working once a blog has dozens of
them. The form is now one comma-separated box, prefilled with the
post's tags, and the blog's tags sit folded under it as chips; a small
script toggles a clicked chip in the box, and without it the chips are
a plain list to copy from.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
A checkbox per existing tag stops working once a blog has dozens of
them. The form is now one comma-separated box, prefilled with the
post's tags, and the blog's tags sit folded under it as chips; a small
script toggles a clicked chip in the box, and without it the chips are
a plain list to copy from.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A
</pre>
</div>
</content>
</entry>
<entry>
<title>Add post tags, with a tag page and two side-column modules</title>
<updated>2026-09-15T20:12:51+00:00</updated>
<author>
<name>grm</name>
<email>grm@eyesin.space</email>
</author>
<published>2026-09-15T20:12:51+00:00</published>
<link rel='alternate' type='text/html' href='https://git.eyesin.space/blogspace/commit/?id=2c85ca1a46ddf4652f8fffc53d6bcfbe32b2b4ff'/>
<id>2c85ca1a46ddf4652f8fffc53d6bcfbe32b2b4ff</id>
<content type='text'>
Posts can now carry tags, set on the post form as a checklist of the
blog's existing tags plus a comma-separated box for new ones (no JS).
A tag is a name and a unique slug, so "Go" and "go" are one tag and
Greek tags get readable URLs; tags no post uses any more are deleted.

On the blog, tags appear under the post date and link to /tag/&lt;slug&gt;,
which lists the published posts from every page, paginated like a page.
Two new layout modules show them: a Tags list (with counts, by use) and
a Tag cloud (alphabetical, sized by use). Both are only fetched when a
visible module needs them.

The article loop and pager move to a shared postlist partial; while
there, the pager stops adding a trailing slash — /news/?p=2 was a 404
because a {page} wildcard never matches one.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
Posts can now carry tags, set on the post form as a checklist of the
blog's existing tags plus a comma-separated box for new ones (no JS).
A tag is a name and a unique slug, so "Go" and "go" are one tag and
Greek tags get readable URLs; tags no post uses any more are deleted.

On the blog, tags appear under the post date and link to /tag/&lt;slug&gt;,
which lists the published posts from every page, paginated like a page.
Two new layout modules show them: a Tags list (with counts, by use) and
a Tag cloud (alphabetical, sized by use). Both are only fetched when a
visible module needs them.

The article loop and pager move to a shared postlist partial; while
there, the pager stops adding a trailing slash — /news/?p=2 was a 404
because a {page} wildcard never matches one.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A
</pre>
</div>
</content>
</entry>
<entry>
<title>Give the editor a Markdown toolbar and preview, restyle the dashboard</title>
<updated>2026-09-15T19:26:13+00:00</updated>
<author>
<name>grm</name>
<email>grm@eyesin.space</email>
</author>
<published>2026-09-15T19:26:13+00:00</published>
<link rel='alternate' type='text/html' href='https://git.eyesin.space/blogspace/commit/?id=55732bdfc25933bab26220ccd82fe5e172d9e673'/>
<id>55732bdfc25933bab26220ccd82fe5e172d9e673</id>
<content type='text'>
The post/page/announcement editor was a bare textarea, so bloggers had to
know the Markdown syntax by heart. It now has a 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
renders the text through the same goldmark + bluemonday pipeline a save
uses (POST /b/{sub}/preview; nothing is stored). Every edit goes through
execCommand("insertText") so browser undo keeps working. The toolbar
stays hidden without JavaScript, leaving the old form untouched.

The dashboard moves from the "paper &amp; ink" look to a lean one: neutral
surfaces, 1px borders, one blue accent, system sans, with custom
properties and flexbox/grid now allowed there (blog.css is unchanged).
Class names were kept so the templates barely change; a global [hidden]
rule keeps flex containers from overriding the hidden attribute.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
The post/page/announcement editor was a bare textarea, so bloggers had to
know the Markdown syntax by heart. It now has a 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
renders the text through the same goldmark + bluemonday pipeline a save
uses (POST /b/{sub}/preview; nothing is stored). Every edit goes through
execCommand("insertText") so browser undo keeps working. The toolbar
stays hidden without JavaScript, leaving the old form untouched.

The dashboard moves from the "paper &amp; ink" look to a lean one: neutral
surfaces, 1px borders, one blue accent, system sans, with custom
properties and flexbox/grid now allowed there (blog.css is unchanged).
Class names were kept so the templates barely change; a global [hidden]
rule keeps flex containers from overriding the hidden attribute.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A
</pre>
</div>
</content>
</entry>
<entry>
<title>Turn the image library into a file library, with a per-blog upload limit</title>
<updated>2026-09-14T20:51:55+00:00</updated>
<author>
<name>grm</name>
<email>grm@eyesin.space</email>
</author>
<published>2026-09-14T20:51:55+00:00</published>
<link rel='alternate' type='text/html' href='https://git.eyesin.space/blogspace/commit/?id=778cb72c8a0902bd0b8159ebd3bb7eff93f28c83'/>
<id>778cb72c8a0902bd0b8159ebd3bb7eff93f28c83</id>
<content type='text'>
Bloggers want to attach PDFs, archives, audio and other files to posts,
not only images. The Images tab becomes Files: any type is accepted,
listed by kind with search, paging, rename and multi-file upload, and the
editor's paste/drop/"Insert file" takes anything (images are shown,
everything else becomes a link). The default limit goes from 5 to 10 MB
and the superadmin can override it per blog from /admin/.

Files stay in Postgres so one pg_dump is still the whole blog. The bytea
column is STORAGE EXTERNAL and /media streams it in substring() slices,
so serving never holds a whole file in memory whatever limit a blog gets.

Serving any type on the root domain, which carries the session cookie,
needs a policy: uploads are typed by sniffing (the extension may only
refine a generic sniff to an allowlisted type) and only images, PDF,
plain text, audio and video render inline; HTML, SVG, XML, scripts,
archives and binaries always go out as application/octet-stream with
Content-Disposition: attachment.

The body cap moves out of requireAuth into guardPOST, which runs after
withBlog has resolved the blog and so knows its limit.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
Bloggers want to attach PDFs, archives, audio and other files to posts,
not only images. The Images tab becomes Files: any type is accepted,
listed by kind with search, paging, rename and multi-file upload, and the
editor's paste/drop/"Insert file" takes anything (images are shown,
everything else becomes a link). The default limit goes from 5 to 10 MB
and the superadmin can override it per blog from /admin/.

Files stay in Postgres so one pg_dump is still the whole blog. The bytea
column is STORAGE EXTERNAL and /media streams it in substring() slices,
so serving never holds a whole file in memory whatever limit a blog gets.

Serving any type on the root domain, which carries the session cookie,
needs a policy: uploads are typed by sniffing (the extension may only
refine a generic sniff to an allowlisted type) and only images, PDF,
plain text, audio and video render inline; HTML, SVG, XML, scripts,
archives and binaries always go out as application/octet-stream with
Content-Disposition: attachment.

The body cap moves out of requireAuth into guardPOST, which runs after
withBlog has resolved the blog and so knows its limit.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A
</pre>
</div>
</content>
</entry>
<entry>
<title>Let the blogger set a post's date, when writing it and afterwards</title>
<updated>2026-09-14T19:58:08+00:00</updated>
<author>
<name>grm</name>
<email>grm@eyesin.space</email>
</author>
<published>2026-09-14T19:58:08+00:00</published>
<link rel='alternate' type='text/html' href='https://git.eyesin.space/blogspace/commit/?id=5f9fc2a8667a9438b6336d75026f9a455fc9c4ce'/>
<id>5f9fc2a8667a9438b6336d75026f9a455fc9c4ce</id>
<content type='text'>
The post date is posts.created_at: it already drives ordering, the
archive, the feed and the displayed dates, so making it editable is a
form field rather than a new column. The datetime-local input degrades
to a text box on old browsers, so the server also accepts the value
typed by hand; blank keeps the current date.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A
</content>
<content type='xhtml'>
<div xmlns='http://www.w3.org/1999/xhtml'>
<pre>
The post date is posts.created_at: it already drives ordering, the
archive, the feed and the displayed dates, so making it editable is a
form field rather than a new column. The datetime-local input degrades
to a text box on old browsers, so the server also accepts the value
typed by hand; blank keeps the current date.

Co-Authored-By: Claude Opus 5 &lt;noreply@anthropic.com&gt;
Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A
</pre>
</div>
</content>
</entry>
</feed>
