From 3eb04b1a2bdf9e53231fe862cfd76327371a9741 Mon Sep 17 00:00:00 2001 From: gramanas Date: Sat, 12 Sep 2026 11:24:17 +0300 Subject: Initial multi-tenant blog host 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 Claude-Session: https://claude.ai/code/session_01Sd8UPWrvyYCLj97JexNw3A --- internal/web/templates/admin/delete_user.html | 12 ++++ internal/web/templates/admin/index.html | 28 ++++++++ internal/web/templates/admin/new_user.html | 15 +++++ internal/web/templates/auth/login.html | 12 ++++ internal/web/templates/blog/404.html | 5 ++ internal/web/templates/blog/page.html | 19 ++++++ internal/web/templates/blog/post.html | 8 +++ internal/web/templates/dashboard/confirm.html | 17 +++++ internal/web/templates/dashboard/design.html | 87 +++++++++++++++++++++++++ internal/web/templates/dashboard/images.html | 22 +++++++ internal/web/templates/dashboard/overview.html | 35 ++++++++++ internal/web/templates/dashboard/page_form.html | 21 ++++++ internal/web/templates/dashboard/pages.html | 24 +++++++ internal/web/templates/dashboard/password.html | 13 ++++ internal/web/templates/dashboard/post_form.html | 26 ++++++++ internal/web/templates/dashboard/posts.html | 25 +++++++ internal/web/templates/dashboard/settings.html | 13 ++++ internal/web/templates/layouts/blog.html | 38 +++++++++++ internal/web/templates/layouts/dashboard.html | 17 +++++ internal/web/templates/partials/dashnav.html | 27 ++++++++ internal/web/templates/partials/imagepick.html | 11 ++++ internal/web/templates/partials/mdhelp.html | 9 +++ 22 files changed, 484 insertions(+) create mode 100644 internal/web/templates/admin/delete_user.html create mode 100644 internal/web/templates/admin/index.html create mode 100644 internal/web/templates/admin/new_user.html create mode 100644 internal/web/templates/auth/login.html create mode 100644 internal/web/templates/blog/404.html create mode 100644 internal/web/templates/blog/page.html create mode 100644 internal/web/templates/blog/post.html create mode 100644 internal/web/templates/dashboard/confirm.html create mode 100644 internal/web/templates/dashboard/design.html create mode 100644 internal/web/templates/dashboard/images.html create mode 100644 internal/web/templates/dashboard/overview.html create mode 100644 internal/web/templates/dashboard/page_form.html create mode 100644 internal/web/templates/dashboard/pages.html create mode 100644 internal/web/templates/dashboard/password.html create mode 100644 internal/web/templates/dashboard/post_form.html create mode 100644 internal/web/templates/dashboard/posts.html create mode 100644 internal/web/templates/dashboard/settings.html create mode 100644 internal/web/templates/layouts/blog.html create mode 100644 internal/web/templates/layouts/dashboard.html create mode 100644 internal/web/templates/partials/dashnav.html create mode 100644 internal/web/templates/partials/imagepick.html create mode 100644 internal/web/templates/partials/mdhelp.html (limited to 'internal/web/templates') diff --git a/internal/web/templates/admin/delete_user.html b/internal/web/templates/admin/delete_user.html new file mode 100644 index 0000000..34d3fb9 --- /dev/null +++ b/internal/web/templates/admin/delete_user.html @@ -0,0 +1,12 @@ +{{define "title"}}Delete user · Blogspace{{end}} +{{define "content"}} +
+

Delete {{.Data.target.Username}}?

+

This permanently deletes the user {{.Data.target.Username}}, their blog, and every page, post and image in it.

+
+ + + Cancel +
+
+{{end}} diff --git a/internal/web/templates/admin/index.html b/internal/web/templates/admin/index.html new file mode 100644 index 0000000..c74d426 --- /dev/null +++ b/internal/web/templates/admin/index.html @@ -0,0 +1,28 @@ +{{define "title"}}Admin · Blogspace{{end}} +{{define "content"}} +

Users & blogs + New blogger

+
+ + + {{range .Data.users}} + + + + + + {{end}} +
UserRoleBlogSinceActions
{{.Username}}{{if .Disabled}} disabled{{end}}{{.Role}}{{if .Subdomain}}{{deref .BlogTitle}} {{deref .Subdomain}} ↗{{else}}—{{end}}{{date .CreatedAt}} + {{if ne .ID $.User.ID}} +
reset password +
+ + + +
+ {{if .Disabled}}
+ {{else}}
{{end}} + delete + {{else}}(you){{end}} +
+
+{{end}} diff --git a/internal/web/templates/admin/new_user.html b/internal/web/templates/admin/new_user.html new file mode 100644 index 0000000..87d1186 --- /dev/null +++ b/internal/web/templates/admin/new_user.html @@ -0,0 +1,15 @@ +{{define "title"}}New blogger · Blogspace{{end}} +{{define "content"}} +
+

New blogger

+
+ + + + + + Cancel +
+
+{{end}} diff --git a/internal/web/templates/auth/login.html b/internal/web/templates/auth/login.html new file mode 100644 index 0000000..2229849 --- /dev/null +++ b/internal/web/templates/auth/login.html @@ -0,0 +1,12 @@ +{{define "title"}}Log in · Blogspace{{end}} +{{define "content"}} +
+

Log in

+
+ + + + +
+
+{{end}} diff --git a/internal/web/templates/blog/404.html b/internal/web/templates/blog/404.html new file mode 100644 index 0000000..b0aa6a9 --- /dev/null +++ b/internal/web/templates/blog/404.html @@ -0,0 +1,5 @@ +{{define "title"}}Not found · {{.Blog.Title}}{{end}} +{{define "content"}} +

Not found

+

There is nothing at this address. Back to the front page.

+{{end}} diff --git a/internal/web/templates/blog/page.html b/internal/web/templates/blog/page.html new file mode 100644 index 0000000..c1fe726 --- /dev/null +++ b/internal/web/templates/blog/page.html @@ -0,0 +1,19 @@ +{{define "title"}}{{if not .Data.page.IsHome}}{{.Data.page.Title}} · {{end}}{{.Blog.Title}}{{end}} +{{define "content"}} +{{if not .Data.page.IsHome}}

{{.Data.page.Title}}

{{end}} +{{if .Data.page.IntroHTML}}
{{html .Data.page.IntroHTML}}
{{end}} +{{range .Data.posts}} +
+

{{.Title}}

+ +
{{html .BodyHTML}}
+
+{{else}}{{if not .Data.page.IntroHTML}}

Nothing here yet.

{{end}}{{end}} +{{if gt .Data.lastPage 1}} +
+ {{if gt .Data.pageNum 1}}← Newer{{end}} + Page {{.Data.pageNum}} of {{.Data.lastPage}} + {{if lt .Data.pageNum .Data.lastPage}}Older →{{end}} +
+{{end}} +{{end}} diff --git a/internal/web/templates/blog/post.html b/internal/web/templates/blog/post.html new file mode 100644 index 0000000..83ce782 --- /dev/null +++ b/internal/web/templates/blog/post.html @@ -0,0 +1,8 @@ +{{define "title"}}{{.Data.post.Title}} · {{.Blog.Title}}{{end}} +{{define "content"}} +
+

{{.Data.post.Title}}

+ +
{{html .Data.post.BodyHTML}}
+
+{{end}} diff --git a/internal/web/templates/dashboard/confirm.html b/internal/web/templates/dashboard/confirm.html new file mode 100644 index 0000000..eefa1da --- /dev/null +++ b/internal/web/templates/dashboard/confirm.html @@ -0,0 +1,17 @@ +{{define "title"}}Confirm · Blogspace{{end}} +{{define "content"}} +
+

Are you sure?

+ {{if .Data.isHome}} +

This is the home page; it cannot be deleted. Make another page the home page first.

+

Back

+ {{else}} +

You are about to permanently delete {{.Data.what}}. This cannot be undone.

+
+ + + Cancel +
+ {{end}} +
+{{end}} diff --git a/internal/web/templates/dashboard/design.html b/internal/web/templates/dashboard/design.html new file mode 100644 index 0000000..27ce3c7 --- /dev/null +++ b/internal/web/templates/dashboard/design.html @@ -0,0 +1,87 @@ +{{define "title"}}Design · {{.Blog.Title}}{{end}} +{{define "content"}} +{{$t := .Data.theme}} +

Design

+

Save, then refresh your blog ↗ to see the result. Colours are hex values like #336699.

+
+ + +
+

Background

+
+ + +
+ {{template "imagepick" (dict "name" "bg_image" "current" $t.BgImage "images" .Data.images "label" "Background image")}} +
+ +
+

Text & content area

+
+ + + +
+
+ + + +
+
+ +
+

Header

+
+ + + +
+ + {{template "imagepick" (dict "name" "header_image" "current" $t.HeaderImage "images" .Data.images "label" "Header image (banner)")}} +
+ +
+

Menu

+
+ + + +
+ +
+ +
+

Footer

+ +
+ + +
+
+ +

+
+{{end}} diff --git a/internal/web/templates/dashboard/images.html b/internal/web/templates/dashboard/images.html new file mode 100644 index 0000000..5383e73 --- /dev/null +++ b/internal/web/templates/dashboard/images.html @@ -0,0 +1,22 @@ +{{define "title"}}Images · {{.Blog.Title}}{{end}} +{{define "content"}} +

Images

+
+
+ + + +
+
+

To put an image in a post, copy its Markdown line into the post text.

+ +{{end}} diff --git a/internal/web/templates/dashboard/overview.html b/internal/web/templates/dashboard/overview.html new file mode 100644 index 0000000..0ec72e6 --- /dev/null +++ b/internal/web/templates/dashboard/overview.html @@ -0,0 +1,35 @@ +{{define "title"}}{{.Blog.Title}} · Blogspace{{end}} +{{define "content"}} +

{{.Blog.Title}}

+

Your blog is live at {{.BlogURL}}. Every change you save here shows up there as soon as you refresh.

+ +
+ +
+

Pages

+
    + {{range .Data.pages}}
  • {{.Title}} ({{.PostCount}} posts{{if .IsHome}}, home{{end}}{{if not .ShowInNav}}, hidden from menu{{end}})
  • {{end}} +
+
+
+ +
+

Latest posts

+ {{if .Data.posts}} + + + {{range .Data.posts}} + + + + + {{end}} +
TitlePageDate
{{.Title}}{{if not .Published}} hidden{{end}}{{.PageTitle}}{{date .CreatedAt}}view ↗
+

All posts

+ {{else}}

No posts yet. Write the first one.

{{end}} +
+{{end}} diff --git a/internal/web/templates/dashboard/page_form.html b/internal/web/templates/dashboard/page_form.html new file mode 100644 index 0000000..df94c7e --- /dev/null +++ b/internal/web/templates/dashboard/page_form.html @@ -0,0 +1,21 @@ +{{define "title"}}{{if .Data.page.ID}}Edit page{{else}}New page{{end}} · {{.Blog.Title}}{{end}} +{{define "content"}} +
+

{{if .Data.page.ID}}Edit page{{else}}New page{{end}}

+
+ + + + + +

+ + {{if .Data.page.ID}}View page ↗{{end}} + Back to pages +

+
+
+{{template "mdhelp"}} +{{end}} diff --git a/internal/web/templates/dashboard/pages.html b/internal/web/templates/dashboard/pages.html new file mode 100644 index 0000000..6703e7f --- /dev/null +++ b/internal/web/templates/dashboard/pages.html @@ -0,0 +1,24 @@ +{{define "title"}}Pages · {{.Blog.Title}}{{end}} +{{define "content"}} +

Pages + New page

+

Pages appear in your blog's menu in this order. Each page holds its own list of posts.

+
+ + + {{range .Data.pages}} + + + + + + {{end}} +
Menu orderTitleAddressPosts
+
+
+
{{.Title}} + {{if .IsHome}}home{{end}}{{if not .ShowInNav}}hidden{{end}}{{if .IsHome}}/{{else}}/{{.Slug}}{{end}} ↗{{.PostCount}} + {{if not .IsHome}}
+ delete{{end}} +
+
+{{end}} diff --git a/internal/web/templates/dashboard/password.html b/internal/web/templates/dashboard/password.html new file mode 100644 index 0000000..f333bd7 --- /dev/null +++ b/internal/web/templates/dashboard/password.html @@ -0,0 +1,13 @@ +{{define "title"}}Change password · Blogspace{{end}} +{{define "content"}} +
+

Change password

+
+ + + + + +
+
+{{end}} diff --git a/internal/web/templates/dashboard/post_form.html b/internal/web/templates/dashboard/post_form.html new file mode 100644 index 0000000..520b9e6 --- /dev/null +++ b/internal/web/templates/dashboard/post_form.html @@ -0,0 +1,26 @@ +{{define "title"}}{{if .Data.post.ID}}Edit post{{else}}New post{{end}} · {{.Blog.Title}}{{end}} +{{define "content"}} +
+

{{if .Data.post.ID}}Edit post{{else}}New post{{end}}

+
+ + +
+ + +
+ + +

+ + {{if .Data.post.ID}}View post ↗{{end}} + Back to posts +

+
+
+{{template "mdhelp"}} +{{end}} diff --git a/internal/web/templates/dashboard/posts.html b/internal/web/templates/dashboard/posts.html new file mode 100644 index 0000000..24e86bb --- /dev/null +++ b/internal/web/templates/dashboard/posts.html @@ -0,0 +1,25 @@ +{{define "title"}}Posts · {{.Blog.Title}}{{end}} +{{define "content"}} +

Posts + New post

+
+ + +
+
+{{if .Data.posts}} + + + {{range .Data.posts}} + + + + + {{end}} +
TitlePageDate
{{.Title}}{{if not .Published}} hidden{{end}}{{.PageTitle}}{{date .CreatedAt}}view ↗ · delete
+{{else}}

No posts here yet.

{{end}} +
+{{end}} diff --git a/internal/web/templates/dashboard/settings.html b/internal/web/templates/dashboard/settings.html new file mode 100644 index 0000000..24dffb4 --- /dev/null +++ b/internal/web/templates/dashboard/settings.html @@ -0,0 +1,13 @@ +{{define "title"}}Settings · {{.Blog.Title}}{{end}} +{{define "content"}} +
+

Blog settings

+
+ + + +

Address: {{.BlogURL}} — only the administrator can change this.

+ +
+
+{{end}} diff --git a/internal/web/templates/layouts/blog.html b/internal/web/templates/layouts/blog.html new file mode 100644 index 0000000..7e3128e --- /dev/null +++ b/internal/web/templates/layouts/blog.html @@ -0,0 +1,38 @@ +{{define "layout"}} + + + + +{{block "title" .}}{{.Blog.Title}}{{end}} + + + + + +{{if eq .Data.theme.NavPosition "top-bar"}}{{template "blognav" .}}{{end}} + +{{if eq .Data.theme.NavPosition "below-header"}}{{template "blognav" .}}{{end}} +
+ {{if eq .Data.theme.NavPosition "left-sidebar"}}{{template "blognav" .}}{{end}} +
+ {{template "content" .}} +
+
+ + +{{end}} + +{{define "blognav"}}{{end}} diff --git a/internal/web/templates/layouts/dashboard.html b/internal/web/templates/layouts/dashboard.html new file mode 100644 index 0000000..6633c29 --- /dev/null +++ b/internal/web/templates/layouts/dashboard.html @@ -0,0 +1,17 @@ +{{define "layout"}} + + + + +{{block "title" .}}Blogspace{{end}} + + + +{{template "dashnav" .}} +
+{{if .Flash}}
{{.Flash}}
{{end}} +{{if .Error}}
{{.Error}}
{{end}} +{{template "content" .}} +
+ +{{end}} diff --git a/internal/web/templates/partials/dashnav.html b/internal/web/templates/partials/dashnav.html new file mode 100644 index 0000000..13a04d0 --- /dev/null +++ b/internal/web/templates/partials/dashnav.html @@ -0,0 +1,27 @@ +{{define "dashnav"}}
+
+ Blogspace + {{if .User}} + {{.User.Username}}{{if .User.IsSuperadmin}} (superadmin){{end}} + + {{if .User.IsSuperadmin}}Admin{{end}} + Password +
+
+ {{end}} +
+
+{{if .Blog}} +
+
+ {{.Blog.Title}} + Overview + Posts + Pages + Design + Images + Settings + View blog ↗ +
+
+{{end}}{{end}} diff --git a/internal/web/templates/partials/imagepick.html b/internal/web/templates/partials/imagepick.html new file mode 100644 index 0000000..687c6cd --- /dev/null +++ b/internal/web/templates/partials/imagepick.html @@ -0,0 +1,11 @@ +{{define "imagepick"}}
+ + + {{if .current}} + {{end}} +
{{end}} diff --git a/internal/web/templates/partials/mdhelp.html b/internal/web/templates/partials/mdhelp.html new file mode 100644 index 0000000..878130e --- /dev/null +++ b/internal/web/templates/partials/mdhelp.html @@ -0,0 +1,9 @@ +{{define "mdhelp"}}
+

Formatting cheat-sheet

+ + + + +
# Heading, ## Smaller heading**bold**, *italic*
[link text](https://example.org)![description](/media/…) — copy the address from Images
- list item / 1. numbered> quote, `code`, --- for a line
+

Blank line = new paragraph. Press Enter once for a line break.

+
{{end}} -- cgit v1.2.3