appview: make the footer Bluesky link configurable

The link was hardcoded before fa34da0 too — it pointed at
bsky.app/profile/atcr.io. That commit was right to switch to a DID, since
handles change and a stale handle link breaks silently, but the DID went into
components/footer.html, a shared template. Every self-hoster's footer therefore
links to the project's Bluesky account.

Now ui.bluesky_profile, following source_url in the same footer exactly:
config field with a default, plumbed through UIDependencies and PageData, and
guarded with {{ with }} so an unset value omits the link rather than rendering
something wrong. It takes a handle or a DID; the comment says to prefer a DID
for the reason fa34da0 changed it.

Defaulting to the project account matches source_url's logic — both name the
upstream project rather than the operator — and self-hosters who want their own
or none set one line.

The aria-label switched to $.ClientShortName: `with` rebinds the dot, so the
label would otherwise have silently rendered empty.

Two things worth recording:

  * `{{ with }}` hides the link on an empty value, but you cannot get an empty
    value from the environment. Viper runs with AllowEmptyEnv(false), so an
    empty env var reads as unset and the default wins. Only "" in YAML works.
    That applies to every string field in this config, not just this one, and
    the comment now says so.
  * config-appview.example.yaml must NOT be regenerated with `config init`,
    despite what the checklist in CLAUDE.md says. The file is hand-curated well
    past the defaults, and regenerating replaces real Stripe price IDs with
    price_xxx placeholders and blanks registry_domains, managed_holds, theme
    and the tier names. Added by hand instead.

Verified live both ways: the configured value renders, and `bluesky_profile: ""`
in YAML drops the link while leaving the Source link intact.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SeaUS5AFPX9gqCahoLRMRh
This commit is contained in:
Evan Jarrett
2026-08-25 16:34:26 -05:00
co-authored by Claude Opus 5
parent 34b4516aa7
commit 5112425673
7 changed files with 16 additions and 1 deletions
+2
View File
@@ -50,6 +50,8 @@ ui:
libsql_sync_interval: 1m0s
# Source code URL displayed in the footer "Source" link. Defaults to the upstream ATCR project.
source_url: https://tangled.org/evan.jarrett.net/at-container-registry
# Bluesky handle or DID linked from the footer. Prefer a DID: handles can change. Defaults to the ATCR project account. Set to "" in YAML to hide the link (an empty env var will not do it: Viper treats it as unset).
bluesky_profile: did:plc:wfj5kyialpmcv2fzk6uqwsln
# Health check and cache settings.
health:
# How long to cache hold health check results.
+5
View File
@@ -96,6 +96,10 @@ type UIConfig struct {
// Source code URL displayed in the footer "Source" link.
SourceURL string `yaml:"source_url" comment:"Source code URL displayed in the footer \"Source\" link. Defaults to the upstream ATCR project."`
// Bluesky profile linked from the footer. Handle or DID; prefer a DID,
// since handles change and a stale handle link breaks silently.
BlueskyProfile string `yaml:"bluesky_profile" comment:"Bluesky handle or DID linked from the footer. Prefer a DID: handles can change. Defaults to the ATCR project account. Set to \"\" in YAML to hide the link (an empty env var will not do it: Viper treats it as unset)."`
}
// HealthConfig defines health check and cache settings
@@ -236,6 +240,7 @@ func setDefaults(v *viper.Viper) {
v.SetDefault("ui.libsql_auth_token", "")
v.SetDefault("ui.libsql_sync_interval", "60s")
v.SetDefault("ui.source_url", "https://tangled.org/evan.jarrett.net/at-container-registry")
v.SetDefault("ui.bluesky_profile", "did:plc:wfj5kyialpmcv2fzk6uqwsln")
// Health defaults
v.SetDefault("health.cache_ttl", "15m")
+1
View File
@@ -53,6 +53,7 @@ type BaseUIHandler struct {
AIAdvisorEnabled bool // True when billing is fully configured AND Claude API key is set
BillingEnabled bool // True when the billing build is compiled in and Stripe is configured
SourceURL string // Source code URL for the footer "Source" link
BlueskyProfile string // Bluesky handle or DID for the footer link ("" hides it)
}
// IsManagedHold reports whether a hold DID is one of the appview's managed
+2
View File
@@ -20,6 +20,7 @@ type PageData struct {
OciClient string // Preferred OCI client for pull commands (e.g., "docker", "podman")
AIAdvisorEnabled bool // True when AI Image Advisor is available
SourceURL string // Source code URL for the footer "Source" link
BlueskyProfile string // Bluesky handle or DID for the footer link ("" hides it)
CurrentPath string // Request path (used for OAuth return_to)
}
@@ -51,6 +52,7 @@ func NewPageData(r *http.Request, h *BaseUIHandler) PageData {
OciClient: ociClient,
AIAdvisorEnabled: h.AIAdvisorEnabled,
SourceURL: h.SourceURL,
BlueskyProfile: h.BlueskyProfile,
CurrentPath: r.URL.RequestURI(),
}
}
+2
View File
@@ -48,6 +48,7 @@ type UIDependencies struct {
WebhookDispatcher *webhooks.Dispatcher // Webhook dispatcher (nil if not configured)
ClaudeAPIKey string // Anthropic API key for AI advisor (empty = disabled)
SourceURL string // Source code URL for the footer "Source" link
BlueskyProfile string // Bluesky handle or DID for the footer link ("" hides it)
}
// RegisterUIRoutes registers all web UI and API routes on the provided router
@@ -88,6 +89,7 @@ func RegisterUIRoutes(router chi.Router, deps UIDependencies) {
BillingEnabled: deps.BillingManager != nil && deps.BillingManager.Enabled(),
AIAdvisorEnabled: deps.BillingManager != nil && deps.BillingManager.Enabled() && deps.ClaudeAPIKey != "",
SourceURL: deps.SourceURL,
BlueskyProfile: deps.BlueskyProfile,
}
// OAuth login routes (public)
+1
View File
@@ -412,6 +412,7 @@ func NewAppViewServer(cfg *Config, branding *BrandingOverrides) (*AppViewServer,
WebhookDispatcher: s.WebhookDispatcher,
ClaudeAPIKey: cfg.AI.APIKey,
SourceURL: cfg.UI.SourceURL,
BlueskyProfile: cfg.UI.BlueskyProfile,
LegalConfig: routes.LegalConfig{
CompanyName: cfg.Legal.CompanyName,
Jurisdiction: cfg.Legal.Jurisdiction,
+3 -1
View File
@@ -5,11 +5,13 @@
<a href="/privacy" class="link link-hover">Privacy</a>
<span aria-hidden="true" class="text-base-content/30">·</span>
<a href="/terms" class="link link-hover">Terms</a>
{{ with .BlueskyProfile }}
<span aria-hidden="true" class="text-base-content/30">·</span>
<a href="https://bsky.app/profile/did:plc:wfj5kyialpmcv2fzk6uqwsln" target="_blank" rel="noopener" aria-label="{{ .ClientShortName }} on Bluesky" class="link link-hover inline-flex items-center gap-1">
<a href="https://bsky.app/profile/{{ . }}" target="_blank" rel="noopener" aria-label="{{ $.ClientShortName }} on Bluesky" class="link link-hover inline-flex items-center gap-1">
<svg aria-hidden="true" class="size-3.5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 568 501"><path fill="currentColor" d="M123.121 33.664C188.241 82.553 258.281 181.68 284 234.873c25.719-53.192 95.759-152.32 160.879-201.21C491.866-1.611 568-28.906 568 57.947c0 17.346-9.945 145.713-15.778 166.555-20.275 72.453-94.155 90.933-159.875 79.748C507.222 323.8 536.444 388.56 473.333 453.32c-119.86 122.992-172.272-30.859-185.702-70.281-2.462-7.227-3.614-10.608-3.631-7.733-.017-2.875-1.169.506-3.631 7.733-13.43 39.422-65.842 193.273-185.702 70.281-63.111-64.76-33.89-129.52 80.986-149.071-65.72 11.185-139.6-7.295-159.875-79.748C9.945 203.659 0 75.291 0 57.946 0-28.906 76.135-1.612 123.121 33.664Z"/></svg>
Bluesky
</a>
{{ end }}
{{ with .SourceURL }}
<span aria-hidden="true" class="text-base-content/30">·</span>
<a href="{{ . }}" target="_blank" rel="noopener" class="link link-hover inline-flex items-center gap-1">