This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / cmd / blog / main.go
4.7 kB 193 lines
1package main 2 3import ( 4 "context" 5 "fmt" 6 "io" 7 "log/slog" 8 "net/http" 9 "os" 10 "path/filepath" 11 12 "tangled.org/core/appview/config" 13 "tangled.org/core/appview/pages" 14 "tangled.org/core/blog" 15 "tangled.org/core/idresolver" 16 tlog "tangled.org/core/log" 17) 18 19const ( 20 postsDir = "blog/posts" 21 templatesDir = "blog/templates" 22) 23 24func main() { 25 if len(os.Args) < 2 { 26 fmt.Fprintln(os.Stderr, "usage: blog <build|serve> [flags]") 27 os.Exit(1) 28 } 29 30 ctx := context.Background() 31 logger := tlog.New("blog") 32 33 switch os.Args[1] { 34 case "build": 35 if err := runBuild(ctx, logger); err != nil { 36 logger.Error("build failed", "err", err) 37 os.Exit(1) 38 } 39 case "serve": 40 addr := "0.0.0.0:3001" 41 if len(os.Args) >= 3 { 42 addr = os.Args[2] 43 } 44 if err := runServe(ctx, logger, addr); err != nil { 45 logger.Error("serve failed", "err", err) 46 os.Exit(1) 47 } 48 default: 49 fmt.Fprintf(os.Stderr, "unknown subcommand: %s\n", os.Args[1]) 50 os.Exit(1) 51 } 52} 53 54func makePages(ctx context.Context, cfg *config.Config, logger *slog.Logger) (*pages.Pages, error) { 55 resolver := idresolver.DefaultResolver(cfg.Plc.PLCURL) 56 return pages.NewPages(cfg, resolver, nil, logger), nil 57} 58 59func runBuild(ctx context.Context, logger *slog.Logger) error { 60 cfg, err := config.LoadConfig(ctx) 61 if err != nil { 62 cfg = &config.Config{} 63 } 64 65 p, err := makePages(ctx, cfg, logger) 66 if err != nil { 67 return fmt.Errorf("creating pages: %w", err) 68 } 69 70 posts, err := blog.Posts(postsDir) 71 if err != nil { 72 return fmt.Errorf("parsing posts: %w", err) 73 } 74 75 outDir := "build" 76 if err := os.MkdirAll(outDir, 0755); err != nil { 77 return err 78 } 79 80 // index 81 if err := renderToFile(outDir, "index.html", func(w io.Writer) error { 82 return blog.RenderIndex(p, templatesDir, posts, w) 83 }); err != nil { 84 return fmt.Errorf("rendering index: %w", err) 85 } 86 87 // posts — each at build/<slug>/index.html directly (no /blog/ prefix) 88 for _, post := range posts { 89 post := post 90 postDir := filepath.Join(outDir, post.Meta.Slug) 91 if err := os.MkdirAll(postDir, 0755); err != nil { 92 return err 93 } 94 if err := renderToFile(postDir, "index.html", func(w io.Writer) error { 95 return blog.RenderPost(p, templatesDir, post, w) 96 }); err != nil { 97 return fmt.Errorf("rendering post %s: %w", post.Meta.Slug, err) 98 } 99 } 100 101 // atom feed — at build/feed.xml 102 baseURL := "https://blog.tangled.org" 103 atom, err := blog.AtomFeed(posts, baseURL) 104 if err != nil { 105 return fmt.Errorf("generating atom feed: %w", err) 106 } 107 if err := os.WriteFile(filepath.Join(outDir, "feed.xml"), []byte(atom), 0644); err != nil { 108 return fmt.Errorf("writing feed: %w", err) 109 } 110 111 logger.Info("build complete", "dir", outDir) 112 return nil 113} 114 115func runServe(ctx context.Context, logger *slog.Logger, addr string) error { 116 cfg, err := config.LoadConfig(ctx) 117 if err != nil { 118 cfg = &config.Config{} 119 } 120 121 p, err := makePages(ctx, cfg, logger) 122 if err != nil { 123 return fmt.Errorf("creating pages: %w", err) 124 } 125 126 mux := http.NewServeMux() 127 128 // index 129 mux.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) { 130 if r.URL.Path != "/" { 131 http.NotFound(w, r) 132 return 133 } 134 posts, err := blog.AllPosts(postsDir) 135 if err != nil { 136 http.Error(w, err.Error(), http.StatusInternalServerError) 137 return 138 } 139 if err := blog.RenderIndex(p, templatesDir, posts, w); err != nil { 140 logger.Error("render index", "err", err) 141 } 142 }) 143 144 // individual posts directly at /<slug> 145 mux.HandleFunc("GET /{slug}", func(w http.ResponseWriter, r *http.Request) { 146 slug := r.PathValue("slug") 147 posts, err := blog.AllPosts(postsDir) 148 if err != nil { 149 http.Error(w, err.Error(), http.StatusInternalServerError) 150 return 151 } 152 for _, post := range posts { 153 if post.Meta.Slug == slug { 154 if err := blog.RenderPost(p, templatesDir, post, w); err != nil { 155 logger.Error("render post", "err", err) 156 } 157 return 158 } 159 } 160 http.NotFound(w, r) 161 }) 162 163 // atom feed at /feed.xml 164 mux.HandleFunc("GET /feed.xml", func(w http.ResponseWriter, r *http.Request) { 165 posts, err := blog.Posts(postsDir) 166 if err != nil { 167 http.Error(w, err.Error(), http.StatusInternalServerError) 168 return 169 } 170 atom, err := blog.AtomFeed(posts, "https://blog.tangled.org") 171 if err != nil { 172 http.Error(w, err.Error(), http.StatusInternalServerError) 173 return 174 } 175 w.Header().Set("Content-Type", "application/atom+xml") 176 fmt.Fprint(w, atom) 177 }) 178 179 // appview static files (tw.css, fonts, icons, logos) 180 mux.Handle("GET /static/", p.Static()) 181 182 logger.Info("serving", "addr", addr) 183 return http.ListenAndServe(addr, mux) 184} 185 186func renderToFile(dir, name string, fn func(io.Writer) error) error { 187 f, err := os.Create(filepath.Join(dir, name)) 188 if err != nil { 189 return err 190 } 191 defer f.Close() 192 return fn(f) 193}