<feed xmlns='http://www.w3.org/2005/Atom'>
<title>blogspace/README.md, branch master</title>
<subtitle>blogspace</subtitle>
<link rel='alternate' type='text/html' href='https://git.eyesin.space/blogspace/'/>
<entry>
<title>Remove the menu fold</title>
<updated>2026-09-18T21:04:08+00:00</updated>
<author>
<name>grm</name>
<email>grm@eyesin.space</email>
</author>
<published>2026-09-18T21:04:08+00:00</published>
<link rel='alternate' type='text/html' href='https://git.eyesin.space/blogspace/commit/?id=c42c56108ebac6673ec5c86f9f0d51f31e8eb2bd'/>
<id>c42c56108ebac6673ec5c86f9f0d51f31e8eb2bd</id>
<content type='text'>
The header menu no longer folds into a ☰ button: the nav_fold and
nav_fold_label options, Layout.Fold/BurgerAt, the burger markup and its
checkbox, the fold rules in blog.css and the measuring script on the
public page are gone, and the Design tab's preview only sizes its frame
again. The public page runs no script of its own once more.

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 header menu no longer folds into a ☰ button: the nav_fold and
nav_fold_label options, Layout.Fold/BurgerAt, the burger markup and its
checkbox, the fold rules in blog.css and the measuring script on the
public page are gone, and the Design tab's preview only sizes its frame
again. The public page runs no script of its own once more.

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>Show a live preview of the header on the Design tab's Menu card</title>
<updated>2026-09-18T16:34:35+00:00</updated>
<author>
<name>grm</name>
<email>grm@eyesin.space</email>
</author>
<published>2026-09-18T16:34:35+00:00</published>
<link rel='alternate' type='text/html' href='https://git.eyesin.space/blogspace/commit/?id=96336d8bacd2bdf81acf9a18e2fe57be9d4e9b4f'/>
<id>96336d8bacd2bdf81acf9a18e2fe57be9d4e9b4f</id>
<content type='text'>
The menu options had grown to a dozen switches with nothing to look at
but the blog after a save. A new POST /b/{sub}/design/preview reads the
form as a save would and renders the header alone — the real blog.css
and theme CSS, the modules in their order, the menu as edited — into a
sandboxed frame that refreshes shortly after every change, with a Wide
screen / Phone toggle. Nothing is stored.

The frame keeps the dashboard's origin (allow-same-origin, no scripts)
so the blog's logo and fonts, linked through /b/&lt;sub&gt;/media since the
root host's /media is the root blog's, get the session cookie; that is
safe because no header module is owner HTML. The frame runs no scripts,
so the page's own script folds the menu in it the way the blog does.

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 menu options had grown to a dozen switches with nothing to look at
but the blog after a save. A new POST /b/{sub}/design/preview reads the
form as a save would and renders the header alone — the real blog.css
and theme CSS, the modules in their order, the menu as edited — into a
sandboxed frame that refreshes shortly after every change, with a Wide
screen / Phone toggle. Nothing is stored.

The frame keeps the dashboard's origin (allow-same-origin, no scripts)
so the blog's logo and fonts, linked through /b/&lt;sub&gt;/media since the
root host's /media is the root blog's, get the session cookie; that is
safe because no header module is owner HTML. The frame runs no scripts,
so the page's own script folds the menu in it the way the blog does.

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>Fold the header menu into a button when its links do not fit</title>
<updated>2026-09-18T16:29:30+00:00</updated>
<author>
<name>grm</name>
<email>grm@eyesin.space</email>
</author>
<published>2026-09-18T16:29:30+00:00</published>
<link rel='alternate' type='text/html' href='https://git.eyesin.space/blogspace/commit/?id=63f9fdff6e5bdf6333a4b7813ebeb5b797838b6d'/>
<id>63f9fdff6e5bdf6333a4b7813ebeb5b797838b6d</id>
<content type='text'>
A long menu wrapped onto two or three lines on phones and narrow
windows. With the new Design switch the header menu folds into a ☰
button (an icon, the word Menu, or both) carried by the title or logo
block just above it, at the right end of its row, or by the menu bar
when nothing is above it; the button opens the links as a stacked list
with the search box on top.

The open/closed state is a hidden checkbox and the button its label,
so opening needs no script and works in old browsers. Whether to fold
is decided by a small inline script — any link on a second line means
the menu does not fit — re-checked on resize; without it blog.css folds
on phones only, and browsers too old for that keep the wrapping menu.

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 long menu wrapped onto two or three lines on phones and narrow
windows. With the new Design switch the header menu folds into a ☰
button (an icon, the word Menu, or both) carried by the title or logo
block just above it, at the right end of its row, or by the menu bar
when nothing is above it; the button opens the links as a stacked list
with the search box on top.

The open/closed state is a hidden checkbox and the button its label,
so opening needs no script and works in old browsers. Whether to fold
is decided by a small inline script — any link on a second line means
the menu does not fit — re-checked on resize; without it blog.css folds
on phones only, and browsers too old for that keep the wrapping menu.

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 menu link size, spacing, a current-page mark and a spread alignment</title>
<updated>2026-09-18T16:18:30+00:00</updated>
<author>
<name>grm</name>
<email>grm@eyesin.space</email>
</author>
<published>2026-09-18T16:18:30+00:00</published>
<link rel='alternate' type='text/html' href='https://git.eyesin.space/blogspace/commit/?id=0a248d9a0e6ef6439fca25aa854968b6b5dac2ef'/>
<id>0a248d9a0e6ef6439fca25aa854968b6b5dac2ef</id>
<content type='text'>
Size and spacing selects, a choice of how the current page's link is
marked (underlined as before, bold, boxed or not at all) and a fourth
alignment that spreads the header's links across the bar. Alignment
joins the other menu classes on the body, so the theme CSS no longer
sets it; spread is justified text with a full-width last line, which
old browsers lay out too, and stays out of the column menus.

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>
Size and spacing selects, a choice of how the current page's link is
marked (underlined as before, bold, boxed or not at all) and a fourth
alignment that spreads the header's links across the bar. Alignment
joins the other menu classes on the body, so the theme CSS no longer
sets it; spread is justified text with a full-width last line, which
old browsers lay out too, and stays out of the column menus.

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 menu link styles switches that combine</title>
<updated>2026-09-18T16:17:17+00:00</updated>
<author>
<name>grm</name>
<email>grm@eyesin.space</email>
</author>
<published>2026-09-18T16:17:17+00:00</published>
<link rel='alternate' type='text/html' href='https://git.eyesin.space/blogspace/commit/?id=b88a7d22c8d79b30de3403c631dd2c5f67669537'/>
<id>b88a7d22c8d79b30de3403c631dd2c5f67669537</id>
<content type='text'>
The one "Link style" select (normal, uppercase, plain) could not give
uppercase links without the bold, or underlined bold links. Bold,
uppercase and the underline (never, on hover, always) are now separate
switches, drawn by blog.css from body classes (Theme.NavClass) so the
theme CSS only colours the links. A stored nav_style is mapped onto the
switches when the theme is read and dropped on the next save; themes
without either keep the bold, underline-on-hover look blog.css always had.

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 one "Link style" select (normal, uppercase, plain) could not give
uppercase links without the bold, or underlined bold links. Bold,
uppercase and the underline (never, on hover, always) are now separate
switches, drawn by blog.css from body classes (Theme.NavClass) so the
theme CSS only colours the links. A stored nav_style is mapped onto the
switches when the theme is read and dropped on the next save; themes
without either keep the bold, underline-on-hover look blog.css always had.

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>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>Security: Document the hardening and the proxy's part in it</title>
<updated>2026-09-18T10:51:16+00:00</updated>
<author>
<name>grm</name>
<email>grm@eyesin.space</email>
</author>
<published>2026-09-18T10:51:16+00:00</published>
<link rel='alternate' type='text/html' href='https://git.eyesin.space/blogspace/commit/?id=1b99463c1908037bb7dff9a766917791c25f484c'/>
<id>1b99463c1908037bb7dff9a766917791c25f484c</id>
<content type='text'>
README: limit_req in the nginx sample, what HTTPS and TRUST_PROXY are
for, and the auth notes cover the throttles, Sec-Fetch-Site, headers
and logging. AGENTS.md records what the security pass checked and
left alone, so the next one need not repeat it.

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>
README: limit_req in the nginx sample, what HTTPS and TRUST_PROXY are
for, and the auth notes cover the throttles, Sec-Fetch-Site, headers
and logging. AGENTS.md records what the security pass checked and
left alone, so the next one need not repeat it.

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>Security: Add HTTPS and TRUST_PROXY settings</title>
<updated>2026-09-18T10:47:49+00:00</updated>
<author>
<name>grm</name>
<email>grm@eyesin.space</email>
</author>
<published>2026-09-18T10:47:49+00:00</published>
<link rel='alternate' type='text/html' href='https://git.eyesin.space/blogspace/commit/?id=3eeaa6d9a33f9294ade64c8ff26144fba86a379e'/>
<id>3eeaa6d9a33f9294ade64c8ff26144fba86a379e</id>
<content type='text'>
The app only ever speaks plain HTTP, so it could not know the site was
served over TLS: generated links were http:// (the /webadmin bounce
from a blog host sent the login page over http), the session cookie
was never Secure and nothing sent HSTS. HTTPS=true fixes all three;
ClearSessionCookie now uses the same attributes as the set, since a
browser only replaces a cookie whose Secure flag matches.

TRUST_PROXY=true makes the client address the last X-Forwarded-For
entry — the one our proxy appended — so throttling and the log see
real addresses instead of the proxy's; earlier entries are whatever
the client sent and are ignored. Production startup warns when HTTPS
is off.

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 app only ever speaks plain HTTP, so it could not know the site was
served over TLS: generated links were http:// (the /webadmin bounce
from a blog host sent the login page over http), the session cookie
was never Secure and nothing sent HSTS. HTTPS=true fixes all three;
ClearSessionCookie now uses the same attributes as the set, since a
browser only replaces a cookie whose Secure flag matches.

TRUST_PROXY=true makes the client address the last X-Forwarded-For
entry — the one our proxy appended — so throttling and the log see
real addresses instead of the proxy's; earlier entries are whatever
the client sent and are ignored. Production startup warns when HTTPS
is off.

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 Fira Code as a built-in font choice</title>
<updated>2026-09-17T19:54:19+00:00</updated>
<author>
<name>grm</name>
<email>grm@eyesin.space</email>
</author>
<published>2026-09-17T19:54:19+00:00</published>
<link rel='alternate' type='text/html' href='https://git.eyesin.space/blogspace/commit/?id=ad30b38759dc222a7137598559998acbd0c06740'/>
<id>ad30b38759dc222a7137598559998acbd0c06740</id>
<content type='text'>
The variable face (v6.2, OFL, licence alongside) is bundled in static/ and
declared as @font-face whenever the text or heading font is "fira"; the
stack falls back through Fira Mono, Menlo and Consolas to Courier.

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 variable face (v6.2, OFL, licence alongside) is bundled in static/ and
declared as @font-face whenever the text or heading font is "fira"; the
stack falls back through Fira Mono, Menlo and Consolas to Courier.

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