summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBrian Picciano <mediocregopher@gmail.com>2022-05-13 11:47:29 -0600
committerBrian Picciano <mediocregopher@gmail.com>2022-05-14 15:22:10 -0600
commit2929b4279c7a8128bd305290cc4187b6afb11cde (patch)
treed5ee6a82a21cc3c74c5f5359479b67291b9dbb27
parentd284fe2d2518c43097c0fea436d2073de14f3ada (diff)
Implement rendering Posts to html
-rw-r--r--srv/default.nix2
-rw-r--r--srv/src/go.mod11
-rw-r--r--srv/src/go.sum11
-rw-r--r--srv/src/post/post.go20
-rw-r--r--srv/src/post/renderer.go96
-rw-r--r--srv/src/post/renderer_test.go92
-rw-r--r--srv/src/tpl/html/base.html65
-rw-r--r--srv/src/tpl/html/post.html48
-rw-r--r--srv/src/tpl/tpl.go12
9 files changed, 333 insertions, 24 deletions
diff --git a/srv/default.nix b/srv/default.nix
index 42018ce..a5aa6af 100644
--- a/srv/default.nix
+++ b/srv/default.nix
@@ -38,7 +38,7 @@
pname = "mediocre-blog-srv";
version = "dev";
src = ./src;
- vendorSha256 = "sha256-MdjPrNSAAiqkAnJRIhMFTVQDKIPuDCHqRQFEtnoe1Cc=";
+ vendorSha256 = "1s5jhis1a2y7m50k29ap7kd0h4bgc3dzy1f9dqf5jrz8n27f3i87";
# disable tests
checkPhase = '''';
diff --git a/srv/src/go.mod b/srv/src/go.mod
index 48ca311..2f9bf4b 100644
--- a/srv/src/go.mod
+++ b/srv/src/go.mod
@@ -3,17 +3,20 @@ module github.com/mediocregopher/blog.mediocregopher.com/srv
go 1.16
require (
- github.com/adrg/frontmatter v0.2.0 // indirect
+ github.com/adrg/frontmatter v0.2.0
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21
github.com/emersion/go-smtp v0.15.0
+ github.com/gomarkdown/markdown v0.0.0-20220510115730-2372b9aa33e5
github.com/google/uuid v1.3.0
- github.com/gorilla/websocket v1.4.2 // indirect
+ github.com/gorilla/websocket v1.4.2
github.com/mattn/go-sqlite3 v1.14.8
github.com/mediocregopher/mediocre-go-lib/v2 v2.0.0-beta.0.0.20220506011745-cbeee71cb1ee
- github.com/mediocregopher/radix/v4 v4.0.0-beta.1.0.20210726230805-d62fa1b2e3cb // indirect
+ github.com/mediocregopher/radix/v4 v4.0.0-beta.1.0.20210726230805-d62fa1b2e3cb
github.com/rubenv/sql-migrate v0.0.0-20210614095031-55d5740dbbcc
github.com/stretchr/testify v1.7.0
github.com/tilinna/clock v1.1.0
github.com/ziutek/mymysql v1.5.4 // indirect
- golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 // indirect
+ golang.org/x/crypto v0.0.0-20210817164053-32db794688a5
+ golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect
+ golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
)
diff --git a/srv/src/go.sum b/srv/src/go.sum
index 79b2bf6..358c3d6 100644
--- a/srv/src/go.sum
+++ b/srv/src/go.sum
@@ -57,6 +57,8 @@ github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4er
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/gomarkdown/markdown v0.0.0-20220510115730-2372b9aa33e5 h1:mjLaNMRojfgo2qgSdEX1CzmG7rHTeOWZdO1T1sgjEb0=
+github.com/gomarkdown/markdown v0.0.0-20220510115730-2372b9aa33e5/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
@@ -109,8 +111,6 @@ github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A
github.com/mattn/go-sqlite3 v1.14.8 h1:gDp86IdQsN/xWjIEmr9MF6o9mpksUgh0fu+9ByFxzIU=
github.com/mattn/go-sqlite3 v1.14.8/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
-github.com/mediocregopher/mediocre-go-lib/v2 v2.0.0-beta.0 h1:i9FBkcCaWXxteJ8458AD8dBL2YqSxVlpsHOMWg5N9Dc=
-github.com/mediocregopher/mediocre-go-lib/v2 v2.0.0-beta.0/go.mod h1:wOZVlnKYvIbkzyCJ3dxy1k40XkirvCd1pisX2O91qoQ=
github.com/mediocregopher/mediocre-go-lib/v2 v2.0.0-beta.0.0.20220506011745-cbeee71cb1ee h1:AWRuhgn7iumyhPuxKwed1F1Ri2dXMwxKfp5YIdpnQIY=
github.com/mediocregopher/mediocre-go-lib/v2 v2.0.0-beta.0.0.20220506011745-cbeee71cb1ee/go.mod h1:wOZVlnKYvIbkzyCJ3dxy1k40XkirvCd1pisX2O91qoQ=
github.com/mediocregopher/radix/v4 v4.0.0-beta.1.0.20210726230805-d62fa1b2e3cb h1:7Y2vAC5q44VJzbBUdxRUEqfz88ySJ/6yXXkpQ+sxke4=
@@ -181,7 +181,6 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191122220453-ac88ee75c92c/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a h1:vclmkQCjlDX5OydZ9wv8rBCcS0QyQY66Mpf/7BZbInM=
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ=
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
@@ -209,12 +208,14 @@ golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM=
+golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
diff --git a/srv/src/post/post.go b/srv/src/post/post.go
index bdc48af..5835995 100644
--- a/srv/src/post/post.go
+++ b/srv/src/post/post.go
@@ -5,7 +5,6 @@ import (
"database/sql"
"errors"
"fmt"
- "path"
"regexp"
"strings"
"time"
@@ -37,6 +36,12 @@ type Post struct {
Body string
}
+// HTTPPath returns the relative URL path of the StoredPost, when querying it
+// over HTTP.
+func (p Post) HTTPPath() string {
+ return fmt.Sprintf("%s.html", p.ID)
+}
+
// StoredPost is a Post which has been stored in a Store, and has been given
// some extra fields as a result.
type StoredPost struct {
@@ -46,19 +51,6 @@ type StoredPost struct {
LastUpdatedAt time.Time
}
-// URL returns the relative URL of the StoredPost.
-func (p StoredPost) URL() string {
- return path.Join(
- fmt.Sprintf(
- "%d/%0d/%0d",
- p.PublishedAt.Year(),
- p.PublishedAt.Month(),
- p.PublishedAt.Day(),
- ),
- p.ID+".html",
- )
-}
-
// Store is used for storing posts to a persistent storage.
type Store interface {
diff --git a/srv/src/post/renderer.go b/srv/src/post/renderer.go
new file mode 100644
index 0000000..74acc25
--- /dev/null
+++ b/srv/src/post/renderer.go
@@ -0,0 +1,96 @@
+package post
+
+import (
+ _ "embed"
+ "fmt"
+ "html/template"
+ "io"
+
+ "github.com/gomarkdown/markdown"
+ "github.com/gomarkdown/markdown/html"
+ "github.com/gomarkdown/markdown/parser"
+ "github.com/mediocregopher/blog.mediocregopher.com/srv/tpl"
+)
+
+// RenderablePost is a Post wrapped with extra information necessary for
+// rendering.
+type RenderablePost struct {
+ StoredPost
+ SeriesPrevious, SeriesNext *StoredPost
+}
+
+// NewRenderablePost wraps an existing Post such that it can be rendered.
+func NewRenderablePost(store Store, post StoredPost) (RenderablePost, error) {
+
+ renderablePost := RenderablePost{
+ StoredPost: post,
+ }
+
+ if post.Series != "" {
+
+ seriesPosts, err := store.GetBySeries(post.Series)
+ if err != nil {
+ return RenderablePost{}, fmt.Errorf(
+ "fetching posts for series %q: %w",
+ post.Series, err,
+ )
+ }
+
+ var foundThis bool
+
+ for i := range seriesPosts {
+
+ seriesPost := seriesPosts[i]
+
+ if seriesPost.ID == post.ID {
+ foundThis = true
+ continue
+ }
+
+ if !foundThis {
+ renderablePost.SeriesPrevious = &seriesPost
+ continue
+ }
+
+ renderablePost.SeriesNext = &seriesPost
+ break
+ }
+ }
+
+ return renderablePost, nil
+}
+
+// Renderer takes a Post and renders it to some encoding.
+type Renderer interface {
+ Render(io.Writer, RenderablePost) error
+}
+
+func mdBodyToHTML(body []byte) []byte {
+ parserExt := parser.CommonExtensions | parser.AutoHeadingIDs
+ parser := parser.NewWithExtensions(parserExt)
+
+ htmlFlags := html.CommonFlags | html.HrefTargetBlank
+ htmlRenderer := html.NewRenderer(html.RendererOptions{Flags: htmlFlags})
+
+ return markdown.ToHTML(body, parser, htmlRenderer)
+}
+
+type mdHTMLRenderer struct{}
+
+// NewMarkdownToHTMLRenderer renders Posts from markdown to HTML.
+func NewMarkdownToHTMLRenderer() Renderer {
+ return mdHTMLRenderer{}
+}
+
+func (r mdHTMLRenderer) Render(into io.Writer, post RenderablePost) error {
+
+ data := struct {
+ RenderablePost
+ Body template.HTML
+ }{
+ RenderablePost: post,
+ Body: template.HTML(mdBodyToHTML([]byte(post.Body))),
+ }
+
+ return tpl.HTML.ExecuteTemplate(into, "post.html", data)
+}
diff --git a/srv/src/post/renderer_test.go b/srv/src/post/renderer_test.go
new file mode 100644
index 0000000..5c01cd2
--- /dev/null
+++ b/srv/src/post/renderer_test.go
@@ -0,0 +1,92 @@
+package post
+
+import (
+ "bytes"
+ "strconv"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestMarkdownBodyToHTML(t *testing.T) {
+
+ tests := []struct {
+ body string
+ exp string
+ }{
+ {
+ body: `
+# Foo
+`,
+ exp: `<h1 id="foo">Foo</h1>`,
+ },
+ {
+ body: `
+this is a body
+
+this is another
+`,
+ exp: `
+<p>this is a body</p>
+
+<p>this is another</p>`,
+ },
+ {
+ body: `this is a [link](somewhere.html)`,
+ exp: `<p>this is a <a href="somewhere.html" target="_blank">link</a></p>`,
+ },
+ }
+
+ for i, test := range tests {
+ t.Run(strconv.Itoa(i), func(t *testing.T) {
+
+ outB := mdBodyToHTML([]byte(test.body))
+ out := string(outB)
+
+ // just to make the tests nicer
+ out = strings.TrimSpace(out)
+ test.exp = strings.TrimSpace(test.exp)
+
+ assert.Equal(t, test.exp, out)
+ })
+ }
+}
+
+func TestMarkdownToHTMLRenderer(t *testing.T) {
+
+ r := NewMarkdownToHTMLRenderer()
+
+ post := RenderablePost{
+ StoredPost: StoredPost{
+ Post: Post{
+ ID: "foo",
+ Title: "Foo",
+ Description: "Bar.",
+ Body: "This is the body.",
+ Series: "baz",
+ },
+ PublishedAt: time.Now(),
+ },
+
+ SeriesPrevious: &StoredPost{
+ Post: Post{
+ ID: "foo-prev",
+ Title: "Foo Prev",
+ },
+ },
+
+ SeriesNext: &StoredPost{
+ Post: Post{
+ ID: "foo-next",
+ Title: "Foo Next",
+ },
+ },
+ }
+
+ buf := new(bytes.Buffer)
+ err := r.Render(buf, post)
+ assert.NoError(t, err)
+ t.Log(buf.String())
+}
diff --git a/srv/src/tpl/html/base.html b/srv/src/tpl/html/base.html
new file mode 100644
index 0000000..bf81032
--- /dev/null
+++ b/srv/src/tpl/html/base.html
@@ -0,0 +1,65 @@
+<!DOCTYPE html>
+<html lang="en">
+
+ <head>
+ <meta charset="utf-8">
+ <link rel="stylesheet" href="/assets/normalize.css">
+ <link rel="stylesheet" href="/assets/skeleton.css">
+ <link rel="stylesheet" href="/assets/friendly.css">
+ <link rel="stylesheet" href="/assets/main.css">
+ <link rel="stylesheet" href="/assets/fontawesome/css/all.css">
+ </head>
+
+ <body>
+
+ <div class="container">
+
+ <header id="title-header" role="banner">
+ <div class="row">
+ <div class="seven columns" style="margin-bottom: 3rem;">
+ <h1 class="title">
+ <a href="/">Mediocre Blog</a>
+ </h1>
+ <div class="light social">
+ <span>By Brian Picciano</span>
+ <span>
+ Even more @
+ <a href="https://mediocregopher.eth.link" target="_blank">https://mediocregopher.eth.link</a>
+ </span>
+ </div>
+ </div>
+
+ <div class="five columns light">
+ <span style="display:block; margin-bottom:0.5rem;">Get notified when new posts are published!</span>
+ <a href="/follow.html">
+ <button class="button-primary">
+ <i class="far fa-envelope"></i>
+ Follow
+ </button>
+ </a>
+ <a href="/feed.xml">
+ <button class="button">
+ <i class="fas fa-rss"></i>
+ RSS
+ </button>
+ </a>
+ </div>
+
+ </div>
+ </header>
+
+ {{ template "body" . }}
+
+ <footer>
+ <p class="license light">
+ Unless otherwised specified, all works are licensed under the
+ <a href="/assets/wtfpl.txt">WTFPL</a>.
+ </p>
+ </footer>
+
+ </div>
+
+ </body>
+
+</html>
+
diff --git a/srv/src/tpl/html/post.html b/srv/src/tpl/html/post.html
new file mode 100644
index 0000000..22a5b97
--- /dev/null
+++ b/srv/src/tpl/html/post.html
@@ -0,0 +1,48 @@
+{{ define "body" }}
+
+<header id="post-header">
+ <h1 id="post-headline">
+ {{ .Title }}
+ </h1>
+ <div class="light">
+ {{ .PublishedAt.Format "2006-01-02" }}
+ &nbsp;•&nbsp;
+ {{ if not .LastUpdatedAt.IsZero }}
+ (Updated {{ .LastUpdatedAt.Format "2006-01-02" }})
+ &nbsp;•&nbsp;
+ {{ end }}
+ <em>{{ .Description }}</em>
+ </div>
+</header>
+
+{{ if (or .SeriesPrevious .SeriesNext) }}
+<p class="light"><em>
+ This post is part of a series:<br/>
+ {{ if .SeriesPrevious }}
+ Previously: <a href="{{ .SeriesPrevious.HTTPPath }}">{{ .SeriesPrevious.Title }}</a></br>
+ {{ end }}
+ {{ if .SeriesNext }}
+ Next: <a href="{{ .SeriesNext.HTTPPath }}">{{ .SeriesNext.Title }}</a></br>
+ {{ end }}
+</em></p>
+{{ end }}
+
+<div id="post-content">
+ {{ .Body }}
+</div>
+
+{{ if (or .SeriesPrevious .SeriesNext) }}
+<p class="light"><em>
+ If you liked this post, consider checking out other posts in the series:<br/>
+ {{ if .SeriesPrevious }}
+ Previously: <a href="{{ .SeriesPrevious.HTTPPath }}">{{ .SeriesPrevious.Title }}</a></br>
+ {{ end }}
+ {{ if .SeriesNext }}
+ Next: <a href="{{ .SeriesNext.HTTPPath }}">{{ .SeriesNext.Title }}</a></br>
+ {{ end }}
+</em></p>
+{{ end }}
+
+{{ end }}
+
+{{ template "base.html" . }}
diff --git a/srv/src/tpl/tpl.go b/srv/src/tpl/tpl.go
new file mode 100644
index 0000000..1dd98ba
--- /dev/null
+++ b/srv/src/tpl/tpl.go
@@ -0,0 +1,12 @@
+// Package tpl contains template files which are used to render the blog.
+package tpl
+
+import (
+ "embed"
+ html_tpl "html/template"
+)
+
+//go:embed *
+var fs embed.FS
+
+var HTML = html_tpl.Must(html_tpl.ParseFS(fs, "html/*"))