aboutsummaryrefslogtreecommitdiffstats
path: root/internal/web/templates
diff options
context:
space:
mode:
authorgrm <grm@eyesin.space>2026-09-12 19:47:28 +0300
committergrm <grm@eyesin.space>2026-09-12 19:47:28 +0300
commite974eb6f3feb06c8951f85c1d34161938905f807 (patch)
tree3bba878b6764e9d18519efd393d4e780eaabd81a /internal/web/templates
parentdd13e4b73a105ee416b6cfd8eb19b217622c0031 (diff)
downloadblogspace-e974eb6f3feb06c8951f85c1d34161938905f807.tar.gz
blogspace-e974eb6f3feb06c8951f85c1d34161938905f807.tar.bz2
blogspace-e974eb6f3feb06c8951f85c1d34161938905f807.zip
Add a favicon and rework the Design page
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
Diffstat (limited to 'internal/web/templates')
-rw-r--r--internal/web/templates/blog/page.html2
-rw-r--r--internal/web/templates/blog/post.html2
-rw-r--r--internal/web/templates/dashboard/design.html138
-rw-r--r--internal/web/templates/dashboard/images.html2
-rw-r--r--internal/web/templates/layouts/blog.html7
-rw-r--r--internal/web/templates/layouts/dashboard.html2
-rw-r--r--internal/web/templates/partials/imagepick.html16
7 files changed, 141 insertions, 28 deletions
diff --git a/internal/web/templates/blog/page.html b/internal/web/templates/blog/page.html
index c1fe726..ba78ff6 100644
--- a/internal/web/templates/blog/page.html
+++ b/internal/web/templates/blog/page.html
@@ -5,7 +5,7 @@
{{range .Data.posts}}
<article class="post">
<h2 class="post-title"><a href="/{{.PageSlug}}/{{.Slug}}">{{.Title}}</a></h2>
- <p class="post-date">{{date .CreatedAt}}</p>
+ {{if $.Data.theme.ShowDates}}<p class="post-date">{{$.Data.theme.FormatDate .CreatedAt}}</p>{{end}}
<div class="post-body">{{html .BodyHTML}}</div>
</article>
{{else}}{{if not .Data.page.IntroHTML}}<p class="muted">Nothing here yet.</p>{{end}}{{end}}
diff --git a/internal/web/templates/blog/post.html b/internal/web/templates/blog/post.html
index 83ce782..5fa8ca4 100644
--- a/internal/web/templates/blog/post.html
+++ b/internal/web/templates/blog/post.html
@@ -2,7 +2,7 @@
{{define "content"}}
<article class="post single">
<h1 class="post-title">{{.Data.post.Title}}</h1>
- <p class="post-date">{{date .Data.post.CreatedAt}} &middot; <a href="{{if .Data.page.IsHome}}/{{else}}/{{.Data.page.Slug}}{{end}}">{{.Data.page.Title}}</a></p>
+ <p class="post-date">{{if .Data.theme.ShowDates}}{{.Data.theme.FormatDate .Data.post.CreatedAt}} &middot; {{end}}<a href="{{if .Data.page.IsHome}}/{{else}}/{{.Data.page.Slug}}{{end}}">{{.Data.page.Title}}</a></p>
<div class="post-body">{{html .Data.post.BodyHTML}}</div>
</article>
{{end}}
diff --git a/internal/web/templates/dashboard/design.html b/internal/web/templates/dashboard/design.html
index 09ea1fe..af482fe 100644
--- a/internal/web/templates/dashboard/design.html
+++ b/internal/web/templates/dashboard/design.html
@@ -1,12 +1,29 @@
{{define "title"}}Design · {{.Blog.Title}}{{end}}
{{define "content"}}
{{$t := .Data.theme}}
-<h1>Design</h1>
-<p class="muted">Save, then refresh <a href="{{.BlogURL}}" target="_blank">your blog &#8599;</a> to see the result. Colours are hex values like <code>#336699</code>.</p>
+<div class="pagehead">
+ <h1>Design</h1>
+ <p class="lead muted">Changes are live as soon as you save; refresh <a href="{{.BlogURL}}" target="_blank">your blog &#8599;</a> to see them.</p>
+</div>
+<p class="jumps"><a href="#schemes">Schemes</a><a href="#background">Background</a><a href="#text">Text</a><a href="#header">Header</a><a href="#menu">Menu</a><a href="#posts">Posts</a><a href="#footer">Footer</a><a href="#icon">Icon</a></p>
+
+<div class="card" id="schemes">
+ <h2>Colour schemes</h2>
+ <p class="muted small">A quick start: applying a scheme sets every colour below and saves immediately. Images, fonts and layout are kept.</p>
+ <form method="post" action="/b/{{.Blog.Subdomain}}/design/preset" class="presets">
+ <input type="hidden" name="_csrf" value="{{.CSRF}}">
+ {{range .Data.presets}}<button type="submit" name="preset" value="{{.Key}}" class="preset" title="Apply {{.Name}}">
+ <span class="sw" style="background:{{.HeaderBg}}"></span><span class="sw" style="background:{{.Bg}}"></span><span class="sw" style="background:{{.ContentBg}}"></span><span class="sw" style="background:{{.Link}}"></span>
+ <span class="pname">{{.Name}}</span>
+ </button>
+ {{end}}
+ </form>
+</div>
+
<form method="post" action="/b/{{.Blog.Subdomain}}/design" enctype="multipart/form-data">
<input type="hidden" name="_csrf" value="{{.CSRF}}">
-<div class="card">
+<div class="card" id="background">
<h2>Background</h2>
<div class="row">
<label>Colour<br><input type="color" name="bg_color" value="{{$t.BgColor}}"></label>
@@ -19,33 +36,60 @@
{{template "imagepick" (dict "name" "bg_image" "current" $t.BgImage "images" .Data.images "label" "Background image")}}
</div>
-<div class="card">
+<div class="card" id="text">
<h2>Text &amp; content area</h2>
<div class="row">
<label>Content background<br><input type="color" name="content_bg" value="{{$t.ContentBg}}"></label>
<label>Text colour<br><input type="color" name="text_color" value="{{$t.TextColor}}"></label>
<label>Link colour<br><input type="color" name="link_color" value="{{$t.LinkColor}}"></label>
</div>
+ <div class="hoverpick">
+ <input type="checkbox" name="link_hover_custom" id="link-hover-custom"{{if $t.LinkHover}} checked{{end}}>
+ <label for="link-hover-custom" class="check">Change colour when the mouse is over a link</label>
+ <label class="hover-color">Hover colour<br><input type="color" name="link_hover" value="{{if $t.LinkHover}}{{$t.LinkHover}}{{else}}{{$t.LinkColor}}{{end}}"></label>
+ </div>
<div class="row">
<label>Font<br><select name="font">
<option value="sans"{{if eq $t.Font "sans"}} selected{{end}}>Sans-serif (Helvetica/Arial)</option>
<option value="serif"{{if eq $t.Font "serif"}} selected{{end}}>Serif (Georgia/Times)</option>
<option value="mono"{{if eq $t.Font "mono"}} selected{{end}}>Monospace (Courier)</option>
</select></label>
+ <label>Heading font<br><select name="heading_font">
+ <option value="same"{{if eq $t.HeadingFont "same"}} selected{{end}}>Same as text</option>
+ <option value="sans"{{if eq $t.HeadingFont "sans"}} selected{{end}}>Sans-serif</option>
+ <option value="serif"{{if eq $t.HeadingFont "serif"}} selected{{end}}>Serif</option>
+ <option value="mono"{{if eq $t.HeadingFont "mono"}} selected{{end}}>Monospace</option>
+ </select></label>
<label>Text size<br><select name="font_size">
<option value="small"{{if eq $t.FontSize "small"}} selected{{end}}>Small</option>
<option value="normal"{{if eq $t.FontSize "normal"}} selected{{end}}>Normal</option>
<option value="large"{{if eq $t.FontSize "large"}} selected{{end}}>Large</option>
</select></label>
+ <label>Underline links<br><select name="link_underline">
+ <option value="always"{{if eq $t.LinkUnderline "always"}} selected{{end}}>Always</option>
+ <option value="hover"{{if eq $t.LinkUnderline "hover"}} selected{{end}}>Only on hover</option>
+ <option value="never"{{if eq $t.LinkUnderline "never"}} selected{{end}}>Never</option>
+ </select></label>
+ </div>
+ <div class="row">
<label>Width<br><select name="content_width">
<option value="narrow"{{if eq $t.ContentWidth "narrow"}} selected{{end}}>Narrow</option>
<option value="medium"{{if eq $t.ContentWidth "medium"}} selected{{end}}>Medium</option>
<option value="wide"{{if eq $t.ContentWidth "wide"}} selected{{end}}>Wide</option>
</select></label>
+ <label>Box style<br><select name="content_style">
+ <option value="card"{{if eq $t.ContentStyle "card"}} selected{{end}}>Card (shadow, rounded corners)</option>
+ <option value="flat"{{if eq $t.ContentStyle "flat"}} selected{{end}}>Flat</option>
+ </select></label>
+ <label>Padding<br><select name="content_padding">
+ <option value="compact"{{if eq $t.ContentPadding "compact"}} selected{{end}}>Compact</option>
+ <option value="normal"{{if eq $t.ContentPadding "normal"}} selected{{end}}>Normal</option>
+ <option value="roomy"{{if eq $t.ContentPadding "roomy"}} selected{{end}}>Roomy</option>
+ </select></label>
</div>
</div>
-<div class="card">
+<div class="card" id="header">
<h2>Header</h2>
<div class="row">
<label>Background<br><input type="color" name="header_bg" value="{{$t.HeaderBg}}"></label>
@@ -54,34 +98,100 @@
<option value="left"{{if eq $t.HeaderAlign "left"}} selected{{end}}>Left</option>
<option value="center"{{if eq $t.HeaderAlign "center"}} selected{{end}}>Centred</option>
</select></label>
+ <label>Image height<br><select name="header_image_height">
+ <option value="short"{{if eq $t.HeaderImageHeight "short"}} selected{{end}}>Short</option>
+ <option value="medium"{{if eq $t.HeaderImageHeight "medium"}} selected{{end}}>Medium</option>
+ <option value="tall"{{if eq $t.HeaderImageHeight "tall"}} selected{{end}}>Tall</option>
+ </select></label>
</div>
<label class="check"><input type="checkbox" name="header_show_title"{{if $t.HeaderShowTitle}} checked{{end}}> Show blog title and tagline in the header</label>
{{template "imagepick" (dict "name" "header_image" "current" $t.HeaderImage "images" .Data.images "label" "Header image (banner)")}}
</div>
-<div class="card">
+<div class="card" id="menu">
<h2>Menu</h2>
+ <p class="muted small">The menu lists the pages you marked "show in menu". Pick where it goes; the options underneath follow your choice.</p>
+ <div class="navpos">
+ <input type="radio" name="nav_position" value="below-header" id="np-below"{{if eq $t.NavPosition "below-header"}} checked{{end}}>
+ <label for="np-below" class="tile"><svg viewBox="0 0 80 56" width="80" height="56" aria-hidden="true"><rect x="0" y="0" width="80" height="56" fill="#f4efe4"/><rect x="0" y="0" width="80" height="14" fill="#1d1a17"/><rect x="0" y="16" width="80" height="6" fill="#c8541a"/><rect x="10" y="28" width="60" height="24" fill="#fffdf8" stroke="#1d1a17"/></svg><span>Below the header</span></label>
+ <input type="radio" name="nav_position" value="top-bar" id="np-top"{{if eq $t.NavPosition "top-bar"}} checked{{end}}>
+ <label for="np-top" class="tile"><svg viewBox="0 0 80 56" width="80" height="56" aria-hidden="true"><rect x="0" y="0" width="80" height="56" fill="#f4efe4"/><rect x="0" y="0" width="80" height="6" fill="#c8541a"/><rect x="0" y="8" width="80" height="14" fill="#1d1a17"/><rect x="10" y="28" width="60" height="24" fill="#fffdf8" stroke="#1d1a17"/></svg><span>Bar at the very top</span></label>
+ <input type="radio" name="nav_position" value="left-sidebar" id="np-side"{{if eq $t.NavPosition "left-sidebar"}} checked{{end}}>
+ <label for="np-side" class="tile"><svg viewBox="0 0 80 56" width="80" height="56" aria-hidden="true"><rect x="0" y="0" width="80" height="56" fill="#f4efe4"/><rect x="0" y="0" width="80" height="14" fill="#1d1a17"/><rect x="8" y="20" width="18" height="32" fill="#c8541a"/><rect x="30" y="20" width="42" height="32" fill="#fffdf8" stroke="#1d1a17"/></svg><span>Sidebar on the left</span></label>
+ <div class="row only-horizontal">
+ <label>Alignment<br><select name="nav_align">
+ <option value="left"{{if eq $t.NavAlign "left"}} selected{{end}}>Left</option>
+ <option value="center"{{if eq $t.NavAlign "center"}} selected{{end}}>Centred</option>
+ <option value="right"{{if eq $t.NavAlign "right"}} selected{{end}}>Right</option>
+ </select></label>
+ </div>
+ <div class="row only-sidebar">
+ <label>Sidebar width<br><select name="nav_sidebar_width">
+ <option value="narrow"{{if eq $t.NavSidebarWidth "narrow"}} selected{{end}}>Narrow</option>
+ <option value="normal"{{if eq $t.NavSidebarWidth "normal"}} selected{{end}}>Normal</option>
+ <option value="wide"{{if eq $t.NavSidebarWidth "wide"}} selected{{end}}>Wide</option>
+ </select></label>
+ </div>
+ <p class="muted small only-sidebar">Links stack under each other; announcements placed "in the sidebar" appear beneath them. On phones the sidebar turns into a row of links above the content.</p>
+ </div>
<div class="row">
- <label>Position<br><select name="nav_position">
- <option value="below-header"{{if eq $t.NavPosition "below-header"}} selected{{end}}>Below the header</option>
- <option value="top-bar"{{if eq $t.NavPosition "top-bar"}} selected{{end}}>Bar at the very top</option>
- <option value="left-sidebar"{{if eq $t.NavPosition "left-sidebar"}} selected{{end}}>Sidebar on the left</option>
+ <label>Link style<br><select name="nav_style">
+ <option value="normal"{{if eq $t.NavStyle "normal"}} selected{{end}}>Normal</option>
+ <option value="uppercase"{{if eq $t.NavStyle "uppercase"}} selected{{end}}>Uppercase, spaced</option>
</select></label>
- <label>Background<br><input type="color" name="nav_bg" value="{{$t.NavBg}}"></label>
- <label>Text colour<br><input type="color" name="nav_text" value="{{$t.NavText}}"></label>
</div>
<label class="check"><input type="checkbox" name="nav_show_home"{{if $t.NavShowHome}} checked{{end}}> Include the home page in the menu</label>
+ <h3>Menu colours</h3>
+ <div class="row">
+ <label>Background<br><input type="color" name="nav_bg" value="{{$t.NavBg}}"></label>
+ <label>Text<br><input type="color" name="nav_text" value="{{$t.NavText}}"></label>
+ </div>
+ <div class="hoverpick">
+ <input type="checkbox" name="nav_hover_custom" id="nav-hover-custom"{{if $t.NavHover}} checked{{end}}>
+ <label for="nav-hover-custom" class="check">Change colour when the mouse is over a link</label>
+ <label class="hover-color">Hover colour<br><input type="color" name="nav_hover" value="{{if $t.NavHover}}{{$t.NavHover}}{{else}}{{$t.NavText}}{{end}}"></label>
+ </div>
</div>
-<div class="card">
+<div class="card" id="posts">
+ <h2>Posts &amp; dates</h2>
+ <label class="check"><input type="checkbox" name="show_dates"{{if $t.ShowDates}} checked{{end}}> Show the date on posts</label>
+ <div class="row">
+ <label>Date format<br><select name="date_format">
+ <option value="long"{{if eq $t.DateFormat "long"}} selected{{end}}>12 September 2026</option>
+ <option value="short"{{if eq $t.DateFormat "short"}} selected{{end}}>12 Sep 2026</option>
+ <option value="iso"{{if eq $t.DateFormat "iso"}} selected{{end}}>2026-09-12</option>
+ </select></label>
+ </div>
+</div>
+
+<div class="card" id="footer">
<h2>Footer</h2>
<label>Footer text<br><input name="footer_text" value="{{$t.FooterText}}" maxlength="2000" placeholder="© 2026 Me"></label>
<div class="row">
<label>Background<br><input type="color" name="footer_bg" value="{{$t.FooterBg}}"></label>
<label>Text colour<br><input type="color" name="footer_color" value="{{$t.FooterColor}}"></label>
</div>
+ <label class="check"><input type="checkbox" name="footer_show_rss"{{if $t.FooterShowRSS}} checked{{end}}> Show the RSS link in the footer</label>
+</div>
+
+<div class="card" id="icon">
+ <h2>Site icon</h2>
+ {{template "imagepick" (dict "name" "favicon" "current" $t.Favicon "images" .Data.images "label" "Favicon" "hint" "The small icon browsers show in the tab. A square PNG or ICO of 32–64 px works best; pick None for the Blogspace icon." "accept" "image/png,image/x-icon,image/gif,image/webp,image/jpeg,.ico")}}
</div>
-<p class="actionrow"><button type="submit">Save design</button></p>
+<div class="savebar">
+ <button type="submit">Save design</button>
+ <a class="btn secondary" href="{{.BlogURL}}" target="_blank">View blog &#8599;</a>
+</div>
</form>
+
+<details class="help reset">
+ <summary>Reset to defaults</summary>
+ <p>Puts every colour, font and layout option back to how a new blog starts and forgets the chosen images (they stay in your library).</p>
+ <form method="post" action="/b/{{.Blog.Subdomain}}/design/reset">
+ <input type="hidden" name="_csrf" value="{{.CSRF}}">
+ <button type="submit" class="danger">Reset everything</button>
+ </form>
+</details>
{{end}}
diff --git a/internal/web/templates/dashboard/images.html b/internal/web/templates/dashboard/images.html
index b44289e..1c64391 100644
--- a/internal/web/templates/dashboard/images.html
+++ b/internal/web/templates/dashboard/images.html
@@ -7,7 +7,7 @@
<div class="card">
<form method="post" action="/b/{{.Blog.Subdomain}}/images/upload" enctype="multipart/form-data">
<input type="hidden" name="_csrf" value="{{.CSRF}}">
- <label>Upload an image <span class="muted">(PNG, JPEG, GIF or WebP)</span><br><input type="file" name="file" accept="image/*" required></label>
+ <label>Upload an image <span class="muted">(PNG, JPEG, GIF, WebP or ICO)</span><br><input type="file" name="file" accept="image/*" required></label>
<p class="actionrow"><button type="submit">Upload</button></p>
</form>
</div>
diff --git a/internal/web/templates/layouts/blog.html b/internal/web/templates/layouts/blog.html
index ac6595b..3447e1f 100644
--- a/internal/web/templates/layouts/blog.html
+++ b/internal/web/templates/layouts/blog.html
@@ -5,7 +5,10 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{block "title" .}}{{.Blog.Title}}{{end}}</title>
<link rel="alternate" type="application/rss+xml" title="{{.Blog.Title}}" href="/feed.xml">
-<link rel="stylesheet" href="/static/blog.css">
+{{if .Data.theme.Favicon}}<link rel="icon" href="/media/{{.Data.theme.Favicon}}">
+{{else}}<link rel="icon" href="/static/favicon.png" sizes="48x48">
+<link rel="icon" href="/static/favicon.svg" type="image/svg+xml">
+{{end}}<link rel="stylesheet" href="/static/blog.css">
<style>{{css .Data.css}}</style>
</head>
<body class="nav-{{.Data.theme.NavPosition}}">
@@ -29,7 +32,7 @@
<div class="site-footer">
<div class="wrap footer-inner">
{{if .Data.theme.FooterText}}<p>{{.Data.theme.FooterText}}</p>{{end}}
- <p class="small"><a href="/feed.xml">RSS</a> &middot; {{.Blog.Title}}</p>
+ <p class="small">{{if .Data.theme.FooterShowRSS}}<a href="/feed.xml">RSS</a> &middot; {{end}}{{.Blog.Title}}</p>
</div>
</div>
</body>
diff --git a/internal/web/templates/layouts/dashboard.html b/internal/web/templates/layouts/dashboard.html
index 6633c29..e724cc6 100644
--- a/internal/web/templates/layouts/dashboard.html
+++ b/internal/web/templates/layouts/dashboard.html
@@ -4,6 +4,8 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{block "title" .}}Blogspace{{end}}</title>
+<link rel="icon" href="/static/favicon.png" sizes="48x48">
+<link rel="icon" href="/static/favicon.svg" type="image/svg+xml">
<link rel="stylesheet" href="/static/dashboard.css">
</head>
<body>
diff --git a/internal/web/templates/partials/imagepick.html b/internal/web/templates/partials/imagepick.html
index 687c6cd..0c6eced 100644
--- a/internal/web/templates/partials/imagepick.html
+++ b/internal/web/templates/partials/imagepick.html
@@ -1,11 +1,9 @@
{{define "imagepick"}}<div class="imagepick">
- <label>{{.label}}<br>
- <select name="{{.name}}">
- <option value="">{{if .current}}(keep current){{else}}(none){{end}}</option>
- {{range .images}}<option value="{{.ID}}"{{if eq .ID.String $.current}} selected{{end}}>{{.Filename}}</option>{{end}}
- </select>
- </label>
- <label>…or upload a new one<br><input type="file" name="{{.name}}_file" accept="image/*"></label>
- {{if .current}}<label class="check"><input type="checkbox" name="{{.name}}_remove"> Remove image</label>
- <img class="preview" src="/media/{{.current}}" alt="">{{end}}
+ <label>{{.label}}</label>
+ {{if .hint}}<p class="muted small">{{.hint}}</p>{{end}}
+ <div class="picks">
+ <label class="pick none"><input type="radio" name="{{.name}}" value="none"{{if not .current}} checked{{end}}><span class="swatch">&times;</span><span class="name">None</span></label>
+ {{range .images}}<label class="pick"><input type="radio" name="{{$.name}}" value="{{.ID}}"{{if eq .ID.String $.current}} checked{{end}}><img src="/media/{{.ID}}" alt=""><span class="name">{{.Filename}}</span></label>{{end}}
+ </div>
+ <label>…or upload a new one<br><input type="file" name="{{.name}}_file" accept="{{if .accept}}{{.accept}}{{else}}image/*{{end}}"></label>
</div>{{end}}