diff options
author | Andrew Gerrand <adg@golang.org> | 2013-03-08 09:22:40 +1100 |
---|---|---|
committer | Andrew Gerrand <adg@golang.org> | 2013-03-08 09:22:40 +1100 |
commit | 0ee9872fcd5cf9023cb43a68faf67203cbe81295 (patch) | |
tree | faa873e0163e1d1b35e86b812de77afd3da7f6bb /cmd/blog | |
parent | c26ca9cbba2bc725e1d7d26754ac5167a44ddb76 (diff) |
go.blog: blog server
R=golang-dev, dsymonds, r
CC=golang-dev
https://golang.org/cl/7382064
Diffstat (limited to 'cmd/blog')
-rw-r--r-- | cmd/blog/appengine.go | 19 | ||||
-rw-r--r-- | cmd/blog/blog.go | 277 | ||||
-rw-r--r-- | cmd/blog/local.go | 34 | ||||
-rw-r--r-- | cmd/blog/rewrite.go | 77 |
4 files changed, 407 insertions, 0 deletions
diff --git a/cmd/blog/appengine.go b/cmd/blog/appengine.go new file mode 100644 index 0000000..174efe4 --- /dev/null +++ b/cmd/blog/appengine.go @@ -0,0 +1,19 @@ +// Copyright 2013 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build appengine + +// This file implements an App Engine blog server. + +package main + +import "net/http" + +func init() { + s, err := NewServer("/", "content/", "template/") + if err != nil { + panic(err) + } + http.Handle("/", s) +} diff --git a/cmd/blog/blog.go b/cmd/blog/blog.go new file mode 100644 index 0000000..cddb5a2 --- /dev/null +++ b/cmd/blog/blog.go @@ -0,0 +1,277 @@ +// Copyright 2013 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// This is a blog server for articles written in present format. +// It powers blog.golang.org. +package main + +import ( + "bytes" + "html/template" + "log" + "net/http" + "os" + "path" + "path/filepath" + "sort" + "strings" + + _ "code.google.com/p/go.talks/pkg/playground" + "code.google.com/p/go.talks/pkg/present" +) + +const ( + baseURL = "http://blog.golang.org" + homeArticles = 5 // number of articles to display on the home page +) + +// Doc represents an article, adorned with presentation data: +// its absolute path and related articles. +type Doc struct { + *present.Doc + Permalink string + Path string + Related []*Doc + Newer, Older *Doc + HTML template.HTML // rendered article +} + +// Server implements an http.Handler that serves blog articles. +type Server struct { + pathPrefix string + docs []*Doc + tags []string + docPaths map[string]*Doc + docTags map[string][]*Doc + template struct { + home, index, article, doc *template.Template + } + content http.Handler +} + +// NewServer constructs a new Server, serving articles from the specified +// contentPath generated from templates from templatePath, under the prefix +// specified by pathPrefix. +func NewServer(pathPrefix, contentPath, templatePath string) (*Server, error) { + present.PlayEnabled = true + + root := filepath.Join(templatePath, "root.tmpl") + parse := func(name string) (*template.Template, error) { + t := template.New("").Funcs(funcMap) + return t.ParseFiles(root, filepath.Join(templatePath, name)) + } + + s := &Server{pathPrefix: pathPrefix} + + // Parse templates. + var err error + s.template.home, err = parse("home.tmpl") + if err != nil { + return nil, err + } + s.template.index, err = parse("index.tmpl") + if err != nil { + return nil, err + } + s.template.article, err = parse("article.tmpl") + if err != nil { + return nil, err + } + p := present.Template().Funcs(funcMap) + s.template.doc, err = p.ParseFiles(filepath.Join(templatePath, "doc.tmpl")) + if err != nil { + return nil, err + } + + // Load content. + err = s.loadDocs(filepath.Clean(contentPath)) + if err != nil { + return nil, err + } + + // Set up content file server. + s.content = http.StripPrefix(pathPrefix, http.FileServer(http.Dir(contentPath))) + + return s, nil +} + +var funcMap = template.FuncMap{ + "sectioned": sectioned, + "authors": authors, +} + +// sectioned returns true if the provided Doc contains more than one section. +// This is used to control whether to display the table of contents and headings. +func sectioned(d *present.Doc) bool { + return len(d.Sections) > 1 +} + +// authors returns a comma-separated list of author names. +func authors(authors []present.Author) string { + var b bytes.Buffer + last := len(authors) - 1 + for i, a := range authors { + if i > 0 { + if i == last { + b.WriteString(" and ") + } else { + b.WriteString(", ") + } + } + b.WriteString(authorName(a)) + } + return b.String() +} + +// authorName returns the first line of the Author text: the author's name. +func authorName(a present.Author) string { + el := a.TextElem() + if len(el) == 0 { + return "" + } + text, ok := el[0].(present.Text) + if !ok || len(text.Lines) == 0 { + return "" + } + return text.Lines[0] +} + +// loadDocs reads all content from the provided file system root, renders all +// the articles it finds, adds them to the Server's docs field, computes the +// denormalized docPaths, docTags, and tags fields, and populates the various +// helper fields (Next, Previous, Related) for each Doc. +func (s *Server) loadDocs(root string) error { + // Read content into docs field. + const ext = ".article" + fn := func(p string, info os.FileInfo, err error) error { + if filepath.Ext(p) != ext { + return nil + } + f, err := os.Open(p) + if err != nil { + return err + } + defer f.Close() + d, err := present.Parse(f, p, 0) + if err != nil { + return err + } + html := new(bytes.Buffer) + err = d.Render(html, s.template.doc) + if err != nil { + return err + } + p = p[len(root) : len(p)-len(ext)] // trim root and extension + s.docs = append(s.docs, &Doc{ + Doc: d, + Path: path.Join(s.pathPrefix, p), + HTML: template.HTML(html.String()), + }) + return nil + } + err := filepath.Walk(root, fn) + if err != nil { + return err + } + sort.Sort(docsByTime(s.docs)) + + // Pull out doc paths and tags and put in reverse-associating maps. + s.docPaths = make(map[string]*Doc) + s.docTags = make(map[string][]*Doc) + for _, d := range s.docs { + s.docPaths[d.Path] = d + for _, t := range d.Tags { + s.docTags[t] = append(s.docTags[t], d) + } + } + + // Pull out unique sorted list of tags. + for t := range s.docTags { + s.tags = append(s.tags, t) + } + sort.Strings(s.tags) + + // Set up presentation-related fields, Newer, Older, and Related. + for _, doc := range s.docs { + // Newer, Older: docs adjacent to doc + for i := range s.docs { + if s.docs[i] != doc { + continue + } + if i > 0 { + doc.Newer = s.docs[i-1] + } + if i+1 < len(s.docs) { + doc.Older = s.docs[i+1] + } + break + } + + // Related: all docs that share tags with doc. + related := make(map[*Doc]bool) + for _, t := range doc.Tags { + for _, d := range s.docTags[t] { + if d != doc { + related[d] = true + } + } + } + for d := range related { + doc.Related = append(doc.Related, d) + } + sort.Sort(docsByTime(doc.Related)) + } + + return nil +} + +// rootData encapsulates data destined for the root template. +type rootData struct { + Doc *Doc + Data interface{} +} + +// ServeHTTP servers either an article list or a single article. +func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { + p := r.URL.Path + if !strings.HasPrefix(p, s.pathPrefix) { + http.Error(w, "not found", http.StatusNotFound) + return + } + var ( + d rootData + t *template.Template + ) + switch p[len(s.pathPrefix):] { + case "": + d.Data = s.docs + if len(s.docs) > homeArticles { + d.Data = s.docs[:homeArticles] + } + t = s.template.home + case "index": + d.Data = s.docs + t = s.template.index + default: + doc, ok := s.docPaths[p] + if !ok { + // Not a doc; try to just serve static content. + s.content.ServeHTTP(w, r) + return + } + d.Doc = doc + t = s.template.article + } + err := t.ExecuteTemplate(w, "root", d) + if err != nil { + log.Println(err) + } +} + +// docsByTime implements sort.Interface, sorting Docs by their Time field. +type docsByTime []*Doc + +func (s docsByTime) Len() int { return len(s) } +func (s docsByTime) Swap(i, j int) { s[i], s[j] = s[j], s[i] } +func (s docsByTime) Less(i, j int) bool { return s[i].Time.After(s[j].Time) } diff --git a/cmd/blog/local.go b/cmd/blog/local.go new file mode 100644 index 0000000..17ebae4 --- /dev/null +++ b/cmd/blog/local.go @@ -0,0 +1,34 @@ +// Copyright 2013 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// +build !appengine + +// This file implements a stand-alone blog server. + +package main + +import ( + "flag" + "log" + "net/http" +) + +var ( + httpAddr = flag.String("http", "localhost:8080", "HTTP listen address") + contentPath = flag.String("content", "content/", "path to content files") + templatePath = flag.String("template", "template/", "path to template files") + staticPath = flag.String("static", "static/", "path to static files") +) + +func main() { + flag.Parse() + s, err := NewServer("/", *contentPath, *templatePath) + if err != nil { + log.Fatal(err) + } + http.Handle("/", s) + fs := http.FileServer(http.Dir(*staticPath)) + http.Handle("/static/", http.StripPrefix("/static/", fs)) + log.Fatal(http.ListenAndServe(*httpAddr, nil)) +} diff --git a/cmd/blog/rewrite.go b/cmd/blog/rewrite.go new file mode 100644 index 0000000..037dcc9 --- /dev/null +++ b/cmd/blog/rewrite.go @@ -0,0 +1,77 @@ +// Copyright 2013 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package main + +import "net/http" + +// Register HTTP handlers that redirect old blog paths to their new locations. +func init() { + for p := range urlMap { + dest := "/" + urlMap[p] + http.HandleFunc(p, func(w http.ResponseWriter, r *http.Request) { + http.Redirect(w, r, dest, http.StatusMovedPermanently) + }) + } +} + +var urlMap = map[string]string{ + "/2010/05/new-talk-and-tutorials.html": "new-talk-and-tutorials", + "/2011/10/learn-go-from-your-browser.html": "learn-go-from-your-browser", + "/2011/05/gif-decoder-exercise-in-go-interfaces.html": "gif-decoder-exercise-in-go-interfaces", + "/2013/01/the-app-engine-sdk-and-workspaces-gopath.html": "the-app-engine-sdk-and-workspaces-gopath", + "/2012/03/go-version-1-is-released.html": "go-version-1-is-released", + "/2010/09/introducing-go-playground.html": "introducing-go-playground", + "/2012/07/go-videos-from-google-io-2012.html": "go-videos-from-google-io-2012", + "/2011/12/building-stathat-with-go.html": "building-stathat-with-go", + "/2011/06/first-class-functions-in-go-and-new-go.html": "first-class-functions-in-go-and-new-go", + "/2011/04/introducing-gofix.html": "introducing-gofix", + "/2012/07/gccgo-in-gcc-471.html": "gccgo-in-gcc-471", + "/2010/11/debugging-go-code-status-report.html": "debugging-go-code-status-report", + "/2010/09/go-concurrency-patterns-timing-out-and.html": "go-concurrency-patterns-timing-out-and", + "/2011/04/go-at-heroku.html": "go-at-heroku", + "/2010/05/upcoming-google-io-go-events.html": "upcoming-google-io-go-events", + "/2013/01/concurrency-is-not-parallelism.html": "concurrency-is-not-parallelism", + "/2011/10/preview-of-go-version-1.html": "preview-of-go-version-1", + "/2013/01/go-fmt-your-code.html": "go-fmt-your-code", + "/2011/03/godoc-documenting-go-code.html": "godoc-documenting-go-code", + "/2010/05/go-at-io-frequently-asked-questions.html": "go-at-io-frequently-asked-questions", + "/2010/03/go-whats-new-in-march-2010.html": "go-whats-new-in-march-2010", + "/2013/02/go-maps-in-action.html": "go-maps-in-action", + "/2011/09/go-image-package.html": "go-image-package", + "/2010/04/third-party-libraries-goprotobuf-and.html": "third-party-libraries-goprotobuf-and", + "/2011/10/go-app-engine-sdk-155-released.html": "go-app-engine-sdk-155-released", + "/2011/07/error-handling-and-go.html": "error-handling-and-go", + "/2011/10/debugging-go-programs-with-gnu-debugger.html": "debugging-go-programs-with-gnu-debugger", + "/2011/06/spotlight-on-external-go-libraries.html": "spotlight-on-external-go-libraries", + "/2011/01/go-slices-usage-and-internals.html": "go-slices-usage-and-internals", + "/2011/09/laws-of-reflection.html": "laws-of-reflection", + "/2011/05/go-and-google-app-engine.html": "go-and-google-app-engine", + "/2010/09/go-wins-2010-bossie-award.html": "go-wins-2010-bossie-award", + "/2011/01/json-and-go.html": "json-and-go", + "/2011/03/go-becomes-more-stable.html": "go-becomes-more-stable", + "/2012/11/go-turns-three.html": "go-turns-three", + "/2011/12/from-zero-to-go-launching-on-google.html": "from-zero-to-go-launching-on-google", + "/2011/11/writing-scalable-app-engine.html": "writing-scalable-app-engine", + "/2013/02/getthee-to-go-meetup.html": "getthee-to-go-meetup", + "/2010/10/real-go-projects-smarttwitter-and-webgo.html": "real-go-projects-smarttwitter-and-webgo", + "/2011/03/c-go-cgo.html": "c-go-cgo", + "/2011/12/getting-to-know-go-community.html": "getting-to-know-go-community", + "/2010/11/go-one-year-ago-today.html": "go-one-year-ago-today", + "/2011/11/go-programming-language-turns-two.html": "go-programming-language-turns-two", + "/2010/07/share-memory-by-communicating.html": "share-memory-by-communicating", + "/2010/07/gos-declaration-syntax.html": "gos-declaration-syntax", + "/2011/09/go-imagedraw-package.html": "go-imagedraw-package", + "/2010/04/json-rpc-tale-of-interfaces.html": "json-rpc-tale-of-interfaces", + "/2010/08/defer-panic-and-recover.html": "defer-panic-and-recover", + "/2013/01/two-recent-go-talks.html": "two-recent-go-talks", + "/2011/03/gobs-of-data.html": "gobs-of-data", + "/2011/05/go-at-google-io-2011-videos.html": "go-at-google-io-2011-videos", + "/2011/06/profiling-go-programs.html": "profiling-go-programs", + "/2012/08/go-updates-in-app-engine-171.html": "go-updates-in-app-engine-171", + "/2011/09/two-go-talks-lexical-scanning-in-go-and.html": "two-go-talks-lexical-scanning-in-go-and", + "/2012/08/organizing-go-code.html": "organizing-go-code", + "/2011/07/go-for-app-engine-is-now-generally.html": "go-for-app-engine-is-now-generally", + "/2010/06/go-programming-session-video-from.html": "go-programming-session-video-from", +} |