aboutsummaryrefslogtreecommitdiffstats
path: root/internal/web/templates
diff options
context:
space:
mode:
Diffstat (limited to 'internal/web/templates')
-rw-r--r--internal/web/templates/admin/delete_user.html12
-rw-r--r--internal/web/templates/admin/index.html28
-rw-r--r--internal/web/templates/admin/new_user.html15
-rw-r--r--internal/web/templates/auth/login.html12
-rw-r--r--internal/web/templates/blog/404.html5
-rw-r--r--internal/web/templates/blog/page.html19
-rw-r--r--internal/web/templates/blog/post.html8
-rw-r--r--internal/web/templates/dashboard/confirm.html17
-rw-r--r--internal/web/templates/dashboard/design.html87
-rw-r--r--internal/web/templates/dashboard/images.html22
-rw-r--r--internal/web/templates/dashboard/overview.html35
-rw-r--r--internal/web/templates/dashboard/page_form.html21
-rw-r--r--internal/web/templates/dashboard/pages.html24
-rw-r--r--internal/web/templates/dashboard/password.html13
-rw-r--r--internal/web/templates/dashboard/post_form.html26
-rw-r--r--internal/web/templates/dashboard/posts.html25
-rw-r--r--internal/web/templates/dashboard/settings.html13
-rw-r--r--internal/web/templates/layouts/blog.html38
-rw-r--r--internal/web/templates/layouts/dashboard.html17
-rw-r--r--internal/web/templates/partials/dashnav.html27
-rw-r--r--internal/web/templates/partials/imagepick.html11
-rw-r--r--internal/web/templates/partials/mdhelp.html9
22 files changed, 484 insertions, 0 deletions
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"}}
+<div class="card narrow">
+ <h1>Delete {{.Data.target.Username}}?</h1>
+ <p>This permanently deletes the user <strong>{{.Data.target.Username}}</strong>, their blog, and every page, post and image in it.</p>
+ <form method="post" action="/admin/users/{{.Data.target.ID}}/delete">
+ <input type="hidden" name="_csrf" value="{{.CSRF}}">
+ <button type="submit" class="danger">Yes, delete everything</button>
+ <a class="btn secondary" href="/admin/">Cancel</a>
+ </form>
+</div>
+{{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"}}
+<h1>Users &amp; blogs <a class="btn small" href="/admin/users/new">+ New blogger</a></h1>
+<div class="card">
+<table>
+ <tr><th>User</th><th>Role</th><th>Blog</th><th>Since</th><th>Actions</th></tr>
+ {{range .Data.users}}<tr{{if .Disabled}} class="disabled"{{end}}>
+ <td>{{.Username}}{{if .Disabled}} <span class="tag">disabled</span>{{end}}</td>
+ <td>{{.Role}}</td>
+ <td>{{if .Subdomain}}<a href="/b/{{deref .Subdomain}}/">{{deref .BlogTitle}}</a> <a href="{{$.Data.cfg.BlogURL (deref .Subdomain)}}" target="_blank" class="muted">{{deref .Subdomain}} &#8599;</a>{{else}}<span class="muted">—</span>{{end}}</td>
+ <td class="nowrap">{{date .CreatedAt}}</td>
+ <td class="nowrap">
+ {{if ne .ID $.User.ID}}
+ <details class="inline"><summary class="mini">reset password</summary>
+ <form method="post" action="/admin/users/{{.ID}}/reset-password" class="inline">
+ <input type="hidden" name="_csrf" value="{{$.CSRF}}">
+ <input type="password" name="password" placeholder="new password" minlength="8" required size="14">
+ <button class="mini">Set</button>
+ </form></details>
+ {{if .Disabled}}<form method="post" action="/admin/users/{{.ID}}/enable" class="inline"><input type="hidden" name="_csrf" value="{{$.CSRF}}"><button class="mini">enable</button></form>
+ {{else}}<form method="post" action="/admin/users/{{.ID}}/disable" class="inline"><input type="hidden" name="_csrf" value="{{$.CSRF}}"><button class="mini">disable</button></form>{{end}}
+ <a class="danger" href="/admin/users/{{.ID}}/delete">delete</a>
+ {{else}}<span class="muted">(you)</span>{{end}}
+ </td>
+ </tr>{{end}}
+</table>
+</div>
+{{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"}}
+<div class="card narrow">
+ <h1>New blogger</h1>
+ <form method="post" action="/admin/users/new">
+ <input type="hidden" name="_csrf" value="{{.CSRF}}">
+ <label>Username<br><input name="username" value="{{.Data.username}}" required pattern="[a-zA-Z0-9_.-]{2,40}" autofocus></label>
+ <label>Password <span class="muted">(min 8 characters; they can change it later)</span><br><input type="password" name="password" required minlength="8"></label>
+ <label>Subdomain <span class="muted">(defaults to the username)</span><br>
+ <span class="nowrap"><input name="subdomain" value="{{.Data.subdomain}}" pattern="[a-z0-9]([a-z0-9-]*[a-z0-9])?" maxlength="63" size="14">.{{.Data.cfg.BaseDomain}}</span></label>
+ <label>Blog title<br><input name="title" value="{{.Data.title}}" maxlength="120"></label>
+ <button type="submit">Create</button> <a href="/admin/">Cancel</a>
+ </form>
+</div>
+{{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"}}
+<div class="card narrow">
+ <h1>Log in</h1>
+ <form method="post" action="/login">
+ <input type="hidden" name="next" value="{{.Data.next}}">
+ <label>Username<br><input name="username" value="{{.Data.username}}" required autofocus autocomplete="username"></label>
+ <label>Password<br><input type="password" name="password" required autocomplete="current-password"></label>
+ <button type="submit">Log in</button>
+ </form>
+</div>
+{{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"}}
+<h1>Not found</h1>
+<p>There is nothing at this address. <a href="/">Back to the front page.</a></p>
+{{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}}<h1 class="page-title">{{.Data.page.Title}}</h1>{{end}}
+{{if .Data.page.IntroHTML}}<div class="intro">{{html .Data.page.IntroHTML}}</div>{{end}}
+{{range .Data.posts}}
+<article class="post">
+ <h2 class="post-title"><a href="/{{.PageSlug}}/{{.Slug}}">{{.Title}}</a></h2>
+ <p class="post-date">{{date .CreatedAt}}</p>
+ <div class="post-body">{{html .BodyHTML}}</div>
+</article>
+{{else}}{{if not .Data.page.IntroHTML}}<p class="muted">Nothing here yet.</p>{{end}}{{end}}
+{{if gt .Data.lastPage 1}}
+<div class="pager">
+ {{if gt .Data.pageNum 1}}<a href="{{.Data.base}}/?p={{sub .Data.pageNum 1}}">&larr; Newer</a>{{end}}
+ <span>Page {{.Data.pageNum}} of {{.Data.lastPage}}</span>
+ {{if lt .Data.pageNum .Data.lastPage}}<a href="{{.Data.base}}/?p={{add .Data.pageNum 1}}">Older &rarr;</a>{{end}}
+</div>
+{{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"}}
+<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>
+ <div class="post-body">{{html .Data.post.BodyHTML}}</div>
+</article>
+{{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"}}
+<div class="card narrow">
+ <h1>Are you sure?</h1>
+ {{if .Data.isHome}}
+ <p>This is the home page; it cannot be deleted. Make another page the home page first.</p>
+ <p><a class="btn secondary" href="{{.Data.back}}">Back</a></p>
+ {{else}}
+ <p>You are about to permanently delete {{.Data.what}}. This cannot be undone.</p>
+ <form method="post" action="{{.Data.action}}">
+ <input type="hidden" name="_csrf" value="{{.CSRF}}">
+ <button type="submit" class="danger">Yes, delete</button>
+ <a class="btn secondary" href="{{.Data.back}}">Cancel</a>
+ </form>
+ {{end}}
+</div>
+{{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}}
+<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>
+<form method="post" action="/b/{{.Blog.Subdomain}}/design" enctype="multipart/form-data">
+<input type="hidden" name="_csrf" value="{{.CSRF}}">
+
+<div class="card">
+ <h2>Background</h2>
+ <div class="row">
+ <label>Colour<br><input type="color" name="bg_color" value="{{$t.BgColor}}"></label>
+ <label>Image style<br><select name="bg_mode">
+ <option value="cover"{{if eq $t.BgMode "cover"}} selected{{end}}>Stretch to fill</option>
+ <option value="fixed"{{if eq $t.BgMode "fixed"}} selected{{end}}>Fill, fixed while scrolling</option>
+ <option value="tile"{{if eq $t.BgMode "tile"}} selected{{end}}>Repeat (tile)</option>
+ </select></label>
+ </div>
+ {{template "imagepick" (dict "name" "bg_image" "current" $t.BgImage "images" .Data.images "label" "Background image")}}
+</div>
+
+<div class="card">
+ <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="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>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>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>
+ </div>
+</div>
+
+<div class="card">
+ <h2>Header</h2>
+ <div class="row">
+ <label>Background<br><input type="color" name="header_bg" value="{{$t.HeaderBg}}"></label>
+ <label>Text colour<br><input type="color" name="header_text" value="{{$t.HeaderText}}"></label>
+ <label>Alignment<br><select name="header_align">
+ <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>
+ </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">
+ <h2>Menu</h2>
+ <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>
+ </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>
+</div>
+
+<div class="card">
+ <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>
+</div>
+
+<p><button type="submit">Save design</button></p>
+</form>
+{{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"}}
+<h1>Images</h1>
+<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>
+ <button type="submit">Upload</button>
+ </form>
+</div>
+<p class="muted">To put an image in a post, copy its Markdown line into the post text.</p>
+<div class="gallery">
+{{range .Data.images}}
+ <div class="card thumb">
+ <a href="/media/{{.ID}}" target="_blank"><img src="/media/{{.ID}}" alt="{{.Filename}}"></a>
+ <div class="meta">{{.Filename}} <span class="muted">({{kb .Size}})</span></div>
+ <input class="copy" readonly value="![{{.Filename}}](/media/{{.ID}})" onclick="this.select()">
+ <form method="post" action="/b/{{$.Blog.Subdomain}}/images/{{.ID}}/delete" class="inline"><input type="hidden" name="_csrf" value="{{$.CSRF}}"><button class="mini danger">delete</button></form>
+ </div>
+{{else}}<p class="muted">No images yet.</p>{{end}}
+</div>
+{{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"}}
+<h1>{{.Blog.Title}}</h1>
+<p class="muted">Your blog is live at <a href="{{.BlogURL}}" target="_blank">{{.BlogURL}}</a>. Every change you save here shows up there as soon as you refresh.</p>
+
+<div class="cols">
+ <div class="card">
+ <h2>Quick actions</h2>
+ <p><a class="btn" href="/b/{{.Blog.Subdomain}}/posts/new">Write a new post</a></p>
+ <p><a href="/b/{{.Blog.Subdomain}}/pages/new">Add a page</a> &middot; <a href="/b/{{.Blog.Subdomain}}/design">Change the look</a> &middot; <a href="/b/{{.Blog.Subdomain}}/images">Upload images</a></p>
+ </div>
+ <div class="card">
+ <h2>Pages</h2>
+ <ul class="plain">
+ {{range .Data.pages}}<li><a href="/b/{{$.Blog.Subdomain}}/posts?page={{.ID}}">{{.Title}}</a> <span class="muted">({{.PostCount}} posts{{if .IsHome}}, home{{end}}{{if not .ShowInNav}}, hidden from menu{{end}})</span></li>{{end}}
+ </ul>
+ </div>
+</div>
+
+<div class="card">
+ <h2>Latest posts</h2>
+ {{if .Data.posts}}
+ <table>
+ <tr><th>Title</th><th>Page</th><th>Date</th><th></th></tr>
+ {{range .Data.posts}}<tr>
+ <td><a href="/b/{{$.Blog.Subdomain}}/posts/{{.ID}}/edit">{{.Title}}</a>{{if not .Published}} <span class="tag">hidden</span>{{end}}</td>
+ <td>{{.PageTitle}}</td>
+ <td>{{date .CreatedAt}}</td>
+ <td><a href="{{$.BlogURL}}/{{.PageSlug}}/{{.Slug}}" target="_blank">view &#8599;</a></td>
+ </tr>{{end}}
+ </table>
+ <p><a href="/b/{{.Blog.Subdomain}}/posts">All posts</a></p>
+ {{else}}<p class="muted">No posts yet. <a href="/b/{{.Blog.Subdomain}}/posts/new">Write the first one.</a></p>{{end}}
+</div>
+{{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"}}
+<div class="card">
+ <h1>{{if .Data.page.ID}}Edit page{{else}}New page{{end}}</h1>
+ <form method="post">
+ <input type="hidden" name="_csrf" value="{{.CSRF}}">
+ <label>Title<br><input name="title" value="{{.Data.page.Title}}" required maxlength="120"></label>
+ <label>Address <span class="muted">(leave empty to make one from the title; e.g. <code>about</code> → {{.BlogURL}}/about)</span><br>
+ <input name="slug" value="{{.Data.page.Slug}}" pattern="[a-z0-9]([a-z0-9-]*[a-z0-9])?" maxlength="80"></label>
+ <label>Intro text <span class="muted">(Markdown, shown above the posts; optional)</span><br>
+ <textarea name="intro" rows="8">{{.Data.page.IntroMD}}</textarea></label>
+ <label class="check"><input type="checkbox" name="show_in_nav"{{if .Data.page.ShowInNav}} checked{{end}}> Show in menu</label>
+ <p>
+ <button type="submit">Save</button>
+ {{if .Data.page.ID}}<a class="btn secondary" href="{{.BlogURL}}{{if .Data.page.IsHome}}/{{else}}/{{.Data.page.Slug}}{{end}}" target="_blank">View page &#8599;</a>{{end}}
+ <a href="/b/{{.Blog.Subdomain}}/pages">Back to pages</a>
+ </p>
+ </form>
+</div>
+{{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"}}
+<h1>Pages <a class="btn small" href="/b/{{.Blog.Subdomain}}/pages/new">+ New page</a></h1>
+<p class="muted">Pages appear in your blog's menu in this order. Each page holds its own list of posts.</p>
+<div class="card">
+<table>
+ <tr><th>Menu order</th><th>Title</th><th>Address</th><th>Posts</th><th></th></tr>
+ {{range .Data.pages}}<tr>
+ <td class="nowrap">
+ <form method="post" action="/b/{{$.Blog.Subdomain}}/pages/{{.ID}}/move" class="inline"><input type="hidden" name="_csrf" value="{{$.CSRF}}"><input type="hidden" name="dir" value="up"><button class="mini" title="Move up">&#9650;</button></form>
+ <form method="post" action="/b/{{$.Blog.Subdomain}}/pages/{{.ID}}/move" class="inline"><input type="hidden" name="_csrf" value="{{$.CSRF}}"><input type="hidden" name="dir" value="down"><button class="mini" title="Move down">&#9660;</button></form>
+ </td>
+ <td><a href="/b/{{$.Blog.Subdomain}}/pages/{{.ID}}/edit">{{.Title}}</a>
+ {{if .IsHome}}<span class="tag">home</span>{{end}}{{if not .ShowInNav}}<span class="tag">hidden</span>{{end}}</td>
+ <td><a href="{{$.BlogURL}}{{if .IsHome}}/{{else}}/{{.Slug}}{{end}}" target="_blank">{{if .IsHome}}/{{else}}/{{.Slug}}{{end}} &#8599;</a></td>
+ <td><a href="/b/{{$.Blog.Subdomain}}/posts?page={{.ID}}">{{.PostCount}}</a></td>
+ <td class="nowrap">
+ {{if not .IsHome}}<form method="post" action="/b/{{$.Blog.Subdomain}}/pages/{{.ID}}/home" class="inline"><input type="hidden" name="_csrf" value="{{$.CSRF}}"><button class="mini">Make home</button></form>
+ <a class="danger" href="/b/{{$.Blog.Subdomain}}/pages/{{.ID}}/delete">delete</a>{{end}}
+ </td>
+ </tr>{{end}}
+</table>
+</div>
+{{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"}}
+<div class="card narrow">
+ <h1>Change password</h1>
+ <form method="post" action="/account/password">
+ <input type="hidden" name="_csrf" value="{{.CSRF}}">
+ <label>Current password<br><input type="password" name="current" required autocomplete="current-password"></label>
+ <label>New password<br><input type="password" name="password" required minlength="8" autocomplete="new-password"></label>
+ <label>Repeat new password<br><input type="password" name="password2" required minlength="8" autocomplete="new-password"></label>
+ <button type="submit">Change password</button>
+ </form>
+</div>
+{{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"}}
+<div class="card">
+ <h1>{{if .Data.post.ID}}Edit post{{else}}New post{{end}}</h1>
+ <form method="post">
+ <input type="hidden" name="_csrf" value="{{.CSRF}}">
+ <label>Title<br><input name="title" value="{{.Data.post.Title}}" required maxlength="200" autofocus></label>
+ <div class="row">
+ <label>Page<br>
+ <select name="page_id">{{range .Data.pages}}<option value="{{.ID}}"{{if eq .ID $.Data.post.PageID}} selected{{end}}>{{.Title}}</option>{{end}}</select>
+ </label>
+ <label>Address <span class="muted">(optional, made from the title)</span><br>
+ <input name="slug" value="{{.Data.post.Slug}}" pattern="[a-z0-9]([a-z0-9-]*[a-z0-9])?" maxlength="80"></label>
+ </div>
+ <label>Content <span class="muted">(Markdown)</span><br>
+ <textarea name="body" rows="24" class="editor">{{.Data.post.BodyMD}}</textarea></label>
+ <label class="check"><input type="checkbox" name="published"{{if .Data.post.Published}} checked{{end}}> Visible on the blog</label>
+ <p>
+ <button type="submit">Save</button>
+ {{if .Data.post.ID}}<a class="btn secondary" href="{{.BlogURL}}/{{.Data.post.PageSlug}}/{{.Data.post.Slug}}" target="_blank">View post &#8599;</a>{{end}}
+ <a href="/b/{{.Blog.Subdomain}}/posts">Back to posts</a>
+ </p>
+ </form>
+</div>
+{{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"}}
+<h1>Posts <a class="btn small" href="/b/{{.Blog.Subdomain}}/posts/new{{if .Data.pageID}}?page={{.Data.pageID}}{{end}}">+ New post</a></h1>
+<form method="get" class="filter">
+ <label>Page:
+ <select name="page" onchange="this.form.submit()">
+ <option value="0">All pages</option>
+ {{range .Data.pages}}<option value="{{.ID}}"{{if eq .ID $.Data.pageID}} selected{{end}}>{{.Title}}</option>{{end}}
+ </select></label>
+ <noscript><button type="submit">Filter</button></noscript>
+</form>
+<div class="card">
+{{if .Data.posts}}
+<table>
+ <tr><th>Title</th><th>Page</th><th>Date</th><th></th></tr>
+ {{range .Data.posts}}<tr>
+ <td><a href="/b/{{$.Blog.Subdomain}}/posts/{{.ID}}/edit">{{.Title}}</a>{{if not .Published}} <span class="tag">hidden</span>{{end}}</td>
+ <td>{{.PageTitle}}</td>
+ <td class="nowrap">{{date .CreatedAt}}</td>
+ <td class="nowrap"><a href="{{$.BlogURL}}/{{.PageSlug}}/{{.Slug}}" target="_blank">view &#8599;</a> &middot; <a class="danger" href="/b/{{$.Blog.Subdomain}}/posts/{{.ID}}/delete">delete</a></td>
+ </tr>{{end}}
+</table>
+{{else}}<p class="muted">No posts here yet.</p>{{end}}
+</div>
+{{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"}}
+<div class="card narrow">
+ <h1>Blog settings</h1>
+ <form method="post" action="/b/{{.Blog.Subdomain}}/settings">
+ <input type="hidden" name="_csrf" value="{{.CSRF}}">
+ <label>Blog title<br><input name="title" value="{{.Blog.Title}}" required maxlength="120"></label>
+ <label>Tagline <span class="muted">(shown under the title)</span><br><input name="tagline" value="{{.Blog.Tagline}}" maxlength="300"></label>
+ <p class="muted">Address: <code>{{.BlogURL}}</code> — only the administrator can change this.</p>
+ <button type="submit">Save</button>
+ </form>
+</div>
+{{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"}}<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<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">
+<style>{{css .Data.css}}</style>
+</head>
+<body class="nav-{{.Data.theme.NavPosition}}">
+{{if eq .Data.theme.NavPosition "top-bar"}}{{template "blognav" .}}{{end}}
+<div class="site-header">
+ {{if .Data.theme.HeaderImage}}<div class="header-image"><a href="/"><img src="/media/{{.Data.theme.HeaderImage}}" alt=""></a></div>{{end}}
+ {{if .Data.theme.HeaderShowTitle}}<div class="wrap header-text">
+ <h1 class="site-title"><a href="/">{{.Blog.Title}}</a></h1>
+ {{if .Blog.Tagline}}<p class="tagline">{{.Blog.Tagline}}</p>{{end}}
+ </div>{{end}}
+</div>
+{{if eq .Data.theme.NavPosition "below-header"}}{{template "blognav" .}}{{end}}
+<div class="wrap body-wrap">
+ {{if eq .Data.theme.NavPosition "left-sidebar"}}{{template "blognav" .}}{{end}}
+ <div class="content">
+ {{template "content" .}}
+ </div>
+</div>
+<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>
+ </div>
+</div>
+</body>
+</html>{{end}}
+
+{{define "blognav"}}<div class="site-nav"><div class="wrap nav-inner">
+{{range .Data.nav}}<a href="{{if .IsHome}}/{{else}}/{{.Slug}}{{end}}"{{if and $.Data.page (eq $.Data.page.ID .ID)}} class="active"{{end}}>{{.Title}}</a>
+{{end}}</div></div>{{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"}}<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>{{block "title" .}}Blogspace{{end}}</title>
+<link rel="stylesheet" href="/static/dashboard.css">
+</head>
+<body>
+{{template "dashnav" .}}
+<main class="main">
+{{if .Flash}}<div class="flash ok">{{.Flash}}</div>{{end}}
+{{if .Error}}<div class="flash err">{{.Error}}</div>{{end}}
+{{template "content" .}}
+</main>
+</body>
+</html>{{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"}}<div class="topbar">
+ <div class="topbar-inner">
+ <a class="brand" href="/">Blogspace</a>
+ {{if .User}}
+ <span class="who">{{.User.Username}}{{if .User.IsSuperadmin}} (superadmin){{end}}</span>
+ <span class="links">
+ {{if .User.IsSuperadmin}}<a href="/admin/">Admin</a>{{end}}
+ <a href="/account/password">Password</a>
+ <form method="post" action="/logout" class="inline"><input type="hidden" name="_csrf" value="{{.CSRF}}"><button type="submit" class="linkbtn">Log out</button></form>
+ </span>
+ {{end}}
+ </div>
+</div>
+{{if .Blog}}
+<div class="blogbar">
+ <div class="topbar-inner">
+ <strong class="blogname">{{.Blog.Title}}</strong>
+ <a href="/b/{{.Blog.Subdomain}}/">Overview</a>
+ <a href="/b/{{.Blog.Subdomain}}/posts">Posts</a>
+ <a href="/b/{{.Blog.Subdomain}}/pages">Pages</a>
+ <a href="/b/{{.Blog.Subdomain}}/design">Design</a>
+ <a href="/b/{{.Blog.Subdomain}}/images">Images</a>
+ <a href="/b/{{.Blog.Subdomain}}/settings">Settings</a>
+ <a class="view" href="{{.BlogURL}}" target="_blank">View blog &#8599;</a>
+ </div>
+</div>
+{{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"}}<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}}
+</div>{{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"}}<div class="card help">
+ <h3>Formatting cheat-sheet</h3>
+ <table class="cheat">
+ <tr><td><code># Heading</code>, <code>## Smaller heading</code></td><td><code>**bold**</code>, <code>*italic*</code></td></tr>
+ <tr><td><code>[link text](https://example.org)</code></td><td><code>![description](/media/…)</code> — copy the address from <em>Images</em></td></tr>
+ <tr><td><code>- list item</code> / <code>1. numbered</code></td><td><code>&gt; quote</code>, <code>`code`</code>, <code>---</code> for a line</td></tr>
+ </table>
+ <p class="muted">Blank line = new paragraph. Press Enter once for a line break.</p>
+</div>{{end}}