aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cmd/christine.website/hash.go14
-rw-r--r--cmd/christine.website/html.go72
-rw-r--r--cmd/christine.website/main.go191
-rw-r--r--cmd/christine.website/rss.go45
-rw-r--r--static/css/hack.css1
-rw-r--r--static/css/solarized-dark.css1
-rw-r--r--templates/base.html45
-rw-r--r--templates/blogindex.html22
-rw-r--r--templates/blogpost.html11
-rw-r--r--templates/error.html9
-rw-r--r--templates/index.html30
-rw-r--r--templates/resume.html9
12 files changed, 450 insertions, 0 deletions
diff --git a/cmd/christine.website/hash.go b/cmd/christine.website/hash.go
new file mode 100644
index 0000000..ed6112c
--- /dev/null
+++ b/cmd/christine.website/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/christine.website/html.go b/cmd/christine.website/html.go
new file mode 100644
index 0000000..a01e6a5
--- /dev/null
+++ b/cmd/christine.website/html.go
@@ -0,0 +1,72 @@
+package main
+
+import (
+ "fmt"
+ "html/template"
+ "net/http"
+ "time"
+
+ "github.com/Xe/ln"
+)
+
+func logTemplateTime(name string, from time.Time) {
+ now := time.Now()
+ ln.Log(ln.F{"action": "template_rendered", "dur": now.Sub(from).String()})
+}
+
+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(err, ln.F{"action": "renderTemplatePage", "page": templateFname})
+ fmt.Fprintf(w, "error: %v", err)
+ }
+
+ ln.Log(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/christine.website/main.go b/cmd/christine.website/main.go
new file mode 100644
index 0000000..cffebd1
--- /dev/null
+++ b/cmd/christine.website/main.go
@@ -0,0 +1,191 @@
+package main
+
+import (
+ "html/template"
+ "io/ioutil"
+ "net/http"
+ "os"
+ "path/filepath"
+ "sort"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/Xe/jsonfeed"
+ "github.com/Xe/ln"
+ "github.com/gorilla/feeds"
+ "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.Fatal(ln.F{"err": err, "action": "Build"})
+ }
+
+ 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(ln.F{"action": "Site.ServeHTTP"})
+ 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.MarkdownCommon(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))
+
+ resume, err := ioutil.ReadFile("./static/resume/resume.md")
+ if err != nil {
+ panic(err)
+ }
+
+ s.Resume = template.HTML(blackfriday.MarkdownCommon(resume))
+
+ 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,
+ })
+ }
+
+ // 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.HandleFunc("/blog/", s.showPost)
+ s.mux.Handle("/static/", http.FileServer(http.Dir(".")))
+
+ 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/christine.website/rss.go b/cmd/christine.website/rss.go
new file mode 100644
index 0000000..737bb70
--- /dev/null
+++ b/cmd/christine.website/rss.go
@@ -0,0 +1,45 @@
+package main
+
+import (
+ "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(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(err, ln.F{
+ "remote_addr": r.RemoteAddr,
+ "action": "generating_rss",
+ "uri": r.RequestURI,
+ "host": r.Host,
+ })
+ }
+}
diff --git a/static/css/hack.css b/static/css/hack.css
new file mode 100644
index 0000000..661a838
--- /dev/null
+++ b/static/css/hack.css
@@ -0,0 +1 @@
+html{font-size:12px}*{box-sizing:border-box;text-rendering:geometricPrecision}body{font-size:1rem;line-height:1.5rem;margin:0;font-family:Menlo,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New,monospace,serif;word-wrap:break-word}h1,h2,h3,h4,h5,h6{line-height:1.3em}fieldset{border:none;padding:0;margin:0}pre{padding:2rem;margin:1.75rem 0;background-color:#fff;border:1px solid #ccc;overflow:auto}code[class*=language-],pre[class*=language-],pre code{font-weight:100;text-shadow:none;margin:1.75rem 0}a{cursor:pointer;color:#ff2e88;text-decoration:none;border-bottom:1px solid #ff2e88}a:hover{background-color:#ff2e88;color:#fff}.grid{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap}.grid.\-top{-ms-flex-align:start;align-items:flex-start}.grid.\-middle{-ms-flex-align:center;align-items:center}.grid.\-bottom{-ms-flex-align:end;align-items:flex-end}.grid.\-stretch{-ms-flex-align:stretch;align-items:stretch}.grid.\-baseline{-ms-flex-align:baseline;align-items:baseline}.grid.\-left{-ms-flex-pack:start;justify-content:flex-start}.grid.\-center{-ms-flex-pack:center;justify-content:center}.grid.\-right{-ms-flex-pack:end;justify-content:flex-end}.grid.\-between{-ms-flex-pack:justify;justify-content:space-between}.grid.\-around{-ms-flex-pack:distribute;justify-content:space-around}.cell{-ms-flex:1;flex:1;box-sizing:border-box}@media screen and (min-width:768px){.cell.\-1of12{-ms-flex:0 0 8.33333%;flex:0 0 8.33333%}.cell.\-2of12{-ms-flex:0 0 16.66667%;flex:0 0 16.66667%}.cell.\-3of12{-ms-flex:0 0 25%;flex:0 0 25%}.cell.\-4of12{-ms-flex:0 0 33.33333%;flex:0 0 33.33333%}.cell.\-5of12{-ms-flex:0 0 41.66667%;flex:0 0 41.66667%}.cell.\-6of12{-ms-flex:0 0 50%;flex:0 0 50%}.cell.\-7of12{-ms-flex:0 0 58.33333%;flex:0 0 58.33333%}.cell.\-8of12{-ms-flex:0 0 66.66667%;flex:0 0 66.66667%}.cell.\-9of12{-ms-flex:0 0 75%;flex:0 0 75%}.cell.\-10of12{-ms-flex:0 0 83.33333%;flex:0 0 83.33333%}.cell.\-11of12{-ms-flex:0 0 91.66667%;flex:0 0 91.66667%}}@media screen and (max-width:768px){.grid{-ms-flex-direction:column;flex-direction:column}.cell{-ms-flex:0 0 auto;flex:0 0 auto}}.hack,.hack blockquote,.hack code,.hack em,.hack h1,.hack h2,.hack h3,.hack h4,.hack h5,.hack h6,.hack strong{font-size:1rem;font-style:normal;font-family:Menlo,Monaco,Lucida Console,Liberation Mono,DejaVu Sans Mono,Bitstream Vera Sans Mono,Courier New,monospace,serif}.hack blockquote,.hack code,.hack em,.hack strong{line-height:20px}.hack blockquote,.hack code,.hack footer,.hack h1,.hack h2,.hack h3,.hack h4,.hack h5,.hack h6,.hack header,.hack li,.hack ol,.hack p,.hack section,.hack ul{float:none;margin:0;padding:0}.hack blockquote,.hack h1,.hack ol,.hack p,.hack ul{margin-top:20px;margin-bottom:20px}.hack h1{position:relative;display:inline-block;display:table-cell;padding:20px 0 30px;margin:0;overflow:hidden}.hack h1:after{content:"====================================================================================================";position:absolute;bottom:10px;left:0}.hack h1+*{margin-top:0}.hack h2,.hack h3,.hack h4,.hack h5,.hack h6{position:relative;margin-bottom:1.75rem}.hack h2:before,.hack h3:before,.hack h4:before,.hack h5:before,.hack h6:before{display:inline}.hack h2:before{content:"## "}.hack h3:before{content:"### "}.hack h4:before{content:"#### "}.hack h5:before{content:"##### "}.hack h6:before{content:"###### "}.hack li{position:relative;display:block;padding-left:20px}.hack li:after{position:absolute;top:0;left:0}.hack ul>li:after{content:"-"}.hack ol{counter-reset:a}.hack ol>li:after{content:counter(a) ".";counter-increment:a}.hack blockquote{position:relative;padding-left:17px;padding-left:2ch;overflow:hidden}.hack blockquote:after{content:">\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>\A>";white-space:pre;position:absolute;top:0;left:0;line-height:20px}.hack em:after,.hack em:before{content:"*";display:inline}.hack pre code:after,.hack pre code:before{content:""}.hack code{font-weight:700}.hack code:after,.hack code:before{content:"`";display:inline}.hack hr{position:relative;height:20px;overflow:hidden;border:0;margin:20px 0}.hack hr:after{content:"----------------------------------------------------------------------------------------------------";position:absolute;top:0;left:0;line-height:20px;width:100%;word-wrap:break-word}@-moz-document url-prefix(){.hack h1{display:block}}.hack-ones ol>li:after{content:"1."}p{margin:0 0 1.75rem}.container{max-width:70rem}.container,.container-fluid{margin:0 auto;padding:0 1rem}.inner{padding:1rem}.inner2x{padding:2rem}.pull-left{float:left}.pull-right{float:right}.progress-bar{height:8px;opacity:.8;background-color:#ccc;margin-top:12px}.progress-bar.progress-bar-show-percent{margin-top:38px}.progress-bar-filled{background-color:gray;height:100%;transition:width .3s ease;position:relative;width:0}.progress-bar-filled:before{content:"";border:6px solid transparent;border-top-color:gray;position:absolute;top:-12px;right:-6px}.progress-bar-filled:after{color:gray;content:attr(data-filled);display:block;font-size:12px;white-space:nowrap;position:absolute;border:6px solid transparent;top:-38px;right:0;-ms-transform:translateX(50%);transform:translateX(50%)}table{width:100%;border-collapse:collapse;margin:1.75rem 0;color:#778087}table td,table th{vertical-align:top;border:1px solid #ccc;line-height:15px;padding:10px}table thead th{font-size:10px}table tbody td:first-child{font-weight:700;color:#333}.form{width:30rem}.form-group{margin-bottom:1.75rem;overflow:auto}.form-group label{border-bottom:2px solid #ccc;color:#333;width:10rem;display:inline-block;height:38px;line-height:38px;padding:0;float:left;position:relative}.form-group.form-success label{color:#4caf50!important;border-color:#4caf50!important}.form-group.form-warning label{color:#ff9800!important;border-color:#ff9800!important}.form-group.form-error label{color:#f44336!important;border-color:#f44336!important}.form-control{outline:none;border:none;border-bottom:2px solid #ccc;padding:.5rem 0;width:20rem;height:38px;background-color:transparent}.form-control:focus{border-color:#555}.form-group.form-textarea label:after{position:absolute;content:"";width:2px;background-color:#fff;right:-2px;top:0;bottom:0}textarea.form-control{height:auto;resize:none;padding:1rem 0;border-bottom:2px solid #ccc;border-left:2px solid #ccc;padding:.5rem}select.form-control{border-radius:0;background-color:transparent;-webkit-appearance:none;-moz-appearance:none;-ms-appearance:none}.help-block{color:#999;margin-top:.5rem}.form-actions{margin-bottom:1.75rem}.btn{display:-ms-inline-flexbox;display:inline-flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:center;justify-content:center;cursor:pointer;outline:none;padding:.65rem 2rem;font-size:1rem;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;position:relative;z-index:1}.btn:active{box-shadow:inset 0 1px 3px rgba(0,0,0,.12)}.btn.btn-ghost{border-color:#757575;color:#757575;background-color:transparent}.btn.btn-ghost:focus,.btn.btn-ghost:hover{border-color:#424242;color:#424242;z-index:2}.btn.btn-ghost:hover{background-color:transparent}.btn-block{width:100%;display:-ms-flexbox;display:flex}.btn-default{color:#fff;background-color:#e0e0e0;border:1px solid #e0e0e0;color:#333}.btn-default:focus:not(.btn-ghost),.btn-default:hover{background-color:#dcdcdc;border-color:#dcdcdc}.btn-success{color:#fff;background-color:#4caf50;border:1px solid #4caf50}.btn-success:focus:not(.btn-ghost),.btn-success:hover{background-color:#43a047;border-color:#43a047}.btn-success.btn-ghost{border-color:#4caf50;color:#4caf50}.btn-success.btn-ghost:focus,.btn-success.btn-ghost:hover{border-color:#388e3c;color:#388e3c;z-index:2}.btn-error{color:#fff;background-color:#f44336;border:1px solid #f44336}.btn-error:focus:not(.btn-ghost),.btn-error:hover{background-color:#e53935;border-color:#e53935}.btn-error.btn-ghost{border-color:#f44336;color:#f44336}.btn-error.btn-ghost:focus,.btn-error.btn-ghost:hover{border-color:#d32f2f;color:#d32f2f;z-index:2}.btn-warning{color:#fff;background-color:#ff9800;border:1px solid #ff9800}.btn-warning:focus:not(.btn-ghost),.btn-warning:hover{background-color:#fb8c00;border-color:#fb8c00}.btn-warning.btn-ghost{border-color:#ff9800;color:#ff9800}.btn-warning.btn-ghost:focus,.btn-warning.btn-ghost:hover{border-color:#f57c00;color:#f57c00;z-index:2}.btn-info{color:#fff;background-color:#00bcd4;border:1px solid #00bcd4}.btn-info:focus:not(.btn-ghost),.btn-info:hover{background-color:#00acc1;border-color:#00acc1}.btn-info.btn-ghost{border-color:#00bcd4;color:#00bcd4}.btn-info.btn-ghost:focus,.btn-info.btn-ghost:hover{border-color:#0097a7;color:#0097a7;z-index:2}.btn-primary{color:#fff;background-color:#2196f3;border:1px solid #2196f3}.btn-primary:focus:not(.btn-ghost),.btn-primary:hover{background-color:#1e88e5;border-color:#1e88e5}.btn-primary.btn-ghost{border-color:#2196f3;color:#2196f3}.btn-primary.btn-ghost:focus,.btn-primary.btn-ghost:hover{border-color:#1976d2;color:#1976d2;z-index:2}.btn-group{overflow:auto}.btn-group .btn{float:left}.btn-group .btn-ghost:not(:first-child){margin-left:-1px}.card{border:1px solid #ccc}.card .card-header{color:#333;text-align:center;background-color:#ddd;padding:.5rem 0}.alert{color:#ccc;padding:1rem;border:1px solid #ccc;margin-bottom:1.75rem}.alert-success{color:#4caf50;border-color:#4caf50}.alert-error{color:#f44336;border-color:#f44336}.alert-info{color:#00bcd4;border-color:#00bcd4}.alert-warning{color:#ff9800;border-color:#ff9800}.media:not(:last-child){margin-bottom:1.25rem}.media-left{padding-right:1rem}.media-left,.media-right{display:table-cell;vertical-align:top}.media-right{padding-left:1rem}.media-body{display:table-cell;vertical-align:top}.media-heading{font-size:1.16667rem;font-weight:700}.media-content{margin-top:.3rem}.avatarholder,.placeholder{background-color:#f0f0f0;text-align:center;color:#b9b9b9;font-size:1rem;border:1px solid #f0f0f0}.avatarholder{width:48px;height:48px;line-height:46px;font-size:2rem;background-size:cover;background-position:50%;background-repeat:no-repeat}.avatarholder.rounded{border-radius:33px}.loading{display:inline-block;content:"&nbsp;";height:20px;width:20px;margin:0 .5rem;animation:a .6s infinite linear;border:2px solid #e91e63;border-right-color:transparent;border-radius:50%}.btn .loading{margin-bottom:0;width:14px;height:14px}.btn div.loading{float:left}.alert .loading{margin-bottom:-5px}@keyframes a{0%{transform:rotate(0deg)}to{transform:rotate(1turn)}}.menu{width:100%}.menu .menu-item{display:block;color:#616161;border-color:#616161}.menu .menu-item.active,.menu .menu-item:hover{color:#000;border-color:#000;background-color:transparent}@media screen and (max-width:768px){.form-group label{display:block;border-bottom:none;width:100%}.form-group.form-textarea label:after{display:none}.form-control{width:100%}textarea.form-control{border-left:none;padding:.5rem 0}pre::-webkit-scrollbar{height:3px}}@media screen and (max-width:480px){.form{width:100%}} \ No newline at end of file
diff --git a/static/css/solarized-dark.css b/static/css/solarized-dark.css
new file mode 100644
index 0000000..4c29736
--- /dev/null
+++ b/static/css/solarized-dark.css
@@ -0,0 +1 @@
+.solarized-dark{background-color:#073642;color:#78909c}.solarized-dark h1,.solarized-dark h2,.solarized-dark h3,.solarized-dark h4,.solarized-dark h5,.solarized-dark h6{color:#1e88e5}.solarized-dark h1 a,.solarized-dark h2 a,.solarized-dark h3 a,.solarized-dark h4 a,.solarized-dark h5 a,.solarized-dark h6 a{color:#1e88e5;border-bottom-color:#1e88e5}.solarized-dark h1 a:hover,.solarized-dark h2 a:hover,.solarized-dark h3 a:hover,.solarized-dark h4 a:hover,.solarized-dark h5 a:hover,.solarized-dark h6 a:hover{background-color:#1e88e5;color:#fff}.solarized-dark pre{background-color:#073642;padding:0;border:none}.solarized-dark pre code{color:#009688}.solarized-dark h1 a,.solarized-dark h2 a,.solarized-dark h3 a,.solarized-dark h4 a,.solarized-dark h5 a{color:#78909c}.solarized-dark code,.solarized-dark strong{color:#90a4ae}.solarized-dark code{font-weight:100}.solarized-dark .progress-bar-filled{background-color:#558b2f}.solarized-dark .progress-bar-filled:after,.solarized-dark .progress-bar-filled:before{color:#90a4ae}.solarized-dark table{color:#78909c}.solarized-dark table td,.solarized-dark table th{border-color:#b0bec5}.solarized-dark table tbody td:first-child{color:#b0bec5}.solarized-dark .form-group label{color:#78909c;border-color:#90a4ae}.solarized-dark .form-group.form-textarea label:after{background-color:#073642}.solarized-dark .form-control{color:#78909c;border-color:#90a4ae}.solarized-dark .form-control:focus{border-color:#cfd8dc;color:#cfd8dc}.solarized-dark textarea.form-control{color:#78909c}.solarized-dark .card{border-color:#90a4ae}.solarized-dark .card .card-header{background-color:transparent;color:#78909c;border-bottom:1px solid #90a4ae}.solarized-dark .btn.btn-ghost.btn-default{border-color:#607d8b;color:#607d8b}.solarized-dark .btn.btn-ghost.btn-default:focus,.solarized-dark .btn.btn-ghost.btn-default:hover{border-color:#90a4ae;color:#90a4ae;z-index:1}.solarized-dark .btn.btn-ghost.btn-default:focus,.solarized-dark .btn.btn-ghost.btn-default:hover{border-color:#e0e0e0;color:#e0e0e0}.solarized-dark .btn.btn-ghost.btn-primary:focus,.solarized-dark .btn.btn-ghost.btn-primary:hover{border-color:#64b5f6;color:#64b5f6}.solarized-dark .btn.btn-ghost.btn-success:focus,.solarized-dark .btn.btn-ghost.btn-success:hover{border-color:#81c784;color:#81c784}.solarized-dark .btn.btn-ghost.btn-info:focus,.solarized-dark .btn.btn-ghost.btn-info:hover{border-color:#4dd0e1;color:#4dd0e1}.solarized-dark .btn.btn-ghost.btn-error:focus,.solarized-dark .btn.btn-ghost.btn-error:hover{border-color:#e57373;color:#e57373}.solarized-dark .btn.btn-ghost.btn-warning:focus,.solarized-dark .btn.btn-ghost.btn-warning:hover{border-color:#ffb74d;color:#ffb74d}.solarized-dark .avatarholder,.solarized-dark .placeholder{background-color:transparent;border-color:#90a4ae}.solarized-dark .menu .menu-item{color:#78909c;border-color:#90a4ae}.solarized-dark .menu .menu-item.active,.solarized-dark .menu .menu-item:hover{color:#fff;border-color:#78909c} \ No newline at end of file
diff --git a/templates/base.html b/templates/base.html
new file mode 100644
index 0000000..4099bcd
--- /dev/null
+++ b/templates/base.html
@@ -0,0 +1,45 @@
+<html>
+ <head>
+ {{ template "title" . }}
+ <link rel="stylesheet" href="/static/css/hack.css" />
+ <link rel="stylesheet" href="/static/css/solarized-dark.css" />
+ <style>
+ .main {
+ padding: 20px 10px;
+ }
+
+ .hack h1 {
+ padding-top: 0;
+ }
+
+ footer.footer {
+ border-top: 1px solid #ccc;
+ margin-top: 80px;
+ margin-top: 5rem;
+ padding: 48px 0;
+ padding: 3rem 0;
+ }
+
+ img {
+ max-width: 100%;
+ max-height: 100%;
+ }
+ </style>
+ {{ template "styles" . }}
+ </head>
+ <body class="hack solarized-dark">
+ {{ template "scripts" . }}
+ <div class="container">
+ <header>
+ <p><a href="/">Christine Dodrill</a> - <a href="/blog">Blog</a> - <a href="/contact">Contact</a> - <a href="/resume">Resume</a></p>
+ </header>
+ {{ template "content" . }}
+ <footer>
+ <blockquote>Copyright 2017 Christine Dodrill. Any and all opinions listed here are my own and not representative of my employer.</blockquote>
+ </footer>
+ </div>
+ </body>
+</html>
+
+{{ define "scripts" }}{{ end }}
+{{ define "styles" }}{{ end }}
diff --git a/templates/blogindex.html b/templates/blogindex.html
new file mode 100644
index 0000000..107e7e1
--- /dev/null
+++ b/templates/blogindex.html
@@ -0,0 +1,22 @@
+{{ define "title" }}
+<title>Blog - Christine Dodrill</title>
+
+<style>
+.blogpost-card {
+ text-align: center;
+}
+</style>
+{{ end }}
+
+{{ define "content" }}
+<div class="grid">
+ {{ range . }}
+ <div class="card cell -4of12 blogpost-card">
+ <header class="card-header">{{ .Title }}</header>
+ <div class="card-content">
+ <p>Posted on {{ .Date }} <br> <a href="{{ .Link }}">Read Post</a></p>
+ </div>
+ </div>
+ {{ end }}
+</div>
+{{ end }}
diff --git a/templates/blogpost.html b/templates/blogpost.html
new file mode 100644
index 0000000..731ea1a
--- /dev/null
+++ b/templates/blogpost.html
@@ -0,0 +1,11 @@
+{{ define "title" }}
+<title>{{ .Title }} - Christine Dodrill</title>
+{{ end }}
+
+{{ define "content" }}
+{{ .BodyHTML }}
+
+<hr />
+
+<i>Content posted on {{ .Date }}, opinions and preferences of the author may have changed since then.</i>
+{{ end }}
diff --git a/templates/error.html b/templates/error.html
new file mode 100644
index 0000000..cb47e83
--- /dev/null
+++ b/templates/error.html
@@ -0,0 +1,9 @@
+{{ define "title" }}
+<title>Error - Christine Dodrill</title>
+{{ end }}
+
+{{ define "content" }}
+<pre>
+{{ . }}
+</pre>
+{{ end }}
diff --git a/templates/index.html b/templates/index.html
new file mode 100644
index 0000000..5bff551
--- /dev/null
+++ b/templates/index.html
@@ -0,0 +1,30 @@
+{{ define "title" }}<title>Christine Dodrill</title>{{ end }}
+
+{{ define "content" }}
+<div class="grid">
+ <div class="cell -3of12 content">
+ <img src="/static/img/avatar.png" width="192px">
+ <br />
+ <a href="/contact" class="justify-content-center">Contact Me</a>
+ </div>
+ <div class="cell -9of12 content">
+ <h1>Christine Dodrill</h1>
+ <h4>Web and Backend Services Devops Specialist</h4>
+ <h5>Skills</h5>
+ <ul>
+ <li>Go, Lua, Nim, Haskell, C, Python (3.x) and other languages</li>
+ <li>Docker (deployment, development & more)</li>
+ <li>Mashups of data</li>
+ <li>Package maintainer for Alpine Linux</li>
+ </ul>
+
+ <h5>Highlighted Projects</h5>
+ <ul>
+ <li><a href="https://github.com/Xe/PonyAPI">PonyAPI</a> - My Little Pony: Friendship is Magic Episode information API</li>
+ <li><a href="https://github.com/PonyvilleFM/aura">Aura</a> - PonyvilleFM live DJ recording bot</li>
+ <li><a href="https://github.com/Elemental-IRCd/elemental-ircd">Elemental-IRCd</a> - IRC Server Software</li>
+ <li><a href="https://github.com/Xe/site">This website</a> - The backend and templates for this website</li>
+ </ul>
+ </div>
+</div>
+{{ end }}
diff --git a/templates/resume.html b/templates/resume.html
new file mode 100644
index 0000000..9789361
--- /dev/null
+++ b/templates/resume.html
@@ -0,0 +1,9 @@
+{{ define "title" }}<title>Resume - Christine Dodrill</title>{{ end }}
+
+{{ define "content" }}
+{{ . }}
+
+<hr />
+
+<a href="/static/resume/resume.md">Plain-text version of this resume here</a>
+{{ end }}