diff --git a/docs/installation.md b/docs/installation.md index 33a03e4..ace1c2d 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -56,6 +56,7 @@ Fœdus reads its configuration from environment variables: | `SPOTIFY_CLIENT_ID` | no | — | Enables the collaborative soundtrack | | `SPOTIFY_CLIENT_SECRET` | no | — | Same as above | | `SPOTIFY_REFRESH_TOKEN` | no | — | Same as above | +| `CARTO_API_KEY` | no | — | Publishable CARTO basemaps API key, appended to map tile requests; without it tiles load keyless | \* The app accepts either the unsuffixed `ADMIN_USER` / `ADMIN_PASSWORD` pair or any number of suffixed `ADMIN_USER1..9` / `ADMIN_PASSWORD1..9` pairs — at least one valid pair must be set, otherwise the process panics at startup rather than expose the dashboard with default credentials. diff --git a/static/parking.js b/static/parking.js index 98d6ce1..f681bd0 100644 --- a/static/parking.js +++ b/static/parking.js @@ -26,6 +26,14 @@ const LEAFLET_JS_URL = "https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"; let leafletAssetsPromise = null; + function cartoTileURL(style) { + const key = document + .querySelector("meta[name='carto-api-key']") + ?.getAttribute("content"); + const url = `https://{s}.basemaps.cartocdn.com/${style}/{z}/{x}/{y}{r}.png`; + return key ? `${url}?key=${encodeURIComponent(key)}` : url; + } + let map = null; let entries = []; @@ -66,25 +74,19 @@ attributionControl: true, }); - L.tileLayer( - "https://{s}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}{r}.png", - { - attribution: - '© OSM © CARTO', - subdomains: "abcd", - maxZoom: 19, - }, - ).addTo(map); - - L.tileLayer( - "https://{s}.basemaps.cartocdn.com/light_only_labels/{z}/{x}/{y}{r}.png", - { - attribution: "", - subdomains: "abcd", - maxZoom: 19, - pane: "overlayPane", - }, - ).addTo(map); + L.tileLayer(cartoTileURL("light_nolabels"), { + attribution: + '© OSM © CARTO', + subdomains: "abcd", + maxZoom: 19, + }).addTo(map); + + L.tileLayer(cartoTileURL("light_only_labels"), { + attribution: "", + subdomains: "abcd", + maxZoom: 19, + pane: "overlayPane", + }).addTo(map); entries = buildEntries(); map.on("move zoom resize", render); diff --git a/static/places.js b/static/places.js index e903a9d..fe7af30 100644 --- a/static/places.js +++ b/static/places.js @@ -23,6 +23,14 @@ const LEAFLET_JS_URL = "https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"; let leafletAssetsPromise = null; + function cartoTileURL(style) { + const key = document + .querySelector("meta[name='carto-api-key']") + ?.getAttribute("content"); + const url = `https://{s}.basemaps.cartocdn.com/${style}/{z}/{x}/{y}{r}.png`; + return key ? `${url}?key=${encodeURIComponent(key)}` : url; + } + if (placesSection) initSection(placesSection, "places"); if (honeymoonSection) initSection(honeymoonSection, "honeymoon"); bindModal(); @@ -81,25 +89,19 @@ attributionControl: true, }); - L.tileLayer( - "https://{s}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}{r}.png", - { - attribution: - '© OSM © CARTO', - subdomains: "abcd", - maxZoom: 19, - }, - ).addTo(map); - - L.tileLayer( - "https://{s}.basemaps.cartocdn.com/light_only_labels/{z}/{x}/{y}{r}.png", - { - attribution: "", - subdomains: "abcd", - maxZoom: 19, - pane: "overlayPane", - }, - ).addTo(map); + L.tileLayer(cartoTileURL("light_nolabels"), { + attribution: + '© OSM © CARTO', + subdomains: "abcd", + maxZoom: 19, + }).addTo(map); + + L.tileLayer(cartoTileURL("light_only_labels"), { + attribution: "", + subdomains: "abcd", + maxZoom: 19, + pane: "overlayPane", + }).addTo(map); entries = buildEntries(mode, items, pinsEl, map); diff --git a/templates/layout.templ b/templates/layout.templ index 7452346..ca705f4 100644 --- a/templates/layout.templ +++ b/templates/layout.templ @@ -1,6 +1,7 @@ package templates import ( + "os" "strconv" "strings" @@ -35,6 +36,12 @@ func coordValue(coord float64) string { return strconv.FormatFloat(coord, 'f', -1, 64) } +// cartoAPIKey returns the publishable CARTO basemaps API key from the +// CARTO_API_KEY environment variable, or "" when unset (tiles stay keyless). +func cartoAPIKey() string { + return strings.TrimSpace(os.Getenv("CARTO_API_KEY")) +} + func publicImageURL(mediaID int) string { if mediaID <= 0 { return "" @@ -66,6 +73,9 @@ templ Layout(title string, lang string, t i18n.T, ogMeta OGMeta, includeLeaflet { title } + if cartoAPIKey() != "" { + + } if ogMeta.Title != "" { if ogMeta.Description != "" { diff --git a/templates/layout_test.go b/templates/layout_test.go new file mode 100644 index 0000000..c0286ac --- /dev/null +++ b/templates/layout_test.go @@ -0,0 +1,50 @@ +package templates + +import ( + "os" + "testing" +) + +func TestCartoAPIKey(t *testing.T) { + tests := []struct { + name string + setup func(t *testing.T) + want string + }{ + { + name: "unset", + setup: func(t *testing.T) { + t.Helper() + if err := os.Unsetenv("CARTO_API_KEY"); err != nil { + t.Fatal(err) + } + }, + want: "", + }, + { + name: "empty", + setup: func(t *testing.T) { + t.Helper() + t.Setenv("CARTO_API_KEY", "") + }, + want: "", + }, + { + name: "trims surrounding whitespace", + setup: func(t *testing.T) { + t.Helper() + t.Setenv("CARTO_API_KEY", " abc123 ") + }, + want: "abc123", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.setup(t) + if got := cartoAPIKey(); got != tt.want { + t.Fatalf("cartoAPIKey() = %q, want %q", got, tt.want) + } + }) + } +}