<feed xmlns='http://www.w3.org/2005/Atom'>
<title>blogspace/internal/web/templates/partials/imagepick.html, branch master</title>
<subtitle>blogspace</subtitle>
<link rel='alternate' type='text/html' href='https://git.eyesin.space/blogspace/'/>
<entry>
<title>Keep a chosen image older than the picker's list, and the layout on a rejected save</title>
<updated>2026-09-17T15:54:33+00:00</updated>
<author>
<name>grm</name>
<email>grm@eyesin.space</email>
</author>
<published>2026-09-17T15:54:33+00:00</published>
<link rel='alternate' type='text/html' href='https://git.eyesin.space/blogspace/commit/?id=616aa3a934951260d5daa81ab3d0f93fbc94ebee'/>
<id>616aa3a934951260d5daa81ab3d0f93fbc94ebee</id>
<content type='text'>
The image picker's select lists the newest recentImages library images and
was meant to add the chosen one when it is older than those, but the fallback
option was gated on the filename being unknown, which designData makes known:
the option never rendered, the browser selected "None", and every save of the
design form cleared a logo or favicon older than the 48 newest images (the
post form's featured image the same way). The partial now checks whether the
choice is listed and otherwise emits it, named.

handleDesign ran the uploads before parsing the module and menu rows and gave
up at the first problem, so a rejected upload rendered the form without any
rows — and, since a missing row means delete, the next save would have wiped
the layout and menu. Everything is read before the first problem is reported.

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 image picker's select lists the newest recentImages library images and
was meant to add the chosen one when it is older than those, but the fallback
option was gated on the filename being unknown, which designData makes known:
the option never rendered, the browser selected "None", and every save of the
design form cleared a logo or favicon older than the 48 newest images (the
post form's featured image the same way). The partial now checks whether the
choice is listed and otherwise emits it, named.

handleDesign ran the uploads before parsing the module and menu rows and gave
up at the first problem, so a rejected upload rendered the form without any
rows — and, since a missing row means delete, the next save would have wiped
the layout and menu. Everything is read before the first problem is reported.

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>Merge the Layout and Design tabs into one form with a single Save</title>
<updated>2026-09-16T16:10:13+00:00</updated>
<author>
<name>grm</name>
<email>grm@eyesin.space</email>
</author>
<published>2026-09-16T16:10:13+00:00</published>
<link rel='alternate' type='text/html' href='https://git.eyesin.space/blogspace/commit/?id=4a6f987bf7f9d73e9dd33d225fd2d73e9bf69a88'/>
<id>4a6f987bf7f9d73e9dd33d225fd2d73e9bf69a88</id>
<content type='text'>
The look and the layout of a blog were split over two tabs that cross-linked
each other, and the Layout tab saved every toggle, move and add straight
away through a dozen tiny forms. Now one Design tab holds it all, organised
by part of the blog (colours &amp; fonts, header, menu, content, side columns,
footer, logo &amp; icon) behind a sticky side index, and one Save stores the
theme, every module and the menu atomically (BlogStore.SaveDesign). Module
settings are edited inline; Discard changes throws the edits away; a
validation error shows the form again exactly as it was sent.

The image pickers no longer render the whole library four times over: a
select of the newest images, and a script-driven panel that fetches the
library page by page (GET /files as JSON) with lazy thumbnails.

New options: posts per page, site title size, footer alignment.

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 look and the layout of a blog were split over two tabs that cross-linked
each other, and the Layout tab saved every toggle, move and add straight
away through a dozen tiny forms. Now one Design tab holds it all, organised
by part of the blog (colours &amp; fonts, header, menu, content, side columns,
footer, logo &amp; icon) behind a sticky side index, and one Save stores the
theme, every module and the menu atomically (BlogStore.SaveDesign). Module
settings are edited inline; Discard changes throws the edits away; a
validation error shows the form again exactly as it was sent.

The image pickers no longer render the whole library four times over: a
select of the newest images, and a script-driven panel that fetches the
library page by page (GET /files as JSON) with lazy thumbnails.

New options: posts per page, site title size, footer alignment.

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 languages: English and Greek, chosen per blog on the Settings tab</title>
<updated>2026-09-13T22:32:25+00:00</updated>
<author>
<name>grm</name>
<email>grm@eyesin.space</email>
</author>
<published>2026-09-13T22:32:25+00:00</published>
<link rel='alternate' type='text/html' href='https://git.eyesin.space/blogspace/commit/?id=e14a4250efb828262347f5a4bbc4067cd71d885f'/>
<id>e14a4250efb828262347f5a4bbc4067cd71d885f</id>
<content type='text'>
Every blog and its dashboard were hard-wired to English. A blogger can
now pick the language of their blog; it switches the whole dashboard and
the blog's fixed text — post dates, archive months, the RSS link, the
pager, the 404 page — while what the blogger wrote is left alone.

The new internal/i18n package keys translations by the English string,
so an untranslated key renders as English rather than blank, and
TestGreekCatalogComplete scans the templates and handlers to fail when
the Greek catalog misses a key or keeps a stale one. Templates are
compiled once per language with t/tf/date/postdate/month closed over the
language, so they need no data plumbing.

The language lives in settings.language (blog migration 00002), not in
the theme, so "Reset design" does not touch it. Public pages use the
blog's language; management pages use the logged-in user's own blog's,
so a superadmin editing someone else's blog keeps theirs; the login page
follows Accept-Language.

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>
Every blog and its dashboard were hard-wired to English. A blogger can
now pick the language of their blog; it switches the whole dashboard and
the blog's fixed text — post dates, archive months, the RSS link, the
pager, the 404 page — while what the blogger wrote is left alone.

The new internal/i18n package keys translations by the English string,
so an untranslated key renders as English rather than blank, and
TestGreekCatalogComplete scans the templates and handlers to fail when
the Greek catalog misses a key or keeps a stale one. Templates are
compiled once per language with t/tf/date/postdate/month closed over the
language, so they need no data plumbing.

The language lives in settings.language (blog migration 00002), not in
the theme, so "Reset design" does not touch it. Public pages use the
blog's language; management pages use the logged-in user's own blog's,
so a superadmin editing someone else's blog keeps theirs; the login page
follows Accept-Language.

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 every blog its own Postgres database</title>
<updated>2026-09-13T20:52:41+00:00</updated>
<author>
<name>grm</name>
<email>grm@eyesin.space</email>
</author>
<published>2026-09-13T20:52:41+00:00</published>
<link rel='alternate' type='text/html' href='https://git.eyesin.space/blogspace/commit/?id=aeb19df4222269c585de55be5568326222df879d'/>
<id>aeb19df4222269c585de55be5568326222df879d</id>
<content type='text'>
A blog is now a database of its own (blog_&lt;sub&gt;) on the same server: one
pg_dump is a complete backup of a blog, one psql restores it, and nothing a
blog's queries do can reach another blog's rows. The control database
(DATABASE_URL) keeps only users and the blog registry (id, owner, subdomain,
db_name); title, tagline and theme move into a one-row settings table next
to the content so the dump really is everything.

db.Cluster holds the control pool plus small, lazily opened per-blog pools.
store.Store (control) hands out a store.BlogStore per blog; every blog_id
parameter and column is gone, the database is the scope. Handlers reach it
through blogStore(r), which resolveBlog puts in the context next to the blog.

Existing data is moved in place by control migration 00006, a Go migration
that runs inside the control transaction: it creates and migrates each blog
database, copies the rows preserving ids, and marks the registry; 00007 then
drops the old tables. Either every blog is moved or the control database is
untouched.

/media/{id} now serves the host's blog only, so dashboard previews on the
root domain use /b/{sub}/media/{id}. Subdomains are capped at 58 chars so
"blog_" + name fits a Postgres identifier. Deleting a user drops their
database. Store.Open resets a blog's pool and retries once so a database
restored under a running app (dropdb --force, createdb, psql) just works.

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 blog is now a database of its own (blog_&lt;sub&gt;) on the same server: one
pg_dump is a complete backup of a blog, one psql restores it, and nothing a
blog's queries do can reach another blog's rows. The control database
(DATABASE_URL) keeps only users and the blog registry (id, owner, subdomain,
db_name); title, tagline and theme move into a one-row settings table next
to the content so the dump really is everything.

db.Cluster holds the control pool plus small, lazily opened per-blog pools.
store.Store (control) hands out a store.BlogStore per blog; every blog_id
parameter and column is gone, the database is the scope. Handlers reach it
through blogStore(r), which resolveBlog puts in the context next to the blog.

Existing data is moved in place by control migration 00006, a Go migration
that runs inside the control transaction: it creates and migrates each blog
database, copies the rows preserving ids, and marks the registry; 00007 then
drops the old tables. Either every blog is moved or the control database is
untouched.

/media/{id} now serves the host's blog only, so dashboard previews on the
root domain use /b/{sub}/media/{id}. Subdomains are capped at 58 chars so
"blog_" + name fits a Postgres identifier. Deleting a user drops their
database. Store.Open resets a blog's pool and retries once so a database
restored under a running app (dropdb --force, createdb, psql) just works.

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 a favicon and rework the Design page</title>
<updated>2026-09-12T16:47:28+00:00</updated>
<author>
<name>grm</name>
<email>grm@eyesin.space</email>
</author>
<published>2026-09-12T16:47:28+00:00</published>
<link rel='alternate' type='text/html' href='https://git.eyesin.space/blogspace/commit/?id=e974eb6f3feb06c8951f85c1d34161938905f807'/>
<id>e974eb6f3feb06c8951f85c1d34161938905f807</id>
<content type='text'>
Blogs had no icon at all, and browsers probing /favicon.ico ended up
rendering the 404 page. Ship a default paper &amp; ink icon (svg + png) and
let each blog replace it from the Design page; /favicon.ico now serves
the default or redirects to the blog's own image. ICO uploads accepted.

The Design page gains colour schemes, reset to defaults, thumbnail
image pickers, jump links and a sticky save bar, plus new options:
heading font, link underline/hover, content box style and padding,
header image height, post dates and format, footer RSS toggle, menu
alignment/style/hover and sidebar width. The Menu card picks the
position with illustrated tiles and shows only the options that apply
to it, using CSS :checked rules rather than JavaScript.

All new defaults reproduce the previous look, so existing blogs are
unchanged until edited. Theme is jsonb, so no migration.

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>
Blogs had no icon at all, and browsers probing /favicon.ico ended up
rendering the 404 page. Ship a default paper &amp; ink icon (svg + png) and
let each blog replace it from the Design page; /favicon.ico now serves
the default or redirects to the blog's own image. ICO uploads accepted.

The Design page gains colour schemes, reset to defaults, thumbnail
image pickers, jump links and a sticky save bar, plus new options:
heading font, link underline/hover, content box style and padding,
header image height, post dates and format, footer RSS toggle, menu
alignment/style/hover and sidebar width. The Menu card picks the
position with illustrated tiles and shows only the options that apply
to it, using CSS :checked rules rather than JavaScript.

All new defaults reproduce the previous look, so existing blogs are
unchanged until edited. Theme is jsonb, so no migration.

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>Initial multi-tenant blog host</title>
<updated>2026-09-12T08:24:17+00:00</updated>
<author>
<name>gramanas</name>
<email>grm@eyesin.space</email>
</author>
<published>2026-09-12T08:24:17+00:00</published>
<link rel='alternate' type='text/html' href='https://git.eyesin.space/blogspace/commit/?id=3eb04b1a2bdf9e53231fe862cfd76327371a9741'/>
<id>3eb04b1a2bdf9e53231fe862cfd76327371a9741</id>
<content type='text'>
Go + Postgres application serving a management dashboard on the base
domain and one public blog per subdomain. Markdown posts organised in
pages, form-based theme customisation, image uploads stored in Postgres,
JWT cookie sessions with CSRF, superadmin user management, RSS feeds.
Docker/compose deployment and a Makefile-driven dev environment with
seed data.

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>
Go + Postgres application serving a management dashboard on the base
domain and one public blog per subdomain. Markdown posts organised in
pages, form-based theme customisation, image uploads stored in Postgres,
JWT cookie sessions with CSRF, superadmin user management, RSS feeds.
Docker/compose deployment and a Makefile-driven dev environment with
seed data.

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