Monorepo for Tangled tangled.org
2

Configure Feed

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

blog: simplify blog templates

- move pages.ParseWith to blog.parseLayout
- blog now builds against a new layouts/blogbase instead of inheriting
appview's base layout

Signed-off-by: oppiliappan <me@oppi.li>

author
oppiliappan
committer
Tangled
date (Jun 17, 2026, 12:22 PM +0300) commit 8cab85a0 parent 86d1e3d3 change-id lnvpvulp
+125 -48
-44
appview/pages/pages.go
··· 136 136 return p.embedFS 137 137 } 138 138 139 - // ParseWith parses the base layout together with all appview fragments and 140 - // an additional template from extraFS identified by extraPath (relative to 141 - // extraFS root). The returned template is ready to ExecuteTemplate with 142 - // "layouts/base" -- primarily for use with the blog. 143 - func (p *Pages) ParseWith(extraFS fs.FS, extraPath string) (*template.Template, error) { 144 - fragmentPaths, err := p.fragmentPaths() 145 - if err != nil { 146 - return nil, err 147 - } 148 - 149 - funcs := p.funcMap() 150 - tpl, err := template.New("layouts/base"). 151 - Funcs(funcs). 152 - ParseFS(p.embedFS, append(fragmentPaths, p.nameToPath("layouts/base"))...) 153 - if err != nil { 154 - return nil, err 155 - } 156 - 157 - err = fs.WalkDir(extraFS, ".", func(path string, d fs.DirEntry, err error) error { 158 - if err != nil { 159 - return err 160 - } 161 - if d.IsDir() || !strings.HasSuffix(path, ".html") { 162 - return nil 163 - } 164 - if path != extraPath && !strings.Contains(path, "fragments/") { 165 - return nil 166 - } 167 - data, err := fs.ReadFile(extraFS, path) 168 - if err != nil { 169 - return err 170 - } 171 - if _, err = tpl.New(path).Parse(string(data)); err != nil { 172 - return err 173 - } 174 - return nil 175 - }) 176 - if err != nil { 177 - return nil, err 178 - } 179 - 180 - return tpl, nil 181 - } 182 - 183 139 func (p *Pages) fragmentPaths() ([]string, error) { 184 140 var fragmentPaths []string 185 141 err := fs.WalkDir(p.embedFS, "templates", func(path string, d fs.DirEntry, err error) error {
+48 -4
blog/blog.go
··· 148 148 return feed.ToAtom() 149 149 } 150 150 151 + // parseLayout builds a template set from the appview's fragments together with 152 + // the blog's own layouts, fragments, and the given page template (relative to 153 + // templatesDir). The result is ready to ExecuteTemplate with "layouts/blogbase". 154 + func parseLayout(p *pages.Pages, templatesDir, page string) (*template.Template, error) { 155 + fragmentPaths, err := p.FragmentPaths() 156 + if err != nil { 157 + return nil, err 158 + } 159 + 160 + tpl, err := template.New("layouts/blogbase"). 161 + Funcs(p.FuncMap()). 162 + ParseFS(p.EmbedFS(), fragmentPaths...) 163 + if err != nil { 164 + return nil, err 165 + } 166 + 167 + extraFS := os.DirFS(templatesDir) 168 + err = fs.WalkDir(extraFS, ".", func(path string, d fs.DirEntry, err error) error { 169 + if err != nil { 170 + return err 171 + } 172 + if d.IsDir() || !strings.HasSuffix(path, ".html") { 173 + return nil 174 + } 175 + // only the requested page, plus shared layouts and fragments 176 + if path != page && !strings.Contains(path, "fragments/") && !strings.Contains(path, "layouts/") { 177 + return nil 178 + } 179 + data, err := fs.ReadFile(extraFS, path) 180 + if err != nil { 181 + return err 182 + } 183 + if _, err = tpl.New(path).Parse(string(data)); err != nil { 184 + return err 185 + } 186 + return nil 187 + }) 188 + if err != nil { 189 + return nil, err 190 + } 191 + 192 + return tpl, nil 193 + } 194 + 151 195 // RenderIndex renders the blog index page to w. 152 196 func RenderIndex(p *pages.Pages, templatesDir string, posts []Post, w io.Writer) error { 153 - tpl, err := p.ParseWith(os.DirFS(templatesDir), "index.html") 197 + tpl, err := parseLayout(p, templatesDir, "index.html") 154 198 if err != nil { 155 199 return err 156 200 } ··· 163 207 } 164 208 } 165 209 } 166 - return tpl.ExecuteTemplate(w, "layouts/base", indexParams{Posts: posts, Featured: featured}) 210 + return tpl.ExecuteTemplate(w, "layouts/blogbase", indexParams{Posts: posts, Featured: featured}) 167 211 } 168 212 169 213 // RenderPost renders a single blog post page to w. 170 214 func RenderPost(p *pages.Pages, templatesDir string, post Post, w io.Writer) error { 171 - tpl, err := p.ParseWith(os.DirFS(templatesDir), "post.html") 215 + tpl, err := parseLayout(p, templatesDir, "post.html") 172 216 if err != nil { 173 217 return err 174 218 } 175 - return tpl.ExecuteTemplate(w, "layouts/base", postParams{Post: post}) 219 + return tpl.ExecuteTemplate(w, "layouts/blogbase", postParams{Post: post}) 176 220 }
+77
blog/templates/layouts/blogbase.html
··· 1 + {{ define "layouts/blogbase" }} 2 + <!doctype html> 3 + <html lang="en" class="dark:bg-gray-900"> 4 + <head> 5 + <meta charset="UTF-8" /> 6 + <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover"/> 7 + <meta name="description" content="The next-generation social coding platform."/> 8 + 9 + <!-- Open Graph defaults --> 10 + <meta property="og:site_name" content="Tangled" /> 11 + <meta property="og:type" content="website" /> 12 + <meta property="og:locale" content="en_US" /> 13 + 14 + <!-- Keywords --> 15 + <meta name="keywords" content="git, code collaboration, AT Protocol, open source, version control, social coding, code hosting" /> 16 + 17 + <!-- Author and copyright --> 18 + <meta name="author" content="Tangled" /> 19 + <meta name="robots" content="index, follow" /> 20 + 21 + <link rel="icon" href="/static/logos/dolly.ico" sizes="48x48"/> 22 + <link rel="icon" href="/static/logos/dolly.svg" sizes="any" type="image/svg+xml"/> 23 + <link rel="apple-touch-icon" href="/static/logos/dolly.png"/> 24 + 25 + <!-- preconnect to image cdn --> 26 + <link rel="preconnect" href="https://avatar.tangled.sh" /> 27 + <link rel="preconnect" href="https://camo.tangled.sh" /> 28 + 29 + <!-- preload main font --> 30 + <link rel="preload" href="/static/fonts/InterVariable.woff2" as="font" type="font/woff2" crossorigin /> 31 + 32 + <link rel="stylesheet" href="/static/tw.css?{{ cssContentHash }}" type="text/css" /> 33 + 34 + <script> 35 + document.addEventListener('DOMContentLoaded', () => { 36 + const nodes = document.querySelectorAll('pre.mermaid'); 37 + if (!nodes.length) return; 38 + const script = document.createElement('script'); 39 + script.src = '/static/mermaid.min.js'; 40 + script.onload = async () => { 41 + mermaid.initialize({ 42 + startOnLoad: true, 43 + theme: window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'default', 44 + }); 45 + await mermaid.run({ nodes }); 46 + }; 47 + document.head.appendChild(script); 48 + }); 49 + </script> 50 + <title>{{ block "title" . }}{{ end }}</title> 51 + {{ block "extrameta" . }}{{ end }} 52 + </head> 53 + <body class="min-h-screen flex flex-col gap-4 bg-slate-100 dark:bg-gray-900 dark:text-white transition-colors duration-200 {{ block "bodyClasses" . }} {{ end }}"> 54 + {{ block "topbarLayout" . }}{{ end }} 55 + 56 + {{ block "mainLayout" . }} 57 + <div class="flex-grow"> 58 + <div class="max-w-screen-lg mx-auto flex flex-col gap-4"> 59 + {{ block "contentLayout" . }} 60 + <main> 61 + {{ block "content" . }}{{ end }} 62 + </main> 63 + {{ end }} 64 + 65 + {{ block "contentAfterLayout" . }} 66 + <main> 67 + {{ block "contentAfter" . }}{{ end }} 68 + </main> 69 + {{ end }} 70 + </div> 71 + </div> 72 + {{ end }} 73 + 74 + {{ block "footerLayout" . }}{{ end }} 75 + </body> 76 + </html> 77 + {{ end }}