diff --git a/pkg/appview/handlers/auth.go b/pkg/appview/handlers/auth.go
index 3c5bb0d..64e5841 100644
--- a/pkg/appview/handlers/auth.go
+++ b/pkg/appview/handlers/auth.go
@@ -17,12 +17,19 @@ func (h *LoginHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
returnTo = "/"
}
+ meta := NewPageMeta(
+ "Login - ATCR",
+ "Sign in to ATCR with your AT Protocol account to push and pull container images",
+ ).WithCanonical("https://" + h.RegistryURL + "/login")
+
data := struct {
PageData
+ Meta *PageMeta
ReturnTo string
Error string
}{
PageData: NewPageData(r, h.RegistryURL),
+ Meta: meta,
ReturnTo: returnTo,
Error: r.URL.Query().Get("error"),
}
diff --git a/pkg/appview/handlers/errors.go b/pkg/appview/handlers/errors.go
index 1fe1497..fe9127e 100644
--- a/pkg/appview/handlers/errors.go
+++ b/pkg/appview/handlers/errors.go
@@ -19,10 +19,17 @@ func (h *NotFoundHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
func RenderNotFound(w http.ResponseWriter, r *http.Request, templates *template.Template, registryURL string) {
w.WriteHeader(http.StatusNotFound)
+ meta := NewPageMeta(
+ "404 - Lost at Sea | ATCR",
+ "Page not found - the requested resource doesn't exist on ATCR",
+ ).WithRobots("noindex")
+
data := struct {
PageData
+ Meta *PageMeta
}{
PageData: NewPageData(r, registryURL),
+ Meta: meta,
}
if err := templates.ExecuteTemplate(w, "404", data); err != nil {
diff --git a/pkg/appview/handlers/home.go b/pkg/appview/handlers/home.go
index 2276e89..fcd3ea4 100644
--- a/pkg/appview/handlers/home.go
+++ b/pkg/appview/handlers/home.go
@@ -41,10 +41,21 @@ func (h *HomeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
data := struct {
PageData
+ Meta *PageMeta
FeaturedRepos []db.RepoCardData
RecentRepos []db.RepoCardData
}{
- PageData: NewPageData(r, h.RegistryURL),
+ PageData: NewPageData(r, h.RegistryURL),
+ Meta: NewPageMeta(
+ "ATCR - Distributed Container Registry",
+ "Push and pull Docker images on the AT Protocol. Same Docker, decentralized.",
+ ).
+ WithCanonical("https://"+h.RegistryURL+"/").
+ WithOGImage("https://"+h.RegistryURL+"/og/home").
+ WithJSONLD(
+ NewJSONLDOrganization(h.RegistryURL),
+ NewJSONLDWebSite(h.RegistryURL),
+ ),
FeaturedRepos: featuredCards,
RecentRepos: recentCards,
}
diff --git a/pkg/appview/handlers/install.go b/pkg/appview/handlers/install.go
index 1136c37..bc055c9 100644
--- a/pkg/appview/handlers/install.go
+++ b/pkg/appview/handlers/install.go
@@ -10,10 +10,17 @@ type InstallHandler struct {
}
func (h *InstallHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ meta := NewPageMeta(
+ "Install ATCR Credential Helper - ATCR",
+ "Install the ATCR credential helper to push and pull containers using your AT Protocol identity",
+ ).WithCanonical("https://" + h.RegistryURL + "/install")
+
data := struct {
PageData
+ Meta *PageMeta
}{
PageData: NewPageData(r, h.RegistryURL),
+ Meta: meta,
}
if err := h.Templates.ExecuteTemplate(w, "install", data); err != nil {
diff --git a/pkg/appview/handlers/jsonld.go b/pkg/appview/handlers/jsonld.go
new file mode 100644
index 0000000..080660d
--- /dev/null
+++ b/pkg/appview/handlers/jsonld.go
@@ -0,0 +1,152 @@
+package handlers
+
+// JSON-LD structured data types for rich results in search engines.
+// These are marshaled to JSON and embedded in
+ {{ end }}
+{{ end }}
diff --git a/pkg/appview/templates/pages/404.html b/pkg/appview/templates/pages/404.html
index cd3e246..5d5d61f 100644
--- a/pkg/appview/templates/pages/404.html
+++ b/pkg/appview/templates/pages/404.html
@@ -2,10 +2,8 @@
- 404 - Lost at Sea | ATCR
-
-
{{ template "head" . }}
+ {{ template "meta" .Meta }}
{{ template "nav-simple" . }}
diff --git a/pkg/appview/templates/pages/home.html b/pkg/appview/templates/pages/home.html
index 59a278b..15d41e3 100644
--- a/pkg/appview/templates/pages/home.html
+++ b/pkg/appview/templates/pages/home.html
@@ -2,53 +2,8 @@
- ATCR - Distributed Container Registry
{{ template "head" . }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ {{ template "meta" .Meta }}
{{ if not .User }}
{{ end }}
diff --git a/pkg/appview/templates/pages/install.html b/pkg/appview/templates/pages/install.html
index 4cafe6d..de255b4 100644
--- a/pkg/appview/templates/pages/install.html
+++ b/pkg/appview/templates/pages/install.html
@@ -2,10 +2,8 @@
- Install ATCR Credential Helper - ATCR
-
-
{{ template "head" . }}
+ {{ template "meta" .Meta }}
{{ template "nav" . }}
diff --git a/pkg/appview/templates/pages/learn-more.html b/pkg/appview/templates/pages/learn-more.html
index 0f27443..2541e47 100644
--- a/pkg/appview/templates/pages/learn-more.html
+++ b/pkg/appview/templates/pages/learn-more.html
@@ -2,14 +2,8 @@
- About ATCR - Decentralized Container Registry on AT Protocol
-
-
-
-
-
-
{{ template "head" . }}
+ {{ template "meta" .Meta }}
{{ template "nav" . }}
diff --git a/pkg/appview/templates/pages/login.html b/pkg/appview/templates/pages/login.html
index fdd16dc..d3b2318 100644
--- a/pkg/appview/templates/pages/login.html
+++ b/pkg/appview/templates/pages/login.html
@@ -2,10 +2,8 @@
- Login - ATCR
-
-
{{ template "head" . }}
+ {{ template "meta" .Meta }}
{{ template "nav-simple" . }}
diff --git a/pkg/appview/templates/pages/privacy.html b/pkg/appview/templates/pages/privacy.html
index 6998e9b..fb68829 100644
--- a/pkg/appview/templates/pages/privacy.html
+++ b/pkg/appview/templates/pages/privacy.html
@@ -2,10 +2,8 @@
- Privacy Policy - ATCR
-
-
{{ template "head" . }}
+ {{ template "meta" .Meta }}
{{ template "nav" . }}
diff --git a/pkg/appview/templates/pages/repository.html b/pkg/appview/templates/pages/repository.html
index 768b9fe..f2897e4 100644
--- a/pkg/appview/templates/pages/repository.html
+++ b/pkg/appview/templates/pages/repository.html
@@ -2,46 +2,8 @@
- {{ if .Repository.Title }}{{ .Repository.Title }}{{ else }}{{ .Owner.Handle }}/{{ .Repository.Name }}{{ end }} - ATCR
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
{{ template "head" . }}
+ {{ template "meta" .Meta }}
{{ template "nav" . }}
diff --git a/pkg/appview/templates/pages/search.html b/pkg/appview/templates/pages/search.html
index ea1619d..fdaa9f8 100644
--- a/pkg/appview/templates/pages/search.html
+++ b/pkg/appview/templates/pages/search.html
@@ -2,15 +2,8 @@
- Search{{ if .SearchQuery }}: {{ .SearchQuery }}{{ end }} - ATCR
-
-
-
-
-
-
-
{{ template "head" . }}
+ {{ template "meta" .Meta }}
{{ template "nav" . }}
diff --git a/pkg/appview/templates/pages/settings.html b/pkg/appview/templates/pages/settings.html
index 0d1943d..5d88eb8 100644
--- a/pkg/appview/templates/pages/settings.html
+++ b/pkg/appview/templates/pages/settings.html
@@ -2,10 +2,8 @@
- Settings - ATCR
-
-
{{ template "head" . }}
+ {{ template "meta" .Meta }}
{{ template "nav" . }}
diff --git a/pkg/appview/templates/pages/terms.html b/pkg/appview/templates/pages/terms.html
index f9b2fa1..335f7e0 100644
--- a/pkg/appview/templates/pages/terms.html
+++ b/pkg/appview/templates/pages/terms.html
@@ -2,10 +2,8 @@
- Terms of Service - ATCR
-
-
{{ template "head" . }}
+ {{ template "meta" .Meta }}
{{ template "nav" . }}
diff --git a/pkg/appview/templates/pages/user.html b/pkg/appview/templates/pages/user.html
index 5350cb7..a524237 100644
--- a/pkg/appview/templates/pages/user.html
+++ b/pkg/appview/templates/pages/user.html
@@ -2,37 +2,8 @@
- {{ .ViewedUser.Handle }} - ATCR
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
{{ template "head" . }}
+ {{ template "meta" .Meta }}
{{ template "nav" . }}
diff --git a/pkg/appview/ui.go b/pkg/appview/ui.go
index 0f4cb03..1702b25 100644
--- a/pkg/appview/ui.go
+++ b/pkg/appview/ui.go
@@ -3,6 +3,7 @@ package appview
import (
"crypto/md5"
"embed"
+ "encoding/json"
"fmt"
"html/template"
"io/fs"
@@ -148,7 +149,7 @@ func Templates() (*template.Template, error) {
return imgURL
}
// Cloudflare uses /cdn-cgi/image/width=X/ path format
- parsed.Path = fmt.Sprintf("/cdn-cgi/image/width=%d%s", width, parsed.Path)
+ parsed.Path = fmt.Sprintf("/cdn-cgi/image/width=%d,format=auto%s", width, parsed.Path)
return parsed.String()
},
@@ -172,6 +173,20 @@ func Templates() (*template.Template, error) {
template.HTMLEscapeString(name),
))
},
+
+ // jsonld marshals a value to indented JSON for JSON-LD script tags
+ // Usage: {{ jsonld .SomeStruct }}
+ "jsonld": func(v any) template.HTML {
+ // If v is already a string, assume it's pre-formatted JSON
+ if s, ok := v.(string); ok {
+ return template.HTML(s)
+ }
+ b, err := json.MarshalIndent(v, " ", " ")
+ if err != nil {
+ return template.HTML("{}")
+ }
+ return template.HTML(b)
+ },
}
tmpl := template.New("").Funcs(funcMap)
diff --git a/pkg/appview/ui_test.go b/pkg/appview/ui_test.go
index 360903a..89662d2 100644
--- a/pkg/appview/ui_test.go
+++ b/pkg/appview/ui_test.go
@@ -764,8 +764,7 @@ func TestTemplateExecution_Alert(t *testing.T) {
}
data := map[string]string{
- "Class": "success",
- "Icon": "check",
+ "Type": "success",
"Message": "Operation completed!",
}
@@ -794,3 +793,171 @@ func TestPublicHandler(t *testing.T) {
// Further testing would require HTTP request/response testing
// which is typically done in integration tests
}
+
+func TestJSONLD(t *testing.T) {
+ tests := []struct {
+ name string
+ input any
+ expectContains []string
+ expectMissing []string
+ }{
+ {
+ name: "struct input - marshals to JSON",
+ input: struct {
+ Context string `json:"@context"`
+ Type string `json:"@type"`
+ Name string `json:"name"`
+ }{
+ Context: "https://schema.org",
+ Type: "Organization",
+ Name: "ATCR",
+ },
+ expectContains: []string{
+ `"@context": "https://schema.org"`,
+ `"@type": "Organization"`,
+ `"name": "ATCR"`,
+ },
+ expectMissing: []string{
+ `\"`, // Should NOT contain escaped quotes (double-encoding)
+ `\n`, // Should NOT contain escaped newlines
+ },
+ },
+ {
+ name: "string input - returns as-is without re-encoding",
+ input: `{"@context": "https://schema.org", "@type": "Thing"}`,
+ expectContains: []string{
+ `{"@context": "https://schema.org", "@type": "Thing"}`,
+ },
+ expectMissing: []string{
+ `\"`, // Should NOT have escaped quotes
+ `\n`, // Should NOT have escaped newlines
+ },
+ },
+ {
+ name: "pre-formatted JSON string - no double encoding",
+ input: "{\n \"@context\": \"https://schema.org\"\n}",
+ expectContains: []string{
+ `"@context": "https://schema.org"`,
+ },
+ expectMissing: []string{
+ `\\n`, // Should NOT have double-escaped newlines
+ `\\"`, // Should NOT have double-escaped quotes
+ },
+ },
+ {
+ name: "nested struct - proper indentation",
+ input: struct {
+ Context string `json:"@context"`
+ Author struct {
+ Type string `json:"@type"`
+ Name string `json:"name"`
+ } `json:"author"`
+ }{
+ Context: "https://schema.org",
+ Author: struct {
+ Type string `json:"@type"`
+ Name string `json:"name"`
+ }{
+ Type: "Person",
+ Name: "Alice",
+ },
+ },
+ expectContains: []string{
+ `"@context": "https://schema.org"`,
+ `"author": {`,
+ `"@type": "Person"`,
+ `"name": "Alice"`,
+ },
+ },
+ {
+ name: "empty struct - returns empty JSON object",
+ input: struct{}{},
+ expectContains: []string{
+ `{}`,
+ },
+ },
+ {
+ name: "empty string - returns empty string",
+ input: "",
+ expectContains: []string{
+ ``,
+ },
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ tmpl, err := Templates()
+ if err != nil {
+ t.Fatalf("Templates() error = %v", err)
+ }
+
+ templateStr := `{{ jsonld . }}`
+ buf := new(bytes.Buffer)
+ temp, err := tmpl.New("test").Parse(templateStr)
+ if err != nil {
+ t.Fatalf("Failed to parse template: %v", err)
+ }
+
+ err = temp.Execute(buf, tt.input)
+ if err != nil {
+ t.Fatalf("Failed to execute template: %v", err)
+ }
+
+ got := buf.String()
+
+ for _, expected := range tt.expectContains {
+ if !strings.Contains(got, expected) {
+ t.Errorf("jsonld output missing expected %q\nGot: %s", expected, got)
+ }
+ }
+
+ for _, notExpected := range tt.expectMissing {
+ if strings.Contains(got, notExpected) {
+ t.Errorf("jsonld output should not contain %q\nGot: %s", notExpected, got)
+ }
+ }
+ })
+ }
+}
+
+func TestJSONLD_Indentation(t *testing.T) {
+ // Test that the indentation uses 8-space prefix (for alignment with