1
1

main.go 3.5 KB

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