diff --git a/blog/blog.go b/blog/blog.go index 1c3bc5487b..3e8f873ba2 100644 --- a/blog/blog.go +++ b/blog/blog.go @@ -65,12 +65,13 @@ type Doc struct { // Server implements an http.Handler that serves blog articles. type Server struct { - cfg Config - docs []*Doc - tags []string - docPaths map[string]*Doc // key is path without BasePath. - docTags map[string][]*Doc - template struct { + cfg Config + docs []*Doc + redirects map[string]string + tags []string + docPaths map[string]*Doc // key is path without BasePath. + docTags map[string][]*Doc + template struct { home, index, article, doc *template.Template } atomFeed []byte // pre-rendered Atom feed @@ -118,7 +119,8 @@ func NewServer(cfg Config) (*Server, error) { } // Load content. - err = s.loadDocs(filepath.Clean(cfg.ContentPath)) + content := filepath.Clean(cfg.ContentPath) + err = s.loadDocs(content) if err != nil { return nil, err } @@ -191,6 +193,7 @@ func (s *Server) loadDocs(root string) error { if err != nil { return err } + if filepath.Ext(p) != ext { return nil } @@ -227,12 +230,27 @@ func (s *Server) loadDocs(root string) error { // Pull out doc paths and tags and put in reverse-associating maps. s.docPaths = make(map[string]*Doc) s.docTags = make(map[string][]*Doc) + s.redirects = make(map[string]string) for _, d := range s.docs { s.docPaths[strings.TrimPrefix(d.Path, s.cfg.BasePath)] = d for _, t := range d.Tags { s.docTags[t] = append(s.docTags[t], d) } } + for _, d := range s.docs { + for _, old := range d.OldURL { + if !strings.HasPrefix(old, "/") { + old = "/" + old + } + if _, ok := s.docPaths[old]; ok { + return fmt.Errorf("redirect %s -> %s conflicts with document %s", old, d.Path, old) + } + if new, ok := s.redirects[old]; ok { + return fmt.Errorf("redirect %s -> %s conflicts with redirect %s -> %s", old, d.Path, old, new) + } + s.redirects[old] = d.Path + } + } // Pull out unique sorted list of tags. for t := range s.docTags { @@ -425,6 +443,10 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.Write(s.jsonFeed) return default: + if redir, ok := s.redirects[p]; ok { + http.Redirect(w, r, redir, http.StatusMovedPermanently) + return + } doc, ok := s.docPaths[p] if !ok { // Not a doc; try to just serve static content. diff --git a/present/doc.go b/present/doc.go index fb84e122e8..cdee8ade76 100644 --- a/present/doc.go +++ b/present/doc.go @@ -17,6 +17,7 @@ and other metadata, which looks like: 15:04 2 Jan 2006 Tags: foo, bar, baz Summary: This is a great document you want to read. + OldURL: former-path-for-this-doc The "# " prefix before the title indicates that this is a Markdown-enabled present file: it uses @@ -33,8 +34,12 @@ the document. The summary line gives a short summary used in blog feeds. +The old URL line, which may be repeated, gives an older (perhaps relative) URL +for this document. +A server might use these to generate appropriate redirects. + Only the title is required; -the subtitle, date, tags, and summary lines are optional. +the subtitle, date, tags, summary, and old URL lines are optional. In Markdown-enabled present, the summary defaults to being empty. In legacy present, the summary defaults to the first paragraph of text. diff --git a/present/parse.go b/present/parse.go index 7f38e6ee33..672a6ffa80 100644 --- a/present/parse.go +++ b/present/parse.go @@ -79,6 +79,7 @@ type Doc struct { TitleNotes []string Sections []Section Tags []string + OldURL []string } // Author represents the person who wrote and/or is presenting the document. @@ -546,6 +547,8 @@ func parseHeader(doc *Doc, isMarkdown bool, lines *Lines) error { doc.Tags = append(doc.Tags, tags...) } else if strings.HasPrefix(text, "Summary:") { doc.Summary = strings.TrimSpace(text[len("Summary:"):]) + } else if strings.HasPrefix(text, "OldURL:") { + doc.OldURL = append(doc.OldURL, strings.TrimSpace(text[len("OldURL:"):])) } else if t, ok := parseTime(text); ok { doc.Time = t } else if doc.Subtitle == "" {