diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..ec6ab95 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,15 @@ +node_modules +dist +.astro +data +.git +.gitignore +.env* +.claude +.vscode +docs +*.md +!src/**/*.md +!src/**/*.mdx +Dockerfile +docker-compose.yml diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..adeecb9 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,30 @@ +# ta1sph.info — Astro static site + small Node server for /api/* (view counter). +# Dokploy: Build Type "Dockerfile", container port 4321, volume at /app/data. + +# ---- build ---- +FROM node:24-slim AS build +WORKDIR /app +COPY package.json package-lock.json ./ +RUN npm ci +COPY . . +RUN npm run build && npm prune --omit=dev + +# ---- runtime ---- +FROM node:24-slim AS runtime +WORKDIR /app +ENV NODE_ENV=production \ + HOST=0.0.0.0 \ + PORT=4321 \ + DATA_DIR=/app/data +COPY --from=build --chown=node:node /app/package.json ./ +COPY --from=build --chown=node:node /app/node_modules ./node_modules +COPY --from=build --chown=node:node /app/dist ./dist +COPY --from=build --chown=node:node /app/server.mjs ./ +# Named volume mounted here keeps the view-counter database across deploys. +RUN mkdir -p /app/data && chown node:node /app/data +VOLUME ["/app/data"] +USER node +EXPOSE 4321 +HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ + CMD node -e "fetch('http://127.0.0.1:4321/api/health/').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))" +CMD ["node", "server.mjs"] diff --git a/PROJECT_MEMORY.md b/PROJECT_MEMORY.md index 05e3c6c..d4bb4bf 100644 --- a/PROJECT_MEMORY.md +++ b/PROJECT_MEMORY.md @@ -1,21 +1,21 @@ # PROJECT_MEMORY — ta1sph.info > Claude'un bu proje için kalıcı hafızası. Her görev sonunda güncellenir. Kısa ve güncel tutulur. -> Son güncelleme: 2026-10-16 · Güncelleyen: Claude (Siteyi Kur — Aşama 7) +> Son güncelleme: 2026-10-16 · Güncelleyen: Claude (Siteyi Kur — Aşama 8) ## 1. Durum -- **Aşama:** 7 tamamlandı (kalite). Sıradaki: **Aşama 8 — Deploy hazırlığı**. -- **Tamamlanan:** Aşama 0–7. Stil önizleme yalnızca dev'de: `http://localhost:4321/styleguide/`. Lighthouse (mobil, 3 koşu ortancası, yerel): A11y/BP/SEO 100 her sayfada; Perf temiz koşullarda 98–100, LCP ~1,4–1,9 sn. Yayında PageSpeed Insights ile doğrulanmalı. -- **Açık işler:** Aşama 8. Node sunucusu yanıtları sıkıştırmıyor → A8'de Traefik `compress` middleware veya eşdeğeri şart. 3 örnek yazı (`örnek` etiketli) kullanıcı isteyince silinecek. Hakkımda tanıtım paragrafı kullanıcı metin verirse. +- **Aşama:** 8 kod tarafı tamamlandı (Docker + sunucu). Sıradaki: **Dokploy kurulumu kullanıcıyla adım adım** (bkz. `references/deployment.md`), sonra `Yayına Al`. +- **Tamamlanan:** Aşama 0–8 (kod). Docker imajı yerelde build + çalıştırma + volume kalıcılığı test edildi (test kalıntıları silindi). Lighthouse (yerel): A11y/BP/SEO 100; Perf temiz koşullarda 98–100. Yayında PageSpeed Insights ile doğrulanmalı. +- **Açık işler:** Dokploy: DNS, port yönlendirme, Application, volume, domain + Let's Encrypt, www→apex, auto deploy, Search Console. 3 örnek yazı (`örnek` etiketli) kullanıcı isteyince silinecek. Hakkımda tanıtım paragrafı kullanıcı metin verirse. - **Bilinen sorunlar:** Arama (Pagefind) `astro dev`'de çalışmaz, yalnızca build sonrası. Kullanıcının elle başlattığı dev sunucusu yeni koleksiyonu görmüyorsa yeniden başlatılmalı. `npm audit`: 2 orta seviye (postcss-selector-parser 6.0.10, @tailwindcss/typography sabitliyor) — yalnızca build zamanı, düzeltme typography'yi eski sürüme düşürdüğü için uygulanmadı. ## 2. Ortam - Astro 7.4.0 · TailwindCSS 4.3.3 (`@tailwindcss/vite`) · TypeScript 6 strict · zod = `astro/zod` (ayrı paket yok) - @astrojs/node 11.1.8 · mdx 8.1.0 · sitemap 3.7.4 · rss 4.0.19 · astro-icon 1.2.0 · pagefind 1.5.2 -- Yerel: Node v26.11.1, npm 12.2.0, Docker 29.8.1 · `package.json` engines: node >=22.22 -- Repo: yerel git (Gitea remote henüz yok; push yalnızca kullanıcı onayıyla) · Branch: `main` +- Yerel: Node v26.11.1, npm 12.2.0, Docker 29.8.1 · `package.json` engines: node >=24 · Docker: `node:24-slim` +- Repo: `origin` = https://gitea.ta1sph.info/suphicakir/ta1sph.info.git · Branch: `main` · **Push'u kullanıcı yapar** (Claude push etmez) - Canlı: https://ta1sph.info (Dokploy, HomeServer Atlas) — _henüz yayında değil_ ## 3. Dosya Haritası @@ -48,10 +48,13 @@ - `public/fonts/*.woff2` + `src/styles/fonts.css` — **üretilir:** `npm run fonts` (`scripts/subset-fonts.mjs`, subset-font; Latin+Türkçe+sembol, wght Rubik 400–900 / Inter 400–700; 188 KB → 66 KB). Kaynak `@fontsource-variable/*` (devDeps). BaseLayout iki latin dosyasını preload eder - `public/favicon.{svg,ico}`, `apple-touch-icon.png`, `icon-192/512.png`, `icon-maskable-512.png` — **üretilir:** `npm run icons` (`scripts/generate-icons.mjs`; kırmızı zemin + siyah kule, 16px için kalın varyant) - `scripts/check-site.mjs` — `npm run check:site` (build sonrası): tek h1, alt, title/description/canonical, tekrar eden id, iç link + anchor +- `server.mjs` — üretim girişi (`npm start`): `ASTRO_NODE_AUTOSTART=disabled` ile Astro handler'ını sarar; `compression` (br/gzip), güvenlik başlıkları, SIGTERM'de düzgün kapanma. `/_astro/*` immutable cache adapter'dan +- `src/pages/api/health.ts` — `GET /api/health/` (trailing slash!) → `{status, db}`; DB yoksa 503 +- `Dockerfile` (node:24-slim, build → `npm prune --omit=dev` → runtime; USER node; HEALTHCHECK; VOLUME /app/data) · `.dockerignore` · `docker-compose.yml` (Dokploy Compose alternatifi) +- Bağımlılıklar: runtime yalnızca `astro`, `@astrojs/node`, `compression`; geri kalan her şey devDependencies (imaj 531 MB, ~200 MB'ı node:24-slim) - `src/dev/Styleguide.astro` — `astro.config.mjs`'deki `devRoutes` entegrasyonu ile sadece `astro dev`'de `/styleguide/` (`_` önekli sayfalar Astro'da rota olmaz) - `src/lib/slug.ts` — Türkçe karakter dönüştürücü `slugify` - `scripts/new-post.mjs` — `npm run new:post -- "Başlık"` → `src/content/blog//index.md` (draft: true); `slug.ts`'yi Node type-stripping ile import eder -- `src/pages/index.astro` — geçici yer tutucu (Aşama 3'te değişecek) - `docs/design-reference/` — Huemint Magazine ekran görüntüleri · `CLAUDE.md` → `AGENTS.md` + bu dosya - Build: `astro build && pagefind --site dist/client` · statik çıktı `dist/client`, sunucu `dist/server/entry.mjs` @@ -68,6 +71,9 @@ | 2026-10-15 | Kanonik alan: apex `ta1sph.info`, www → 301 | SEO tekilleştirme | | 2026-10-15 | Fontlar self-host (Fontsource: Rubik + Inter) | Performans + gizlilik | | 2026-10-16 | Ek token'lar: `accent-fill`/`on-accent-fill`, `accent-text` (khaki'de fg'ye döner) | Ölçüm: ink/red 4.46, red-strong/khaki 3.85 → küçük metin AA'yı geçmiyordu. Kırmızı zeminli butonlar ≥19px bold | +| 2026-10-16 | Sıkıştırma konteyner içinde (`server.mjs` + `compression`), Traefik ayarı yok | Kullanıcının Dokploy'da ek middleware kurmasına gerek kalmaz | +| 2026-10-16 | Docker taban imajı `node:24-slim` (LTS) | `node:sqlite` 24 ve 26'da uyarısız test edildi; 24 LTS Nisan 2028'e kadar destekli | +| 2026-10-16 | Kalıcı veri için Dokploy'da named volume (bind mount değil) | Named volume ilk mount'ta imajdaki `node` sahipliğini alır; bind mount root olur → yazma hatası | | 2026-10-16 | Yazı başına OG görseli build'de (Satori) — kullanıcı kararı bana bıraktı | Paylaşımlarda başlıklı kart; harici servis yok | | 2026-10-16 | Favicon = kırmızı kare + siyah anten kulesi (Tabler) | "TA1" 16px'te okunmuyor; vurgu rengi + İletişim ikonuyla tutarlı | | 2026-10-16 | Fontlar alt kümelenip `public/fonts`'tan; `build.inlineStylesheets: 'always'` | LCP'yi geciktiren render-blocking CSS + 190 KB font; ölçümde FCP 2,3 → ~1 sn | @@ -100,6 +106,7 @@ ## 6. Değişiklik Günlüğü (en yeni üstte) +- 2026-10-16 | Siteyi Kur A8 | Dockerfile, .dockerignore, compose, server.mjs (sıkıştırma + güvenlik başlıkları), /api/health, build-only bağımlılıklar devDeps'e; Docker'da uçtan uca test | `Dockerfile`, `.dockerignore`, `docker-compose.yml`, `server.mjs`, `src/pages/api/health.ts`, `src/lib/views.ts`, `package.json`, `README.md` - 2026-10-16 | Siteyi Kur A7 | OG görselleri (yazı başına + varsayılan), favicon seti + manifest, robots.txt, font alt kümeleme + preload, CSS inline, site denetim script'i, Lighthouse | `src/lib/og.ts`, `src/pages/{og,og-default.png,robots.txt,site.webmanifest}*`, `scripts/{generate-icons,subset-fonts,check-site}.mjs`, `public/`, `src/styles/fonts.css`, `Seo.astro`, `BaseLayout.astro`, `astro.config.mjs` - 2026-10-16 | Siteyi Kur A6 | Hakkımda, İletişim (Leaflet harita, gizlenmiş e-posta), Gizlilik/Lisans/Sorumluluk Reddi, 404, LICENSE dosyaları; PageHeader + CodeCopy ortak bileşenleri; axe temiz | `src/pages/{hakkimda,iletisim,[legal],404}.astro`, `src/content/legal/`, `src/data/{about,contact,pages}.json`, `src/components/{PageHeader,CodeCopy,Card}.astro`, `LICENSE*`, `global.css` - 2026-10-16 | Siteyi Kur A5 | Görüntülenme sayacı (node:sqlite, anonim, 24 sa tekil, bot filtresi), top API, sidebar öne çıkanlar API + yedek; uçtan uca test | `src/lib/views.ts`, `src/pages/api/views/`, `src/components/blog/Sidebar.astro`, `src/pages/blog/[slug].astro`, `src/lib/posts.ts` diff --git a/README.md b/README.md index dda2b50..0988272 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,14 @@ Suphi Çakır (TA1SPH) — amatör telsizcilik blog & portfolyo sitesi. | `npm run format` | Prettier ile biçimlendirir | | `npm run new:post -- "Başlık"` | `src/content/blog//index.md` taslağı oluşturur | +## Yayın (Docker · Dokploy) + +- `Dockerfile` (Node 24 LTS, çok aşamalı) → konteyner `node server.mjs`, port **4321**, kullanıcı `node`. +- Kalıcı veri: görüntülenme sayacı `/app/data/views.db` → Dokploy'da **Volume Mount** `/app/data`. +- Sağlık kontrolü: `GET /api/health/` (`{"status":"ok","db":true}`). +- Ortam değişkenleri: `HOST` (0.0.0.0), `PORT` (4321), `DATA_DIR` (/app/data) — imajda varsayılan. +- `docker-compose.yml`: Dokploy "Compose" türü için alternatif. + ## Lisans Kod [MIT](LICENSE), içerik [CC BY-SA 4.0](LICENSE-CONTENT). diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..fe345e9 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,16 @@ +# Alternative to Dokploy's "Application" type: Dokploy → Create Service → Compose. +# Domains/HTTPS are still configured in Dokploy's UI (Traefik); no host ports are published. +services: + web: + build: . + restart: unless-stopped + environment: + NODE_ENV: production + DATA_DIR: /app/data + volumes: + - ta1sph-data:/app/data + expose: + - '4321' + +volumes: + ta1sph-data: diff --git a/package-lock.json b/package-lock.json index f24911c..455e007 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,26 +8,27 @@ "name": "ta1sph.info", "version": "0.0.1", "dependencies": { - "@astrojs/markdown-satteri": "^0.4.4", - "@astrojs/mdx": "^8.1.0", "@astrojs/node": "^11.1.8", - "@astrojs/rss": "^4.0.19", - "@astrojs/sitemap": "^3.7.4", - "@iconify-json/tabler": "^1.2.42", - "@tailwindcss/vite": "^4.3.3", "astro": "^7.4.0", - "astro-icon": "^1.2.0", - "leaflet": "^1.9.4", - "tailwindcss": "^4.3.3" + "compression": "^1.8.2" }, "devDependencies": { "@astrojs/check": "^0.9.10", + "@astrojs/markdown-satteri": "^0.4.4", + "@astrojs/mdx": "^8.1.0", + "@astrojs/rss": "^4.0.19", + "@astrojs/sitemap": "^3.7.4", "@fontsource-variable/inter": "^5.3.0", "@fontsource-variable/rubik": "^5.3.0", "@fontsource/inter": "^5.3.0", "@fontsource/rubik": "^5.3.0", + "@iconify-json/tabler": "^1.2.42", "@tailwindcss/typography": "^0.5.20", + "@tailwindcss/vite": "^4.3.3", + "@types/compression": "^1.8.1", "@types/leaflet": "^1.9.22", + "astro-icon": "^1.2.0", + "leaflet": "^1.9.4", "pagefind": "^1.5.2", "prettier": "^3.9.10", "prettier-plugin-astro": "^1.1.1", @@ -35,16 +36,18 @@ "satori": "^0.60.0", "sharp": "^0.35.5", "subset-font": "^2.9.0", + "tailwindcss": "^4.3.3", "typescript": "^6.0.3" }, "engines": { - "node": ">=22.22.0" + "node": ">=24" } }, "node_modules/@antfu/install-pkg": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-2.1.0.tgz", "integrity": "sha512-sdg9NxU3zR4Mnawfbc/x6GB5Wf17WYud5qOuEuxXjaKpYpMkISSJEjItGebXJ2bQ4DIcly4NYH23mtkGJjvKUw==", + "dev": true, "license": "MIT", "dependencies": { "package-manager-detector": "^1.8.0", @@ -418,6 +421,7 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/@astrojs/mdx/-/mdx-8.1.0.tgz", "integrity": "sha512-xzrlXrjpBYvC27MTkP+2NMiJG4Jjl2DcoNc/EF9RNxAp0dwhvXC3b22+7OO4kHJQKfISk2uy38HYp1UxX7JfSg==", + "dev": true, "license": "MIT", "dependencies": { "@astrojs/internal-helpers": "0.13.0", @@ -468,6 +472,7 @@ "version": "4.0.19", "resolved": "https://registry.npmjs.org/@astrojs/rss/-/rss-4.0.19.tgz", "integrity": "sha512-e+z5wYeYtffQdHQO8c2tkSd2JEBdAuRXJV4ZEU5IxkYeE6e39woDd7nw1PH1Kk2tEYNCYuKdylnnbhGmt61awA==", + "dev": true, "license": "MIT", "dependencies": { "fast-xml-parser": "^5.5.7", @@ -479,6 +484,7 @@ "version": "3.7.4", "resolved": "https://registry.npmjs.org/@astrojs/sitemap/-/sitemap-3.7.4.tgz", "integrity": "sha512-LbKNC24bdUWcQf/pThB6qLlSqHojxGjZDURIzFocY8rlWnAn2t74nnhnK6S5x0NHriHoAduLEpVjRykmeGiVvA==", + "dev": true, "license": "MIT", "dependencies": { "sitemap": "^9.0.0", @@ -765,6 +771,7 @@ "version": "1.2.19", "resolved": "https://registry.npmjs.org/@cyberalien/svg-utils/-/svg-utils-1.2.19.tgz", "integrity": "sha512-paDJoDu+LhuH5Ma1q0BDqBpg6d16Xk22cZZrlg/s/J9oB8KgnnpU/snaEETJu2G1pwYcWQINUz6WUqu0Wm9k6A==", + "dev": true, "license": "MIT", "dependencies": { "@iconify/types": "^2.0.0" @@ -907,6 +914,7 @@ "version": "1.2.42", "resolved": "https://registry.npmjs.org/@iconify-json/tabler/-/tabler-1.2.42.tgz", "integrity": "sha512-fObaBh/H2zJphk3lS1dWPUf/buuGaCHQXnB4UYgKOPXQub5Ac1qEu0AHRj/0+E2FEl8pWfgM2xQWzfz7ffcY+A==", + "dev": true, "license": "MIT", "dependencies": { "@iconify/types": "*" @@ -916,6 +924,7 @@ "version": "5.0.14", "resolved": "https://registry.npmjs.org/@iconify/tools/-/tools-5.0.14.tgz", "integrity": "sha512-wJ4ZE3/HZh2iBqLwG+8FOMHe7CMJAJqwSXZSpPwMdj1gZM3Z4t2hbQMVw4Vm45oQ985qn0g89PUIcDEdKTheBA==", + "dev": true, "license": "MIT", "dependencies": { "@cyberalien/svg-utils": "^1.2.19", @@ -931,12 +940,14 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", + "dev": true, "license": "MIT" }, "node_modules/@iconify/utils": { "version": "3.1.7", "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-3.1.7.tgz", "integrity": "sha512-JZHlwdID+dy+lTgbYC8NEC4zeugqeYsc6jewvzb4c58kHauJn+X7rNwQjxz5p2qSjqaEeQoLkCIQ9v/H4PK0/w==", + "dev": true, "license": "MIT", "dependencies": { "@antfu/install-pkg": "^2.0.1", @@ -1497,6 +1508,7 @@ "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", @@ -1507,6 +1519,7 @@ "version": "2.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -1517,6 +1530,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -1532,6 +1546,7 @@ "version": "0.3.31", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -1563,6 +1578,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-3.1.0.tgz", "integrity": "sha512-LsS/DjHr+uDM647Gru/cA8+J3a3HfhttwCKLyuoyN7yXTFCxKBtSgMQBvrW9yNPa2/zDRUNuGPaH5QUFoa4arQ==", + "dev": true, "funding": [ { "type": "github", @@ -2083,6 +2099,7 @@ "version": "4.3.3", "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz", "integrity": "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/remapping": "^2.3.5", @@ -2098,6 +2115,7 @@ "version": "1.32.0", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, "license": "MPL-2.0", "dependencies": { "detect-libc": "^2.0.3" @@ -2130,6 +2148,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2150,6 +2169,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2170,6 +2190,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2190,6 +2211,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2210,6 +2232,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2230,6 +2253,7 @@ "cpu": [ "arm64" ], + "dev": true, "libc": [ "glibc" ], @@ -2253,6 +2277,7 @@ "cpu": [ "arm64" ], + "dev": true, "libc": [ "musl" ], @@ -2276,6 +2301,7 @@ "cpu": [ "x64" ], + "dev": true, "libc": [ "glibc" ], @@ -2299,6 +2325,7 @@ "cpu": [ "x64" ], + "dev": true, "libc": [ "musl" ], @@ -2322,6 +2349,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2342,6 +2370,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -2359,6 +2388,7 @@ "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" @@ -2368,6 +2398,7 @@ "version": "4.3.3", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.3.tgz", "integrity": "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==", + "dev": true, "license": "MIT", "engines": { "node": ">= 20" @@ -2394,6 +2425,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2410,6 +2442,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2426,6 +2459,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2442,6 +2476,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2458,6 +2493,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2474,6 +2510,7 @@ "cpu": [ "arm64" ], + "dev": true, "libc": [ "glibc" ], @@ -2493,6 +2530,7 @@ "cpu": [ "arm64" ], + "dev": true, "libc": [ "musl" ], @@ -2512,6 +2550,7 @@ "cpu": [ "x64" ], + "dev": true, "libc": [ "glibc" ], @@ -2531,6 +2570,7 @@ "cpu": [ "x64" ], + "dev": true, "libc": [ "musl" ], @@ -2558,6 +2598,7 @@ "cpu": [ "wasm32" ], + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -2579,6 +2620,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2595,6 +2637,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -2621,6 +2664,7 @@ "version": "4.3.3", "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.3.tgz", "integrity": "sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw==", + "dev": true, "license": "MIT", "dependencies": { "@tailwindcss/node": "4.3.3", @@ -2641,6 +2685,38 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/compression": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@types/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-kCFuWS0ebDbmxs0AXYn6e2r2nrGAb5KwQhknjSPSPgJcGd8+HVSILlUyFhGqML2gk39HcG7D1ydW9/qpYkN00Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", @@ -2656,6 +2732,31 @@ "@types/estree": "*" } }, + "node_modules/@types/express": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^2" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.3.tgz", + "integrity": "sha512-dPfW8NFiOF4wOHc7+N/QSxlY9cfSsenewGbAz8C8U/MULPd/YZ27LvJUIlzaXie7e6Ove9YunJGgC9tbHD2cKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, "node_modules/@types/geojson": { "version": "7946.0.16", "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", @@ -2672,6 +2773,13 @@ "@types/unist": "*" } }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/leaflet": { "version": "1.9.22", "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.22.tgz", @@ -2704,20 +2812,57 @@ "version": "24.19.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.19.2.tgz", "integrity": "sha512-93d49wCWJkCyN87MxLpNN6KXPCwFw7bVUg79mGUV1OS40JGWwVCr8n61DXCSqz7CV6/VfPkb7xec7uP2M+QHWQ==", + "devOptional": true, "license": "MIT", "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, + "node_modules/@types/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/sax": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/@types/sax/-/sax-1.2.7.tgz", "integrity": "sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==", + "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" } }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*" + } + }, "node_modules/@types/unist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", @@ -2944,6 +3089,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/anynum/-/anynum-1.0.1.tgz", "integrity": "sha512-N6//FLET/tXYNM/F6ABca1oH6fWB+KlTt909Le28WMDBk8oaT4vY17DCrwg2MvmuqUKt3Ni4N5dGJ/EoBgcO6A==", + "dev": true, "funding": [ { "type": "github", @@ -2956,6 +3102,7 @@ "version": "5.0.2", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, "license": "MIT" }, "node_modules/argparse": { @@ -3061,6 +3208,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/astro-icon/-/astro-icon-1.2.0.tgz", "integrity": "sha512-0848V2WZuTPhqGSqLeZimLJZ5wmamMtWYzvA2kvg8s2DrUPZ3/nrTcHJWN0eJHt62jy7uvNxgTNiUJGIr7JNxg==", + "dev": true, "license": "MIT", "dependencies": { "@iconify/tools": "^5.0.12", @@ -3106,6 +3254,15 @@ "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", "license": "ISC" }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/camelize": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", @@ -3253,6 +3410,56 @@ "node": ">= 18" } }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "license": "MIT", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.2.tgz", + "integrity": "sha512-o8vI5RE5A6EVVOd9o41jKp41aJom+QTEO/Bx8MYNjexMo/Bv2WOjUfZr+aL0WnYSgymUy6zeguqLTsIhV0gMvQ==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "destroy": "1.2.0", + "negotiator": "~0.6.4", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, "node_modules/cookie": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/cookie/-/cookie-2.0.1.tgz", @@ -3461,6 +3668,16 @@ "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", "license": "MIT" }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -3618,6 +3835,7 @@ "version": "5.26.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.26.0.tgz", "integrity": "sha512-9vhedylFonb2YGogzUKX6+Ja72gOJbN1QHAqdrvqLwhdl/QWbKopzoUC9EbQNVsAns/bx/4uyqalrQAoy1IByw==", + "dev": true, "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", @@ -3734,6 +3952,7 @@ "version": "1.3.1", "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.3.1.tgz", "integrity": "sha512-pIM/1n3ntFXKYrUZwW7QCK0gAW7XY+wzj1YMIV3tLDvPj/V+zTGJK5e3/4WJfwj0qWw2ElNXiTixda/R+3YSug==", + "dev": true, "funding": [ { "type": "github", @@ -3750,6 +3969,7 @@ "version": "5.11.2", "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.11.2.tgz", "integrity": "sha512-R9iDuNrQYeQut46cn2r2wHKn4HYzVDvDm5J1wW+koZewykv0yuO2HChTYeZtrULyHIDM9cj9TUXloCsueCQUog==", + "dev": true, "funding": [ { "type": "github", @@ -3790,6 +4010,7 @@ "version": "0.8.3", "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", + "dev": true, "license": "MIT" }, "node_modules/flattie": { @@ -3910,6 +4131,7 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, "license": "ISC" }, "node_modules/h3": { @@ -4031,6 +4253,7 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz", "integrity": "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==", + "dev": true, "license": "MIT", "funding": { "type": "github", @@ -4083,6 +4306,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-unsafe/-/is-unsafe-2.0.2.tgz", "integrity": "sha512-HgbIHPBH0KHHCcjLfGsCvhtPTVxjaAZlXjwdz7/GQC40SjSe4sfQsar8J5VFo8JOSbarkpV0OLG95bbaNd9aAQ==", + "dev": true, "funding": [ { "type": "github", @@ -4095,6 +4319,7 @@ "version": "2.7.0", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "devOptional": true, "license": "MIT", "bin": { "jiti": "lib/jiti-cli.mjs" @@ -4127,6 +4352,7 @@ "version": "1.9.4", "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==", + "dev": true, "license": "BSD-2-Clause" }, "node_modules/lightningcss": { @@ -4575,6 +4801,7 @@ "version": "0.8.6", "resolved": "https://registry.npmjs.org/modern-tar/-/modern-tar-0.8.6.tgz", "integrity": "sha512-LYSks0tNVqbtez/9OB0YFP6qLRSqkdAxJWOQpVaR67lNTbj2zNLyYpowkRMia5lXLCZKzxwoxkxH2MbbBR2aEg==", + "dev": true, "license": "MIT", "engines": { "node": ">=18.0.0" @@ -4620,6 +4847,15 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/neotraverse": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/neotraverse/-/neotraverse-1.0.1.tgz", @@ -4711,6 +4947,15 @@ "node": ">= 0.8" } }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/oniguruma-parser": { "version": "0.12.2", "resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.2.tgz", @@ -4825,6 +5070,7 @@ "version": "1.6.2", "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.6.2.tgz", "integrity": "sha512-enSlaiat05iasnzmgNxRj8reFdj3puY2QpNgP1aPIaVfT6nn9ICuPoFlKHk8EN22HcwewshO+mN2DGbkCEOtqQ==", + "dev": true, "funding": [ { "type": "github", @@ -4840,6 +5086,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, "license": "MIT" }, "node_modules/piccolore": { @@ -5410,6 +5657,26 @@ "dev": true, "license": "MIT" }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/sass-formatter": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/sass-formatter/-/sass-formatter-0.8.0.tgz", @@ -5616,6 +5883,7 @@ "version": "9.0.1", "resolved": "https://registry.npmjs.org/sitemap/-/sitemap-9.0.1.tgz", "integrity": "sha512-S6hzjGJSG3d6if0YoF5kTyeRJvia6FSTBroE5fQ0bu1QNxyJqhhinfUsXi9fH3MgtXODWvwo2BDyQSnhPQ88uQ==", + "dev": true, "license": "MIT", "dependencies": { "@types/node": "^24.9.2", @@ -5735,6 +6003,7 @@ "version": "2.5.1", "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.5.1.tgz", "integrity": "sha512-UM4nTLWzQiDBseVxUGxN0wJzIk0UU89Qi7pKXz9qISo+lhviJikTuikBsK/oFRlpvYHvSdz0TVoLyQ+sEpYwxA==", + "dev": true, "funding": [ { "type": "github", @@ -5833,12 +6102,14 @@ "version": "4.3.3", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz", "integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==", + "dev": true, "license": "MIT" }, "node_modules/tapable": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -5992,6 +6263,7 @@ "version": "7.24.6", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "devOptional": true, "license": "MIT" }, "node_modules/unicode-trie": { @@ -6207,6 +6479,15 @@ "dev": true, "license": "MIT" }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/verkit": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/verkit/-/verkit-0.5.0.tgz", @@ -6697,6 +6978,7 @@ "version": "0.3.0", "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.3.0.tgz", "integrity": "sha512-ghig2TBE/H11aOVgmahA3MhimvkBr6JIYknH/Dhdk10nXwdbIqBJsbfMxpvFPG8bAw77gN29aQWvKpmVoPlvPQ==", + "dev": true, "funding": [ { "type": "github", diff --git a/package.json b/package.json index a3abbf6..84aa610 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "type": "module", "version": "0.0.1", "engines": { - "node": ">=22.22.0" + "node": ">=24" }, "scripts": { "dev": "astro dev", @@ -16,29 +16,31 @@ "astro": "astro", "icons": "node scripts/generate-icons.mjs", "check:site": "node scripts/check-site.mjs", - "fonts": "node scripts/subset-fonts.mjs" + "fonts": "node scripts/subset-fonts.mjs", + "start": "node server.mjs" }, "dependencies": { - "@astrojs/markdown-satteri": "^0.4.4", - "@astrojs/mdx": "^8.1.0", "@astrojs/node": "^11.1.8", - "@astrojs/rss": "^4.0.19", - "@astrojs/sitemap": "^3.7.4", - "@iconify-json/tabler": "^1.2.42", - "@tailwindcss/vite": "^4.3.3", "astro": "^7.4.0", - "astro-icon": "^1.2.0", - "leaflet": "^1.9.4", - "tailwindcss": "^4.3.3" + "compression": "^1.8.2" }, "devDependencies": { "@astrojs/check": "^0.9.10", + "@astrojs/markdown-satteri": "^0.4.4", + "@astrojs/mdx": "^8.1.0", + "@astrojs/rss": "^4.0.19", + "@astrojs/sitemap": "^3.7.4", "@fontsource-variable/inter": "^5.3.0", "@fontsource-variable/rubik": "^5.3.0", "@fontsource/inter": "^5.3.0", "@fontsource/rubik": "^5.3.0", + "@iconify-json/tabler": "^1.2.42", "@tailwindcss/typography": "^0.5.20", + "@tailwindcss/vite": "^4.3.3", + "@types/compression": "^1.8.1", "@types/leaflet": "^1.9.22", + "astro-icon": "^1.2.0", + "leaflet": "^1.9.4", "pagefind": "^1.5.2", "prettier": "^3.9.10", "prettier-plugin-astro": "^1.1.1", @@ -46,6 +48,7 @@ "satori": "^0.60.0", "sharp": "^0.35.5", "subset-font": "^2.9.0", + "tailwindcss": "^4.3.3", "typescript": "^6.0.3" } } diff --git a/server.mjs b/server.mjs new file mode 100644 index 0000000..92153e9 --- /dev/null +++ b/server.mjs @@ -0,0 +1,34 @@ +// Production entry: wraps Astro's Node handler with compression, security headers and graceful shutdown. +// Run after `npm run build`: node server.mjs (env: HOST, PORT, DATA_DIR) +import http from 'node:http'; +import compression from 'compression'; + +process.env.ASTRO_NODE_AUTOSTART = 'disabled'; +const { handler } = await import('./dist/server/entry.mjs'); + +const HOST = process.env.HOST ?? '0.0.0.0'; +const PORT = Number(process.env.PORT ?? 4321); + +const compress = compression({ threshold: 1024 }); + +const securityHeaders = { + 'X-Content-Type-Options': 'nosniff', + 'Referrer-Policy': 'strict-origin-when-cross-origin', + 'X-Frame-Options': 'SAMEORIGIN', + 'Permissions-Policy': 'camera=(), microphone=(), geolocation=(), interest-cohort=()', +}; + +const server = http.createServer((req, res) => { + for (const [k, v] of Object.entries(securityHeaders)) res.setHeader(k, v); + compress(req, res, () => handler(req, res)); +}); + +server.listen(PORT, HOST, () => console.log(`ta1sph.info listening on http://${HOST}:${PORT}`)); + +// Docker/Dokploy send SIGTERM on redeploy: finish in-flight requests, then exit. +for (const signal of ['SIGTERM', 'SIGINT']) { + process.on(signal, () => { + server.close(() => process.exit(0)); + setTimeout(() => process.exit(0), 10_000).unref(); + }); +} diff --git a/src/lib/views.ts b/src/lib/views.ts index 8c6dedd..fbb138d 100644 --- a/src/lib/views.ts +++ b/src/lib/views.ts @@ -64,3 +64,13 @@ export function clientIp(request: Request, fallback: string): string { export const isBot = (ua: string | null) => !ua || /bot|crawl|spider|slurp|preview|facebookexternalhit|headless|lighthouse/i.test(ua); + +/** Health probe: the database opens and answers a trivial query. */ +export function dbHealthy(): boolean { + try { + open().prepare('SELECT 1').get(); + return true; + } catch { + return false; + } +} diff --git a/src/pages/api/health.ts b/src/pages/api/health.ts new file mode 100644 index 0000000..aae3130 --- /dev/null +++ b/src/pages/api/health.ts @@ -0,0 +1,13 @@ +import type { APIRoute } from 'astro'; +import { dbHealthy } from '../../lib/views'; + +export const prerender = false; + +// GET /api/health — Docker HEALTHCHECK / Dokploy. 503 if the view-counter DB is unavailable. +export const GET: APIRoute = () => { + const db = dbHealthy(); + return Response.json( + { status: db ? 'ok' : 'degraded', db }, + { status: db ? 200 : 503, headers: { 'Cache-Control': 'no-store' } }, + ); +};