├── 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 |
7 | {{with .Blog}} 8 |
9 |
10 |

{{.Title}}

11 | {{.Date | dateTime}} 12 |
13 | 14 | {{.Content | noEscape}} 15 |
16 | {{end}} 17 |
18 | {{end}} -------------------------------------------------------------------------------- /templates/views/blogs.html: -------------------------------------------------------------------------------- 1 | {{template "layout.html" .}} 2 | 3 | {{define "title"}}Blog{{end}} 4 | 5 | {{ define "content" }} 6 |
7 |
8 |

Blog

9 | {{range .Blogs}} 10 |
11 | 12 |

{{.Title}}

13 |
14 | 17 | {{.Excerpt | noEscape}} 18 |
19 | {{else}} 20 |

Nothing here.

21 | {{end}} 22 |
23 |
24 | {{end}} -------------------------------------------------------------------------------- /templates/views/index.html: -------------------------------------------------------------------------------- 1 | {{template "layout.html" .}} 2 | 3 | {{define "title"}}Home{{end}} 4 | 5 | {{ define "content" }} 6 |
7 |
8 |

Home

9 |

10 | Go to Blog → 11 |

12 |
13 |
14 | {{end}} -------------------------------------------------------------------------------- /templates/views/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | {{block "title" .}}{{end}} 9 | 10 | {{if .Feed}} 11 | 12 | {{end}} 13 | 14 | 15 | 16 |
17 | {{block "content" .}}{{end}} 18 |
19 | 20 | 21 | --------------------------------------------------------------------------------