Browse Source

Initial commit

Signed-off-by: jolheiser <john.olheiser@gmail.com>
main
jolheiser 2 months ago
commit
e118ca3bd9
Signed by: jolheiser GPG Key ID: B853ADA5DA7BBF7A
  1. 17
      .drone.yml
  2. 5
      .gitignore
  3. 7
      LICENSE
  4. 7
      README.md
  5. 20
      _example/test.md
  6. 74
      build/build.go
  7. 19
      go.mod
  8. 82
      go.sum
  9. 52
      main.go
  10. 46
      markdown/markdown.go
  11. 66
      markdown/meta.go
  12. 79
      post/post.go
  13. 70
      router/router.go
  14. 19
      static/index.tmpl
  15. 11
      static/post.tmpl
  16. 204
      static/sakura.css
  17. 31
      static/static.go

17
.drone.yml

@ -0,0 +1,17 @@
---
kind: pipeline
name: compliance
trigger:
event:
- pull_request
steps:
- name: test
pull: always
image: golang:1.17
commands:
- go test -race ./...
- name: vet
pull: always
image: golang:1.17
commands:
- go vet -race ./...

5
.gitignore

@ -0,0 +1,5 @@
# GoLand
.idea/
# Binaries
/blog*

7
LICENSE

@ -0,0 +1,7 @@
Copyright 2020 John Olheiser
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

7
README.md

@ -0,0 +1,7 @@
# blog
A simple blog tool.
## License
[MIT](LICENSE)

20
_example/test.md

@ -0,0 +1,20 @@
---
title = "Test"
author = "jolheiser"
date = 2021-10-01
tags = ["test", "example"]
---
# Testing
This is a test
```go
package main
import "fmt"
func main() {
fmt.Println("Hello, blog!")
}
```

74
build/build.go

@ -0,0 +1,74 @@
package main
import (
"fmt"
"github.com/goyek/goyek"
)
const zerologLintVer = "1.24.0"
func main() {
flow().Main()
}
func flow() *goyek.Flow {
flow := &goyek.Flow{}
fmt := flow.Register(taskFmt)
test := flow.Register(taskTest)
vet := flow.Register(taskVet)
lint := flow.Register(taskLint)
flow.DefaultTask = flow.Register(goyek.Task{
Name: "all",
Usage: "Run all flows",
Deps: []goyek.RegisteredTask{
fmt,
lint,
test,
vet,
},
})
return flow
}
var taskFmt = goyek.Task{
Name: "fmt",
Usage: "go fmt",
Action: func(tf *goyek.TF) {
if err := tf.Cmd("go", "fmt", "./...").Run(); err != nil {
tf.Fatal(err)
}
},
}
var taskTest = goyek.Task{
Name: "test",
Usage: "go test",
Action: func(tf *goyek.TF) {
if err := tf.Cmd("go", "test", "-race", "./...").Run(); err != nil {
tf.Fatal(err)
}
},
}
var taskVet = goyek.Task{
Name: "vet",
Usage: "go vet",
Action: func(tf *goyek.TF) {
if err := tf.Cmd("go", "vet", "-race", "./...").Run(); err != nil {
tf.Fatal(err)
}
},
}
var taskLint = goyek.Task{
Name: "lint",
Usage: "Run linter(s)",
Action: func(tf *goyek.TF) {
if err := tf.Cmd("go", "run", fmt.Sprintf("github.com/rs/zerolog/cmd/lint@v%s", zerologLintVer), ".").Run(); err != nil {
tf.Fatal(err)
}
},
}

19
go.mod

@ -0,0 +1,19 @@
module go.jolheiser.com/blog
go 1.17
require (
github.com/alecthomas/chroma v0.7.2-0.20200305040604-4f3623dce67a
github.com/go-chi/chi/v5 v5.0.4
github.com/goyek/goyek v0.6.0
github.com/pelletier/go-toml/v2 v2.0.0-beta.3
github.com/peterbourgon/ff/v3 v3.1.0
github.com/rs/zerolog v1.24.0
github.com/yuin/goldmark v1.4.0
github.com/yuin/goldmark-highlighting v0.0.0-20210516132338-9216f9c5aa01
)
require (
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 // indirect
github.com/dlclark/regexp2 v1.2.0 // indirect
)

82
go.sum

@ -0,0 +1,82 @@
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 h1:smF2tmSOzy2Mm+0dGI2AIUHY+w0BUc+4tn40djz7+6U=
github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38/go.mod h1:r7bzyVFMNntcxPZXK3/+KdruV1H5KSlyVY0gc+NgInI=
github.com/alecthomas/chroma v0.7.2-0.20200305040604-4f3623dce67a h1:3v1NrYWWqp2S72e4HLgxKt83B3l0lnORDholH/ihoMM=
github.com/alecthomas/chroma v0.7.2-0.20200305040604-4f3623dce67a/go.mod h1:fv5SzZPFJbwp2NXJWpFIX7DZS4HgV1K4ew4Pc2OZD9s=
github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721 h1:JHZL0hZKJ1VENNfmXvHbgYlbUOvpzYzvy2aZU5gXVeo=
github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0=
github.com/alecthomas/kong v0.2.1-0.20190708041108-0548c6b1afae/go.mod h1:+inYUSluD+p4L8KdviBSgzcqEjUQOfC5fQDRFuc36lI=
github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897 h1:p9Sln00KOTlrYkxI1zYWl1QLnEqAqEARBEYa8FQnQcY=
github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ=
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 h1:y5HC9v93H5EPKqaS1UYVg1uYah5Xf51mBfIoWehClUQ=
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9hchkHSWYkEqJwUGisez3G1QY8Ryz0sdWrLPMGjLk=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.1.6/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/dlclark/regexp2 v1.2.0 h1:8sAhBGEM0dRWogWqWyQeIJnxjWO6oIjl8FKqREDsGfk=
github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/go-chi/chi/v5 v5.0.4 h1:5e494iHzsYBiyXQAHHuI4tyJS9M3V84OuX3ufIIGHFo=
github.com/go-chi/chi/v5 v5.0.4/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/goyek/goyek v0.6.0 h1:2YQ4V3X7q+zFF98IBWMc1WRwfzs0TQ8jrwOKY3XRQRk=
github.com/goyek/goyek v0.6.0/go.mod h1:UGjZz3juJL2l2eMqRbxQYjG8ieyKb7WMYPv0KB0KVxA=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/pelletier/go-toml v1.6.0 h1:aetoXYr0Tv7xRU/V4B4IZJ2QcbtMUFoNb3ORp7TzIK4=
github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys=
github.com/pelletier/go-toml/v2 v2.0.0-beta.3 h1:PNCTU4naEJ8mKal97P3A2qDU74QRQGlv4FXiL1XDqi4=
github.com/pelletier/go-toml/v2 v2.0.0-beta.3/go.mod h1:aNseLYu/uKskg0zpr/kbr2z8yGuWtotWf/0BpGIAL2Y=
github.com/peterbourgon/ff/v3 v3.1.0 h1:5JAeDK5j/zhKFjyHEZQXwXBoDijERaos10RE+xamOsY=
github.com/peterbourgon/ff/v3 v3.1.0/go.mod h1:XNJLY8EIl6MjMVjBS4F0+G0LYoAqs0DTa4rmHHukKDE=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.24.0 h1:76ivFxmVSRs1u2wUwJVg5VZDYQgeH1JpoS6ndgr9Wy8=
github.com/rs/zerolog v1.24.0/go.mod h1:7KHcEGe0QZPOm2IE4Kpb5rTh6n1h2hIgS5OOnu1rUaI=
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.1-0.20210427113832-6241f9ab9942 h1:t0lM6y/M5IiUZyvbBTcngso8SZEZICH7is9B6g/obVU=
github.com/stretchr/testify v1.7.1-0.20210427113832-6241f9ab9942/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.3.6/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.0 h1:OtISOGfH6sOWa1/qXqqAiOIAO6Z5J3AEAE18WAq6BiQ=
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark-highlighting v0.0.0-20210516132338-9216f9c5aa01 h1:0SJnXjE4jDClMW6grE0xpNhwpqbPwkBTn8zpVw5C0SI=
github.com/yuin/goldmark-highlighting v0.0.0-20210516132338-9216f9c5aa01/go.mod h1:TwKQPa5XkCCRC2GRZ5wtfNUTQ2+9/i19mGRijFeJ4BE=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35/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-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007 h1:gG67DSER+11cZvqIMb8S8bt0vZtiN6xWYARwirrOSfE=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
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/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

52
main.go

@ -0,0 +1,52 @@
package main
import (
"flag"
"fmt"
"net/http"
"os"
"os/signal"
"go.jolheiser.com/blog/post"
"go.jolheiser.com/blog/router"
"github.com/go-chi/chi/v5/middleware"
"github.com/peterbourgon/ff/v3"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)
func main() {
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
middleware.DefaultLogger = middleware.RequestLogger(&middleware.DefaultLogFormatter{Logger: &log.Logger, NoColor: true})
fs := flag.NewFlagSet("blog", flag.ExitOnError)
postPath := fs.String("post-dir", "posts", "Path to posts directory")
port := fs.Int("port", 8080, "Port to serve on")
fs.String("config", "", "Config file")
if err := ff.Parse(fs, os.Args[1:],
ff.WithEnvVarPrefix("BLOG"),
ff.WithConfigFileFlag("config"),
ff.WithAllowMissingConfigFile(true),
); err != nil {
log.Error().Err(err).Msg("could not parse flags")
return
}
b, err := post.NewBlog(*postPath)
if err != nil {
log.Error().Err(err).Msg("could not init blog")
return
}
r := router.New(b)
go func() {
if err := http.ListenAndServe(fmt.Sprintf(":%d", *port), r); err != nil {
log.Error().Err(err).Msg("could not open server")
}
}()
ch := make(chan os.Signal, 1)
signal.Notify(ch, os.Interrupt, os.Kill)
<-ch
}

46
markdown/markdown.go

@ -0,0 +1,46 @@
package markdown
import (
"bytes"
"io"
chromahtml "github.com/alecthomas/chroma/formatters/html"
"github.com/yuin/goldmark"
highlighting "github.com/yuin/goldmark-highlighting"
"github.com/yuin/goldmark/extension"
"github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/renderer/html"
)
var gm = goldmark.New(
goldmark.WithExtensions(
extension.GFM,
highlighting.NewHighlighting(
highlighting.WithStyle("monokai"),
highlighting.WithFormatOptions(
chromahtml.WithLineNumbers(true),
chromahtml.LinkableLineNumbers(true, "code-"),
chromahtml.LineNumbersInTable(true),
),
),
),
goldmark.WithParserOptions(
parser.WithAutoHeadingID(),
),
goldmark.WithRendererOptions(
html.WithHardWraps(),
),
)
// Convert transforms a markdown document into HTML
func Convert(r io.Reader) (string, error) {
content, err := Content(r)
if err != nil {
return "", err
}
var buf bytes.Buffer
if err := gm.Convert([]byte(content), &buf); err != nil {
return "", err
}
return buf.String(), nil
}

66
markdown/meta.go

@ -0,0 +1,66 @@
package markdown
import (
"bufio"
"errors"
"io"
"strings"
"github.com/pelletier/go-toml/v2"
)
func isSeparator(line string) bool {
line = strings.TrimSpace(line)
for i := 0; i < len(line); i++ {
if line[i] != '-' {
return false
}
}
return len(line) > 2
}
// Meta reads from r to extract TOML frontmatter
func Meta(r io.Reader, out interface{}) error {
var content strings.Builder
scanner := bufio.NewScanner(r)
var seps int
for scanner.Scan() {
line := scanner.Text()
if seps == 0 && content.Len() == 0 && !isSeparator(line) {
return errors.New("no beginning separator")
}
if isSeparator(line) {
seps++
if content.Len() == 0 {
continue
}
break
}
content.WriteString(line + "\n")
}
if seps != 2 {
return errors.New("no ending separator")
}
if content.Len() > 0 {
return toml.Unmarshal([]byte(content.String()), out)
}
return errors.New("no content")
}
// Content skips meta and returns document content
func Content(r io.Reader) (string, error) {
var s strings.Builder
scanner := bufio.NewScanner(r)
var seps int
for scanner.Scan() {
if seps < 2 && isSeparator(scanner.Text()) {
seps++
continue
}
if seps >= 2 {
s.WriteString(scanner.Text() + "\n")
}
}
return s.String(), nil
}

79
post/post.go

@ -0,0 +1,79 @@
package post
import (
"os"
"path/filepath"
"sort"
"strings"
"time"
"go.jolheiser.com/blog/markdown"
"github.com/rs/zerolog/log"
)
func NewBlog(basePath string) (*Blog, error) {
posts, err := Scan(basePath)
if err != nil {
return nil, err
}
return &Blog{
Path: basePath,
Posts: posts,
}, nil
}
func Scan(basePath string) (map[string]*Post, error) {
posts := make(map[string]*Post)
ents, err := os.ReadDir(basePath)
if err != nil {
return nil, err
}
for _, ent := range ents {
if ent.IsDir() || !strings.HasSuffix(ent.Name(), ".md") {
continue
}
apn := filepath.Join(basePath, ent.Name())
fi, err := os.Open(apn)
if err != nil {
log.Error().Err(err).Msg("could not open file")
continue
}
post := &Post{Path: apn, Slug: strings.TrimSuffix(ent.Name(), ".md")}
if err := markdown.Meta(fi, &post); err != nil {
log.Error().Err(err).Msg("could not extract meta")
continue
}
posts[post.Slug] = post
if err := fi.Close(); err != nil {
log.Error().Err(err).Msg("could not close file")
continue
}
}
return posts, nil
}
type Blog struct {
Path string
Posts map[string]*Post
}
func (b *Blog) SortedPosts() []*Post {
posts := make([]*Post, 0, len(b.Posts))
for _, post := range b.Posts {
posts = append(posts, post)
}
sort.Slice(posts, func(i, j int) bool {
return posts[i].Date.Before(posts[j].Date)
})
return posts
}
type Post struct {
Path string `toml:"-"`
Slug string `toml:"-"`
Title string `toml:"title"`
Author string `toml:"author"`
Date time.Time `toml:"date"`
Tags []string `toml:"tags"`
}

70
router/router.go

@ -0,0 +1,70 @@
package router
import (
"html/template"
"net/http"
"os"
"go.jolheiser.com/blog/markdown"
"go.jolheiser.com/blog/post"
"go.jolheiser.com/blog/static"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/rs/zerolog/log"
)
func New(blog *post.Blog) *chi.Mux {
m := chi.NewMux()
m.Use(middleware.Logger)
m.Use(middleware.Recoverer)
m.Get("/", indexHandler(blog))
m.Get("/{post}", fileHandler(blog))
m.Get("/sakura.css", static.SakuraCSS)
return m
}
func indexHandler(blog *post.Blog) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if err := static.IndexTemplate.Execute(w, map[string]interface{}{
"Blog": blog,
}); err != nil {
log.Error().Err(err).Msg("could not execute template")
}
}
}
func fileHandler(blog *post.Blog) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
postName := chi.URLParam(r, "post")
p, ok := blog.Posts[postName]
if !ok {
w.WriteHeader(http.StatusNotFound)
return
}
fi, err := os.Open(p.Path)
if err != nil {
log.Error().Err(err).Msg("could not open post")
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer fi.Close()
md, err := markdown.Convert(fi)
if err != nil {
log.Error().Err(err).Msg("could not convert")
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if err := static.PostTemplate.Execute(w, map[string]interface{}{
"Post": p,
"Content": template.HTML(md),
}); err != nil {
log.Error().Err(err).Msg("could not execute template")
}
}
}

19
static/index.tmpl

@ -0,0 +1,19 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Blog</title>
<link rel="stylesheet" href="sakura.css"/>
</head>
<body>
<h1>Blog Posts</h1>
{{range .Blog.Posts}}
<p>
<a href="{{.Slug}}"><strong>{{.Title}}</strong></a><br/>
<small>@{{.Author}} - <i>{{.Date.Format "01/02/2006"}}</i></small>
</p>
<hr/>
{{end}}
</body>
</html>

11
static/post.tmpl

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{{.Title}}</title>
<link rel="stylesheet" href="sakura.css"/>
</head>
<body>
{{.Content}}
</body>
</html>

204
static/sakura.css

@ -0,0 +1,204 @@
/* Sakura.css v1.3.1
* ================
* Minimal css theme.
* Project: https://github.com/oxalorg/sakura/
*/
/* Default Sakura Theme */
:root {
--color-blossom: #1d7484;
--color-fade: #982c61;
--color-bg: #f9f9f9;
--color-bg-alt: #f1f1f1;
--color-text: #4a4a4a;
}
/* Sakura Dark Theme */
@media (prefers-color-scheme: dark) {
:root {
--color-blossom: #ffffff;
--color-fade: #c9c9c9;
--color-bg: #222222;
--color-bg-alt: #4a4a4a;
--color-text: #c9c9c9;
}
}
html {
font-size: 62.5%;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif; }
body {
font-size: 1.8rem;
line-height: 1.618;
max-width: 38em;
margin: auto;
color: var(--color-text);
background-color: var(--color-bg);
padding: 13px; }
@media (max-width: 684px) {
body {
font-size: 1.53rem; } }
@media (max-width: 382px) {
body {
font-size: 1.35rem; } }
h1, h2, h3, h4, h5, h6 {
line-height: 1.1;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif;
font-weight: 700;
margin-top: 3rem;
margin-bottom: 1.5rem;
overflow-wrap: break-word;
word-wrap: break-word;
-ms-word-break: break-all;
word-break: break-word; }
h1 {
font-size: 2.35em; }
h2 {
font-size: 2.00em; }
h3 {
font-size: 1.75em; }
h4 {
font-size: 1.5em; }
h5 {
font-size: 1.25em; }
h6 {
font-size: 1em; }
p {
margin-top: 0px;
margin-bottom: 2.5rem; }
small, sub, sup {
font-size: 75%; }
hr {
border-color: var(--color-blossom); }
a {
text-decoration: none;
color: var(--color-blossom); }
a:hover {
color: var(--color-fade);
border-bottom: 2px solid var(--color-text); }
a:visited {
color: var(--color-blossom); }
ul {
padding-left: 1.4em;
margin-top: 0px;
margin-bottom: 2.5rem; }
li {
margin-bottom: 0.4em; }
blockquote {
margin-left: 0px;
margin-right: 0px;
padding-left: 1em;
padding-top: 0.8em;
padding-bottom: 0.8em;
padding-right: 0.8em;
border-left: 5px solid var(--color-blossom);
margin-bottom: 2.5rem;
background-color: var(--color-bg-alt); }
blockquote p {
margin-bottom: 0; }
img, video {
height: auto;
max-width: 100%;
margin-top: 0px;
margin-bottom: 2.5rem; }
/* Pre and Code */
pre {
background-color: var(--color-bg-alt);
display: block;
padding: 1em;
overflow-x: auto;
margin-top: 0px;
margin-bottom: 2.5rem; }
code {
font-size: 0.9em;
padding: 0 0.5em;
background-color: var(--color-bg-alt);
white-space: pre-wrap; }
pre > code {
padding: 0;
background-color: transparent;
white-space: pre; }
/* Tables */
table {
text-align: justify;
width: 100%;
border-collapse: collapse; }
td, th {
padding: 0.5em;
border-bottom: 1px solid var(--color-bg-alt); }
/* Buttons, forms and input */
input, textarea {
border: 1px solid var(--color-text); }
input:focus, textarea:focus {
border: 1px solid var(--color-blossom); }
textarea {
width: 100%; }
.button, button, input[type="submit"], input[type="reset"], input[type="button"] {
display: inline-block;
padding: 5px 10px;
text-align: center;
text-decoration: none;
white-space: nowrap;
background-color: var(--color-blossom);
color: var(--color-bg);
border-radius: 1px;
border: 1px solid var(--color-blossom);
cursor: pointer;
box-sizing: border-box; }
.button[disabled], button[disabled], input[type="submit"][disabled], input[type="reset"][disabled], input[type="button"][disabled] {
cursor: default;
opacity: .5; }
.button:focus:enabled, .button:hover:enabled, button:focus:enabled, button:hover:enabled, input[type="submit"]:focus:enabled, input[type="submit"]:hover:enabled, input[type="reset"]:focus:enabled, input[type="reset"]:hover:enabled, input[type="button"]:focus:enabled, input[type="button"]:hover:enabled {
background-color: var(--color-fade);
border-color: var(--color-fade);
color: var(--color-bg);
outline: 0; }
textarea, select, input {
color: var(--color-text);
padding: 6px 10px;
/* The 6px vertically centers text on FF, ignored by Webkit */
margin-bottom: 10px;
background-color: var(--color-bg-alt);
border: 1px solid var(--color-bg-alt );
border-radius: 4px;
box-shadow: none;
box-sizing: border-box; }
textarea:focus, select:focus, input:focus {
border: 1px solid var(--color-blossom);
outline: 0; }
input[type="checkbox"]:focus {
outline: 1px dotted var(--color-blossom); }
label, legend, fieldset {
display: block;
margin-bottom: .5rem;
font-weight: 600; }

31
static/static.go

@ -0,0 +1,31 @@
package static
import (
_ "embed"
"html/template"
"net/http"
"github.com/rs/zerolog/log"
)
//go:embed sakura.css
var sakuraCSS []byte
func SakuraCSS(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "text/css")
if _, err := w.Write(sakuraCSS); err != nil {
log.Error().Err(err).Msg("could not write sakura.css")
}
}
var (
//go:embed post.tmpl
postTmpl string
PostTemplate = template.Must(template.New("").Parse(postTmpl))
)
var (
//go:embed index.tmpl
indexTmpl string
IndexTemplate = template.Must(template.New("").Parse(indexTmpl))
)
Loading…
Cancel
Save