aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--AGENTS.md10
-rw-r--r--README.md3
-rw-r--r--internal/i18n/el.go17
-rw-r--r--internal/web/static/blog.css6
-rw-r--r--internal/web/templates.go7
-rw-r--r--internal/web/templates/blog/post.html2
-rw-r--r--internal/web/templates/blog/tag.html4
-rw-r--r--internal/web/templates/dashboard/design.html7
-rw-r--r--internal/web/templates/layouts/blog.html6
-rw-r--r--internal/web/templates/partials/postlist.html5
-rw-r--r--internal/web/theme.go19
-rw-r--r--internal/web/web_test.go17
12 files changed, 85 insertions, 18 deletions
diff --git a/AGENTS.md b/AGENTS.md
index d4a9284..bfed5cb 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -184,7 +184,15 @@ internal/web/ server.go (host router, middleware, render helpers)
as tall as the brand and old browsers agree) in `currentColor`, i.e. the
header text colour; hidden on phones so a long title can wrap. `NavStyle`
`plain` is regular weight + underline for the menu links (blog.css makes
- them bold).
+ them bold). `TitleRule` (`none` | `line` | `dots` | `stripes`) is the same
+ rule under page and post titles (`.page-title`, `.post-title`): a body
+ class from `Theme.TitleRuleClass` (`titles-<kind>`) that blog.css draws
+ (border width/style only — never the shorthand, or it would reset the
+ colour; stripes are an `::after` gradient in `currentColor`), coloured
+ by the theme CSS in the link colour; `overflow:hidden` ends it at a floated
+ thumbnail. `TagHash` lists tags as `#name` wherever they appear: the
+ `posttags` partial (takes `dict "tags" … "hash" …`), the two tag modules
+ and the tag page title (`tagname` template func).
**Custom fonts** (`CustomFont`, `CustomHeadingFont`: font file ids or ""):
a chosen file is declared as `@font-face` (`"BlogFont"` / `"BlogHeadingFont"`,
`src:url(/media/<id>)`, same-origin so no CORS) and put first in the stack,
diff --git a/README.md b/README.md
index b6adffe..800c30d 100644
--- a/README.md
+++ b/README.md
@@ -25,7 +25,8 @@ Server-rendered HTML, no JavaScript required, works on old browsers and phones.
- **Design** tab: one form, one Save, organised by part of the blog — colours & fonts
(schemes, background colour/image, text and link colours, fonts — built-in
stacks or your own uploaded WOFF2/WOFF/TTF/OTF files), header, menu,
- content, side columns, footer, logo & site icon. The page is five areas — header,
+ content (dates, summaries, a rule under titles — line, dots or stripes — and
+ tags shown as #tag), side columns, footer, logo & site icon. The page is five areas — header,
left column, main content, right column, footer — each holding modules in any
order: blog title, logo, menu, archive (posts by year and month), recent posts,
tags (with counts), tag cloud, a search box (finds posts by title and text, any
diff --git a/internal/i18n/el.go b/internal/i18n/el.go
index 852d9ab..5603659 100644
--- a/internal/i18n/el.go
+++ b/internal/i18n/el.go
@@ -385,13 +385,16 @@ var el = map[string]string{
"Uppercase, spaced": "Κεφαλαία, αραιά",
"Plain, underlined": "Απλοί, υπογραμμισμένοι",
"Show the date on posts": "Εμφάνιση ημερομηνίας στις δημοσιεύσεις",
- "Date format": "Μορφή ημερομηνίας",
- "Logo & site icon": "Λογότυπο & εικονίδιο ιστολογίου",
- "Logo size": "Μέγεθος λογότυπου",
- "Small (48 px tall)": "Μικρό (48 px ύψος)",
- "Medium (80 px)": "Μεσαίο (80 px)",
- "Large (140 px)": "Μεγάλο (140 px)",
- "Favicon": "Εικονίδιο (favicon)",
+ "Show tags with a # in front": "Εμφάνιση των ετικετών με # μπροστά",
+ "Rule under titles": "Γραμμή κάτω από τους τίτλους",
+ "page and post titles, in the link colour": "τίτλοι σελίδων και δημοσιεύσεων, στο χρώμα των συνδέσμων",
+ "Date format": "Μορφή ημερομηνίας",
+ "Logo & site icon": "Λογότυπο & εικονίδιο ιστολογίου",
+ "Logo size": "Μέγεθος λογότυπου",
+ "Small (48 px tall)": "Μικρό (48 px ύψος)",
+ "Medium (80 px)": "Μεσαίο (80 px)",
+ "Large (140 px)": "Μεγάλο (140 px)",
+ "Favicon": "Εικονίδιο (favicon)",
"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.": "Το μικρό εικονίδιο που δείχνουν οι φυλλομετρητές στην καρτέλα. Ένα τετράγωνο PNG ή ICO 32–64 px είναι ιδανικό· επιλέξτε Κανένα για το εικονίδιο του Blogspace.",
"Reset to defaults": "Επαναφορά προεπιλογών",
"Puts every colour, font and layout option back to how a new blog starts and forgets the chosen images and fonts (they stay in your library).": "Επαναφέρει κάθε χρώμα, γραμματοσειρά και επιλογή διάταξης όπως ξεκινά ένα νέο ιστολόγιο και ξεχνά τις επιλεγμένες εικόνες και γραμματοσειρές (μένουν στη συλλογή σας).",
diff --git a/internal/web/static/blog.css b/internal/web/static/blog.css
index 10568fa..155269d 100644
--- a/internal/web/static/blog.css
+++ b/internal/web/static/blog.css
@@ -37,6 +37,12 @@ img { max-width: 100%; height: auto; }
.outro { padding-top: 1em; margin-top: 1.5em; border-top: 1px solid rgba(0,0,0,0.1); }
.post { margin-bottom: 2.5em; }
.post-title { margin: 0 0 0.1em; font-size: 1.5em; line-height: 1.25; }
+/* title rule (body.titles-*): a line under page and post titles, coloured by the
+ theme CSS. overflow:hidden ends it at a floated thumbnail instead of under it. */
+.titles-line .page-title, .titles-line .post-title, .titles-dots .page-title, .titles-dots .post-title, .titles-stripes .page-title, .titles-stripes .post-title { overflow: hidden; padding-bottom: 0.25em; }
+.titles-line .page-title, .titles-line .post-title { border-bottom-width: 1px; border-bottom-style: solid; } /* the colour is the theme's, so no shorthand */
+.titles-dots .page-title, .titles-dots .post-title { border-bottom-width: 3px; border-bottom-style: dotted; }
+.titles-stripes .page-title::after, .titles-stripes .post-title::after { content: ""; display: block; height: 0.4em; margin-top: 0.35em; background: repeating-linear-gradient(90deg, currentColor, currentColor 2px, transparent 2px, transparent 10px); }
.post-title a { text-decoration: none; }
.post-title a:hover { text-decoration: underline; }
.post-date { margin: 0 0 0.8em; font-size: 0.85em; opacity: 0.7; }
diff --git a/internal/web/templates.go b/internal/web/templates.go
index c2f9847..ea39a6d 100644
--- a/internal/web/templates.go
+++ b/internal/web/templates.go
@@ -116,6 +116,13 @@ var funcs = template.FuncMap{
"filemd": fileMarkdown,
"add": func(a, b int) int { return a + b },
"sub": func(a, b int) int { return a - b },
+ // tagname is a tag as the theme lists it: with a # in front when TagHash is on
+ "tagname": func(t Theme, name string) string {
+ if t.TagHash {
+ return "#" + name
+ }
+ return name
+ },
"deref": func(p *string) string {
if p == nil {
return ""
diff --git a/internal/web/templates/blog/post.html b/internal/web/templates/blog/post.html
index 10d4c3a..d16e842 100644
--- a/internal/web/templates/blog/post.html
+++ b/internal/web/templates/blog/post.html
@@ -3,7 +3,7 @@
<article class="post single">
<h1 class="post-title">{{.Data.post.Title}}</h1>
<p class="post-date">{{if .Data.theme.ShowDates}}{{postdate .Data.theme.DateFormat .Data.post.CreatedAt}} &middot; {{end}}<a href="{{if .Data.page.IsHome}}/{{else}}/{{.Data.page.Slug}}{{end}}">{{.Data.page.Title}}</a></p>
- {{template "posttags" .Data.post.Tags}}
+ {{template "posttags" (dict "tags" .Data.post.Tags "hash" .Data.theme.TagHash)}}
{{$img := .Data.post.Image}}{{$place := .Data.theme.PostImage}}
{{if and $img (eq $place "top")}}<p class="post-image"><img src="/media/{{$img}}" alt=""></p>{{end}}
<div class="post-body">{{html .Data.post.BodyHTML}}</div>
diff --git a/internal/web/templates/blog/tag.html b/internal/web/templates/blog/tag.html
index fd3aa13..acb530b 100644
--- a/internal/web/templates/blog/tag.html
+++ b/internal/web/templates/blog/tag.html
@@ -1,5 +1,5 @@
-{{define "title"}}{{tf "Tagged “%s”" .Data.tag.Name}} · {{.Blog.Title}}{{end}}
+{{define "title"}}{{tf "Tagged “%s”" (tagname .Data.theme .Data.tag.Name)}} · {{.Blog.Title}}{{end}}
{{define "content"}}
-<h1 class="page-title">{{tf "Tagged “%s”" .Data.tag.Name}}</h1>
+<h1 class="page-title">{{tf "Tagged “%s”" (tagname .Data.theme .Data.tag.Name)}}</h1>
{{template "postlist" .}}
{{end}}
diff --git a/internal/web/templates/dashboard/design.html b/internal/web/templates/dashboard/design.html
index 5c79dbb..798a0d0 100644
--- a/internal/web/templates/dashboard/design.html
+++ b/internal/web/templates/dashboard/design.html
@@ -185,7 +185,14 @@
</div>
<h3>{{t "Posts"}}</h3>
<label class="check"><input type="checkbox" name="show_dates"{{if $t.ShowDates}} checked{{end}}> {{t "Show the date on posts"}}</label>
+ <label class="check"><input type="checkbox" name="tag_hash"{{if $t.TagHash}} checked{{end}}> {{t "Show tags with a # in front"}} <span class="muted">(#garden)</span></label>
<div class="row">
+ <label>{{t "Rule under titles"}} <span class="muted">({{t "page and post titles, in the link colour"}})</span><br><select name="title_rule">
+ <option value="none"{{if eq $t.TitleRule "none"}} selected{{end}}>{{t "None"}}</option>
+ <option value="line"{{if eq $t.TitleRule "line"}} selected{{end}}>{{t "Line"}}</option>
+ <option value="dots"{{if eq $t.TitleRule "dots"}} selected{{end}}>{{t "Dots"}}</option>
+ <option value="stripes"{{if eq $t.TitleRule "stripes"}} selected{{end}}>{{t "Stripes"}}</option>
+ </select></label>
<label>{{t "Date format"}}<br><select name="date_format">
<option value="long"{{if eq $t.DateFormat "long"}} selected{{end}}>{{postdate "long" .Data.today}}</option>
<option value="short"{{if eq $t.DateFormat "short"}} selected{{end}}>{{postdate "short" .Data.today}}</option>
diff --git a/internal/web/templates/layouts/blog.html b/internal/web/templates/layouts/blog.html
index ba9ea24..5b1f984 100644
--- a/internal/web/templates/layouts/blog.html
+++ b/internal/web/templates/layouts/blog.html
@@ -12,7 +12,7 @@
<style>{{css .Data.css}}</style>
</head>
{{$l := .Data.layout}}
-<body class="{{if $l.HasLeft}}has-left {{end}}{{if $l.HasRight}}has-right{{end}}">
+<body class="{{if $l.HasLeft}}has-left {{end}}{{if $l.HasRight}}has-right {{end}}{{.Data.theme.TitleRuleClass}}">
{{if or $l.Header .Data.theme.HeaderImage}}{{if .Data.theme.HeaderOn}}<div class="site-header">
{{if .Data.theme.HeaderImage}}<div class="header-image"><a href="/"><img src="/media/{{.Data.theme.HeaderImage}}" alt=""></a></div>{{end}}
{{range $l.Header}}{{template "module" (dict "m" . "root" $)}}{{end}}
@@ -73,10 +73,10 @@
</details>{{else}}<p class="muted">{{t "No posts yet."}}</p>{{end}}
</div>
{{- else if eq $m.Kind "tags"}}<div class="mod mod-tags">{{template "modtitle" $m}}
- <ul>{{range $i, $t := $r.Data.tags}}{{if or (eq $m.Count 0) (lt $i $m.Count)}}<li><a href="/tag/{{$t.Slug}}">{{$t.Name}}</a> <span class="muted">({{$t.Count}})</span></li>{{end}}{{else}}<li class="muted">{{t "No tags yet."}}</li>{{end}}</ul>
+ <ul>{{range $i, $t := $r.Data.tags}}{{if or (eq $m.Count 0) (lt $i $m.Count)}}<li><a href="/tag/{{$t.Slug}}">{{if $r.Data.theme.TagHash}}#{{end}}{{$t.Name}}</a> <span class="muted">({{$t.Count}})</span></li>{{end}}{{else}}<li class="muted">{{t "No tags yet."}}</li>{{end}}</ul>
</div>
{{- else if eq $m.Kind "tagcloud"}}<div class="mod mod-tagcloud">{{template "modtitle" $m}}
- <p class="tagcloud">{{range $r.Data.tagcloud}}<a class="tc-{{.Size}}" href="/tag/{{.Slug}}" title="{{.Count}}">{{.Name}}</a> {{else}}<span class="muted">{{t "No tags yet."}}</span>{{end}}</p>
+ <p class="tagcloud">{{range $r.Data.tagcloud}}<a class="tc-{{.Size}}" href="/tag/{{.Slug}}" title="{{.Count}}">{{if $r.Data.theme.TagHash}}#{{end}}{{.Name}}</a> {{else}}<span class="muted">{{t "No tags yet."}}</span>{{end}}</p>
</div>
{{- else if eq $m.Kind "search"}}{{if eq $m.Area "header"}}{{if not $r.Data.layout.HeaderMenu}}<div class="site-search"><div class="wrap search-inner">{{template "searchform" $r}}</div></div>{{end}}
{{else}}<div class="mod mod-search">{{template "modtitle" $m}}{{template "searchform" $r}}</div>{{end}}
diff --git a/internal/web/templates/partials/postlist.html b/internal/web/templates/partials/postlist.html
index b1e43c6..91e248c 100644
--- a/internal/web/templates/partials/postlist.html
+++ b/internal/web/templates/partials/postlist.html
@@ -7,7 +7,7 @@
{{if .Image}}<a class="post-thumb" href="/{{.PageSlug}}/{{.Slug}}"><img src="/media/{{.Image}}" alt=""></a>{{end}}
<h2 class="post-title"><a href="/{{.PageSlug}}/{{.Slug}}">{{.Title}}</a></h2>
{{if $.Data.theme.ShowDates}}<p class="post-date">{{postdate $.Data.theme.DateFormat .CreatedAt}}</p>{{end}}
- {{template "posttags" .Tags}}
+ {{template "posttags" (dict "tags" .Tags "hash" $.Data.theme.TagHash)}}
<div class="post-body">{{html .Excerpt}}</div>
{{if .HasMore}}<p class="read-more"><a href="/{{.PageSlug}}/{{.Slug}}">{{t "Read more"}} &rarr;</a></p>{{end}}
</article>
@@ -21,4 +21,5 @@
{{end}}
{{end}}
-{{define "posttags"}}{{if .}}<p class="post-tags">{{range .}}<a href="/tag/{{.Slug}}">{{.Name}}</a> {{end}}</p>{{end}}{{end}}
+{{/* .tags and .hash (the theme's TagHash: list them as #name) */}}
+{{define "posttags"}}{{if .tags}}<p class="post-tags">{{range .tags}}<a href="/tag/{{.Slug}}">{{if $.hash}}#{{end}}{{.Name}}</a> {{end}}</p>{{end}}{{end}}
diff --git a/internal/web/theme.go b/internal/web/theme.go
index 26181c2..0dc8eff 100644
--- a/internal/web/theme.go
+++ b/internal/web/theme.go
@@ -53,6 +53,8 @@ type Theme struct {
PostsPerPage int `json:"posts_per_page"` // on listings and tag pages; one of postsPerPageChoices
ListStyle string `json:"list_style"` // full | summary (see summary.go)
PostImage string `json:"post_image"` // where the post page shows the featured image: top | bottom | list (listings only)
+ TitleRule string `json:"title_rule"` // none | line | dots | stripes, drawn under page and post titles in the link colour
+ TagHash bool `json:"tag_hash"` // tags are listed as #name
FooterBg string `json:"footer_bg"`
FooterColor string `json:"footer_color"`
@@ -99,7 +101,7 @@ func DefaultTheme() Theme {
Font: "sans", HeadingFont: "same", FontSize: "normal",
HeaderImageHeight: "medium", HeaderAlign: "left", HeaderBg: "#2f3a4a", HeaderText: "#ffffff", TitleSize: "normal", HeaderRule: "none",
NavAlign: "left", NavStyle: "normal", NavBg: "#ffffff", NavText: "#222222",
- ShowDates: true, DateFormat: "long", PostsPerPage: postsPerPageChoices[0], ListStyle: "full", PostImage: "top",
+ ShowDates: true, DateFormat: "long", PostsPerPage: postsPerPageChoices[0], ListStyle: "full", PostImage: "top", TitleRule: "none",
FooterBg: "#2f3a4a", FooterColor: "#d0d5dc", FooterAlign: "center",
LogoSize: "medium",
HeaderOn: true, LeftOn: true, RightOn: true, FooterOn: true, LeftWidth: 25, RightWidth: 25,
@@ -188,6 +190,7 @@ func (t *Theme) normalize() {
t.PostsPerPage = pickInt(t.PostsPerPage, postsPerPageChoices...)
t.ListStyle = pick(t.ListStyle, "full", "summary")
t.PostImage = pick(t.PostImage, "top", "bottom", "list")
+ t.TitleRule = pick(t.TitleRule, "none", "line", "dots", "stripes")
t.FooterBg = color(t.FooterBg, d.FooterBg)
t.FooterColor = color(t.FooterColor, d.FooterColor)
t.FooterAlign = pick(t.FooterAlign, "center", "left")
@@ -253,6 +256,8 @@ func ThemeFromForm(cur Theme, f url.Values) Theme {
t.PostsPerPage = num("posts_per_page")
t.ListStyle = get("list_style")
t.PostImage = get("post_image")
+ t.TitleRule = get("title_rule")
+ t.TagHash = on("tag_hash")
t.FooterBg = get("footer_bg")
t.FooterColor = get("footer_color")
t.FooterAlign = get("footer_align")
@@ -455,6 +460,15 @@ func (t Theme) RuleClass() string {
return "ruled rule-" + t.HeaderRule
}
+// TitleRuleClass is the body class blog.css draws the rule under page and
+// post titles from; the theme CSS only colours it.
+func (t Theme) TitleRuleClass() string {
+ if t.TitleRule == "none" {
+ return ""
+ }
+ return "titles-" + t.TitleRule
+}
+
func (t Theme) HeaderImageMaxHeight() string {
switch t.HeaderImageHeight {
case "short":
@@ -497,6 +511,9 @@ a:hover {{"{"}}{{if .LinkHover}} color:{{.LinkHover}};{{end}}{{if eq .LinkUnderl
.content, .mod { background:{{.ContentBg}};{{if eq .ContentStyle "flat"}} border-radius:0; box-shadow:none;{{end}} }
.content {{"{"}}{{with .PaddingDesktop}} padding:{{.}};{{end}} }
.notice-gizmo { border-color:{{.LinkColor}}; }
+{{- if ne .TitleRule "none"}}
+.page-title, .post-title { border-color:{{.LinkColor}}; } .page-title::after, .post-title::after { color:{{.LinkColor}}; }
+{{- end}}
.logo { max-height:{{.LogoMaxHeight}}; }
@media (min-width: 701px) {
.col-left { width:{{.LeftWidth}}%; } .col-right { width:{{.RightWidth}}%; }
diff --git a/internal/web/web_test.go b/internal/web/web_test.go
index 42d29e4..205e42f 100644
--- a/internal/web/web_test.go
+++ b/internal/web/web_test.go
@@ -133,6 +133,23 @@ func TestThemeNormalizeAndCSS(t *testing.T) {
}
}
+// The rule under titles is a body class blog.css draws from, coloured by the
+// theme CSS; the tag hash is a plain flag the templates read.
+func TestTitleRuleAndTagHash(t *testing.T) {
+ th := DefaultTheme()
+ if th.TitleRuleClass() != "" || th.TagHash || strings.Contains(th.CSS(), ".page-title") {
+ t.Errorf("defaults: %+v", th)
+ }
+ th = ThemeFromForm(th, url.Values{"title_rule": {"dots"}, "tag_hash": {"on"}, "link_color": {"#b03a48"}})
+ if th.TitleRuleClass() != "titles-dots" || !th.TagHash || !strings.Contains(th.CSS(), ".page-title, .post-title { border-color:#b03a48; }") {
+ t.Errorf("from form: %+v\n%s", th, th.CSS())
+ }
+ th = ThemeFromForm(th, url.Values{"title_rule": {"neon"}})
+ if th.TitleRuleClass() != "" || th.TagHash {
+ t.Errorf("junk rule falls back, unticked box is off: %+v", th)
+ }
+}
+
// An uploaded font is declared as a face and put first in the stack, the
// built-in choice behind it; the heading font falls back to the text's stack.
func TestCustomFontCSS(t *testing.T) {