feat: add OG images, favicon set, manifest and performance tuning
- Build-time Open Graph cards per post and a default card (Satori + Sharp) - Favicon/ICO/apple-touch/manifest icons generated from one SVG (npm run icons) - robots.txt and web manifest generated from site.json - Subset self-hosted fonts to Latin + Turkish (188 KB -> 66 KB) with preload - Inline the stylesheet to remove the render-blocking request - Post-build site audit (npm run check:site): h1, alt, SEO tags, links - Omit canonical on noindex pages Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@@ -1,13 +1,13 @@
|
|||||||
# PROJECT_MEMORY — ta1sph.info
|
# 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.
|
> 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 6)
|
> Son güncelleme: 2026-10-16 · Güncelleyen: Claude (Siteyi Kur — Aşama 7)
|
||||||
|
|
||||||
## 1. Durum
|
## 1. Durum
|
||||||
|
|
||||||
- **Aşama:** 6 tamamlandı (Hakkımda, İletişim, yasal, 404). Sıradaki: **Aşama 7 — Kalite**.
|
- **Aşama:** 7 tamamlandı (kalite). Sıradaki: **Aşama 8 — Deploy hazırlığı**.
|
||||||
- **Tamamlanan:** Aşama 0–6. Stil önizleme yalnızca dev'de: `http://localhost:4321/styleguide/`.
|
- **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 7–8. Favicon + `og-default.png` yok (A7); kapaksız yazılarda og:image yok. 3 örnek yazı (`örnek` etiketli) kullanıcı isteyince silinecek. Hakkımda'ya tanıtım paragrafı kullanıcı metin verirse eklenecek (`about.json` → `lead`/yeni alan).
|
- **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.
|
||||||
- **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ı.
|
- **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
|
## 2. Ortam
|
||||||
@@ -43,6 +43,11 @@
|
|||||||
- `src/pages/hakkimda.astro` (ProfilePage JSON-LD) · `src/pages/iletisim.astro` (Leaflet + OSM, görünür olunca dinamik import; `divIcon` içinde `.tower-pin`) · `src/pages/[legal].astro` + `src/content/legal/{gizlilik,lisans,sorumluluk-reddi}.md` (koleksiyon `legal`, frontmatter `updated`) · `src/pages/404.astro`
|
- `src/pages/hakkimda.astro` (ProfilePage JSON-LD) · `src/pages/iletisim.astro` (Leaflet + OSM, görünür olunca dinamik import; `divIcon` içinde `.tower-pin`) · `src/pages/[legal].astro` + `src/content/legal/{gizlilik,lisans,sorumluluk-reddi}.md` (koleksiyon `legal`, frontmatter `updated`) · `src/pages/404.astro`
|
||||||
- `src/components/PageHeader.astro` (khaki başlık + tek h1; blog listesi de kullanır) · `src/components/CodeCopy.astro` (Markdown'ı sarar, kod bloklarına Kopyala düğmesi)
|
- `src/components/PageHeader.astro` (khaki başlık + tek h1; blog listesi de kullanır) · `src/components/CodeCopy.astro` (Markdown'ı sarar, kod bloklarına Kopyala düğmesi)
|
||||||
- `LICENSE` (MIT) · `LICENSE-CONTENT` (CC BY-SA 4.0, resmî legalcode.txt'den)
|
- `LICENSE` (MIT) · `LICENSE-CONTENT` (CC BY-SA 4.0, resmî legalcode.txt'den)
|
||||||
|
- `src/lib/og.ts` — Satori + Sharp ile 1200×630 OG PNG (khaki, grid çizgileri, kule ikonu); latin-ext fontlar ayrı aile adıyla yedek (`RubikExt`, `InterExt`) · `src/pages/og/[slug].png.ts` (yazı başına) · `src/pages/og-default.png.ts` (diğer sayfalar). `Seo` varsayılanı `/og-default.png`; kapaklı yazıda kapak 1200px JPEG
|
||||||
|
- `src/pages/site.webmanifest.ts`, `src/pages/robots.txt.ts` (`/api/` kapalı, sitemap satırı) — `site.json`'dan
|
||||||
|
- `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
|
||||||
- `src/dev/Styleguide.astro` — `astro.config.mjs`'deki `devRoutes` entegrasyonu ile sadece `astro dev`'de `/styleguide/` (`_` önekli sayfalar Astro'da rota olmaz)
|
- `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`
|
- `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/<slug>/index.md` (draft: true); `slug.ts`'yi Node type-stripping ile import eder
|
- `scripts/new-post.mjs` — `npm run new:post -- "Başlık"` → `src/content/blog/<slug>/index.md` (draft: true); `slug.ts`'yi Node type-stripping ile import eder
|
||||||
@@ -63,6 +68,11 @@
|
|||||||
| 2026-10-15 | Kanonik alan: apex `ta1sph.info`, www → 301 | SEO tekilleştirme |
|
| 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-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 | 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 | 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 |
|
||||||
|
| 2026-10-16 | Leaflet CSS iletisim.astro'da statik import | `inlineStylesheets: 'always'` dinamik CSS importunu 404'e düşürüyordu |
|
||||||
|
| 2026-10-16 | noindex sayfalarda canonical yok (404) | 404'ün `/404/` canonical'ı var olmayan URL'yi gösteriyordu |
|
||||||
| 2026-10-16 | Yasal metinler Markdown koleksiyonu (`src/content/legal`), tek dinamik rota | Uzun metin için prose; yeni yasal sayfa = yeni .md |
|
| 2026-10-16 | Yasal metinler Markdown koleksiyonu (`src/content/legal`), tek dinamik rota | Uzun metin için prose; yeni yasal sayfa = yeni .md |
|
||||||
| 2026-10-16 | Leaflet işaretçisinde döndürme iç elemanda (`.tower-pin`) | CSS `rotate` özelliği Leaflet'in `translate3d`'sinden önce uygulanıp konumu kaydırıyordu |
|
| 2026-10-16 | Leaflet işaretçisinde döndürme iç elemanda (`.tower-pin`) | CSS `rotate` özelliği Leaflet'in `translate3d`'sinden önce uygulanıp konumu kaydırıyordu |
|
||||||
| 2026-10-16 | Karanlık harita = karolara CSS `invert` filtresi | İkinci karo sağlayıcısı / API anahtarı gerekmez |
|
| 2026-10-16 | Karanlık harita = karolara CSS `invert` filtresi | İkinci karo sağlayıcısı / API anahtarı gerekmez |
|
||||||
@@ -90,6 +100,7 @@
|
|||||||
|
|
||||||
## 6. Değişiklik Günlüğü (en yeni üstte)
|
## 6. Değişiklik Günlüğü (en yeni üstte)
|
||||||
|
|
||||||
|
- 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 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`
|
- 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`
|
||||||
- 2026-10-16 | Siteyi Kur A4 | Blog: koleksiyon, liste/kategori/etiket arşivleri + sayfalama, detay (paylaş, TOC, DIY, kod kopyala, lisans satırı), sidebar (Pagefind arama), RSS, ana sayfa son yazılar, 3 örnek yazı; axe temiz | `src/content*`, `src/pages/blog/`, `src/pages/rss.xml.ts`, `src/components/blog/`, `src/lib/{posts,dates,markdown-plugins}`, `src/data/{blog,categories}.json`, `astro.config.mjs`, `global.css`
|
- 2026-10-16 | Siteyi Kur A4 | Blog: koleksiyon, liste/kategori/etiket arşivleri + sayfalama, detay (paylaş, TOC, DIY, kod kopyala, lisans satırı), sidebar (Pagefind arama), RSS, ana sayfa son yazılar, 3 örnek yazı; axe temiz | `src/content*`, `src/pages/blog/`, `src/pages/rss.xml.ts`, `src/components/blog/`, `src/lib/{posts,dates,markdown-plugins}`, `src/data/{blog,categories}.json`, `astro.config.mjs`, `global.css`
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ export default defineConfig({
|
|||||||
// Static by default; only routes with `export const prerender = false` (view counter API) run on the server.
|
// Static by default; only routes with `export const prerender = false` (view counter API) run on the server.
|
||||||
output: 'static',
|
output: 'static',
|
||||||
adapter: node({ mode: 'standalone' }),
|
adapter: node({ mode: 'standalone' }),
|
||||||
|
// One small stylesheet: inlining it removes a render-blocking request on every page.
|
||||||
|
build: { inlineStylesheets: 'always' },
|
||||||
integrations: [mdx(), sitemap(), icon({ include: { tabler: ['*'] } }), devRoutes],
|
integrations: [mdx(), sitemap(), icon({ include: { tabler: ['*'] } }), devRoutes],
|
||||||
markdown: {
|
markdown: {
|
||||||
processor: satteri({ hastPlugins: [figurePlugin, externalLinksPlugin] }),
|
processor: satteri({ hastPlugins: [figurePlugin, externalLinksPlugin] }),
|
||||||
|
|||||||
@@ -13,8 +13,6 @@
|
|||||||
"@astrojs/node": "^11.1.8",
|
"@astrojs/node": "^11.1.8",
|
||||||
"@astrojs/rss": "^4.0.19",
|
"@astrojs/rss": "^4.0.19",
|
||||||
"@astrojs/sitemap": "^3.7.4",
|
"@astrojs/sitemap": "^3.7.4",
|
||||||
"@fontsource-variable/inter": "^5.3.0",
|
|
||||||
"@fontsource-variable/rubik": "^5.3.0",
|
|
||||||
"@iconify-json/tabler": "^1.2.42",
|
"@iconify-json/tabler": "^1.2.42",
|
||||||
"@tailwindcss/vite": "^4.3.3",
|
"@tailwindcss/vite": "^4.3.3",
|
||||||
"astro": "^7.4.0",
|
"astro": "^7.4.0",
|
||||||
@@ -24,12 +22,19 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@astrojs/check": "^0.9.10",
|
"@astrojs/check": "^0.9.10",
|
||||||
|
"@fontsource-variable/inter": "^5.3.0",
|
||||||
|
"@fontsource-variable/rubik": "^5.3.0",
|
||||||
|
"@fontsource/inter": "^5.3.0",
|
||||||
|
"@fontsource/rubik": "^5.3.0",
|
||||||
"@tailwindcss/typography": "^0.5.20",
|
"@tailwindcss/typography": "^0.5.20",
|
||||||
"@types/leaflet": "^1.9.22",
|
"@types/leaflet": "^1.9.22",
|
||||||
"pagefind": "^1.5.2",
|
"pagefind": "^1.5.2",
|
||||||
"prettier": "^3.9.10",
|
"prettier": "^3.9.10",
|
||||||
"prettier-plugin-astro": "^1.1.1",
|
"prettier-plugin-astro": "^1.1.1",
|
||||||
"prettier-plugin-tailwindcss": "^0.8.1",
|
"prettier-plugin-tailwindcss": "^0.8.1",
|
||||||
|
"satori": "^0.60.0",
|
||||||
|
"sharp": "^0.35.5",
|
||||||
|
"subset-font": "^2.9.0",
|
||||||
"typescript": "^6.0.3"
|
"typescript": "^6.0.3"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -862,6 +867,7 @@
|
|||||||
"version": "5.3.0",
|
"version": "5.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/@fontsource-variable/inter/-/inter-5.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@fontsource-variable/inter/-/inter-5.3.0.tgz",
|
||||||
"integrity": "sha512-OupL48va4JNofb97w6NYeF9S7W/kHNKM0Er8Dem5nqi4jeOLrVJDoE8tZEpnMJmtkvNbB1EIPPwHcdkF6b1oUA==",
|
"integrity": "sha512-OupL48va4JNofb97w6NYeF9S7W/kHNKM0Er8Dem5nqi4jeOLrVJDoE8tZEpnMJmtkvNbB1EIPPwHcdkF6b1oUA==",
|
||||||
|
"dev": true,
|
||||||
"license": "OFL-1.1",
|
"license": "OFL-1.1",
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/ayuhito"
|
"url": "https://github.com/sponsors/ayuhito"
|
||||||
@@ -871,6 +877,27 @@
|
|||||||
"version": "5.3.0",
|
"version": "5.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/@fontsource-variable/rubik/-/rubik-5.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@fontsource-variable/rubik/-/rubik-5.3.0.tgz",
|
||||||
"integrity": "sha512-yQUf8qle24MJYA+EuauyF+cqjjN6I715nGjitt3sGmwZA0XfA4fWJcLq1C/diREZl4Qzn1E80nLMc0ipC/a4Lg==",
|
"integrity": "sha512-yQUf8qle24MJYA+EuauyF+cqjjN6I715nGjitt3sGmwZA0XfA4fWJcLq1C/diREZl4Qzn1E80nLMc0ipC/a4Lg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "OFL-1.1",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ayuhito"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@fontsource/inter": {
|
||||||
|
"version": "5.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fontsource/inter/-/inter-5.3.0.tgz",
|
||||||
|
"integrity": "sha512-RofMylZmjlJEfELXeNHFWBRcSs75rGU/6bV2S2jfnvv/3rPXPGe0LgUJTklcHZ9lM4OZmAVFhcJPnACfb91A3g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "OFL-1.1",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ayuhito"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@fontsource/rubik": {
|
||||||
|
"version": "5.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fontsource/rubik/-/rubik-5.3.0.tgz",
|
||||||
|
"integrity": "sha512-Nks2mn7vlE1jZVHuVgoKU2Ejqjvd+GJzLRdraCjo1xc79oo7O6vJ8pcvSfmKPGX3/Ln7nt09XWFzPeu2jWf0xQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "OFL-1.1",
|
"license": "OFL-1.1",
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/ayuhito"
|
"url": "https://github.com/sponsors/ayuhito"
|
||||||
@@ -921,8 +948,8 @@
|
|||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz",
|
||||||
"integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==",
|
"integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==",
|
||||||
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
@@ -2028,6 +2055,30 @@
|
|||||||
"integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==",
|
"integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@shuding/opentype.js": {
|
||||||
|
"version": "1.4.0-beta.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@shuding/opentype.js/-/opentype.js-1.4.0-beta.0.tgz",
|
||||||
|
"integrity": "sha512-3NgmNyH3l/Hv6EvsWJbsvpcpUba6R8IREQ83nH83cyakCw7uM1arZKNfHwv1Wz6jgqrF/j4x5ELvR6PnK9nTcA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"fflate": "^0.7.3",
|
||||||
|
"string.prototype.codepointat": "^0.2.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"ot": "bin/ot"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 8.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@shuding/opentype.js/node_modules/fflate": {
|
||||||
|
"version": "0.7.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.7.5.tgz",
|
||||||
|
"integrity": "sha512-QieYf//cis6ywHNi5qW1+PXPQ4bC+XVJAtS4AXIML8P76GroEiOxm/oQtn1f02UkJY1+KsXMJcC+R2v/Eg4G3g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@tailwindcss/node": {
|
"node_modules/@tailwindcss/node": {
|
||||||
"version": "4.3.3",
|
"version": "4.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz",
|
||||||
@@ -2907,6 +2958,13 @@
|
|||||||
"integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
|
"integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/argparse": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Python-2.0"
|
||||||
|
},
|
||||||
"node_modules/aria-query": {
|
"node_modules/aria-query": {
|
||||||
"version": "5.3.2",
|
"version": "5.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz",
|
||||||
@@ -3032,12 +3090,32 @@
|
|||||||
"url": "https://github.com/sponsors/wooorm"
|
"url": "https://github.com/sponsors/wooorm"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/base64-js": {
|
||||||
|
"version": "0.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz",
|
||||||
|
"integrity": "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/boolbase": {
|
"node_modules/boolbase": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
|
||||||
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
|
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/camelize": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ccount": {
|
"node_modules/ccount": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz",
|
||||||
@@ -3140,6 +3218,13 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/color-name": {
|
||||||
|
"version": "1.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||||
|
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/comma-separated-tokens": {
|
"node_modules/comma-separated-tokens": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
|
||||||
@@ -3196,6 +3281,40 @@
|
|||||||
"uncrypto": "^0.1.3"
|
"uncrypto": "^0.1.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/css-background-parser": {
|
||||||
|
"version": "0.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/css-background-parser/-/css-background-parser-0.1.0.tgz",
|
||||||
|
"integrity": "sha512-2EZLisiZQ+7m4wwur/qiYJRniHX4K5Tc9w93MT3AS0WS1u5kaZ4FKXlOTBhOjc+CgEgPiGY+fX1yWD8UwpEqUA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/css-box-shadow": {
|
||||||
|
"version": "1.0.0-3",
|
||||||
|
"resolved": "https://registry.npmjs.org/css-box-shadow/-/css-box-shadow-1.0.0-3.tgz",
|
||||||
|
"integrity": "sha512-9jaqR6e7Ohds+aWwmhe6wILJ99xYQbfmK9QQB9CcMjDbTxPZjwEmUQpU91OG05Xgm8BahT5fW+svbsQGjS/zPg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/css-color-keywords": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/css-gradient-parser": {
|
||||||
|
"version": "0.0.17",
|
||||||
|
"resolved": "https://registry.npmjs.org/css-gradient-parser/-/css-gradient-parser-0.0.17.tgz",
|
||||||
|
"integrity": "sha512-w2Xy9UMMwlKtou0vlRnXvWglPAceXCTtcmVSo8ZBUvqCV5aXEFP/PC6d+I464810I9FT++UACwTD5511bmGPUg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/css-select": {
|
"node_modules/css-select": {
|
||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/css-select/-/css-select-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/css-select/-/css-select-6.0.0.tgz",
|
||||||
@@ -3212,6 +3331,18 @@
|
|||||||
"url": "https://github.com/sponsors/fb55"
|
"url": "https://github.com/sponsors/fb55"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/css-to-react-native": {
|
||||||
|
"version": "3.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz",
|
||||||
|
"integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"camelize": "^1.0.0",
|
||||||
|
"css-color-keywords": "^1.0.0",
|
||||||
|
"postcss-value-parser": "^4.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/css-tree": {
|
"node_modules/css-tree": {
|
||||||
"version": "3.2.1",
|
"version": "3.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz",
|
||||||
@@ -3464,6 +3595,16 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/emoji-regex-xs": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/emoji-regex-xs/-/emoji-regex-xs-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-1QFuh8l7LqUcKe24LsPUNzjrzJQ7pgRwp1QMcZ5MX6mFplk2zQ08NVCM84++1cveaUUYtcCYHmeFEuNg16sU4g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/encodeurl": {
|
"node_modules/encodeurl": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
||||||
@@ -3687,6 +3828,17 @@
|
|||||||
"node": ">=20"
|
"node": ">=20"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fontverter": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fontverter/-/fontverter-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-DFVX5hvXuhi1Jven1tbpebYTCT9XYnvx6/Z+HFUPb7ZRMCW+pj2clU9VMhoTPgWKPhAs7JJDSk3CW1jNUvKCZQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"wawoff2": "^2.0.0",
|
||||||
|
"woff2sfnt-sfnt2woff": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fresh": {
|
"node_modules/fresh": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
|
||||||
@@ -3777,6 +3929,13 @@
|
|||||||
"uncrypto": "^0.1.3"
|
"uncrypto": "^0.1.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/harfbuzzjs": {
|
||||||
|
"version": "0.10.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/harfbuzzjs/-/harfbuzzjs-0.10.0.tgz",
|
||||||
|
"integrity": "sha512-SN8LVwCOzvTq3OPNd0+EAghgXugItz56wst567D1vs7LnnKGWprhi3EG58aVOBwhN8HEbQxL4I9NDWkcXUutRw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/hast-util-to-html": {
|
"node_modules/hast-util-to-html": {
|
||||||
"version": "9.0.5",
|
"version": "9.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz",
|
||||||
@@ -3813,6 +3972,19 @@
|
|||||||
"url": "https://opencollective.com/unified"
|
"url": "https://opencollective.com/unified"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/hex-rgb": {
|
||||||
|
"version": "4.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/hex-rgb/-/hex-rgb-4.3.0.tgz",
|
||||||
|
"integrity": "sha512-Ox1pJVrDCyGHMG9CFg1tmrRUMRPRsAWYc/PinY0XzJU4K7y7vjNoLKIQ7BR5UJMCxNN8EM1MNDmHWA/B3aZUuw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/html-escaper": {
|
"node_modules/html-escaper": {
|
||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz",
|
||||||
@@ -4218,6 +4390,17 @@
|
|||||||
"url": "https://opencollective.com/parcel"
|
"url": "https://opencollective.com/parcel"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/linebreak": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/linebreak/-/linebreak-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"base64-js": "0.0.8",
|
||||||
|
"unicode-trie": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/lru-cache": {
|
"node_modules/lru-cache": {
|
||||||
"version": "11.5.3",
|
"version": "11.5.3",
|
||||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz",
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz",
|
||||||
@@ -4613,6 +4796,24 @@
|
|||||||
"@pagefind/windows-x64": "1.5.2"
|
"@pagefind/windows-x64": "1.5.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/pako": {
|
||||||
|
"version": "0.2.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz",
|
||||||
|
"integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/parse-css-color": {
|
||||||
|
"version": "0.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/parse-css-color/-/parse-css-color-0.2.1.tgz",
|
||||||
|
"integrity": "sha512-bwS/GGIFV3b6KS4uwpzCFj4w297Yl3uqnSgIPsoQkx7GMLROXfMnWvxfNkL0oh8HVhZA4hvJoEoEIqonfJ3BWg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"color-name": "^1.1.4",
|
||||||
|
"hex-rgb": "^4.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/path-browserify": {
|
"node_modules/path-browserify": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
|
||||||
@@ -4707,6 +4908,13 @@
|
|||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/postcss-value-parser": {
|
||||||
|
"version": "4.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
|
||||||
|
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/prettier": {
|
"node_modules/prettier": {
|
||||||
"version": "3.9.10",
|
"version": "3.9.10",
|
||||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.10.tgz",
|
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.10.tgz",
|
||||||
@@ -5215,6 +5423,37 @@
|
|||||||
"sass-formatter": "dist/cli.js"
|
"sass-formatter": "dist/cli.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/satori": {
|
||||||
|
"version": "0.60.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/satori/-/satori-0.60.0.tgz",
|
||||||
|
"integrity": "sha512-OSnpxSr166mj2M9i6Cm3kFWYG6i36FnYDfQU0SR8nsZe5zgp/PskLkyfNAqV8phdGkDfQdFHyJZvrtdss3HAVw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MPL-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@shuding/opentype.js": "1.4.0-beta.0",
|
||||||
|
"css-background-parser": "^0.1.0",
|
||||||
|
"css-box-shadow": "1.0.0-3",
|
||||||
|
"css-gradient-parser": "^0.0.17",
|
||||||
|
"css-to-react-native": "^3.0.0",
|
||||||
|
"emoji-regex-xs": "^2.0.1",
|
||||||
|
"escape-html": "^1.0.3",
|
||||||
|
"fflate": "0.7.3",
|
||||||
|
"harfbuzzjs": "0.10.0",
|
||||||
|
"linebreak": "^1.1.0",
|
||||||
|
"parse-css-color": "^0.2.1",
|
||||||
|
"postcss-value-parser": "^4.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/satori/node_modules/fflate": {
|
||||||
|
"version": "0.7.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.7.3.tgz",
|
||||||
|
"integrity": "sha512-0Zz1jOzJWERhyhsimS54VTqOteCNwRtIlh8isdL0AXLo0g7xNTfTL7oWrkmCnPhZGocKIkWHBistBrrpoNH3aw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/satteri": {
|
"node_modules/satteri": {
|
||||||
"version": "0.10.5",
|
"version": "0.10.5",
|
||||||
"resolved": "https://registry.npmjs.org/satteri/-/satteri-0.10.5.tgz",
|
"resolved": "https://registry.npmjs.org/satteri/-/satteri-0.10.5.tgz",
|
||||||
@@ -5302,8 +5541,8 @@
|
|||||||
"version": "0.35.5",
|
"version": "0.35.5",
|
||||||
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.5.tgz",
|
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.5.tgz",
|
||||||
"integrity": "sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==",
|
"integrity": "sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==",
|
||||||
|
"devOptional": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"optional": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@img/colour": "^1.1.0",
|
"@img/colour": "^1.1.0",
|
||||||
"detect-libc": "^2.1.2",
|
"detect-libc": "^2.1.2",
|
||||||
@@ -5455,6 +5694,13 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/string.prototype.codepointat": {
|
||||||
|
"version": "0.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/string.prototype.codepointat/-/string.prototype.codepointat-0.2.1.tgz",
|
||||||
|
"integrity": "sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/stringify-entities": {
|
"node_modules/stringify-entities": {
|
||||||
"version": "4.0.4",
|
"version": "4.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz",
|
||||||
@@ -5500,6 +5746,54 @@
|
|||||||
"anynum": "^1.0.1"
|
"anynum": "^1.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/subset-font": {
|
||||||
|
"version": "2.9.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/subset-font/-/subset-font-2.9.0.tgz",
|
||||||
|
"integrity": "sha512-WJulQeFwbewrXIGcLJbSZFWI44fHW8gNRuPzLywfDL8w38IPRbN1JFxwG76aoohw9kcFdvjVIn2QmID1iy119A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"fontverter": "^2.0.0",
|
||||||
|
"harfbuzzjs": "^1.6.1",
|
||||||
|
"p-limit": "^3.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/subset-font/node_modules/harfbuzzjs": {
|
||||||
|
"version": "1.6.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/harfbuzzjs/-/harfbuzzjs-1.6.3.tgz",
|
||||||
|
"integrity": "sha512-R5Mfo1UUvQmZJmtScP7fBLKYKnYu82ZdtFT7WbQYeGb3Ut5PR3xfFCJ5/FjbcW5SAfb9cqCSny/KNjp+NJBx0w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/subset-font/node_modules/p-limit": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"yocto-queue": "^0.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/subset-font/node_modules/yocto-queue": {
|
||||||
|
"version": "0.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
||||||
|
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/suf-log": {
|
"node_modules/suf-log": {
|
||||||
"version": "2.5.3",
|
"version": "2.5.3",
|
||||||
"resolved": "https://registry.npmjs.org/suf-log/-/suf-log-2.5.3.tgz",
|
"resolved": "https://registry.npmjs.org/suf-log/-/suf-log-2.5.3.tgz",
|
||||||
@@ -5700,6 +5994,17 @@
|
|||||||
"integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==",
|
"integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/unicode-trie": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"pako": "^0.2.5",
|
||||||
|
"tiny-inflate": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/unified": {
|
"node_modules/unified": {
|
||||||
"version": "11.0.5",
|
"version": "11.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz",
|
||||||
@@ -6321,6 +6626,37 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/wawoff2": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/wawoff2/-/wawoff2-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-r0CEmvpH63r4T15ebFqeOjGqU4+EgTx4I510NtK35EMciSdcTxCw3Byy3JnBonz7iyIFZ0AbVo0bbFpEVuhCYA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"argparse": "^2.0.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"woff2_compress.js": "bin/woff2_compress.js",
|
||||||
|
"woff2_decompress.js": "bin/woff2_decompress.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/woff2sfnt-sfnt2woff": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/woff2sfnt-sfnt2woff/-/woff2sfnt-sfnt2woff-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-edK4COc1c1EpRfMqCZO1xJOvdUtM5dbVb9iz97rScvnTevqEB3GllnLWCmMVp1MfQBdF1DFg/11I0rSyAdS4qQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"pako": "^1.0.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/woff2sfnt-sfnt2woff/node_modules/pako": {
|
||||||
|
"version": "1.0.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
|
||||||
|
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "(MIT AND Zlib)"
|
||||||
|
},
|
||||||
"node_modules/wrap-ansi": {
|
"node_modules/wrap-ansi": {
|
||||||
"version": "9.0.2",
|
"version": "9.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz",
|
||||||
|
|||||||
@@ -13,7 +13,10 @@
|
|||||||
"format": "prettier --write .",
|
"format": "prettier --write .",
|
||||||
"format:check": "prettier --check .",
|
"format:check": "prettier --check .",
|
||||||
"new:post": "node scripts/new-post.mjs",
|
"new:post": "node scripts/new-post.mjs",
|
||||||
"astro": "astro"
|
"astro": "astro",
|
||||||
|
"icons": "node scripts/generate-icons.mjs",
|
||||||
|
"check:site": "node scripts/check-site.mjs",
|
||||||
|
"fonts": "node scripts/subset-fonts.mjs"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/markdown-satteri": "^0.4.4",
|
"@astrojs/markdown-satteri": "^0.4.4",
|
||||||
@@ -21,8 +24,6 @@
|
|||||||
"@astrojs/node": "^11.1.8",
|
"@astrojs/node": "^11.1.8",
|
||||||
"@astrojs/rss": "^4.0.19",
|
"@astrojs/rss": "^4.0.19",
|
||||||
"@astrojs/sitemap": "^3.7.4",
|
"@astrojs/sitemap": "^3.7.4",
|
||||||
"@fontsource-variable/inter": "^5.3.0",
|
|
||||||
"@fontsource-variable/rubik": "^5.3.0",
|
|
||||||
"@iconify-json/tabler": "^1.2.42",
|
"@iconify-json/tabler": "^1.2.42",
|
||||||
"@tailwindcss/vite": "^4.3.3",
|
"@tailwindcss/vite": "^4.3.3",
|
||||||
"astro": "^7.4.0",
|
"astro": "^7.4.0",
|
||||||
@@ -32,12 +33,19 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@astrojs/check": "^0.9.10",
|
"@astrojs/check": "^0.9.10",
|
||||||
|
"@fontsource-variable/inter": "^5.3.0",
|
||||||
|
"@fontsource-variable/rubik": "^5.3.0",
|
||||||
|
"@fontsource/inter": "^5.3.0",
|
||||||
|
"@fontsource/rubik": "^5.3.0",
|
||||||
"@tailwindcss/typography": "^0.5.20",
|
"@tailwindcss/typography": "^0.5.20",
|
||||||
"@types/leaflet": "^1.9.22",
|
"@types/leaflet": "^1.9.22",
|
||||||
"pagefind": "^1.5.2",
|
"pagefind": "^1.5.2",
|
||||||
"prettier": "^3.9.10",
|
"prettier": "^3.9.10",
|
||||||
"prettier-plugin-astro": "^1.1.1",
|
"prettier-plugin-astro": "^1.1.1",
|
||||||
"prettier-plugin-tailwindcss": "^0.8.1",
|
"prettier-plugin-tailwindcss": "^0.8.1",
|
||||||
|
"satori": "^0.60.0",
|
||||||
|
"sharp": "^0.35.5",
|
||||||
|
"subset-font": "^2.9.0",
|
||||||
"typescript": "^6.0.3"
|
"typescript": "^6.0.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
|
After Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 655 B After Width: | Height: | Size: 2.7 KiB |
@@ -1,9 +1 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><rect width="32" height="32" rx="6" fill="#c95748"/><g transform="translate(4.48 4.48) scale(0.96)"><g fill="none" stroke="#111012" stroke-linecap="round" stroke-linejoin="round" stroke-width="2.4"><path d="M11 12a1 1 0 1 0 2 0a1 1 0 1 0-2 0"/><path d="M16.616 13.924a5 5 0 1 0-9.23 0"/><path d="M20.307 15.469a9 9 0 1 0-16.615 0"/><path d="m9 21l3-9l3 9m-5-2h4"/></g></g></svg>
|
||||||
<path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
|
|
||||||
<style>
|
|
||||||
path { fill: #000; }
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
path { fill: #FFF; }
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 749 B After Width: | Height: | Size: 438 B |
|
After Width: | Height: | Size: 4.6 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1,75 @@
|
|||||||
|
// Post-build audit of dist/client: one h1, alt text, SEO tags, unique ids, internal links/anchors.
|
||||||
|
// Usage: npm run build && npm run check:site
|
||||||
|
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
||||||
|
import { join, relative } from 'node:path';
|
||||||
|
|
||||||
|
const ROOT = 'dist/client';
|
||||||
|
const SERVER_ROUTES = [/^\/api\//]; // on-demand routes, not files
|
||||||
|
|
||||||
|
const walk = (dir) =>
|
||||||
|
readdirSync(dir).flatMap((f) => {
|
||||||
|
const p = join(dir, f);
|
||||||
|
return statSync(p).isDirectory() ? walk(p) : [p];
|
||||||
|
});
|
||||||
|
|
||||||
|
const pages = walk(ROOT).filter((f) => f.endsWith('.html') && !f.includes('/pagefind/'));
|
||||||
|
const idsByPage = new Map();
|
||||||
|
const problems = [];
|
||||||
|
const report = (page, msg) => problems.push(`${page}: ${msg}`);
|
||||||
|
|
||||||
|
const urlOf = (file) =>
|
||||||
|
'/' +
|
||||||
|
relative(ROOT, file)
|
||||||
|
.replace(/index\.html$/, '')
|
||||||
|
.replace(/\.html$/, '');
|
||||||
|
const html = new Map(pages.map((f) => [urlOf(f), readFileSync(f, 'utf8')]));
|
||||||
|
|
||||||
|
for (const [url, src] of html) {
|
||||||
|
const ids = [...src.matchAll(/\sid="([^"]+)"/g)].map((m) => m[1]);
|
||||||
|
idsByPage.set(url, new Set(ids));
|
||||||
|
const dupes = ids.filter((id, i) => ids.indexOf(id) !== i);
|
||||||
|
if (dupes.length) report(url, `duplicate id(s): ${[...new Set(dupes)].join(', ')}`);
|
||||||
|
|
||||||
|
const h1 = (src.match(/<h1[\s>]/g) ?? []).length;
|
||||||
|
if (h1 !== 1) report(url, `${h1} <h1> elements`);
|
||||||
|
if (!/<html[^>]+lang="/.test(src)) report(url, 'missing <html lang>');
|
||||||
|
if (!/<title>[^<]+<\/title>/.test(src)) report(url, 'missing <title>');
|
||||||
|
if (url !== '/404' && !/<meta name="description" content="[^"]+"/.test(src))
|
||||||
|
report(url, 'missing meta description');
|
||||||
|
if (url !== '/404' && !/<link rel="canonical" href="https:\/\/ta1sph\.info\//.test(src))
|
||||||
|
report(url, 'missing canonical');
|
||||||
|
for (const img of src.match(/<img\b[^>]*>/g) ?? []) {
|
||||||
|
if (!/\salt="/.test(img)) report(url, `img without alt: ${img.slice(0, 80)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [url, src] of html) {
|
||||||
|
const refs = [...src.matchAll(/\s(?:href|src)="([^"]+)"/g)].map((m) => m[1]);
|
||||||
|
for (const ref of refs) {
|
||||||
|
if (/^(https?:|mailto:|tel:|data:|javascript:)/.test(ref) || ref.startsWith('//')) {
|
||||||
|
if (!ref.startsWith('https://ta1sph.info')) continue;
|
||||||
|
}
|
||||||
|
const target = new URL(ref.replaceAll('&', '&'), `https://ta1sph.info${url}`);
|
||||||
|
if (target.host !== 'ta1sph.info') continue;
|
||||||
|
const path = decodeURIComponent(target.pathname);
|
||||||
|
if (SERVER_ROUTES.some((r) => r.test(path))) continue;
|
||||||
|
const file = path.endsWith('/')
|
||||||
|
? join(ROOT, path, 'index.html')
|
||||||
|
: ([join(ROOT, path), join(ROOT, `${path}.html`)].find(existsSync) ?? join(ROOT, path));
|
||||||
|
if (!existsSync(file)) {
|
||||||
|
report(url, `broken link → ${path}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (target.hash && idsByPage.has(path)) {
|
||||||
|
const id = decodeURIComponent(target.hash.slice(1));
|
||||||
|
if (!idsByPage.get(path).has(id)) report(url, `missing anchor → ${path}${target.hash}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Checked ${html.size} pages.`);
|
||||||
|
if (problems.length) {
|
||||||
|
console.error(problems.map((p) => ` ✗ ${p}`).join('\n'));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
console.log(' ✓ no problems found');
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
// Regenerates the favicon set in public/ from the Tabler antenna-tower icon.
|
||||||
|
// Usage: npm run icons
|
||||||
|
import { writeFileSync } from 'node:fs';
|
||||||
|
import { createRequire } from 'node:module';
|
||||||
|
import sharp from 'sharp';
|
||||||
|
|
||||||
|
const require = createRequire(import.meta.url);
|
||||||
|
const { icons } = require('@iconify-json/tabler/icons.json');
|
||||||
|
|
||||||
|
const RED = '#c95748';
|
||||||
|
const INK = '#111012';
|
||||||
|
const tower = (stroke) =>
|
||||||
|
icons['building-broadcast-tower'].body
|
||||||
|
.replaceAll('currentColor', INK)
|
||||||
|
.replaceAll('stroke-width="2"', `stroke-width="${stroke}"`);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Square icon: red background, ink tower.
|
||||||
|
* `pad` = icon inset (0..1), `radius` in viewBox units, `stroke` = Tabler stroke width.
|
||||||
|
*/
|
||||||
|
const svg = ({ pad = 0.14, radius = 6, stroke = 2.4 } = {}) => {
|
||||||
|
const size = 32;
|
||||||
|
const inner = size * (1 - pad * 2);
|
||||||
|
const offset = size * pad;
|
||||||
|
const scale = inner / 24;
|
||||||
|
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${size} ${size}"><rect width="${size}" height="${size}" rx="${radius}" fill="${RED}"/><g transform="translate(${offset} ${offset}) scale(${scale})">${tower(stroke)}</g></svg>`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const png = (source, px) =>
|
||||||
|
sharp(Buffer.from(source), { density: 72 * (px / 32) * 4 })
|
||||||
|
.resize(px, px)
|
||||||
|
.png()
|
||||||
|
.toBuffer();
|
||||||
|
|
||||||
|
/** ICO container with embedded PNG images. */
|
||||||
|
function ico(images) {
|
||||||
|
const header = Buffer.alloc(6 + images.length * 16);
|
||||||
|
header.writeUInt16LE(0, 0);
|
||||||
|
header.writeUInt16LE(1, 2);
|
||||||
|
header.writeUInt16LE(images.length, 4);
|
||||||
|
let offset = header.length;
|
||||||
|
images.forEach(({ size, data }, i) => {
|
||||||
|
const e = 6 + i * 16;
|
||||||
|
header.writeUInt8(size >= 256 ? 0 : size, e);
|
||||||
|
header.writeUInt8(size >= 256 ? 0 : size, e + 1);
|
||||||
|
header.writeUInt16LE(1, e + 4); // colour planes
|
||||||
|
header.writeUInt16LE(32, e + 6); // bits per pixel
|
||||||
|
header.writeUInt32LE(data.length, e + 8);
|
||||||
|
header.writeUInt32LE(offset, e + 12);
|
||||||
|
offset += data.length;
|
||||||
|
});
|
||||||
|
return Buffer.concat([header, ...images.map((i) => i.data)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const rounded = svg();
|
||||||
|
const tiny = svg({ pad: 0.06, radius: 4, stroke: 3.2 }); // 16 px tab icon: bigger, bolder
|
||||||
|
const square = svg({ radius: 0 }); // platforms apply their own mask
|
||||||
|
const maskable = svg({ pad: 0.24, radius: 0 }); // keep the icon inside the 80% safe zone
|
||||||
|
|
||||||
|
writeFileSync('public/favicon.svg', rounded);
|
||||||
|
writeFileSync(
|
||||||
|
'public/favicon.ico',
|
||||||
|
ico(
|
||||||
|
await Promise.all(
|
||||||
|
[16, 32, 48].map(async (size) => ({
|
||||||
|
size,
|
||||||
|
data: await png(size === 16 ? tiny : rounded, size),
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
writeFileSync('public/apple-touch-icon.png', await png(svg({ pad: 0.18, radius: 0 }), 180));
|
||||||
|
writeFileSync('public/icon-192.png', await png(square, 192));
|
||||||
|
writeFileSync('public/icon-512.png', await png(square, 512));
|
||||||
|
writeFileSync('public/icon-maskable-512.png', await png(maskable, 512));
|
||||||
|
console.log('Icons written to public/');
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
// Builds small self-hosted variable fonts (Latin + Turkish + symbols used in posts) into public/fonts/.
|
||||||
|
// Sources: @fontsource-variable/{rubik,inter} (SIL OFL 1.1). Usage: npm run fonts
|
||||||
|
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
||||||
|
import { createRequire } from 'node:module';
|
||||||
|
import subsetFont from 'subset-font';
|
||||||
|
|
||||||
|
const require = createRequire(import.meta.url);
|
||||||
|
|
||||||
|
const ranges = [
|
||||||
|
[0x20, 0x7e], // Basic Latin
|
||||||
|
[0xa0, 0xff], // Latin-1 (ç ö ü · ° × …)
|
||||||
|
[0x11e, 0x11f], // Ğ ğ
|
||||||
|
[0x130, 0x131], // İ ı
|
||||||
|
[0x15e, 0x15f], // Ş ş
|
||||||
|
[0x394, 0x394], // Δ
|
||||||
|
[0x3a9, 0x3a9], // Ω
|
||||||
|
[0x3bb, 0x3bc], // λ μ
|
||||||
|
[0x2013, 0x2014], // – —
|
||||||
|
[0x2018, 0x201e], // ‘ ’ ‚ “ ” „
|
||||||
|
[0x2022, 0x2022], // •
|
||||||
|
[0x2026, 0x2026], // …
|
||||||
|
[0x2032, 0x2033], // ′ ″
|
||||||
|
[0x20ba, 0x20ba], // ₺
|
||||||
|
[0x2122, 0x2122], // ™
|
||||||
|
[0x2190, 0x2193], // ← ↑ → ↓
|
||||||
|
[0x2212, 0x2212], // −
|
||||||
|
[0x2248, 0x2248], // ≈
|
||||||
|
[0x2264, 0x2265], // ≤ ≥
|
||||||
|
];
|
||||||
|
const text = ranges
|
||||||
|
.flatMap(([a, b]) => Array.from({ length: b - a + 1 }, (_, i) => String.fromCodePoint(a + i)))
|
||||||
|
.join('');
|
||||||
|
|
||||||
|
// Fontsource ships each family split by unicode-range (latin / latin-ext). Each part is subset
|
||||||
|
// on its own and keeps Fontsource's unicode-range, so browsers fetch only the parts a page needs.
|
||||||
|
const families = [
|
||||||
|
{ pkg: '@fontsource-variable/rubik', family: 'Rubik Variable', slug: 'rubik', wght: [400, 900] },
|
||||||
|
{ pkg: '@fontsource-variable/inter', family: 'Inter Variable', slug: 'inter', wght: [400, 700] },
|
||||||
|
];
|
||||||
|
const parts = ['latin', 'latin-ext'];
|
||||||
|
|
||||||
|
/** unicode-range of one Fontsource part, read from the package's own CSS. */
|
||||||
|
function unicodeRange(pkg, slug, part) {
|
||||||
|
const css = readFileSync(require.resolve(`${pkg}/index.css`), 'utf8');
|
||||||
|
const block = css
|
||||||
|
.split('@font-face')
|
||||||
|
.find((b) => b.includes(`${slug}-${part}-wght-normal.woff2`));
|
||||||
|
const m = block?.match(/unicode-range:\s*([^;]+);/);
|
||||||
|
if (!m) throw new Error(`unicode-range not found for ${slug} ${part}`);
|
||||||
|
return m[1].trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
mkdirSync('public/fonts', { recursive: true });
|
||||||
|
const faces = [];
|
||||||
|
for (const f of families) {
|
||||||
|
for (const part of parts) {
|
||||||
|
const source = readFileSync(
|
||||||
|
require.resolve(`${f.pkg}/files/${f.slug}-${part}-wght-normal.woff2`),
|
||||||
|
);
|
||||||
|
const out = await subsetFont(source, text, {
|
||||||
|
targetFormat: 'woff2',
|
||||||
|
variationAxes: { wght: { min: f.wght[0], max: f.wght[1] } },
|
||||||
|
});
|
||||||
|
const file = `${f.slug}-${part}.woff2`;
|
||||||
|
writeFileSync(`public/fonts/${file}`, out);
|
||||||
|
console.log(`${file}: ${source.length} → ${out.length} bytes`);
|
||||||
|
faces.push(`@font-face {
|
||||||
|
font-family: '${f.family}';
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
font-weight: ${f.wght[0]} ${f.wght[1]};
|
||||||
|
src: url('/fonts/${file}') format('woff2-variations');
|
||||||
|
unicode-range: ${unicodeRange(f.pkg, f.slug, part)};
|
||||||
|
}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writeFileSync(
|
||||||
|
'src/styles/fonts.css',
|
||||||
|
`/* Generated by scripts/subset-fonts.mjs — do not edit. Fonts: SIL OFL 1.1 (Rubik, Inter). */\n${faces.join('\n')}\n`,
|
||||||
|
);
|
||||||
|
console.log('Wrote src/styles/fonts.css');
|
||||||
@@ -5,9 +5,12 @@ export interface Props {
|
|||||||
/** Page title without the site suffix. Omit on the home page. */
|
/** Page title without the site suffix. Omit on the home page. */
|
||||||
title?: string;
|
title?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
/** Absolute or root-relative image URL for OG/Twitter. */
|
/** Absolute or root-relative image URL for OG/Twitter. Defaults to the generated /og-default.png. */
|
||||||
image?: string;
|
image?: string;
|
||||||
imageAlt?: string;
|
imageAlt?: string;
|
||||||
|
/** Pixel size of `image`, when known. */
|
||||||
|
imageWidth?: number;
|
||||||
|
imageHeight?: number;
|
||||||
type?: 'website' | 'article';
|
type?: 'website' | 'article';
|
||||||
publishedTime?: Date;
|
publishedTime?: Date;
|
||||||
modifiedTime?: Date;
|
modifiedTime?: Date;
|
||||||
@@ -21,6 +24,8 @@ const {
|
|||||||
description = site.description,
|
description = site.description,
|
||||||
image,
|
image,
|
||||||
imageAlt,
|
imageAlt,
|
||||||
|
imageWidth,
|
||||||
|
imageHeight,
|
||||||
type = 'website',
|
type = 'website',
|
||||||
publishedTime,
|
publishedTime,
|
||||||
modifiedTime,
|
modifiedTime,
|
||||||
@@ -30,7 +35,11 @@ const {
|
|||||||
|
|
||||||
const fullTitle = title ? `${title}${site.titleSeparator}${site.name}` : site.title;
|
const fullTitle = title ? `${title}${site.titleSeparator}${site.name}` : site.title;
|
||||||
const canonical = new URL(Astro.url.pathname, Astro.site ?? site.url).href;
|
const canonical = new URL(Astro.url.pathname, Astro.site ?? site.url).href;
|
||||||
const imageUrl = image ? new URL(image, Astro.site ?? site.url).href : undefined;
|
// Pages without their own image share the default card (1200×630).
|
||||||
|
const ogImage = image ?? '/og-default.png';
|
||||||
|
const imageUrl = new URL(ogImage, Astro.site ?? site.url).href;
|
||||||
|
const [ogWidth, ogHeight] = image ? [imageWidth, imageHeight] : [1200, 630];
|
||||||
|
const ogAlt = imageAlt ?? (image ? undefined : site.title);
|
||||||
const twitterHandle = social.find((s) => s.id === 'x')?.handle;
|
const twitterHandle = social.find((s) => s.id === 'x')?.handle;
|
||||||
|
|
||||||
const personId = `${site.url}/#person`;
|
const personId = `${site.url}/#person`;
|
||||||
@@ -61,8 +70,11 @@ const graph = {
|
|||||||
|
|
||||||
<title>{fullTitle}</title>
|
<title>{fullTitle}</title>
|
||||||
<meta name="description" content={description} />
|
<meta name="description" content={description} />
|
||||||
|
{noindex ? (
|
||||||
|
<meta name="robots" content="noindex, nofollow" />
|
||||||
|
) : (
|
||||||
<link rel="canonical" href={canonical} />
|
<link rel="canonical" href={canonical} />
|
||||||
{noindex && <meta name="robots" content="noindex, nofollow" />}
|
)}
|
||||||
|
|
||||||
<meta property="og:type" content={type} />
|
<meta property="og:type" content={type} />
|
||||||
<meta property="og:site_name" content={site.title} />
|
<meta property="og:site_name" content={site.title} />
|
||||||
@@ -70,16 +82,19 @@ const graph = {
|
|||||||
<meta property="og:title" content={fullTitle} />
|
<meta property="og:title" content={fullTitle} />
|
||||||
<meta property="og:description" content={description} />
|
<meta property="og:description" content={description} />
|
||||||
<meta property="og:url" content={canonical} />
|
<meta property="og:url" content={canonical} />
|
||||||
{imageUrl && <meta property="og:image" content={imageUrl} />}
|
<meta property="og:image" content={imageUrl} />
|
||||||
{imageUrl && imageAlt && <meta property="og:image:alt" content={imageAlt} />}
|
{ogWidth && <meta property="og:image:width" content={String(ogWidth)} />}
|
||||||
|
{ogHeight && <meta property="og:image:height" content={String(ogHeight)} />}
|
||||||
|
{ogAlt && <meta property="og:image:alt" content={ogAlt} />}
|
||||||
{publishedTime && <meta property="article:published_time" content={publishedTime.toISOString()} />}
|
{publishedTime && <meta property="article:published_time" content={publishedTime.toISOString()} />}
|
||||||
{modifiedTime && <meta property="article:modified_time" content={modifiedTime.toISOString()} />}
|
{modifiedTime && <meta property="article:modified_time" content={modifiedTime.toISOString()} />}
|
||||||
|
|
||||||
<meta name="twitter:card" content={imageUrl ? 'summary_large_image' : 'summary'} />
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
{twitterHandle && <meta name="twitter:site" content={twitterHandle} />}
|
{twitterHandle && <meta name="twitter:site" content={twitterHandle} />}
|
||||||
{twitterHandle && <meta name="twitter:creator" content={twitterHandle} />}
|
{twitterHandle && <meta name="twitter:creator" content={twitterHandle} />}
|
||||||
<meta name="twitter:title" content={fullTitle} />
|
<meta name="twitter:title" content={fullTitle} />
|
||||||
<meta name="twitter:description" content={description} />
|
<meta name="twitter:description" content={description} />
|
||||||
{imageUrl && <meta name="twitter:image" content={imageUrl} />}
|
<meta name="twitter:image" content={imageUrl} />
|
||||||
|
{ogAlt && <meta name="twitter:image:alt" content={ogAlt} />}
|
||||||
|
|
||||||
<script type="application/ld+json" set:html={JSON.stringify(graph)} />
|
<script type="application/ld+json" set:html={JSON.stringify(graph)} />
|
||||||
|
|||||||
@@ -17,10 +17,14 @@ const props = Astro.props;
|
|||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<ThemeInit />
|
<ThemeInit />
|
||||||
|
<link rel="preload" href="/fonts/rubik-latin.woff2" as="font" type="font/woff2" crossorigin />
|
||||||
|
<link rel="preload" href="/fonts/inter-latin.woff2" as="font" type="font/woff2" crossorigin />
|
||||||
<meta name="theme-color" content="#f2efee" media="(prefers-color-scheme: light)" />
|
<meta name="theme-color" content="#f2efee" media="(prefers-color-scheme: light)" />
|
||||||
<meta name="theme-color" content="#111012" media="(prefers-color-scheme: dark)" />
|
<meta name="theme-color" content="#111012" media="(prefers-color-scheme: dark)" />
|
||||||
|
<link rel="icon" href="/favicon.ico" sizes="48x48" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<link rel="icon" href="/favicon.ico" sizes="32x32" />
|
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||||
|
<link rel="manifest" href="/site.webmanifest" />
|
||||||
<link rel="alternate" type="application/rss+xml" title={site.title} href="/rss.xml" />
|
<link rel="alternate" type="application/rss+xml" title={site.title} href="/rss.xml" />
|
||||||
<meta name="generator" content={Astro.generator} />
|
<meta name="generator" content={Astro.generator} />
|
||||||
<Seo {...props} />
|
<Seo {...props} />
|
||||||
|
|||||||
@@ -0,0 +1,164 @@
|
|||||||
|
/**
|
||||||
|
* Build-time Open Graph images (1200×630 PNG) with Satori + Sharp.
|
||||||
|
* Used by the prerendered /og/<slug>.png and /og-default.png endpoints.
|
||||||
|
*/
|
||||||
|
import { readFileSync } from 'node:fs';
|
||||||
|
import { createRequire } from 'node:module';
|
||||||
|
import satori from 'satori';
|
||||||
|
import sharp from 'sharp';
|
||||||
|
import { site } from './data';
|
||||||
|
|
||||||
|
const require = createRequire(import.meta.url);
|
||||||
|
const font = (pkg: string, file: string) => readFileSync(require.resolve(`${pkg}/files/${file}`));
|
||||||
|
|
||||||
|
// Satori needs woff/ttf (not woff2). latin-ext (İ, Ş, Ğ…) gets its own family name and is
|
||||||
|
// listed as a fallback in fontFamily; same-name entries are not used as glyph fallbacks.
|
||||||
|
const fonts = [
|
||||||
|
{
|
||||||
|
name: 'Rubik',
|
||||||
|
weight: 900 as const,
|
||||||
|
data: font('@fontsource/rubik', 'rubik-latin-900-normal.woff'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'RubikExt',
|
||||||
|
weight: 900 as const,
|
||||||
|
data: font('@fontsource/rubik', 'rubik-latin-ext-900-normal.woff'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Rubik',
|
||||||
|
weight: 700 as const,
|
||||||
|
data: font('@fontsource/rubik', 'rubik-latin-700-normal.woff'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'RubikExt',
|
||||||
|
weight: 700 as const,
|
||||||
|
data: font('@fontsource/rubik', 'rubik-latin-ext-700-normal.woff'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Inter',
|
||||||
|
weight: 600 as const,
|
||||||
|
data: font('@fontsource/inter', 'inter-latin-600-normal.woff'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'InterExt',
|
||||||
|
weight: 600 as const,
|
||||||
|
data: font('@fontsource/inter', 'inter-latin-ext-600-normal.woff'),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Image-only palette (Huemint Magazine); theme tokens are CSS-only.
|
||||||
|
const C = {
|
||||||
|
paper: '#f2efee',
|
||||||
|
khaki: '#d5d09b',
|
||||||
|
ink: '#111012',
|
||||||
|
red: '#c95748',
|
||||||
|
line: 'rgba(17,16,18,0.18)',
|
||||||
|
};
|
||||||
|
|
||||||
|
function towerIcon(color: string): string {
|
||||||
|
const { icons, width = 24, height = 24 } = require('@iconify-json/tabler/icons.json');
|
||||||
|
const body: string = icons['building-broadcast-tower'].body.replaceAll('currentColor', color);
|
||||||
|
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}">${body}</svg>`;
|
||||||
|
return `data:image/svg+xml;base64,${Buffer.from(svg).toString('base64')}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Node = { type: string; props: Record<string, unknown> & { children?: unknown } };
|
||||||
|
const h = (type: string, style: Record<string, unknown>, children?: unknown, extra = {}): Node => ({
|
||||||
|
type,
|
||||||
|
props: { style, children, ...extra },
|
||||||
|
});
|
||||||
|
|
||||||
|
export interface OgInput {
|
||||||
|
title: string;
|
||||||
|
eyebrow?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function renderOg({ title, eyebrow }: OgInput): Promise<Buffer> {
|
||||||
|
const upper = title.toLocaleUpperCase('tr-TR');
|
||||||
|
const size = upper.length <= 28 ? 96 : upper.length <= 48 ? 78 : 62;
|
||||||
|
|
||||||
|
const tree = h(
|
||||||
|
'div',
|
||||||
|
{
|
||||||
|
width: 1200,
|
||||||
|
height: 630,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
position: 'relative',
|
||||||
|
backgroundColor: C.khaki,
|
||||||
|
color: C.ink,
|
||||||
|
fontFamily: 'Rubik, RubikExt',
|
||||||
|
},
|
||||||
|
[
|
||||||
|
// Magazine grid lines
|
||||||
|
...[140, 380, 700, 1000].map((x) =>
|
||||||
|
h('div', {
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
bottom: 0,
|
||||||
|
left: x,
|
||||||
|
width: 2,
|
||||||
|
backgroundColor: C.line,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
h(
|
||||||
|
'div',
|
||||||
|
{ display: 'flex', flexDirection: 'column', flex: 1, padding: '56px 72px 40px 72px' },
|
||||||
|
[
|
||||||
|
h('div', { display: 'flex', alignItems: 'center', justifyContent: 'space-between' }, [
|
||||||
|
h('div', { display: 'flex', alignItems: 'center', gap: 16 }, [
|
||||||
|
h('img', { width: 56, height: 56 }, undefined, {
|
||||||
|
src: towerIcon(C.ink),
|
||||||
|
width: 56,
|
||||||
|
height: 56,
|
||||||
|
}),
|
||||||
|
h('div', { fontSize: 44, fontWeight: 900, letterSpacing: -1 }, site.author.callsign),
|
||||||
|
]),
|
||||||
|
eyebrow
|
||||||
|
? h('div', { display: 'flex', alignItems: 'center', gap: 16 }, [
|
||||||
|
h(
|
||||||
|
'div',
|
||||||
|
{ fontSize: 26, fontWeight: 700, letterSpacing: 1 },
|
||||||
|
eyebrow.toLocaleUpperCase('tr-TR'),
|
||||||
|
),
|
||||||
|
h('div', { width: 64, height: 4, backgroundColor: C.red }),
|
||||||
|
])
|
||||||
|
: h('div', {}, ''),
|
||||||
|
]),
|
||||||
|
h('div', { display: 'flex', flex: 1, alignItems: 'center' }, [
|
||||||
|
h(
|
||||||
|
'div',
|
||||||
|
{
|
||||||
|
fontSize: size,
|
||||||
|
fontWeight: 900,
|
||||||
|
lineHeight: 0.98,
|
||||||
|
letterSpacing: -2,
|
||||||
|
maxWidth: 1040,
|
||||||
|
},
|
||||||
|
upper,
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
h(
|
||||||
|
'div',
|
||||||
|
{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
fontFamily: 'Inter, InterExt',
|
||||||
|
fontWeight: 600,
|
||||||
|
fontSize: 26,
|
||||||
|
},
|
||||||
|
[h('div', {}, new URL(site.url).host), h('div', {}, `${site.author.locator} · 73`)],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
h('div', { height: 16, backgroundColor: C.red }),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
const svg = await satori(tree as Parameters<typeof satori>[0], {
|
||||||
|
width: 1200,
|
||||||
|
height: 630,
|
||||||
|
fonts,
|
||||||
|
});
|
||||||
|
return sharp(Buffer.from(svg)).png({ compressionLevel: 9 }).toBuffer();
|
||||||
|
}
|
||||||
@@ -37,10 +37,12 @@ const { Content, headings } = await render(post);
|
|||||||
const category = categoryOf(post);
|
const category = categoryOf(post);
|
||||||
const url = new URL(postUrl(post), site.url).href;
|
const url = new URL(postUrl(post), site.url).href;
|
||||||
|
|
||||||
// 1200px OG/Twitter image from the cover (absolute URL in Seo).
|
// Share image: 1200px JPEG of the cover, else the generated /og/<slug>.png card.
|
||||||
const ogImage = cover
|
const coverOg = cover ? await getImage({ src: cover, width: 1200, format: 'jpeg' }) : undefined;
|
||||||
? (await getImage({ src: cover, width: 1200, format: 'jpeg' })).src
|
const ogImage = coverOg?.src ?? `/og/${post.id}.png`;
|
||||||
: undefined;
|
const [ogWidth, ogHeight] = coverOg
|
||||||
|
? [Number(coverOg.attributes.width), Number(coverOg.attributes.height)]
|
||||||
|
: [1200, 630];
|
||||||
|
|
||||||
const crumbs = [
|
const crumbs = [
|
||||||
{ label: site.ui.home, href: '/' },
|
{ label: site.ui.home, href: '/' },
|
||||||
@@ -64,7 +66,7 @@ const jsonLd = [
|
|||||||
publisher: { '@id': `${site.url}/#person` },
|
publisher: { '@id': `${site.url}/#person` },
|
||||||
articleSection: category.name,
|
articleSection: category.name,
|
||||||
keywords: tags.join(', '),
|
keywords: tags.join(', '),
|
||||||
...(ogImage && { image: new URL(ogImage, site.url).href }),
|
image: new URL(ogImage, site.url).href,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'@type': 'BreadcrumbList',
|
'@type': 'BreadcrumbList',
|
||||||
@@ -83,7 +85,9 @@ const jsonLd = [
|
|||||||
description={description}
|
description={description}
|
||||||
type="article"
|
type="article"
|
||||||
image={ogImage}
|
image={ogImage}
|
||||||
imageAlt={coverAlt}
|
imageAlt={coverAlt ?? title}
|
||||||
|
imageWidth={ogWidth}
|
||||||
|
imageHeight={ogHeight}
|
||||||
publishedTime={pubDate}
|
publishedTime={pubDate}
|
||||||
modifiedTime={updatedDate}
|
modifiedTime={updatedDate}
|
||||||
jsonLd={jsonLd}
|
jsonLd={jsonLd}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
---
|
---
|
||||||
|
// Static import: inlined on this page only (a dynamic CSS import breaks with inlineStylesheets).
|
||||||
|
import 'leaflet/dist/leaflet.css';
|
||||||
import { Icon } from 'astro-icon/components';
|
import { Icon } from 'astro-icon/components';
|
||||||
import Container from '../components/Container.astro';
|
import Container from '../components/Container.astro';
|
||||||
import ExternalLink from '../components/ExternalLink.astro';
|
import ExternalLink from '../components/ExternalLink.astro';
|
||||||
@@ -116,10 +118,7 @@ const sectionTitle =
|
|||||||
const el = document.getElementById('map');
|
const el = document.getElementById('map');
|
||||||
if (el) {
|
if (el) {
|
||||||
const init = async () => {
|
const init = async () => {
|
||||||
const [{ default: L }] = await Promise.all([
|
const { default: L } = await import('leaflet');
|
||||||
import('leaflet'),
|
|
||||||
import('leaflet/dist/leaflet.css'),
|
|
||||||
]);
|
|
||||||
const { lat, lng, zoom, popup = '' } = el.dataset;
|
const { lat, lng, zoom, popup = '' } = el.dataset;
|
||||||
const center: [number, number] = [Number(lat), Number(lng)];
|
const center: [number, number] = [Number(lat), Number(lng)];
|
||||||
el.querySelector('[data-map-loading]')?.remove();
|
el.querySelector('[data-map-loading]')?.remove();
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import type { APIRoute } from 'astro';
|
||||||
|
import { home } from '../lib/data';
|
||||||
|
import { renderOg } from '../lib/og';
|
||||||
|
|
||||||
|
// /og-default.png — share image for pages without their own.
|
||||||
|
export const GET: APIRoute = async () => {
|
||||||
|
const png = await renderOg({ title: home.hero.title });
|
||||||
|
return new Response(new Uint8Array(png), { headers: { 'Content-Type': 'image/png' } });
|
||||||
|
};
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import type { APIRoute, GetStaticPaths } from 'astro';
|
||||||
|
import { renderOg } from '../../lib/og';
|
||||||
|
import { categoryOf, getPosts, type Post } from '../../lib/posts';
|
||||||
|
|
||||||
|
// /og/<slug>.png — per-post share image, generated at build time.
|
||||||
|
export const getStaticPaths = (async () => {
|
||||||
|
const posts = await getPosts();
|
||||||
|
return posts.map((post) => ({ params: { slug: post.id }, props: { post } }));
|
||||||
|
}) satisfies GetStaticPaths;
|
||||||
|
|
||||||
|
export const GET: APIRoute<{ post: Post }> = async ({ props }) => {
|
||||||
|
const png = await renderOg({
|
||||||
|
title: props.post.data.title,
|
||||||
|
eyebrow: categoryOf(props.post).name,
|
||||||
|
});
|
||||||
|
return new Response(new Uint8Array(png), { headers: { 'Content-Type': 'image/png' } });
|
||||||
|
};
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import type { APIRoute } from 'astro';
|
||||||
|
import { site } from '../lib/data';
|
||||||
|
|
||||||
|
export const GET: APIRoute = () =>
|
||||||
|
new Response(
|
||||||
|
[
|
||||||
|
'User-agent: *',
|
||||||
|
'Allow: /',
|
||||||
|
'Disallow: /api/',
|
||||||
|
'',
|
||||||
|
`Sitemap: ${site.url}/sitemap-index.xml`,
|
||||||
|
'',
|
||||||
|
].join('\n'),
|
||||||
|
{ headers: { 'Content-Type': 'text/plain; charset=utf-8' } },
|
||||||
|
);
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import type { APIRoute } from 'astro';
|
||||||
|
import { site } from '../lib/data';
|
||||||
|
|
||||||
|
export const GET: APIRoute = () =>
|
||||||
|
Response.json(
|
||||||
|
{
|
||||||
|
name: site.title,
|
||||||
|
short_name: site.name,
|
||||||
|
description: site.description,
|
||||||
|
lang: site.lang,
|
||||||
|
start_url: '/',
|
||||||
|
display: 'minimal-ui',
|
||||||
|
background_color: '#f2efee',
|
||||||
|
theme_color: '#d5d09b',
|
||||||
|
icons: [
|
||||||
|
{ src: '/icon-192.png', sizes: '192x192', type: 'image/png' },
|
||||||
|
{ src: '/icon-512.png', sizes: '512x512', type: 'image/png' },
|
||||||
|
{ src: '/icon-maskable-512.png', sizes: '512x512', type: 'image/png', purpose: 'maskable' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ headers: { 'Content-Type': 'application/manifest+json' } },
|
||||||
|
);
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
/* Generated by scripts/subset-fonts.mjs — do not edit. Fonts: SIL OFL 1.1 (Rubik, Inter). */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Rubik Variable';
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
font-weight: 400 900;
|
||||||
|
src: url('/fonts/rubik-latin.woff2') format('woff2-variations');
|
||||||
|
unicode-range:
|
||||||
|
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
|
||||||
|
U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Rubik Variable';
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
font-weight: 400 900;
|
||||||
|
src: url('/fonts/rubik-latin-ext.woff2') format('woff2-variations');
|
||||||
|
unicode-range:
|
||||||
|
U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329,
|
||||||
|
U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F,
|
||||||
|
U+A720-A7FF;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter Variable';
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
font-weight: 400 700;
|
||||||
|
src: url('/fonts/inter-latin.woff2') format('woff2-variations');
|
||||||
|
unicode-range:
|
||||||
|
U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
|
||||||
|
U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter Variable';
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
font-weight: 400 700;
|
||||||
|
src: url('/fonts/inter-latin-ext.woff2') format('woff2-variations');
|
||||||
|
unicode-range:
|
||||||
|
U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329,
|
||||||
|
U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F,
|
||||||
|
U+A720-A7FF;
|
||||||
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
@import 'tailwindcss';
|
@import 'tailwindcss';
|
||||||
@import '@fontsource-variable/rubik';
|
@import './fonts.css'; /* generated: npm run fonts */
|
||||||
@import '@fontsource-variable/inter';
|
|
||||||
@plugin '@tailwindcss/typography';
|
@plugin '@tailwindcss/typography';
|
||||||
|
|
||||||
@custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *));
|
@custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *));
|
||||||
|
|||||||