aboutsummaryrefslogtreecommitdiff
path: root/cmd/site
diff options
context:
space:
mode:
authorChristine Dodrill <me@christine.website>2018-07-01 13:20:01 -0700
committerChristine Dodrill <me@christine.website>2018-07-01 13:20:01 -0700
commit599712fab9127013e2d89dadabd839c847730637 (patch)
tree2a69843e6a5fbdb69cf4c4600e5a8693a3c4a708 /cmd/site
parent7d8c210f1499bce3558f107402f2c7ccf8417e7d (diff)
downloadxesite-599712fab9127013e2d89dadabd839c847730637.tar.xz
xesite-599712fab9127013e2d89dadabd839c847730637.zip
rip out mage
Diffstat (limited to 'cmd/site')
-rw-r--r--cmd/site/gopreload.go9
-rw-r--r--cmd/site/gops.go13
-rw-r--r--cmd/site/hash.go14
-rw-r--r--cmd/site/html.go73
-rw-r--r--cmd/site/main.go205
-rw-r--r--cmd/site/rss.go64
6 files changed, 378 insertions, 0 deletions
diff --git a/cmd/site/gopreload.go b/cmd/site/gopreload.go
new file mode 100644
index 0000000..6829ae5
--- /dev/null
+++ b/cmd/site/gopreload.go
@@ -0,0 +1,9 @@
+// gopreload.go
+package main
+
+/*
+ This file is separate to make it very easy to both add into an application, but
+ also very easy to remove.
+*/
+
+import _ "github.com/Xe/gopreload"
diff --git a/cmd/site/gops.go b/cmd/site/gops.go
new file mode 100644
index 0000000..184b656
--- /dev/null
+++ b/cmd/site/gops.go
@@ -0,0 +1,13 @@
+package main
+
+import (
+ "log"
+
+ "github.com/google/gops/agent"
+)
+
+func init() {
+ if err := agent.Listen(nil); err != nil {
+ log.Fatal(err)
+ }
+}
diff --git a/cmd/site/hash.go b/cmd/site/hash.go
new file mode 100644
index 0000000..ed6112c
--- /dev/null
+++ b/cmd/site/hash.go
@@ -0,0 +1,14 @@
+package main
+
+import (
+ "crypto/md5"
+ "fmt"
+)
+
+// Hash is a simple wrapper around the MD5 algorithm implementation in the
+// Go standard library. It takes in data and a salt and returns the hashed
+// representation.
+func Hash(data string, salt string) string {
+ output := md5.Sum([]byte(data + salt))
+ return fmt.Sprintf("%x", output)
+}
diff --git a/cmd/site/html.go b/cmd/site/html.go
new file mode 100644
index 0000000..ba304c5
--- /dev/null
+++ b/cmd/site/html.go
@@ -0,0 +1,73 @@
+package main
+
+import (
+ "context"
+ "fmt"
+ "html/template"
+ "net/http"
+ "time"
+
+ "github.com/Xe/ln"
+)
+
+func logTemplateTime(name string, from time.Time) {
+ now := time.Now()
+ ln.Log(context.Background(), ln.F{"action": "template_rendered", "dur": now.Sub(from).String(), "name": name})
+}
+
+func (s *Site) renderTemplatePage(templateFname string, data interface{}) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ defer logTemplateTime(templateFname, time.Now())
+ s.tlock.RLock()
+ defer s.tlock.RUnlock()
+
+ var t *template.Template
+ var err error
+
+ if s.templates[templateFname] == nil {
+ t, err = template.ParseFiles("templates/base.html", "templates/"+templateFname)
+ if err != nil {
+ w.WriteHeader(http.StatusInternalServerError)
+ ln.Error(context.Background(), err, ln.F{"action": "renderTemplatePage", "page": templateFname})
+ fmt.Fprintf(w, "error: %v", err)
+ }
+
+ ln.Log(context.Background(), ln.F{"action": "loaded_new_template", "fname": templateFname})
+
+ s.tlock.RUnlock()
+ s.tlock.Lock()
+ s.templates[templateFname] = t
+ s.tlock.Unlock()
+ s.tlock.RLock()
+ } else {
+ t = s.templates[templateFname]
+ }
+
+ err = t.Execute(w, data)
+ if err != nil {
+ panic(err)
+ }
+ })
+}
+
+func (s *Site) showPost(w http.ResponseWriter, r *http.Request) {
+ if r.RequestURI == "/blog/" {
+ http.Redirect(w, r, "/blog", http.StatusSeeOther)
+ return
+ }
+
+ var p *Post
+ for _, pst := range s.Posts {
+ if pst.Link == r.RequestURI[1:] {
+ p = pst
+ }
+ }
+
+ if p == nil {
+ w.WriteHeader(http.StatusNotFound)
+ s.renderTemplatePage("error.html", "no such post found: "+r.RequestURI).ServeHTTP(w, r)
+ return
+ }
+
+ s.renderTemplatePage("blogpost.html", p).ServeHTTP(w, r)
+}
diff --git a/cmd/site/main.go b/cmd/site/main.go
new file mode 100644
index 0000000..e0cc20d
--- /dev/null
+++ b/cmd/site/main.go
@@ -0,0 +1,205 @@
+package main
+
+import (
+ "context"
+ "html/template"
+ "io/ioutil"
+ "net/http"
+ "os"
+ "path/filepath"
+ "sort"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/GeertJohan/go.rice"
+ "github.com/Xe/jsonfeed"
+ "github.com/Xe/ln"
+ "github.com/gorilla/feeds"
+ blackfriday "github.com/russross/blackfriday"
+ "github.com/tj/front"
+)
+
+var port = os.Getenv("PORT")
+
+func main() {
+ if port == "" {
+ port = "29384"
+ }
+
+ s, err := Build()
+ if err != nil {
+ ln.FatalErr(context.Background(), err, ln.Action("Build"))
+ }
+
+ ln.Log(context.Background(), ln.F{"action": "http_listening", "port": port})
+ http.ListenAndServe(":"+port, s)
+}
+
+// Site is the parent object for https://christine.website's backend.
+type Site struct {
+ Posts Posts
+ Resume template.HTML
+
+ rssFeed *feeds.Feed
+ jsonFeed *jsonfeed.Feed
+
+ mux *http.ServeMux
+
+ templates map[string]*template.Template
+ tlock sync.RWMutex
+}
+
+func (s *Site) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ ln.Log(r.Context(), ln.F{"action": "Site.ServeHTTP", "user_ip_address": r.RemoteAddr, "path": r.RequestURI})
+ s.mux.ServeHTTP(w, r)
+}
+
+// Build creates a new Site instance or fails.
+func Build() (*Site, error) {
+ type postFM struct {
+ Title string
+ Date string
+ }
+
+ s := &Site{
+ rssFeed: &feeds.Feed{
+ Title: "Christine Dodrill's Blog",
+ Link: &feeds.Link{Href: "https://christine.website/blog"},
+ Description: "My blog posts and rants about various technology things.",
+ Author: &feeds.Author{Name: "Christine Dodrill", Email: "me@christine.website"},
+ Created: bootTime,
+ Copyright: "This work is copyright Christine Dodrill. My viewpoints are my own and not the view of any employer past, current or future.",
+ },
+ jsonFeed: &jsonfeed.Feed{
+ Version: jsonfeed.CurrentVersion,
+ Title: "Christine Dodrill's Blog",
+ HomePageURL: "https://christine.website",
+ FeedURL: "https://christine.website/blog.json",
+ Description: "My blog posts and rants about various technology things.",
+ UserComment: "This is a JSON feed of my blogposts. For more information read: https://jsonfeed.org/version/1",
+ Icon: icon,
+ Favicon: icon,
+ Author: jsonfeed.Author{
+ Name: "Christine Dodrill",
+ Avatar: icon,
+ },
+ },
+ mux: http.NewServeMux(),
+ templates: map[string]*template.Template{},
+ }
+
+ err := filepath.Walk("./blog/", func(path string, info os.FileInfo, err error) error {
+ if err != nil {
+ return err
+ }
+
+ if info.IsDir() {
+ return nil
+ }
+
+ fin, err := os.Open(path)
+ if err != nil {
+ return err
+ }
+ defer fin.Close()
+
+ content, err := ioutil.ReadAll(fin)
+ if err != nil {
+ return err
+ }
+
+ var fm postFM
+ remaining, err := front.Unmarshal(content, &fm)
+ if err != nil {
+ return err
+ }
+
+ output := blackfriday.Run(remaining)
+
+ p := &Post{
+ Title: fm.Title,
+ Date: fm.Date,
+ Link: strings.Split(path, ".")[0],
+ Body: string(remaining),
+ BodyHTML: template.HTML(output),
+ }
+
+ s.Posts = append(s.Posts, p)
+
+ return nil
+ })
+ if err != nil {
+ return nil, err
+ }
+
+ sort.Sort(sort.Reverse(s.Posts))
+
+ cb, err := rice.FindBox("css")
+ if err != nil {
+ return nil, err
+ }
+
+ sb, err := rice.FindBox("static")
+ if err != nil {
+ return nil, err
+ }
+
+ s.Resume = template.HTML(blackfriday.Run(sb.MustBytes("resume/resume.md")))
+
+ for _, item := range s.Posts {
+ itime, _ := time.Parse("2006-01-02", item.Date)
+ s.rssFeed.Items = append(s.rssFeed.Items, &feeds.Item{
+ Title: item.Title,
+ Link: &feeds.Link{Href: "https://christine.website/" + item.Link},
+ Description: item.Summary,
+ Created: itime,
+ })
+
+ s.jsonFeed.Items = append(s.jsonFeed.Items, jsonfeed.Item{
+ ID: "https://christine.website/" + item.Link,
+ URL: "https://christine.website/" + item.Link,
+ Title: item.Title,
+ DatePublished: itime,
+ ContentHTML: string(item.BodyHTML),
+ })
+ }
+
+ // Add HTTP routes here
+ s.mux.Handle("/", s.renderTemplatePage("index.html", nil))
+ s.mux.Handle("/resume", s.renderTemplatePage("resume.html", s.Resume))
+ s.mux.Handle("/blog", s.renderTemplatePage("blogindex.html", s.Posts))
+ s.mux.Handle("/contact", s.renderTemplatePage("contact.html", nil))
+ s.mux.HandleFunc("/blog.rss", s.createFeed)
+ s.mux.HandleFunc("/blog.atom", s.createAtom)
+ s.mux.HandleFunc("/blog.json", s.createJsonFeed)
+ s.mux.HandleFunc("/blog/", s.showPost)
+ s.mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(sb.HTTPBox())))
+ s.mux.Handle("/css/", http.StripPrefix("/css/", http.FileServer(cb.HTTPBox())))
+
+ return s, nil
+}
+
+const icon = "https://christine.website/static/img/avatar.png"
+
+// Post is a single blogpost.
+type Post struct {
+ Title string `json:"title"`
+ Link string `json:"link"`
+ Summary string `json:"summary,omitifempty"`
+ Body string `json:"-"`
+ BodyHTML template.HTML `json:"body"`
+ Date string `json:"date"`
+}
+
+// Posts implements sort.Interface for a slice of Post objects.
+type Posts []*Post
+
+func (p Posts) Len() int { return len(p) }
+func (p Posts) Less(i, j int) bool {
+ iDate, _ := time.Parse("2006-01-02", p[i].Date)
+ jDate, _ := time.Parse("2006-01-02", p[j].Date)
+
+ return iDate.Unix() < jDate.Unix()
+}
+func (p Posts) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
diff --git a/cmd/site/rss.go b/cmd/site/rss.go
new file mode 100644
index 0000000..15e9163
--- /dev/null
+++ b/cmd/site/rss.go
@@ -0,0 +1,64 @@
+package main
+
+import (
+ "encoding/json"
+ "net/http"
+ "time"
+
+ "github.com/Xe/ln"
+)
+
+var bootTime = time.Now()
+
+// IncrediblySecureSalt *******
+const IncrediblySecureSalt = "hunter2"
+
+func (s *Site) createFeed(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "application/rss+xml")
+ w.Header().Set("ETag", Hash(bootTime.String(), IncrediblySecureSalt))
+
+ err := s.rssFeed.WriteRss(w)
+ if err != nil {
+ http.Error(w, "Internal server error", http.StatusInternalServerError)
+ ln.Error(r.Context(), err, ln.F{
+ "remote_addr": r.RemoteAddr,
+ "action": "generating_rss",
+ "uri": r.RequestURI,
+ "host": r.Host,
+ })
+ }
+}
+
+func (s *Site) createAtom(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "application/atom+xml")
+ w.Header().Set("ETag", Hash(bootTime.String(), IncrediblySecureSalt))
+
+ err := s.rssFeed.WriteAtom(w)
+ if err != nil {
+ http.Error(w, "Internal server error", http.StatusInternalServerError)
+ ln.Error(r.Context(), err, ln.F{
+ "remote_addr": r.RemoteAddr,
+ "action": "generating_atom",
+ "uri": r.RequestURI,
+ "host": r.Host,
+ })
+ }
+}
+
+func (s *Site) createJsonFeed(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ w.Header().Set("ETag", Hash(bootTime.String(), IncrediblySecureSalt))
+
+ e := json.NewEncoder(w)
+ e.SetIndent("", "\t")
+ err := e.Encode(s.jsonFeed)
+ if err != nil {
+ http.Error(w, "Internal server error", http.StatusInternalServerError)
+ ln.Error(r.Context(), err, ln.F{
+ "remote_addr": r.RemoteAddr,
+ "action": "generating_jsonfeed",
+ "uri": r.RequestURI,
+ "host": r.Host,
+ })
+ }
+}