page rank/speed/seo improvements

This commit is contained in:
Evan Jarrett
2026-01-16 23:19:41 -06:00
parent 2d7d2fd5ca
commit dbe0efd949
21 changed files with 201 additions and 16 deletions

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,33 @@
# ATCR Container Registry - robots.txt
User-agent: *
# Allow public pages
Allow: /
Allow: /r/
Allow: /u/
Allow: /search
Allow: /install
Allow: /privacy
Allow: /terms
# Block API endpoints
Disallow: /api/
Disallow: /v2/
# Block auth and session pages
Disallow: /auth/
Disallow: /login
Disallow: /settings
Disallow: /device/
# Block OG image generation endpoints
Disallow: /og/
# Block static assets from indexing (optional, saves crawl budget)
Disallow: /js/
Disallow: /css/
Disallow: /static/
# Sitemap
Sitemap: https://atcr.io/sitemap.xml

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://atcr.io/</loc>
<changefreq>daily</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://atcr.io/search</loc>
<changefreq>daily</changefreq>
<priority>0.8</priority>
</url>
<url>
<loc>https://atcr.io/install</loc>
<changefreq>monthly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://atcr.io/privacy</loc>
<changefreq>yearly</changefreq>
<priority>0.3</priority>
</url>
<url>
<loc>https://atcr.io/terms</loc>
<changefreq>yearly</changefreq>
<priority>0.3</priority>
</url>
</urlset>

View File

@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<sitemap>
<loc>https://atcr.io/sitemap-static.xml</loc>
</sitemap>
<!-- Future: uncomment when dynamic generation is implemented
<sitemap>
<loc>https://atcr.io/sitemap-users.xml</loc>
</sitemap>
<sitemap>
<loc>https://atcr.io/sitemap-repos.xml</loc>
</sitemap>
-->
</sitemapindex>

View File

@@ -8,7 +8,7 @@
<div class="cmd group" onclick="event.stopPropagation()">
<i data-lucide="terminal" class="size-4 shrink-0 text-base-content/60"></i>
<code>{{ . }}</code>
<button class="btn btn-ghost btn-xs absolute right-2 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity" onclick="event.stopPropagation(); copyToClipboard(this.getAttribute('data-cmd'))" data-cmd="{{ . }}">
<button class="btn btn-ghost btn-xs absolute right-2 top-1/2 -translate-y-1/2 opacity-0 group-hover:opacity-100 transition-opacity" onclick="event.stopPropagation(); copyToClipboard(this.getAttribute('data-cmd'))" data-cmd="{{ . }}" aria-label="Copy command to clipboard">
<i data-lucide="copy" class="size-4"></i>
</button>
</div>

View File

@@ -1,6 +1,10 @@
{{ define "head" }}
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="robots" content="index, follow">
<meta name="theme-color" content="#570df8" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#661ae6" media="(prefers-color-scheme: dark)">
<meta property="og:locale" content="en_US">
<!-- Favicons -->
<link rel="icon" type="image/png" href="/favicon-96x96.png" sizes="96x96" />

View File

@@ -5,7 +5,7 @@
*/}}
<section class="hero bg-base-200 min-h-[60vh] py-16 pb-24 relative overflow-hidden">
<!-- Background mascot -->
<img src="/amathea_manatee.png" alt="" class="absolute right-1/2 translate-x-1/2 top-[12%] md:top-[18%] lg:right-[20%] lg:top-[40%] -translate-y-1/2 w-96 md:w-md lg:w-xl opacity-30 pointer-events-none select-none" aria-hidden="true">
<img src="/amathea_manatee.png" alt="" fetchpriority="high" class="absolute right-1/2 translate-x-1/2 top-[12%] md:top-[18%] lg:right-[20%] lg:top-[40%] -translate-y-1/2 w-96 md:w-md lg:w-xl opacity-30 pointer-events-none select-none" aria-hidden="true">
<div class="hero-content text-center flex-col relative z-10">
<h1 class="text-4xl md:text-5xl font-bold">your registry <span class="text-primary">at</span> sea.</h1>
<p class="text-lg text-base-content/70 max-w-lg mt-4">
@@ -31,7 +31,7 @@
<div class="text-primary mb-4 flex justify-center">
<i data-lucide="{{ .Icon }}" class="size-8"></i>
</div>
<h3 class="font-semibold text-lg">{{ .Title }}</h3>
<h2 class="font-semibold text-lg">{{ .Title }}</h2>
<p class="text-base-content/70 mt-2">{{ .Description }}</p>
</div>
{{ end }}

View File

@@ -1,7 +1,7 @@
{{ define "manifest-modal" }}
<dialog class="modal modal-open" onclick="if(event.target===this)this.remove()">
<div class="modal-box">
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" onclick="this.closest('dialog').remove()"></button>
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" onclick="this.closest('dialog').remove()" aria-label="Close dialog"></button>
<h2 class="text-xl font-semibold mb-4">Manifest Details</h2>

View File

@@ -15,7 +15,7 @@
</div>
{{ end }}
{{ if .IsOwner }}
<label class="absolute inset-0 flex items-center justify-center bg-black/50 opacity-0 hover:opacity-100 transition-opacity cursor-pointer rounded-lg" for="avatar-upload">
<label class="absolute inset-0 flex items-center justify-center bg-black/50 opacity-0 hover:opacity-100 transition-opacity cursor-pointer rounded-lg" for="avatar-upload" aria-label="Upload repository icon">
<i data-lucide="plus" class="size-8 text-white"></i>
</label>
<input type="file" id="avatar-upload" name="avatar"

View File

@@ -20,9 +20,9 @@
<div class="card card-interactive bg-base-200 border-2 border-base-300 p-6 flex flex-col justify-between min-h-60 w-full" onclick="window.location='/r/{{ .OwnerHandle }}/{{ .Repository }}'">
<div class="flex gap-4 items-start">
{{ if .IconURL }}
<img src="{{ .IconURL }}" alt="{{ .Repository }}" class="w-12 rounded-lg object-cover shrink-0">
<img src="{{ resizeImage .IconURL 96 }}" alt="{{ .Repository }}" loading="lazy" class="w-12 rounded-lg object-cover shrink-0">
{{ else if .OwnerAvatarURL }}
<img src="{{ .OwnerAvatarURL }}" alt="{{ .OwnerHandle }}" class="w-12 rounded-lg object-cover shrink-0">
<img src="{{ resizeImage .OwnerAvatarURL 96 }}" alt="{{ .OwnerHandle }}" loading="lazy" class="w-12 rounded-lg object-cover shrink-0">
{{ else }}
<div class="avatar avatar-placeholder">
<div class="bg-neutral text-neutral-content w-12 rounded-lg shadow-sm uppercase">

View File

@@ -3,6 +3,8 @@
<html lang="en">
<head>
<title>404 - Lost at Sea | ATCR</title>
<meta name="description" content="Page not found - the requested resource doesn't exist on ATCR">
<meta name="robots" content="noindex">
{{ template "head" . }}
</head>
<body>

View File

@@ -3,6 +3,7 @@
<html lang="en">
<head>
<title>ATCR - Distributed Container Registry</title>
<meta name="description" content="Push and pull Docker images on the AT Protocol. Same Docker, decentralized.">
<!-- Open Graph -->
<meta property="og:title" content="ATCR - Distributed Container Registry">
<meta property="og:description" content="Push and pull Docker images on the AT Protocol. Same Docker, decentralized.">
@@ -17,6 +18,36 @@
<meta name="twitter:title" content="ATCR - Distributed Container Registry">
<meta name="twitter:description" content="Push and pull Docker images on the AT Protocol. Same Docker, decentralized.">
<meta name="twitter:image" content="https://{{ .RegistryURL }}/og/home">
<link rel="canonical" href="https://{{ .RegistryURL }}/">
<!-- JSON-LD Structured Data -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Organization",
"name": "ATCR",
"alternateName": "AT Protocol Container Registry",
"url": "https://{{ .RegistryURL }}",
"logo": "https://{{ .RegistryURL }}/favicon.svg",
"description": "Decentralized container registry using AT Protocol. Push and pull Docker images with your AT Protocol identity.",
"sameAs": []
}
</script>
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "WebSite",
"name": "ATCR",
"url": "https://{{ .RegistryURL }}",
"potentialAction": {
"@type": "SearchAction",
"target": {
"@type": "EntryPoint",
"urlTemplate": "https://{{ .RegistryURL }}/search?q={search_term_string}"
},
"query-input": "required name=search_term_string"
}
}
</script>
{{ template "head" . }}
</head>
<body>
@@ -34,10 +65,10 @@
<div class="flex justify-between items-center mb-6">
<h2 class="text-2xl font-bold">Featured</h2>
<div class="flex gap-2">
<button id="carousel-prev" class="btn btn-circle btn-ghost btn-sm">
<button id="carousel-prev" class="btn btn-circle btn-ghost btn-sm" aria-label="Previous featured repository">
<i data-lucide="chevron-left" class="size-5"></i>
</button>
<button id="carousel-next" class="btn btn-circle btn-ghost btn-sm">
<button id="carousel-next" class="btn btn-circle btn-ghost btn-sm" aria-label="Next featured repository">
<i data-lucide="chevron-right" class="size-5"></i>
</button>
</div>

View File

@@ -3,6 +3,8 @@
<html lang="en">
<head>
<title>Install ATCR Credential Helper - ATCR</title>
<meta name="description" content="Install the ATCR credential helper to push and pull containers using your AT Protocol identity">
<link rel="canonical" href="https://{{ .RegistryURL }}/install">
{{ template "head" . }}
</head>
<body>

View File

@@ -3,6 +3,8 @@
<html lang="en">
<head>
<title>Login - ATCR</title>
<meta name="description" content="Sign in to ATCR with your AT Protocol account to push and pull container images">
<link rel="canonical" href="https://{{ .RegistryURL }}/login">
{{ template "head" . }}
</head>
<body>

View File

@@ -3,6 +3,8 @@
<html lang="en">
<head>
<title>Privacy Policy - ATCR</title>
<meta name="description" content="ATCR privacy policy - how we collect, use, and protect your data on the decentralized container registry">
<link rel="canonical" href="https://{{ .RegistryURL }}/privacy">
{{ template "head" . }}
</head>
<body>

View File

@@ -3,6 +3,8 @@
<html lang="en">
<head>
<title>{{ if .Repository.Title }}{{ .Repository.Title }}{{ else }}{{ .Owner.Handle }}/{{ .Repository.Name }}{{ end }} - ATCR</title>
<meta name="description" content="{{ if .Repository.Description }}{{ .Repository.Description }}{{ else }}Container image {{ .Owner.Handle }}/{{ .Repository.Name }} on ATCR{{ end }}">
<link rel="canonical" href="https://{{ .RegistryURL }}/r/{{ .Owner.Handle }}/{{ .Repository.Name }}">
<!-- Open Graph -->
<meta property="og:title" content="{{ .Owner.Handle }}/{{ .Repository.Name }} - ATCR">
<meta property="og:description" content="{{ if .Repository.Description }}{{ .Repository.Description }}{{ else }}Container image on ATCR{{ end }}">
@@ -17,6 +19,28 @@
<meta name="twitter:title" content="{{ .Owner.Handle }}/{{ .Repository.Name }} - ATCR">
<meta name="twitter:description" content="{{ if .Repository.Description }}{{ .Repository.Description }}{{ else }}Container image on ATCR{{ end }}">
<meta name="twitter:image" content="https://{{ .RegistryURL }}/og/r/{{ .Owner.Handle }}/{{ .Repository.Name }}">
<!-- JSON-LD Structured Data -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "SoftwareSourceCode",
"name": "{{ .Owner.Handle }}/{{ .Repository.Name }}",
"description": {{ if .Repository.Description }}"{{ .Repository.Description }}"{{ else }}"Container image on ATCR"{{ end }},
"codeRepository": "https://{{ .RegistryURL }}/r/{{ .Owner.Handle }}/{{ .Repository.Name }}",
"author": {
"@type": "Person",
"name": "{{ .Owner.Handle }}",
"url": "https://{{ .RegistryURL }}/u/{{ .Owner.Handle }}"
},
"publisher": {
"@type": "Organization",
"name": "ATCR",
"url": "https://{{ .RegistryURL }}"
}{{ if .Repository.Licenses }},
"license": "{{ .Repository.Licenses }}"{{ end }}{{ if .Repository.SourceURL }},
"isBasedOn": "{{ .Repository.SourceURL }}"{{ end }}
}
</script>
{{ template "head" . }}
</head>
<body>
@@ -87,7 +111,7 @@
<!-- Pull Command -->
<div class="space-y-2">
{{ if eq .ArtifactType "helm-chart" }}
<h3 class="font-semibold">Pull this chart</h3>
<p class="font-semibold">Pull this chart</p>
{{ if .Tags }}
{{ $firstTag := index .Tags 0 }}
{{ template "docker-command" (print "helm pull oci://" $.RegistryURL "/" $.Owner.Handle "/" $.Repository.Name " --version " $firstTag.Tag.Tag) }}
@@ -95,7 +119,7 @@
{{ template "docker-command" (print "helm pull oci://" $.RegistryURL "/" $.Owner.Handle "/" $.Repository.Name) }}
{{ end }}
{{ else }}
<h3 class="font-semibold">Pull this image</h3>
<p class="font-semibold">Pull this image</p>
{{ if .Tags }}
{{ $firstTag := index .Tags 0 }}
{{ template "docker-command" (print "docker pull " $.RegistryURL "/" $.Owner.Handle "/" $.Repository.Name ":" $firstTag.Tag.Tag) }}
@@ -149,7 +173,8 @@
hx-delete="/api/images/{{ $.Repository.Name }}/tags/{{ .Tag.Tag }}"
hx-confirm="Delete tag {{ .Tag.Tag }}?"
hx-target="#tag-{{ sanitizeID .Tag.Tag }}"
hx-swap="outerHTML">
hx-swap="outerHTML"
aria-label="Delete tag">
<i data-lucide="trash-2" class="size-4"></i>
</button>
{{ end }}
@@ -159,7 +184,7 @@
<div class="flex flex-wrap justify-between items-center gap-2">
<div class="flex items-center gap-2">
<code class="font-mono text-xs text-base-content/60 truncate max-w-40" title="{{ .Tag.Digest }}">{{ .Tag.Digest }}</code>
<button class="btn btn-ghost btn-xs" onclick="copyToClipboard('{{ .Tag.Digest }}')"><i data-lucide="copy" class="size-3"></i></button>
<button class="btn btn-ghost btn-xs" onclick="copyToClipboard('{{ .Tag.Digest }}')" aria-label="Copy digest to clipboard"><i data-lucide="copy" class="size-3"></i></button>
</div>
{{ if .Platforms }}
<div class="flex flex-wrap gap-1">
@@ -222,7 +247,7 @@
</div>
<div class="flex items-center gap-2">
<code class="font-mono text-xs text-base-content/60 truncate max-w-40" title="{{ .Manifest.Digest }}">{{ .Manifest.Digest }}</code>
<button class="btn btn-ghost btn-xs" onclick="copyToClipboard('{{ .Manifest.Digest }}')"><i data-lucide="copy" class="size-3"></i></button>
<button class="btn btn-ghost btn-xs" onclick="copyToClipboard('{{ .Manifest.Digest }}')" aria-label="Copy digest to clipboard"><i data-lucide="copy" class="size-3"></i></button>
</div>
</div>
<div class="flex items-center gap-2">
@@ -231,7 +256,8 @@
</time>
{{ if $.IsOwner }}
<button class="btn btn-ghost btn-sm text-error"
onclick="deleteManifest('{{ $.Repository.Name }}', '{{ .Manifest.Digest }}', '{{ sanitizeID .Manifest.Digest }}')">
onclick="deleteManifest('{{ $.Repository.Name }}', '{{ .Manifest.Digest }}', '{{ sanitizeID .Manifest.Digest }}')"
aria-label="Delete manifest">
<i data-lucide="trash-2" class="size-4"></i>
</button>
{{ end }}

View File

@@ -3,6 +3,13 @@
<html lang="en">
<head>
<title>Search{{ if .SearchQuery }}: {{ .SearchQuery }}{{ end }} - ATCR</title>
<meta name="description" content="{{ if .SearchQuery }}Search results for '{{ .SearchQuery }}' on ATCR container registry{{ else }}Search for container images on ATCR, the decentralized container registry{{ end }}">
<!-- Open Graph -->
<meta property="og:title" content="Search{{ if .SearchQuery }}: {{ .SearchQuery }}{{ end }} - ATCR">
<meta property="og:description" content="{{ if .SearchQuery }}Search results for '{{ .SearchQuery }}' on ATCR container registry{{ else }}Search for container images on ATCR{{ end }}">
<meta property="og:type" content="website">
<meta property="og:url" content="https://{{ .RegistryURL }}/search{{ if .SearchQuery }}?q={{ .SearchQuery }}{{ end }}">
<meta property="og:site_name" content="ATCR">
{{ template "head" . }}
</head>
<body>

View File

@@ -3,6 +3,8 @@
<html lang="en">
<head>
<title>Settings - ATCR</title>
<meta name="description" content="Manage your ATCR account settings, authorized devices, and storage preferences">
<meta name="robots" content="noindex">
{{ template "head" . }}
</head>
<body>

View File

@@ -3,6 +3,8 @@
<html lang="en">
<head>
<title>Terms of Service - ATCR</title>
<meta name="description" content="ATCR terms of service - rules and guidelines for using the decentralized container registry">
<link rel="canonical" href="https://{{ .RegistryURL }}/terms">
{{ template "head" . }}
</head>
<body>

View File

@@ -3,6 +3,8 @@
<html lang="en">
<head>
<title>{{ .ViewedUser.Handle }} - ATCR</title>
<meta name="description" content="Container images by {{ .ViewedUser.Handle }} on ATCR, the decentralized container registry">
<link rel="canonical" href="https://{{ .RegistryURL }}/u/{{ .ViewedUser.Handle }}">
<!-- Open Graph -->
<meta property="og:title" content="{{ .ViewedUser.Handle }} - ATCR">
<meta property="og:description" content="Container images by {{ .ViewedUser.Handle }} on ATCR">
@@ -17,6 +19,19 @@
<meta name="twitter:title" content="{{ .ViewedUser.Handle }} - ATCR">
<meta name="twitter:description" content="Container images by {{ .ViewedUser.Handle }} on ATCR">
<meta name="twitter:image" content="https://{{ .RegistryURL }}/og/u/{{ .ViewedUser.Handle }}">
<!-- JSON-LD Structured Data -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "ProfilePage",
"mainEntity": {
"@type": "Person",
"name": "{{ .ViewedUser.Handle }}",
"url": "https://{{ .RegistryURL }}/u/{{ .ViewedUser.Handle }}"{{ if .ViewedUser.Avatar }},
"image": "{{ .ViewedUser.Avatar }}"{{ end }}
}
}
</script>
{{ template "head" . }}
</head>
<body>

View File

@@ -6,6 +6,7 @@ import (
"html/template"
"io/fs"
"net/http"
"net/url"
"strings"
"time"
@@ -104,6 +105,20 @@ func Templates() (*template.Template, error) {
}
return dict
},
"resizeImage": func(imgURL string, width int) string {
if imgURL == "" {
return ""
}
// Only apply Cloudflare Image Resizing to imgs.blue URLs
parsed, err := url.Parse(imgURL)
if err != nil || parsed.Host != "imgs.blue" {
return imgURL
}
// Cloudflare uses /cdn-cgi/image/width=X/ path format
parsed.Path = fmt.Sprintf("/cdn-cgi/image/width=%d%s", width, parsed.Path)
return parsed.String()
},
}
tmpl := template.New("").Funcs(funcMap)