├── README.md
├── blog
├── articles
│ ├── .keep
│ ├── first-blog.md
│ └── second-blog.md
├── handler.go
└── service.go
├── cmd
└── blog
│ └── main.go
├── dist
├── handler.go
└── static
│ └── bin
│ └── style.css
├── go.mod
├── go.sum
├── index
└── handler.go
└── templates
├── funcs.go
├── template.go
└── views
├── blog.html
├── blogs.html
├── index.html
└── layout.html
/README.md:
--------------------------------------------------------------------------------
1 | A static blog written in Go, packaged in one binary.
2 |
3 | Run it with
4 |
5 | $ go run ./cmd/blog
6 |
7 | Or see it in production
8 |
9 | - en: https://metnews.co/blog/
10 | - zh: https://metword.co/blog/
11 |
12 | ### Features
13 |
14 | - Rendered from markdown with metadata.
15 | - Packaged in one binary using Go [embed] package.
16 | - Static file hosting using Go [fs] package.
17 | - Best practices for base template layout using Go [html/template] package.
18 | - RSS feed supported.
19 |
20 | [embed]: https://pkg.go.dev/embed
21 | [fs]: https://pkg.go.dev/io/fs
22 | [html/template]: https://pkg.go.dev/html/template#URL
--------------------------------------------------------------------------------
/blog/articles/.keep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kidlj/miniblog/37eaf45734231a9902e52e7c5521019a357bd77c/blog/articles/.keep
--------------------------------------------------------------------------------
/blog/articles/first-blog.md:
--------------------------------------------------------------------------------
1 | ---
2 | Title: The Very First Blog Article
3 | Author: Jian Li
4 | Date: 2022/11/25 11:17:00
5 | ---
6 |
7 | This is the very first blog article.
8 |
9 | ### The Header
10 |
11 | This is another paragraph.
12 |
13 | This is another paragraph.
14 |
15 | This is another paragraph.
--------------------------------------------------------------------------------
/blog/articles/second-blog.md:
--------------------------------------------------------------------------------
1 | ---
2 | Title: The Second Blog Article
3 | Author: Jian Li
4 | Date: 2022/11/25 11:20:00
5 | ---
6 |
7 | This is the second blog article.
8 |
9 | ### The Header
10 |
11 | This is another paragraph.
12 |
13 | This is another paragraph.
14 |
15 | This is another paragraph.
--------------------------------------------------------------------------------
/blog/handler.go:
--------------------------------------------------------------------------------
1 | package blog
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/labstack/echo/v4"
7 | )
8 |
9 | func NewHandler(service *Service) *Handler {
10 | return &Handler{
11 | service: service,
12 | }
13 | }
14 |
15 | type Handler struct {
16 | service *Service
17 | }
18 |
19 | func (h *Handler) InstallRoutes(router *echo.Echo) {
20 | g := router.Group("/blog")
21 |
22 | g.GET("/", h.list)
23 | g.GET("/:path", h.get)
24 | g.GET("/feed", h.feed)
25 | }
26 |
27 | func (h *Handler) list(c echo.Context) error {
28 | blogs, err := h.service.list()
29 | if err != nil {
30 | return echo.ErrNotFound
31 | }
32 |
33 | return c.Render(http.StatusOK, "blogs.html", echo.Map{
34 | "Blogs": blogs,
35 | "Feed": BLOG_FEED,
36 | })
37 | }
38 |
39 | func (h *Handler) get(c echo.Context) error {
40 | path := c.Param("path") + ".md"
41 | blog, err := h.service.get(path)
42 | if err != nil {
43 | return echo.ErrNotFound
44 | }
45 |
46 | return c.Render(http.StatusOK, "blog.html", echo.Map{
47 | "Blog": blog,
48 | "Feed": BLOG_FEED,
49 | })
50 | }
51 |
52 | func (h *Handler) feed(c echo.Context) error {
53 | feed, err := h.service.feed()
54 | if err != nil {
55 | return echo.ErrNotFound
56 | }
57 |
58 | c.Response().Header().Set(echo.HeaderContentType, "application/atom+xml")
59 | return c.String(http.StatusOK, feed)
60 | }
61 |
--------------------------------------------------------------------------------
/blog/service.go:
--------------------------------------------------------------------------------
1 | package blog
2 |
3 | import (
4 | "bytes"
5 | "embed"
6 | "fmt"
7 | "io/fs"
8 | "sort"
9 | "strings"
10 | "time"
11 |
12 | "github.com/gorilla/feeds"
13 | "github.com/kidlj/blog/templates"
14 | "github.com/yuin/goldmark"
15 | meta "github.com/yuin/goldmark-meta"
16 | "github.com/yuin/goldmark/parser"
17 | )
18 |
19 | const (
20 | BLOG_LISTEN_ADDR = "127.0.0.1:8080"
21 | BLOG_PREFIX = "http://127.0.0.1:8080/blog/"
22 | BLOG_FEED = "http://127.0.0.1:8080/blog/feed"
23 | )
24 |
25 | //go:embed articles/*
26 | var blogFS embed.FS
27 |
28 | var markdown = goldmark.New(
29 | goldmark.WithExtensions(
30 | meta.Meta,
31 | ),
32 | )
33 |
34 | type Service struct{}
35 |
36 | func NewService() *Service {
37 | return &Service{}
38 | }
39 |
40 | type BlogEntry struct {
41 | Title string
42 | Author string
43 | Date time.Time
44 | Excerpt string
45 | Content string
46 | URL string
47 | Path string
48 | }
49 |
50 | func (s *Service) list() ([]*BlogEntry, error) {
51 | pathName := "articles"
52 | entries, err := fs.ReadDir(blogFS, pathName)
53 | if err != nil {
54 | return nil, err
55 | }
56 | blogs := make([]*BlogEntry, 0, len(entries))
57 |
58 | for _, e := range entries {
59 | if !e.IsDir() && strings.HasSuffix(e.Name(), "md") {
60 | blog, err := s.get(e.Name())
61 | if err != nil {
62 | return nil, err
63 | }
64 | blogs = append(blogs, blog)
65 | }
66 | }
67 |
68 | sort.Slice(blogs, func(i, j int) bool {
69 | return blogs[i].Date.After(blogs[j].Date)
70 | })
71 |
72 | return blogs, nil
73 | }
74 |
75 | func (s *Service) get(path string) (*BlogEntry, error) {
76 | pathName := fmt.Sprintf("articles/%s", path)
77 | f, err := fs.ReadFile(blogFS, pathName)
78 | if err != nil {
79 | return nil, err
80 | }
81 |
82 | var buf bytes.Buffer
83 | context := parser.NewContext()
84 | if err := markdown.Convert([]byte(f), &buf, parser.WithContext(context)); err != nil {
85 | return nil, err
86 | }
87 | metaData := meta.Get(context)
88 | title := metaData["Title"].(string)
89 | author := metaData["Author"].(string)
90 | dateStr := metaData["Date"].(string)
91 | date, err := time.Parse(templates.DateTimeFormat, dateStr)
92 | if err != nil {
93 | return nil, err
94 | }
95 | content := buf.String()
96 | excerpt, _, _ := strings.Cut(content, "\n")
97 | seg, _, _ := strings.Cut(path, ".")
98 | url := fmt.Sprintf("/blog/%s", seg)
99 |
100 | blog := &BlogEntry{
101 | Title: title,
102 | Author: author,
103 | Date: date,
104 | Excerpt: excerpt,
105 | Content: content,
106 | URL: url,
107 | Path: seg,
108 | }
109 |
110 | return blog, nil
111 | }
112 |
113 | func (s *Service) feed() (string, error) {
114 | entries, err := s.list()
115 | if err != nil {
116 | return "", err
117 | }
118 |
119 | now := time.Now()
120 | feed := &feeds.Feed{
121 | Title: "The Blog",
122 | Link: &feeds.Link{Href: BLOG_PREFIX},
123 | Description: "A Go static blog",
124 | Author: &feeds.Author{Name: "Jian Li"},
125 | Created: now,
126 | }
127 | for _, e := range entries {
128 | item := &feeds.Item{
129 | Title: e.Title,
130 | Link: &feeds.Link{Href: fmt.Sprintf("%s%s", BLOG_PREFIX, e.Path)},
131 | Description: e.Excerpt,
132 | Author: &feeds.Author{Name: e.Author},
133 | Created: e.Date,
134 | }
135 | feed.Items = append(feed.Items, item)
136 | }
137 |
138 | return feed.ToAtom()
139 | }
140 |
--------------------------------------------------------------------------------
/cmd/blog/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/kidlj/blog/blog"
5 | "github.com/kidlj/blog/dist"
6 | "github.com/kidlj/blog/index"
7 | "github.com/kidlj/blog/templates"
8 |
9 | "github.com/labstack/echo/v4"
10 | "github.com/labstack/echo/v4/middleware"
11 | )
12 |
13 | func main() {
14 | e := echo.New()
15 | e.Use(middleware.Recover())
16 |
17 | t := templates.NewTemplate()
18 | e.Renderer = t
19 |
20 | indexHandler := index.NewHandler()
21 | indexHandler.InstallRoutes(e)
22 |
23 | staticHandler := dist.NewHandler()
24 | staticHandler.InstallRoutes(e)
25 |
26 | blogService := blog.NewService()
27 | blogHandler := blog.NewHandler(blogService)
28 | blogHandler.InstallRoutes(e)
29 |
30 | err := e.Start(blog.BLOG_LISTEN_ADDR)
31 | if err != nil {
32 | panic(err)
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/dist/handler.go:
--------------------------------------------------------------------------------
1 | package dist
2 |
3 | import (
4 | "embed"
5 | "net/http"
6 |
7 | "github.com/labstack/echo/v4"
8 | )
9 |
10 | //go:embed static/*
11 | var static embed.FS
12 |
13 | func NewHandler() *Handler {
14 | return &Handler{}
15 | }
16 |
17 | type Handler struct {
18 | }
19 |
20 | func (h *Handler) InstallRoutes(router *echo.Echo) {
21 | g := router.Group("/static")
22 | contentHandler := echo.WrapHandler(http.FileServer(http.FS(static)))
23 | g.GET("/*", contentHandler)
24 | }
25 |
--------------------------------------------------------------------------------
/dist/static/bin/style.css:
--------------------------------------------------------------------------------
1 | body {
2 | font-family: ui-serif, Georgia, Times, 'Times New Roman', serif;
3 | font-size: 16px;
4 | }
5 |
6 | .container {
7 | max-width: 1000px;
8 | margin: 0 auto;
9 | padding: 2em;
10 |
11 | }
12 |
13 | .container a {
14 | text-decoration: none;
15 | color: black;
16 | }
17 |
18 | main {
19 | padding: 4em;
20 | display: flex;
21 | flex-direction: column;
22 | align-items: center;
23 | justify-content: center;
24 | row-gap: 3em;
25 | }
26 |
27 |
28 | article {
29 | display: flex;
30 | flex-direction: column;
31 | padding: 4em 8em;
32 | }
33 |
34 | article .content {
35 | display: flex;
36 | flex-direction: column;
37 |
38 | }
39 |
40 | article .header {
41 | margin-bottom: 2em;
42 | }
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/kidlj/blog
2 |
3 | go 1.19
4 |
5 | require (
6 | github.com/gorilla/feeds v1.1.1
7 | github.com/labstack/echo/v4 v4.9.1
8 | github.com/yuin/goldmark v1.5.2
9 | github.com/yuin/goldmark-meta v1.1.0
10 | )
11 |
12 | require (
13 | github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
14 | github.com/kr/pretty v0.3.1 // indirect
15 | github.com/labstack/gommon v0.4.0 // indirect
16 | github.com/mattn/go-colorable v0.1.11 // indirect
17 | github.com/mattn/go-isatty v0.0.14 // indirect
18 | github.com/valyala/bytebufferpool v1.0.0 // indirect
19 | github.com/valyala/fasttemplate v1.2.1 // indirect
20 | golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 // indirect
21 | golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f // indirect
22 | golang.org/x/sys v0.0.0-20211103235746-7861aae1554b // indirect
23 | golang.org/x/text v0.3.7 // indirect
24 | golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 // indirect
25 | gopkg.in/yaml.v2 v2.3.0 // indirect
26 | )
27 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
5 | github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
6 | github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
7 | github.com/gorilla/feeds v1.1.1 h1:HwKXxqzcRNg9to+BbvJog4+f3s/xzvtZXICcQGutYfY=
8 | github.com/gorilla/feeds v1.1.1/go.mod h1:Nk0jZrvPFZX1OBe5NPiddPw7CfwF6Q9eqzaBbaightA=
9 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
10 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
11 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
12 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
13 | github.com/labstack/echo/v4 v4.9.1 h1:GliPYSpzGKlyOhqIbG8nmHBo3i1saKWFOgh41AN3b+Y=
14 | github.com/labstack/echo/v4 v4.9.1/go.mod h1:Pop5HLc+xoc4qhTZ1ip6C0RtP7Z+4VzRLWZZFKqbbjo=
15 | github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8=
16 | github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM=
17 | github.com/mattn/go-colorable v0.1.11 h1:nQ+aFkoE2TMGc0b68U2OKSexC+eq46+XwZzWXHRmPYs=
18 | github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
19 | github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
20 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
21 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
22 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
23 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
24 | github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
25 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
26 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
27 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
28 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
29 | github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
30 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
31 | github.com/valyala/fasttemplate v1.2.1 h1:TVEnxayobAdVkhQfrfes2IzOB6o+z4roRkPF52WA1u4=
32 | github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
33 | github.com/yuin/goldmark v1.5.2 h1:ALmeCk/px5FSm1MAcFBAsVKZjDuMVj8Tm7FFIlMJnqU=
34 | github.com/yuin/goldmark v1.5.2/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
35 | github.com/yuin/goldmark-meta v1.1.0 h1:pWw+JLHGZe8Rk0EGsMVssiNb/AaPMHfSRszZeUeiOUc=
36 | github.com/yuin/goldmark-meta v1.1.0/go.mod h1:U4spWENafuA7Zyg+Lj5RqK/MF+ovMYtBvXi1lBb2VP0=
37 | golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ=
38 | golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
39 | golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f h1:OfiFi4JbukWwe3lzw+xunroH1mnC1e2Gy5cxNJApiSY=
40 | golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
41 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
42 | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
43 | golang.org/x/sys v0.0.0-20211103235746-7861aae1554b h1:1VkfZQv42XQlA/jchYumAnv1UPo6RgF9rJFkTgZIxO4=
44 | golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
45 | golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
46 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
47 | golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 h1:Hir2P/De0WpUhtrKGGjvSb2YxUgyZ7EFOSLIcSSpiwE=
48 | golang.org/x/time v0.0.0-20201208040808-7e3f01d25324/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
49 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
50 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
51 | gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
52 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
53 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
54 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
55 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
56 |
--------------------------------------------------------------------------------
/index/handler.go:
--------------------------------------------------------------------------------
1 | package index
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/labstack/echo/v4"
7 | )
8 |
9 | func NewHandler() *Handler {
10 | return &Handler{}
11 | }
12 |
13 | type Handler struct {
14 | }
15 |
16 | func (h *Handler) InstallRoutes(router *echo.Echo) {
17 | router.GET("/", h.index)
18 | }
19 |
20 | func (h *Handler) index(c echo.Context) error {
21 | return c.Render(http.StatusOK, "index.html", nil)
22 | }
23 |
--------------------------------------------------------------------------------
/templates/funcs.go:
--------------------------------------------------------------------------------
1 | package templates
2 |
3 | import (
4 | "html/template"
5 | "time"
6 | )
7 |
8 | const DateTimeFormat = "2006/01/02 15:04:05"
9 |
10 | func dateTime(t time.Time) string {
11 | return t.Format(DateTimeFormat)
12 | }
13 |
14 | func noEscape(str string) template.HTML {
15 | return template.HTML(str)
16 | }
17 |
--------------------------------------------------------------------------------
/templates/template.go:
--------------------------------------------------------------------------------
1 | package templates
2 |
3 | import (
4 | "embed"
5 | "fmt"
6 | "html/template"
7 | "io"
8 |
9 | "github.com/labstack/echo/v4"
10 | )
11 |
12 | //go:embed views/*.html
13 | var templateFS embed.FS
14 |
15 | type Template struct {
16 | templates *template.Template
17 | }
18 |
19 | func NewTemplate() *Template {
20 | templates := template.Must(template.New("").Funcs(funcMap()).ParseFS(templateFS, "views/*.html"))
21 | return &Template{
22 | templates: templates,
23 | }
24 | }
25 |
26 | func (t *Template) Render(w io.Writer, name string, data interface{}, c echo.Context) error {
27 | tmpl := template.Must(t.templates.Clone())
28 | tmpl = template.Must(tmpl.ParseFS(templateFS, fmt.Sprintf("views/%s", name)))
29 | return tmpl.ExecuteTemplate(w, name, data)
30 | }
31 |
32 | func funcMap() template.FuncMap {
33 | return template.FuncMap{
34 | "dateTime": dateTime,
35 | "noEscape": noEscape,
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/templates/views/blog.html:
--------------------------------------------------------------------------------
1 | {{template "layout.html" .}}
2 |
3 | {{define "title"}}{{.Blog.Title}}{{end}}
4 |
5 | {{ define "content" }}
6 | Nothing here.
10 | Go to Blog →
11 | {{.Title}}
11 | {{.Date | dateTime}}
12 | Blog
9 | {{range .Blogs}}
10 | {{.Title}}
13 |
14 |
17 | {{.Excerpt | noEscape}}
18 | Home
9 |