diff options
| -rw-r--r-- | AGENTS.md | 10 | ||||
| -rw-r--r-- | README.md | 3 | ||||
| -rw-r--r-- | internal/i18n/el.go | 17 | ||||
| -rw-r--r-- | internal/web/static/blog.css | 6 | ||||
| -rw-r--r-- | internal/web/templates.go | 7 | ||||
| -rw-r--r-- | internal/web/templates/blog/post.html | 2 | ||||
| -rw-r--r-- | internal/web/templates/blog/tag.html | 4 | ||||
| -rw-r--r-- | internal/web/templates/dashboard/design.html | 7 | ||||
| -rw-r--r-- | internal/web/templates/layouts/blog.html | 6 | ||||
| -rw-r--r-- | internal/web/templates/partials/postlist.html | 5 | ||||
| -rw-r--r-- | internal/web/theme.go | 19 | ||||
| -rw-r--r-- | internal/web/web_test.go | 17 |
12 files changed, 85 insertions, 18 deletions
@@ -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, @@ -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}} · {{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"}} →</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) { |
