aboutsummaryrefslogtreecommitdiffstats
path: root/internal/web/static
Commit message (Collapse)AuthorAgeFilesLines
* Fold the header menu into a button when its links do not fitgrm2026-09-181-0/+47
| | | | | | | | | | | | | | | | | | 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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A
* Add menu link size, spacing, a current-page mark and a spread alignmentgrm2026-09-181-1/+17
| | | | | | | | | | | | 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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A
* Make the menu link styles switches that combinegrm2026-09-181-2/+5
| | | | | | | | | | | | | 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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A
* Revert "Special pages: a gallery page of the blog's images"grm2026-09-182-16/+6
| | | | | | | | | 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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A
* Special pages: a gallery page of the blog's imagesgrm2026-09-182-6/+16
| | | | | | | | | | | | | | | | 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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A
* Give the double-dotted title rule the same gap as the othersgrm2026-09-181-1/+1
| | | | | | | | | | The second dotted row is positioned 5px inside the title's padding, so with dots2 the text sat 5px closer to the rule than with line, dots or stripes. The padding grows by that amount (0.7em for browsers without calc). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A
* Pair headed gizmo announcements two to a row, split by text lengthgrm2026-09-181-4/+10
| | | | | | | | | | | | Headed gizmo boxes used to share a row equally, up to three of them. Now placeNotices pairs consecutive ones in the main column and gives the pair one of the fixed splits — 25/75, 33/67, 50/50, 67/33, 75/25 — nearest the ratio of the text each shows, so the wordier box gets the room. The bases add up to 90% so a third box never joins the row, and a side column, too narrow for two, stacks them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A
* Give the rule under titles more room above and belowgrm2026-09-181-2/+3
| | | | | | | | | | The rule sat 0.25em under the title text with the date or body starting 0.1em beneath it. Titles with a rule now keep 0.4em above it and 0.7em below; the stripes, being the ::after, get their gap from its margin instead of the padding so they sit as evenly as the borders. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A
* Add Fira Code as a built-in font choicegrm2026-09-172-0/+93
| | | | | | | | | 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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A
* Keep the tags off the title when a post shows no dategrm2026-09-171-1/+2
| | | | | | | | | The tags' negative top margin tucks them under the date line; without a date (hidden on the post, or blog-wide) it pulled them into the title. It now applies only when a date precedes them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A
* Replace the colons title rule with a double dotted bordergrm2026-09-171-3/+4
| | | | | | | | | A row of ":" characters did not read as a rule. "Double dots" is what the reference site does: the 3px dotted border and an absolutely positioned ::after carrying a second one 2px above it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A
* Add a colons kind to the rule under titles, and stop its label wrappinggrm2026-09-171-1/+3
| | | | | | | | | | The design form's hint on "Rule under titles" wrapped to three lines and pushed the select below its neighbours; it is a note under the row now. "Colons" is a row of ":" (a long string in an ::after, clipped to the title's width), the look of the reference site's dotted borders in text. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A
* Add a rule under page and post titles, and tags shown with a # in frontgrm2026-09-171-0/+6
| | | | | | | | | | | | Two Design tab options in the Posts section. The title rule reuses the header rule's kinds (line, dots, stripes): a body class blog.css draws from — width and style only, so the theme CSS's link colour is not reset — ending at a floated thumbnail instead of running under it. The tag hash is applied everywhere tags are listed: under posts, in both tag modules and in the tag page's title. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A
* Add custom fonts: upload WOFF2/WOFF/TTF/OTF files and pick them on the ↵grm2026-09-171-0/+5
| | | | | | | | | | | | | | | Design tab Fonts become a file kind of their own (sniffed from the bytes like images, served inline from /media), and the theme gets custom_font and custom_heading_font: a chosen file is declared as @font-face and put first in the stack, the built-in Font / Heading font choice behind it as the fallback, so there is no "custom" enum value and no invalid state. The pickers are plain selects with an upload input, no script. Fonts uploaded before this are reclassified by the migration. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A
* Add a header rule beside the title or logo, and plain menu linksgrm2026-09-171-0/+17
| | | | | | | | | | | | | | gizmolab's header is a logo with a striped rule filling the rest of the row, and a menu of plain underlined links under it; nothing in the Design tab could draw either. The rule (line, dots or stripes) is a theme option drawn in the header text colour beside the title and logo modules — on both sides when the header is centred — as table cells, so it is as tall as the brand and old browsers lay it out the same; it is hidden on phones so a long title can wrap. The menu's link style gains "plain, underlined" next to bold and uppercase. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A
* Add the Gizmo look for announcements: framed boxes that share a rowgrm2026-09-171-1/+13
| | | | | | | | | | | | | gizmolab's "next event" and "next assembly" boxes were hand-written HTML in the home page's intro; they are announcements by nature, so now they can be: a fourth style, a box framed in the theme's link colour with an italic heading. Headed Gizmo boxes in the same spot share a row on wide screens (the notices wrapper is a flex row, stacking on old browsers like .cols); one without a heading takes the whole row, which is how a banner line goes above a pair of boxes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A
* Add post summaries, featured images and a page outrogrm2026-09-172-0/+12
| | | | | | | | | | | | | | | | | | | | | | | | | | | 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 <!--more-->; 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 <name>_file so two editors fit in one form. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A
* Merge the Layout and Design tabs into one form with a single Savegrm2026-09-161-31/+75
| | | | | | | | | | | | | | | | | | | | 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 & fonts, header, menu, content, side columns, footer, logo & 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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A
* Add an HTML mode to posts, page intros and announcementsgrm2026-09-161-0/+8
| | | | | | | | | | | | | | | | | 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
* Put the header search box in the menu row, and stop saying "Search"grm2026-09-161-5/+10
| | | | | | | | | | | | | The header search was a band of its own under the menu; now it floats at the right end of the menu row and only gets a thin bar when the header has no menu. The forms are a single input — Enter and the phone keyboard's search key submit a one-field form everywhere — with the placeholder as the label, side boxes start without a heading, and the results page shows a box only when no module does, so a page no longer reads "Search" a dozen times. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A
* Add a search box module and a search pagegrm2026-09-161-0/+12
| | | | | | | | | | | | | | | | | | Readers had no way to find a post. The new "search" module goes in the header (a bar under the nav) or a side column (a box with a heading) and is a plain GET form to /search, so it works without JavaScript. The results page lists the published posts of every page whose title or Markdown body matches the query — case-insensitive, each word literal, spaces meaning "anything in between", in order — as title, date and a short snippet with the match marked, 20 per page. modules.kind is a CHECK constraint, so a migration widens it; "search" becomes a reserved page slug so the literal route keeps winning over /{page}. moduleHasSettings now takes the module: a header search box has no heading, hence nothing to edit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A
* Make the post form's tag input a box with a folded list of chipsgrm2026-09-151-4/+7
| | | | | | | | | | | 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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A
* Add post tags, with a tag page and two side-column modulesgrm2026-09-152-0/+13
| | | | | | | | | | | | | | | | | | | | 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/<slug>, 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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A
* Give the editor a Markdown toolbar and preview, restyle the dashboardgrm2026-09-151-151/+191
| | | | | | | | | | | | | | | | | | | | 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 & 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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A
* Turn the image library into a file library, with a per-blog upload limitgrm2026-09-141-7/+18
| | | | | | | | | | | | | | | | | | | | | | | | | | 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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A
* Let the blogger set a post's date, when writing it and afterwardsgrm2026-09-141-1/+1
| | | | | | | | | | | 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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A
* Drop the content width option: the page is always full widthgrm2026-09-131-2/+1
| | | | | | | | | | With the column grid, a centred max-width box meant the main content did not fill the page when no side columns were shown. The column percentages now divide the whole page; a blogger who wants a narrower, centred main column keeps the columns' space instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A
* Add the Layout tab: five areas with modules, columns and a menu editorgrm2026-09-132-27/+68
| | | | | | | | | | | | | | | | | | | A blog page is now header, left column, main content, right column and footer, each holding an ordered list of modules (blog title, logo, menu, archive by year/month, recent posts, custom HTML, footer text, RSS link, site map). Side columns have percentage widths, can be hidden, and can keep their space when empty; on phones they stack under the posts. The menu becomes its own ordered list mixing pages and outside links. Announcements are placed per column at its top or bottom. Modules and menu items get tables; the migration derives them from each blog's old theme (nav position, footer text, show-in-nav pages) so existing blogs look the same. Custom HTML is deliberately stored and served as-is (owner's decision; see AGENTS.md for why the blast radius is the blogger's own origin). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A
* Add a favicon and rework the Design pagegrm2026-09-124-3/+50
| | | | | | | | | | | | | | | | | | | | | Blogs had no icon at all, and browsers probing /favicon.ico ended up rendering the 404 page. Ship a default paper & 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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A
* Add announcements: blog-wide notices on every page and postgrm2026-09-121-0/+11
| | | | | | | | | | | A blog can have any number of announcements ("sections" in the schema): a book club's next meeting, this month's pick, a holiday closure. Each has a placement (above or below the posts, or under the menu when the menu is a left sidebar), a look (highlighted, warning, plain), an order, and an on/off switch so recurring notices can be kept around while hidden. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A
* Insert images straight from the Markdown editorgrm2026-09-121-1/+7
| | | | | | | | | | | A shared editor partial adds an "Insert image" file picker plus paste and drag-and-drop on the textarea; a few lines of inline JS upload the file to the existing images endpoint (which now answers JSON when asked) and put the Markdown at the caret. Without JS the same file rides along with the form and is appended to the text on save, so nothing depends on scripting. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A
* Redesign the dashboard: paper & inkgrm2026-09-121-62/+115
| | | | | | | | | | | | Flat colours, 2px rules, hard offset shadows and square corners; mono headings and labels; the blog nav is a tab strip that scrolls sideways on phones instead of stacking; list pages get a page header with the primary action on the right; the Markdown cheat-sheet collapses into a details element so forms stay calm. Plain CSS only, no variables or flex, so it still renders on old browsers. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A
* Highlight the current tab in the dashboard navgrm2026-09-121-0/+2
| | | | | Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A
* Initial multi-tenant blog hostgramanas2026-09-122-0/+137
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A