main.go 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  1. // posts serves a Markdown blog: each .md file in CONTENT_DIR is one post.
  2. package main
  3. import (
  4. "bytes"
  5. "html/template"
  6. "io/fs"
  7. "log"
  8. "net/http"
  9. "net/url"
  10. "os"
  11. "path/filepath"
  12. "strings"
  13. "time"
  14. )
  15. type app struct {
  16. store *Store
  17. config *configFile
  18. pages map[string]*template.Template
  19. about template.HTML
  20. siteURL string // absolute base URL, no trailing slash
  21. siteName string
  22. commit string
  23. }
  24. func newApp(contentDir, siteURL string) (*app, error) {
  25. pages, err := loadTemplates()
  26. if err != nil {
  27. return nil, err
  28. }
  29. siteURL = strings.TrimRight(siteURL, "/")
  30. u, err := url.Parse(siteURL)
  31. if err != nil {
  32. return nil, err
  33. }
  34. md := newMarkdown()
  35. var about bytes.Buffer
  36. if err := md.Convert(aboutMarkdown, &about); err != nil {
  37. return nil, err
  38. }
  39. return &app{
  40. store: NewStore(contentDir, md),
  41. config: newConfigFile(filepath.Join(contentDir, "site.json")),
  42. pages: pages,
  43. about: template.HTML(about.String()),
  44. siteURL: siteURL,
  45. siteName: u.Host,
  46. commit: buildCommit(),
  47. }, nil
  48. }
  49. func (a *app) routes() http.Handler {
  50. static, err := fs.Sub(staticFS, "static")
  51. if err != nil {
  52. panic(err)
  53. }
  54. files := http.StripPrefix("/static/", http.FileServerFS(static))
  55. mux := http.NewServeMux()
  56. mux.HandleFunc("GET /{$}", a.index)
  57. mux.HandleFunc("GET /posts/{slug}", a.post)
  58. mux.HandleFunc("GET /about", a.aboutPage)
  59. mux.HandleFunc("GET /feed.xml", a.feed)
  60. mux.HandleFunc("GET /static/", func(w http.ResponseWriter, r *http.Request) {
  61. if strings.HasSuffix(r.URL.Path, "/") { // no directory listings
  62. a.notFound(w, r)
  63. return
  64. }
  65. w.Header().Set("Cache-Control", "public, max-age=3600")
  66. files.ServeHTTP(w, r)
  67. })
  68. mux.HandleFunc("/", a.notFound)
  69. return securityHeaders(mux)
  70. }
  71. func (a *app) index(w http.ResponseWriter, r *http.Request) {
  72. idx, err := a.store.Index()
  73. if err != nil {
  74. a.serverError(w, err)
  75. return
  76. }
  77. a.render(w, http.StatusOK, "index", pageData{Posts: idx.posts})
  78. }
  79. func (a *app) post(w http.ResponseWriter, r *http.Request) {
  80. idx, err := a.store.Index()
  81. if err != nil {
  82. a.serverError(w, err)
  83. return
  84. }
  85. i, ok := idx.bySlug[r.PathValue("slug")]
  86. if !ok {
  87. a.notFound(w, r)
  88. return
  89. }
  90. data := pageData{Post: idx.posts[i]}
  91. if i > 0 {
  92. data.Newer = idx.posts[i-1]
  93. }
  94. if i+1 < len(idx.posts) {
  95. data.Older = idx.posts[i+1]
  96. }
  97. a.render(w, http.StatusOK, "post", data)
  98. }
  99. func (a *app) aboutPage(w http.ResponseWriter, r *http.Request) {
  100. idx, err := a.store.Index()
  101. if err != nil {
  102. a.serverError(w, err)
  103. return
  104. }
  105. a.render(w, http.StatusOK, "about", pageData{
  106. Posts: idx.posts,
  107. Version: version,
  108. Commit: a.commit,
  109. About: a.about,
  110. })
  111. }
  112. func (a *app) notFound(w http.ResponseWriter, r *http.Request) {
  113. a.render(w, http.StatusNotFound, "error", pageData{Status: 404, Message: "That page doesn't exist."})
  114. }
  115. func (a *app) serverError(w http.ResponseWriter, err error) {
  116. log.Printf("error: %v", err)
  117. a.render(w, http.StatusInternalServerError, "error", pageData{Status: 500, Message: "Something went wrong."})
  118. }
  119. // securityHeaders sets a strict policy: posts never contain scripts, and
  120. // images may come from anywhere over HTTPS.
  121. func securityHeaders(next http.Handler) http.Handler {
  122. return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  123. h := w.Header()
  124. h.Set("Content-Security-Policy", "default-src 'self'; img-src 'self' https: data:; script-src 'none'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'")
  125. h.Set("X-Content-Type-Options", "nosniff")
  126. h.Set("Referrer-Policy", "strict-origin-when-cross-origin")
  127. next.ServeHTTP(w, r)
  128. })
  129. }
  130. func getenv(key, def string) string {
  131. if v := os.Getenv(key); v != "" {
  132. return v
  133. }
  134. return def
  135. }
  136. func main() {
  137. port := os.Getenv("PORT")
  138. if port == "" {
  139. log.Fatal("PORT not set")
  140. }
  141. a, err := newApp(getenv("CONTENT_DIR", "content"), getenv("SITE_URL", "https://posts.monoaf.com"))
  142. if err != nil {
  143. log.Fatal(err)
  144. }
  145. srv := &http.Server{
  146. Addr: "127.0.0.1:" + port,
  147. Handler: a.routes(),
  148. ReadHeaderTimeout: 5 * time.Second,
  149. ReadTimeout: 10 * time.Second,
  150. WriteTimeout: 30 * time.Second,
  151. IdleTimeout: 2 * time.Minute,
  152. }
  153. log.Printf("listening on %s", srv.Addr)
  154. log.Fatal(srv.ListenAndServe())
  155. }