aboutsummaryrefslogtreecommitdiff
path: root/cmd/blog
diff options
context:
space:
mode:
authorAndrew Gerrand <adg@golang.org>2013-03-08 09:22:40 +1100
committerAndrew Gerrand <adg@golang.org>2013-03-08 09:22:40 +1100
commit0ee9872fcd5cf9023cb43a68faf67203cbe81295 (patch)
treefaa873e0163e1d1b35e86b812de77afd3da7f6bb /cmd/blog
parentc26ca9cbba2bc725e1d7d26754ac5167a44ddb76 (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.go19
-rw-r--r--cmd/blog/blog.go277
-rw-r--r--cmd/blog/local.go34
-rw-r--r--cmd/blog/rewrite.go77
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",
+}