<feed xmlns='http://www.w3.org/2005/Atom'>
<title>blogspace/internal/web/web_test.go, 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: Clamp page numbers so a huge ?p= cannot overflow the offset</title>
<updated>2026-09-18T10:49:53+00:00</updated>
<author>
<name>grm</name>
<email>grm@eyesin.space</email>
</author>
<published>2026-09-18T10:49:53+00:00</published>
<link rel='alternate' type='text/html' href='https://git.eyesin.space/blogspace/commit/?id=c47397ac1e2ceafafe2be3cdec86366dd396ed6f'/>
<id>c47397ac1e2ceafafe2be3cdec86366dd396ed6f</id>
<content type='text'>
?p=9223372036854775807 made (n-1)*per wrap negative, Postgres refused
the OFFSET and every listing, tag and search page answered 500 — a
line in the log per request for any bot fuzzing query strings. Page
numbers now stop at 100000.

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>
?p=9223372036854775807 made (n-1)*per wrap negative, Postgres refused
the OFFSET and every listing, tag and search page answered 500 — a
line in the log per request for any bot fuzzing query strings. Page
numbers now stop at 100000.

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: Send security headers; refuse to frame the dashboard</title>
<updated>2026-09-18T10:49:28+00:00</updated>
<author>
<name>grm</name>
<email>grm@eyesin.space</email>
</author>
<published>2026-09-18T10:49:19+00:00</published>
<link rel='alternate' type='text/html' href='https://git.eyesin.space/blogspace/commit/?id=d23fe805546e992c8033d64d7177fe1454ad7716'/>
<id>d23fe805546e992c8033d64d7177fe1454ad7716</id>
<content type='text'>
Pages carried no X-Content-Type-Options, no Referrer-Policy, and the
dashboard could be framed by any site (clickjacking a superadmin's
delete buttons). Every response now says nosniff and
strict-origin-when-cross-origin, and the management paths on the root
host add X-Frame-Options: DENY plus a CSP of frame-ancestors, base-uri,
form-action and object-src — the directives that do not touch the
dashboard's inline scripts, which are a product constraint. Blog pages
get no framing rule: they are the owner's content and may be embedded
on purpose. HSTS moves into the same helper.

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>
Pages carried no X-Content-Type-Options, no Referrer-Policy, and the
dashboard could be framed by any site (clickjacking a superadmin's
delete buttons). Every response now says nosniff and
strict-origin-when-cross-origin, and the management paths on the root
host add X-Frame-Options: DENY plus a CSP of frame-ancestors, base-uri,
form-action and object-src — the directives that do not touch the
dashboard's inline scripts, which are a product constraint. Blog pages
get no framing rule: they are the owner's content and may be embedded
on purpose. HSTS moves into the same helper.

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: Refuse form posts a browser marks as coming from another origin</title>
<updated>2026-09-18T10:48:50+00:00</updated>
<author>
<name>grm</name>
<email>grm@eyesin.space</email>
</author>
<published>2026-09-18T10:48:50+00:00</published>
<link rel='alternate' type='text/html' href='https://git.eyesin.space/blogspace/commit/?id=abc1898daebae33405688618caffa01cece3b850'/>
<id>abc1898daebae33405688618caffa01cece3b850</id>
<content type='text'>
Every blog is a subdomain of the root domain, which makes a blog page
"same-site" to the dashboard: SameSite=Lax sends the session cookie
with a form a blog's custom HTML submits to example.com. The HMAC token
already stops those, but the login form had nothing (login CSRF), and
a second, independent check costs one header lookup. A POST whose
Sec-Fetch-Site is cross-site or same-site is now refused in guardPOST
and on login; old browsers without the header keep working under the
token alone.

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 is a subdomain of the root domain, which makes a blog page
"same-site" to the dashboard: SameSite=Lax sends the session cookie
with a form a blog's custom HTML submits to example.com. The HMAC token
already stops those, but the login form had nothing (login CSRF), and
a second, independent check costs one header lookup. A POST whose
Sec-Fetch-Site is cross-site or same-site is now refused in guardPOST
and on login; old browsers without the header keep working under the
token alone.

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