Astro site (site/)
Standalone pnpm project (own lockfile, not part of the plugin build). Astro + @astrojs/sitemap + @astrojs/check. Deployed to GitHub Pages at https://vadimonix.github.io/xray-decky/.
cd site
pnpm install
pnpm run check # astro check — type-checks locale dictionaries; CI gate
pnpm run build # → site/dist
pnpm run dev
URL & SEO contract (the part that silently breaks)
Everything hangs together only if these stay consistent:
build.format: 'preserve'→ flat pages build tochangelog.html, locale roots toru/index.html(directories).- Canonical forms: flat pages end in
.html; locale roots use trailing slash (…/ru/); site root is…/xray-decky/. Generated bysrc/lib/urls.ts(absoluteUrl,withBase,localeHref). - Sitemap must string-match canonicals. The
serializehook inastro.config.mjsrewrites both<loc>and the auto-generated hreflang<xhtml:link>alternates: locale-root segments (LOCALE_DIRS) get/, everything else extensionless gets.html. History: before 2026-08 it only rewrote<loc>and blanket-appended.html, sending crawlers to nonexistentru.html/zh.html/fa.html/es.html. - Sitemap hreflang set must equal the page's hreflang set.
Base.astroemits 5 locales +x-default; the integration emits only the 5, soserializeappendsx-default(= theenURL). Google reads annotations from both delivery methods and treats differing sets as conflicting, not as a subset.seo-check --sitecompares them per URL. - robots.txt is inert on this deploy. It ships to
/xray-decky/robots.txt, but crawlers read onlyhttps://vadimonix.github.io/robots.txt, which 404s (no user-site repo) — a 404 means "crawl everything", so nothing is blocked, but theSitemap:line is never seen. Sitemap discovery rests entirely on the Search Console submission. Real fix, outside this repo: aVadimOnix/vadimonix.github.iorepo serving a host-root robots.txt, or a custom domain. - GitHub Pages is lenient (extensionless file → 200, dir without slash → 301) — never rely on that; match canonicals exactly.
compressHTML: falseis deliberate: keeps<head>(canonical, hreflang, OG, JSON-LD) greppable for SEO audits; gzip recovers the size.
Adding a locale = one place: src/i18n/locales.ts (LOCALES, localePrefixes, hreflangCodes) + dictionary file typed by src/i18n/types.ts + LOCALE_DIRS in astro.config.mjs + locales map in the sitemap integration config. astro check fails on missing dictionary keys — that's why CI runs it before build.
Deploy
.github/workflows/pages.yml: triggers on push to master touching site/** (or the workflow itself). Node 22, pnpm 9, --frozen-lockfile, check → build → upload site/dist → deploy. No PR builds — PR CI does not build the site, so a green PR says nothing about the site.
Verifying an Astro major bump (empirical, not changelog-reading)
Dependabot bumps only astro — peer integrations (@astrojs/sitemap, @astrojs/check) lag; verify they still work rather than assuming.
- Build the bumped version locally:
pnpm install --frozen-lockfile && pnpm run check && pnpm run build. - Diff
dist/layout against the previous build (same 11+ routes, locale dirs intact, nothing flattened). grep -o '<loc>[^<]*</loc>' dist/sitemap-0.xml— every URL must match a real file's canonical form.- Spot-check
<link rel="canonical">and hreflang indist/index.htmland one locale page. - After merge, watch the Pages run and probe live URLs (
curl -s -o /dev/null -w "%{http_code}").
Known thresholds: Astro 6 requires Node ≥22 (pages.yml already on 22) and dropped CJS configs (ours is .mjs); i18n.routing.redirectToDefaultLocale default changed (irrelevant here — prefixDefaultLocale: false). Astro 7 moved to Vite 8/Rolldown + Rust compiler — mostly affects integrations/custom Vite config, which this site doesn't have. Upgrade guides: https://docs.astro.build/en/guides/upgrade-to/v6/, https://docs.astro.build/en/guides/upgrade-to/v7/.
Misc
optimize-images.sh— image pipeline forpublic/; run after adding heavy assets.- Search Console / Yandex verification tags live in the layout head — don't strip them.
- Wording/positioning rules for the site copy (and every other user-facing surface — READMEs, plugin.json, repo metadata) live in the
seo-surfacesskill + AGENTS.md "SEO surfaces". CI:node scripts/seo-check.mjs --siteruns againstdist/in the Pages workflow.