Monorepo for Tangled tangled.org

appview/pages: seo improvements, sitemap, schemae and more

Signed-off-by: Anirudh Oppiliappan <anirudh@tangled.org>

+319 -43
+46
appview/pages/repoinfo/repoinfo.go
··· 1 package repoinfo 2 3 import ( 4 "fmt" 5 "path" 6 "slices" ··· 117 func (r RolesInRepo) IsPushAllowed() bool { 118 return slices.Contains(r.Roles, "repo:push") 119 }
··· 1 package repoinfo 2 3 import ( 4 + "encoding/json" 5 "fmt" 6 "path" 7 "slices" ··· 118 func (r RolesInRepo) IsPushAllowed() bool { 119 return slices.Contains(r.Roles, "repo:push") 120 } 121 + 122 + // PrimaryLanguage returns the first (most used) language from a list, or empty string if none 123 + func PrimaryLanguage(languages []interface{}) string { 124 + if len(languages) == 0 { 125 + return "" 126 + } 127 + 128 + // Languages are already sorted by percentage in descending order 129 + // Just get the first one 130 + if firstLang, ok := languages[0].(map[string]interface{}); ok { 131 + if name, ok := firstLang["Name"].(string); ok { 132 + return name 133 + } 134 + } 135 + 136 + return "" 137 + } 138 + 139 + // StructuredData generates Schema.org JSON-LD structured data for the repository 140 + func (r RepoInfo) StructuredData(primaryLanguage string) string { 141 + data := map[string]interface{}{ 142 + "@context": "https://schema.org", 143 + "@type": "SoftwareSourceCode", 144 + "name": r.Name, 145 + "description": r.Description, 146 + "codeRepository": "https://tangled.org/" + r.FullName(), 147 + "url": "https://tangled.org/" + r.FullName(), 148 + "author": map[string]interface{}{ 149 + "@type": "Person", 150 + "name": r.owner(), 151 + "url": "https://tangled.org/" + r.owner(), 152 + }, 153 + } 154 + 155 + // Add programming language if available 156 + if primaryLanguage != "" { 157 + data["programmingLanguage"] = primaryLanguage 158 + } 159 + 160 + jsonBytes, err := json.Marshal(data) 161 + if err != nil { 162 + return "{}" 163 + } 164 + return string(jsonBytes) 165 + }
+22
appview/pages/templates/fragments/breadcrumb.html
···
··· 1 + {{ define "fragments/breadcrumb" }} 2 + {{ $items := . }} 3 + {{ if gt (len $items) 0 }} 4 + <script type="application/ld+json"> 5 + { 6 + "@context": "https://schema.org", 7 + "@type": "BreadcrumbList", 8 + "itemListElement": [ 9 + {{ range $idx, $item := $items }} 10 + {{ if gt $idx 0 }},{{ end }} 11 + { 12 + "@type": "ListItem", 13 + "position": {{ add $idx 1 }}, 14 + "name": "{{ index $item 0 }}", 15 + "item": "{{ index $item 1 }}" 16 + } 17 + {{ end }} 18 + ] 19 + } 20 + </script> 21 + {{ end }} 22 + {{ end }}
+27 -2
appview/pages/templates/goodfirstissues/index.html
··· 1 {{ define "title" }}good first issues{{ end }} 2 3 {{ define "extrameta" }} 4 <meta property="og:title" content="good first issues · tangled" /> 5 - <meta property="og:type" content="object" /> 6 <meta property="og:url" content="https://tangled.org/goodfirstissues" /> 7 - <meta property="og:description" content="Find good first issues to contribute to open source projects" /> 8 {{ end }} 9 10 {{ define "content" }} 11 <div class="grid grid-cols-10">
··· 1 {{ define "title" }}good first issues{{ end }} 2 3 {{ define "extrameta" }} 4 + <meta name="description" content="Discover beginner-friendly good first issues across open source projects on Tangled. Perfect for new contributors looking to get started with open source development." /> 5 + <meta name="keywords" content="good first issues, beginner issues, open source contribution, first time contributor, beginner friendly, open source projects" /> 6 + 7 <meta property="og:title" content="good first issues · tangled" /> 8 + <meta property="og:type" content="website" /> 9 <meta property="og:url" content="https://tangled.org/goodfirstissues" /> 10 + <meta property="og:description" content="Find beginner-friendly issues across all repositories to get started with open source contributions on Tangled." /> 11 + 12 + <meta name="twitter:card" content="summary" /> 13 + <meta name="twitter:title" content="good first issues · tangled" /> 14 + <meta name="twitter:description" content="Find beginner-friendly issues to get started with open source contributions." /> 15 + 16 + <!-- structured data for good first issues page --> 17 + <script type="application/ld+json"> 18 + { 19 + "@context": "https://schema.org", 20 + "@type": "CollectionPage", 21 + "name": "Good First Issues", 22 + "description": "A curated collection of beginner-friendly issues across open source projects", 23 + "url": "https://tangled.org/goodfirstissues", 24 + "isPartOf": { 25 + "@type": "WebSite", 26 + "name": "Tangled", 27 + "url": "https://tangled.org" 28 + } 29 + } 30 + </script> 31 {{ end }} 32 + 33 + {{ define "canonical" }}https://tangled.org/goodfirstissues{{ end }} 34 35 {{ define "content" }} 36 <div class="grid grid-cols-10">
+26 -2
appview/pages/templates/layouts/base.html
··· 4 <head> 5 <meta charset="UTF-8" /> 6 <meta name="viewport" content="width=device-width, initial-scale=1.0"/> 7 - <meta name="description" content="Social coding, but for real this time!"/> 8 <meta name="htmx-config" content='{"includeIndicatorStyles": false}'> 9 10 <script defer src="/static/htmx.min.js"></script> 11 <script defer src="/static/htmx-ext-ws.min.js"></script> ··· 19 <link rel="preconnect" href="https://avatar.tangled.sh" /> 20 <link rel="preconnect" href="https://camo.tangled.sh" /> 21 22 <!-- pwa manifest --> 23 <link rel="manifest" href="/pwa-manifest.json" /> 24 ··· 26 <link rel="preload" href="/static/fonts/InterVariable.woff2" as="font" type="font/woff2" crossorigin /> 27 28 <link rel="stylesheet" href="/static/tw.css?{{ cssContentHash }}" type="text/css" /> 29 - <title>{{ block "title" . }}{{ end }} · tangled</title> 30 {{ block "extrameta" . }}{{ end }} 31 </head> 32 <body class="min-h-screen flex flex-col gap-4 bg-slate-100 dark:bg-gray-900 dark:text-white transition-colors duration-200">
··· 4 <head> 5 <meta charset="UTF-8" /> 6 <meta name="viewport" content="width=device-width, initial-scale=1.0"/> 7 + <meta name="description" content="tightly-knit social coding"/> 8 + <meta name="keywords" content="git hosting, social coding, version control, pull requests, CI/CD, code collaboration, open source, decentralized"/> 9 <meta name="htmx-config" content='{"includeIndicatorStyles": false}'> 10 + <meta name="author" content="Tangled"/> 11 + 12 + <!-- Canonical URL --> 13 + <link rel="canonical" href="{{ block "canonical" . }}https://tangled.org{{ .Request.URL.Path }}{{ end }}" /> 14 15 <script defer src="/static/htmx.min.js"></script> 16 <script defer src="/static/htmx-ext-ws.min.js"></script> ··· 24 <link rel="preconnect" href="https://avatar.tangled.sh" /> 25 <link rel="preconnect" href="https://camo.tangled.sh" /> 26 27 + <!-- RSS Feed Discovery --> 28 + {{ block "rss" . }}{{ end }} 29 + 30 <!-- pwa manifest --> 31 <link rel="manifest" href="/pwa-manifest.json" /> 32 ··· 34 <link rel="preload" href="/static/fonts/InterVariable.woff2" as="font" type="font/woff2" crossorigin /> 35 36 <link rel="stylesheet" href="/static/tw.css?{{ cssContentHash }}" type="text/css" /> 37 + <title>{{ block "title" . }}{{ end }}</title> 38 + 39 + <!-- Structured Data --> 40 + {{ block "structuredData" . }} 41 + <script type="application/ld+json"> 42 + { 43 + "@context": "https://schema.org", 44 + "@type": "Organization", 45 + "name": "Tangled", 46 + "url": "https://tangled.org", 47 + "logo": "https://tangled.org/favicon.svg", 48 + "description": "tightly-knit social coding", 49 + "sameAs": [] 50 + } 51 + </script> 52 + {{ end }} 53 + 54 {{ block "extrameta" . }}{{ end }} 55 </head> 56 <body class="min-h-screen flex flex-col gap-4 bg-slate-100 dark:bg-gray-900 dark:text-white transition-colors duration-200">
+20 -1
appview/pages/templates/layouts/profilebase.html
··· 10 <meta property="og:image" content="{{ $avatarUrl }}" /> 11 <meta property="og:image:width" content="512" /> 12 <meta property="og:image:height" content="512" /> 13 - 14 <meta name="twitter:card" content="summary" /> 15 <meta name="twitter:title" content="{{ $handle }}" /> 16 <meta name="twitter:description" content="{{ or .Card.Profile.Description $handle }}" /> 17 <meta name="twitter:image" content="{{ $avatarUrl }}" /> 18 {{ end }} 19 20 {{ define "content" }}
··· 10 <meta property="og:image" content="{{ $avatarUrl }}" /> 11 <meta property="og:image:width" content="512" /> 12 <meta property="og:image:height" content="512" /> 13 + 14 <meta name="twitter:card" content="summary" /> 15 <meta name="twitter:title" content="{{ $handle }}" /> 16 <meta name="twitter:description" content="{{ or .Card.Profile.Description $handle }}" /> 17 <meta name="twitter:image" content="{{ $avatarUrl }}" /> 18 + 19 + <!-- structured data for user profile --> 20 + <script type="application/ld+json"> 21 + { 22 + "@context": "https://schema.org", 23 + "@type": "Person", 24 + "name": "{{ or .Card.Profile.DisplayName .Card.UserHandle .Card.UserDid }}", 25 + "url": "https://tangled.org/{{ or .Card.UserHandle .Card.UserDid }}", 26 + "image": "{{ $avatarUrl }}", 27 + "description": "{{ .Card.Profile.Description }}"{{ if .Card.UserHandle }}, 28 + "identifier": "{{ .Card.UserHandle }}"{{ end }} 29 + } 30 + </script> 31 + {{ end }} 32 + 33 + {{ define "canonical" }}https://tangled.org/{{ or .Card.UserHandle .Card.UserDid }}{{ end }} 34 + 35 + {{ define "rss" }} 36 + <link rel="alternate" type="application/atom+xml" title="{{ or .Card.UserHandle .Card.UserDid }} Activity Feed" href="https://tangled.org/{{ or .Card.UserHandle .Card.UserDid }}/feed.atom" /> 37 {{ end }} 38 39 {{ define "content" }}
+36
appview/pages/templates/repo/index.html
··· 5 {{ template "repo/fragments/meta" . }} 6 7 {{ template "repo/fragments/og" (dict "RepoInfo" .RepoInfo) }} 8 {{ end }} 9 10 {{ define "repoContent" }}
··· 5 {{ template "repo/fragments/meta" . }} 6 7 {{ template "repo/fragments/og" (dict "RepoInfo" .RepoInfo) }} 8 + 9 + <!-- Structured Data for Repository --> 10 + <script type="application/ld+json"> 11 + { 12 + "@context": "https://schema.org", 13 + "@type": "SoftwareSourceCode", 14 + "name": "{{ .RepoInfo.Name }}", 15 + "description": "{{ .RepoInfo.Description }}", 16 + "codeRepository": "https://tangled.org/{{ .RepoInfo.FullName }}", 17 + "programmingLanguage": {{ if .Languages }}{{ range $idx, $lang := .Languages }}{{ if eq $idx 0 }}"{{ $lang.Name }}"{{ end }}{{ end }}{{ else }}"Unknown"{{ end }}, 18 + "url": "https://tangled.org/{{ .RepoInfo.FullName }}", 19 + "author": { 20 + "@type": "Person", 21 + "name": "{{ .RepoInfo.OwnerWithAt }}", 22 + "url": "https://tangled.org/{{ .RepoInfo.OwnerWithAt }}" 23 + }{{ if .RepoInfo.Source }}, 24 + "isBasedOn": { 25 + "@type": "SoftwareSourceCode", 26 + "name": "{{ .RepoInfo.Source.Name }}", 27 + "url": "https://tangled.org/{{ didOrHandle .RepoInfo.Source.Did .RepoInfo.SourceHandle }}/{{ .RepoInfo.Source.Name }}" 28 + }{{ end }} 29 + } 30 + </script> 31 + 32 + <!-- Breadcrumb Navigation --> 33 + {{ template "fragments/breadcrumb" (list 34 + (list "Home" "https://tangled.org") 35 + (list .RepoInfo.OwnerWithAt (printf "https://tangled.org/%s" .RepoInfo.OwnerWithAt)) 36 + (list .RepoInfo.Name (printf "https://tangled.org/%s" .RepoInfo.FullName)) 37 + ) }} 38 + {{ end }} 39 + 40 + {{ define "canonical" }}https://tangled.org/{{ .RepoInfo.FullName }}{{ end }} 41 + 42 + {{ define "rss" }} 43 + <link rel="alternate" type="application/atom+xml" title="{{ .RepoInfo.FullName }} Activity Feed" href="https://tangled.org/{{ .RepoInfo.FullName }}/feed.atom" /> 44 {{ end }} 45 46 {{ define "repoContent" }}
-19
appview/pages/templates/repo/issues/fragments/og.html
··· 1 - {{ define "repo/issues/fragments/og" }} 2 - {{ $title := printf "%s #%d" .Issue.Title .Issue.IssueId }} 3 - {{ $description := or .Issue.Body .RepoInfo.Description }} 4 - {{ $url := printf "https://tangled.org/%s/issues/%d" .RepoInfo.FullName .Issue.IssueId }} 5 - {{ $imageUrl := printf "https://tangled.org/%s/issues/%d/opengraph" .RepoInfo.FullName .Issue.IssueId }} 6 - 7 - <meta property="og:title" content="{{ unescapeHtml $title }}" /> 8 - <meta property="og:type" content="object" /> 9 - <meta property="og:url" content="{{ $url }}" /> 10 - <meta property="og:description" content="{{ $description }}" /> 11 - <meta property="og:image" content="{{ $imageUrl }}" /> 12 - <meta property="og:image:width" content="1200" /> 13 - <meta property="og:image:height" content="600" /> 14 - 15 - <meta name="twitter:card" content="summary_large_image" /> 16 - <meta name="twitter:title" content="{{ unescapeHtml $title }}" /> 17 - <meta name="twitter:description" content="{{ $description }}" /> 18 - <meta name="twitter:image" content="{{ $imageUrl }}" /> 19 - {{ end }}
···
+16 -16
appview/pages/templates/repo/pulls/fragments/og.html
··· 1 - {{ define "repo/pulls/fragments/og" }} 2 - {{ $title := printf "%s #%d" .Pull.Title .Pull.PullId }} 3 - {{ $description := or .Pull.Body .RepoInfo.Description }} 4 - {{ $url := printf "https://tangled.org/%s/pulls/%d" .RepoInfo.FullName .Pull.PullId }} 5 - {{ $imageUrl := printf "https://tangled.org/%s/pulls/%d/opengraph" .RepoInfo.FullName .Pull.PullId }} 6 7 - <meta property="og:title" content="{{ unescapeHtml $title }}" /> 8 - <meta property="og:type" content="object" /> 9 - <meta property="og:url" content="{{ $url }}" /> 10 - <meta property="og:description" content="{{ $description }}" /> 11 - <meta property="og:image" content="{{ $imageUrl }}" /> 12 - <meta property="og:image:width" content="1200" /> 13 - <meta property="og:image:height" content="600" /> 14 15 - <meta name="twitter:card" content="summary_large_image" /> 16 - <meta name="twitter:title" content="{{ unescapeHtml $title }}" /> 17 - <meta name="twitter:description" content="{{ $description }}" /> 18 - <meta name="twitter:image" content="{{ $imageUrl }}" /> 19 {{ end }}
··· 1 + {{ define "pulls/fragments/og" }} 2 + {{ $title := printf "%s #%d" .Pull.Title .Pull.PullId }} 3 + {{ $description := or .Pull.Body .RepoInfo.Description }} 4 + {{ $url := printf "https://tangled.org/%s/pulls/%d" .RepoInfo.FullName .Pull.PullId }} 5 + {{ $imageUrl := printf "https://tangled.org/%s/pulls/%d/opengraph" .RepoInfo.FullName .Pull.PullId }} 6 7 + <meta property="og:title" content="{{ unescapeHtml $title }}" /> 8 + <meta property="og:type" content="object" /> 9 + <meta property="og:url" content="{{ $url }}" /> 10 + <meta property="og:description" content="{{ $description }}" /> 11 + <meta property="og:image" content="{{ $imageUrl }}" /> 12 + <meta property="og:image:width" content="1200" /> 13 + <meta property="og:image:height" content="600" /> 14 15 + <meta name="twitter:card" content="summary_large_image" /> 16 + <meta name="twitter:title" content="{{ unescapeHtml $title }}" /> 17 + <meta name="twitter:description" content="{{ $description }}" /> 18 + <meta name="twitter:image" content="{{ $imageUrl }}" /> 19 {{ end }}
+49 -3
appview/pages/templates/timeline/home.html
··· 1 {{ define "title" }}tangled &middot; tightly-knit social coding{{ end }} 2 3 {{ define "extrameta" }} 4 - <meta property="og:title" content="timeline · tangled" /> 5 - <meta property="og:type" content="object" /> 6 <meta property="og:url" content="https://tangled.org" /> 7 - <meta property="og:description" content="tightly-knit social coding" /> 8 {{ end }} 9 10 11 {{ define "content" }}
··· 1 {{ define "title" }}tangled &middot; tightly-knit social coding{{ end }} 2 3 {{ define "extrameta" }} 4 + {{ $desc := "Collaborate on code with decentralized git hosting, modern contribution and review workflows, and lightweight CI/CD pipelines." }} 5 + {{ $title = "tangled · tightly-knit social coding" }} 6 + 7 + <meta name="description" content="{{ $desc }}" /> 8 + <meta property="og:title" content="{{ $title }}" /> 9 + <meta property="og:type" content="website" /> 10 <meta property="og:url" content="https://tangled.org" /> 11 + <meta property="og:description" content="Decentralized git hosting with improved pull requests and lightweight CI/CD. Host repositories on your own infrastructure." /> 12 + <meta property="og:image" content="https://assets.tangled.network/tangled_og.png" /> 13 + <meta property="og:image:width" content="1200" /> 14 + <meta property="og:image:height" content="630" /> 15 + 16 + <meta name="twitter:card" content="summary_large_image" /> 17 + <meta name="twitter:title" content="{{ $title }}" /> 18 + <meta name="twitter:description" content="{{ $desc }}" /> 19 + <meta name="twitter:image" content="https://assets.tangled.network/tangled_og.png" /> 20 + 21 + <!-- Enhanced Structured Data for Homepage --> 22 + <script type="application/ld+json"> 23 + { 24 + "@context": "https://schema.org", 25 + "@type": "WebSite", 26 + "name": "Tangled", 27 + "alternateName": "Tangled", 28 + "url": "https://tangled.org", 29 + "description": "{{ $desc }}", 30 + "potentialAction": { 31 + "@type": "SearchAction", 32 + "target": "https://tangled.org/?q={search_term_string}", 33 + "query-input": "required name=search_term_string" 34 + } 35 + } 36 + </script> 37 + <script type="application/ld+json"> 38 + { 39 + "@context": "https://schema.org", 40 + "@type": "SoftwareApplication", 41 + "name": "Tangled", 42 + "applicationCategory": "DeveloperTool", 43 + "offers": { 44 + "@type": "Offer", 45 + "price": "0", 46 + "priceCurrency": "USD" 47 + }, 48 + "operatingSystem": "Web", 49 + "description": "{{ $desc }}" 50 + } 51 + </script> 52 {{ end }} 53 + 54 + {{ define "canonical" }}https://tangled.org{{ end }} 55 56 57 {{ define "content" }}
+1
appview/state/router.go
··· 34 35 router.Get("/pwa-manifest.json", s.WebAppManifest) 36 router.Get("/robots.txt", s.RobotsTxt) 37 38 userRouter := s.UserRouter(&middleware) 39 standardRouter := s.StandardRouter(&middleware)
··· 34 35 router.Get("/pwa-manifest.json", s.WebAppManifest) 36 router.Get("/robots.txt", s.RobotsTxt) 37 + router.Get("/sitemap.xml", s.Sitemap) 38 39 userRouter := s.UserRouter(&middleware) 40 standardRouter := s.StandardRouter(&middleware)
+76
appview/state/state.go
··· 208 209 robotsTxt := `User-agent: * 210 Allow: / 211 ` 212 w.Write([]byte(robotsTxt)) 213 } 214 215 func (s *State) TermsOfService(w http.ResponseWriter, r *http.Request) {
··· 208 209 robotsTxt := `User-agent: * 210 Allow: / 211 + Disallow: /settings 212 + Disallow: /notifications 213 + Disallow: /login 214 + Disallow: /logout 215 + Disallow: /signup 216 + Disallow: /oauth 217 + Disallow: */settings$ 218 + Disallow: */settings/* 219 + 220 + Crawl-delay: 1 221 + 222 + Sitemap: https://tangled.org/sitemap.xml 223 ` 224 w.Write([]byte(robotsTxt)) 225 + } 226 + 227 + func (s *State) Sitemap(w http.ResponseWriter, r *http.Request) { 228 + w.Header().Set("Content-Type", "application/xml; charset=utf-8") 229 + w.Header().Set("Cache-Control", "public, max-age=3600") 230 + 231 + // basic sitemap with static pages 232 + sitemap := `<?xml version="1.0" encoding="UTF-8"?> 233 + <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> 234 + <url> 235 + <loc>https://tangled.org</loc> 236 + <changefreq>daily</changefreq> 237 + <priority>1.0</priority> 238 + </url> 239 + <url> 240 + <loc>https://tangled.org/timeline</loc> 241 + <changefreq>hourly</changefreq> 242 + <priority>0.9</priority> 243 + </url> 244 + <url> 245 + <loc>https://tangled.org/goodfirstissues</loc> 246 + <changefreq>daily</changefreq> 247 + <priority>0.8</priority> 248 + </url> 249 + <url> 250 + <loc>https://tangled.org/terms</loc> 251 + <changefreq>monthly</changefreq> 252 + <priority>0.3</priority> 253 + </url> 254 + <url> 255 + <loc>https://tangled.org/privacy</loc> 256 + <changefreq>monthly</changefreq> 257 + <priority>0.3</priority> 258 + </url> 259 + <url> 260 + <loc>https://tangled.org/brand</loc> 261 + <changefreq>monthly</changefreq> 262 + <priority>0.5</priority> 263 + </url> 264 + </urlset>` 265 + w.Write([]byte(sitemap)) 266 + } 267 + 268 + // https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Manifest 269 + const manifestJson = `{ 270 + "name": "tangled", 271 + "description": "tightly-knit social coding.", 272 + "icons": [ 273 + { 274 + "src": "/favicon.svg", 275 + "sizes": "144x144" 276 + } 277 + ], 278 + "start_url": "/", 279 + "id": "org.tangled", 280 + 281 + "display": "standalone", 282 + "background_color": "#111827", 283 + "theme_color": "#111827" 284 + }` 285 + 286 + func (p *State) PWAManifest(w http.ResponseWriter, r *http.Request) { 287 + w.Header().Set("Content-Type", "application/json") 288 + w.Write([]byte(manifestJson)) 289 } 290 291 func (s *State) TermsOfService(w http.ResponseWriter, r *http.Request) {