mirror of
https://github.com/Ku6epXBOCTuK/easy-png-tools.git
synced 2026-11-13 17:08:20 +00:00
chore: add sitemap generator
This commit is contained in:
+28
-19
@@ -104,13 +104,12 @@
|
|||||||
Чего нет (пробелы закрываются этапами ниже):
|
Чего нет (пробелы закрываются этапами ниже):
|
||||||
|
|
||||||
| Пробел | Где сейчас | Этап |
|
| Пробел | Где сейчас | Этап |
|
||||||
| -------------------------------------------------- | ----------------------------------- | ---- |
|
| -------------------------------------------------- | -------------------------------- | ---- |
|
||||||
| `noindex` на страницах инструментов | стоит (`+page.svelte`) | S1 |
|
| `noindex` на страницах инструментов | стоит (`+page.svelte`) | S1 |
|
||||||
| `sitemap.xml` | нет | S1 |
|
| `sitemap.xml` | генерируется при сборке (S1b) | — |
|
||||||
| `robots.txt` без `Sitemap:` | allow-all (`web/static/robots.txt`) | S1 |
|
| `robots.txt` без `Sitemap:` | allow-all + `Sitemap:` (S1c) | — |
|
||||||
| `rel=canonical` | нет | S1 |
|
| `rel=canonical` | есть, `Seo.svelte` (S1d) | — |
|
||||||
| OG/Twitter-теги, OG-картинки | нет | S1 |
|
| OG/Twitter-теги | есть; per-tool картинки — S1e | S1 |
|
||||||
| Канонический домен в абсолютных URL | нет, деплой на GitHub Pages с базой | S1 |
|
|
||||||
| Контент страницы (how-to, FAQ, related) | title, robots и отрендеренный H1 | S2 |
|
| Контент страницы (how-to, FAQ, related) | title, robots и отрендеренный H1 | S2 |
|
||||||
| JSON-LD (SoftwareApplication, FAQPage, Breadcrumb) | нет | S3 |
|
| JSON-LD (SoftwareApplication, FAQPage, Breadcrumb) | нет | S3 |
|
||||||
| `llms.txt`, хабы категорий с текстом | нет | S3 |
|
| `llms.txt`, хабы категорий с текстом | нет | S3 |
|
||||||
@@ -211,19 +210,29 @@
|
|||||||
(301) делает сам GitHub Pages при включённом кастомном домене. Делается
|
(301) делает сам GitHub Pages при включённом кастомном домене. Делается
|
||||||
одновременно с S1d (canonical), чтобы Google проиндексировал сразу с
|
одновременно с S1d (canonical), чтобы Google проиндексировал сразу с
|
||||||
правильным доменом.
|
правильным доменом.
|
||||||
- [ ] **[S1b]** Скрипт `web/scripts/gen-sitemap.mjs`: генерирует `sitemap.xml` в
|
- [x] **[S1b]** Скрипт `web/scripts/gen-sitemap.mjs` — **сделано 2026-10-12**:
|
||||||
`build/` из `PAGES` реестра (`web/src/lib/registry`) + статических
|
пишет `sitemap.xml` в `build/` после `vite build` (вызов добавлен в
|
||||||
страниц. Взять адреса у страниц, а не у инструментов: страниц может быть
|
`pnpm build`). Адреса берутся **из собранного HTML**, а не из импорта
|
||||||
несколько на один инструмент, у инструмента адреса нет. Вызов из
|
`PAGES`: список prerender-страниц и есть фактический набор URL, а страницы
|
||||||
`pnpm build` после `vite build` (adapter-static пишет в `build/`,
|
с `noindex` в HTML автоматически исключаются — sitemap не может разойтись
|
||||||
дописываем файл там же). Origin — из env (`PUBLIC_SITE_ORIGIN`), учёт
|
с тем, что реально задеплоено, и не рекламирует то, что мы просим
|
||||||
опционального `BASE_PATH`.
|
краулеров игнорировать. Проверено: в sitemap попадают `/`, `/list-tools`,
|
||||||
- [ ] **[S1c]** `robots.txt`: `Sitemap:` + закрыть служебный `/kit`; после
|
`/chains`, но не `/kit`, `/pipeline` и страницы инструментов (пока S1h).
|
||||||
cleanup — вернуть минимальный allow-all.
|
Origin — `PUBLIC_SITE_ORIGIN`, фолбэк — `static/CNAME` (тот же домен, что
|
||||||
- [ ] **[S1d]** Компонент `Seo.svelte` (kit): canonical (абсолютный URL по
|
публикует GitHub Pages, без третьей копии литерала); опциональный
|
||||||
актуальному маршруту), OG-теги (title/description/type/url/image),
|
`BASE_PATH` учитывается как в `vite.config.ts`.
|
||||||
twitter:card. Подключить в root-layout. **Требует `PUBLIC_SITE_ORIGIN`**
|
- [x] **[S1c]** `robots.txt` — **сделано 2026-10-12**: `Sitemap:` добавлен,
|
||||||
для абсолютных URL в canonical и OG:url.
|
allow-all сохранён. **Служебный `/kit` закрыт не через `Disallow:`, а
|
||||||
|
через `noindex` в HTML** (как `/pipeline`): заблокированный краулером, но
|
||||||
|
не помеченный noindex URL Google помечает «проиндексирован, несмотря на
|
||||||
|
блокировку в robots.txt» и оставляет старый сниппет. Побочная выгода: тот
|
||||||
|
же признак автоматически выкидывает `/kit` из sitemap.
|
||||||
|
- [x] **[S1d]** Компонент `Seo.svelte` (kit) — **был сделан до S1b, отмечено
|
||||||
|
2026-10-12**: canonical (абсолютный URL от `PUBLIC_SITE_ORIGIN` +
|
||||||
|
`SITE_URL`, `web/src/lib/seo.ts`), OG (title/description/type/url/image) и
|
||||||
|
`twitter:card`. Подключён на `/`, `/list-tools`, `/chains`,
|
||||||
|
`/tools/[slug]`. Картинка пока общая `og/default.png` — per-tool остаётся
|
||||||
|
за S1e.
|
||||||
|
|
||||||
> **Порядок S1:** S1e/S1f/S1g → покупка домена → S1a+S1b+S1c+S1d → **S1h**
|
> **Порядок S1:** S1e/S1f/S1g → покупка домена → S1a+S1b+S1c+S1d → **S1h**
|
||||||
> (снятие `noindex`) → S2-контент. Гейты: `pnpm --dir web build` зелёный; в
|
> (снятие `noindex`) → S2-контент. Гейты: `pnpm --dir web build` зелёный; в
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev",
|
"dev": "vite dev",
|
||||||
"build": "vite build",
|
"build": "vite build && node scripts/gen-sitemap.mjs",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"prepare": "svelte-kit sync || echo ''",
|
"prepare": "svelte-kit sync || echo ''",
|
||||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
// Writes build/sitemap.xml right after `vite build` (adapter-static already
|
||||||
|
// emitted the HTML there). The URL set is read back from the build output
|
||||||
|
// instead of the registry: prerendered pages are exactly the pages that exist,
|
||||||
|
// and a page whose HTML carries `noindex` is skipped, so the sitemap can never
|
||||||
|
// drift from what was shipped or advertise a page we ask crawlers to ignore.
|
||||||
|
import { existsSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
||||||
|
import { join, relative, sep } from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
const BUILD_DIR = fileURLToPath(new URL("../build/", import.meta.url));
|
||||||
|
|
||||||
|
function siteOrigin() {
|
||||||
|
const fromEnv = (process.env.PUBLIC_SITE_ORIGIN ?? "").trim();
|
||||||
|
if (fromEnv !== "") return fromEnv.replace(/\/+$/, "");
|
||||||
|
|
||||||
|
// Local builds run without CI env vars; `static/CNAME` is the domain GitHub
|
||||||
|
// Pages publishes under, so it is the same truth as the deploy-time env.
|
||||||
|
const cnamePath = fileURLToPath(new URL("../static/CNAME", import.meta.url));
|
||||||
|
if (!existsSync(cnamePath)) {
|
||||||
|
throw new Error(
|
||||||
|
"gen-sitemap: no site origin — set PUBLIC_SITE_ORIGIN or add web/static/CNAME",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const cname = readFileSync(cnamePath, "utf8").trim();
|
||||||
|
if (cname === "") {
|
||||||
|
throw new Error("gen-sitemap: web/static/CNAME is empty");
|
||||||
|
}
|
||||||
|
return `https://${cname}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Mirrors the `base` normalization in vite.config.ts. */
|
||||||
|
const rawBase = (process.env.BASE_PATH ?? "").trim();
|
||||||
|
const base =
|
||||||
|
rawBase === "" || rawBase === "/"
|
||||||
|
? ""
|
||||||
|
: `/${rawBase.replace(/^\/+/, "").replace(/\/+$/, "")}`;
|
||||||
|
|
||||||
|
function* htmlFiles(dir) {
|
||||||
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
||||||
|
const full = join(dir, entry.name);
|
||||||
|
if (entry.isDirectory()) yield* htmlFiles(full);
|
||||||
|
else if (entry.name.endsWith(".html")) yield full;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* adapter-static names prerendered pages `<route>.html` (`tools/crop-png.html`)
|
||||||
|
* because `trailingSlash` is "never"; the `index.html` form covers a future
|
||||||
|
* switch. GitHub Pages serves the flat file at the extension-less URL.
|
||||||
|
*/
|
||||||
|
function routePath(file) {
|
||||||
|
const rel = relative(BUILD_DIR, file).split(sep).join("/");
|
||||||
|
if (rel === "404.html") return null;
|
||||||
|
if (rel === "index.html") return "/";
|
||||||
|
if (rel.endsWith("/index.html"))
|
||||||
|
return `/${rel.slice(0, -"index.html".length)}`;
|
||||||
|
return `/${rel.slice(0, -".html".length)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeXml(value) {
|
||||||
|
return value.replace(/&/g, "&").replace(/</g, "<");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!existsSync(BUILD_DIR)) {
|
||||||
|
throw new Error("gen-sitemap: build/ is missing — run `vite build` first");
|
||||||
|
}
|
||||||
|
|
||||||
|
const origin = siteOrigin();
|
||||||
|
const locs = [];
|
||||||
|
|
||||||
|
for (const file of htmlFiles(BUILD_DIR)) {
|
||||||
|
const html = readFileSync(file, "utf8");
|
||||||
|
if (/<meta\s+name="robots"\s+content="[^"]*noindex/.test(html)) continue;
|
||||||
|
|
||||||
|
const route = routePath(file);
|
||||||
|
if (route === null) continue;
|
||||||
|
|
||||||
|
locs.push(`${origin}${base}${route}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (locs.length === 0) {
|
||||||
|
throw new Error("gen-sitemap: no indexable pages found in build/");
|
||||||
|
}
|
||||||
|
|
||||||
|
locs.sort();
|
||||||
|
|
||||||
|
const xml = [
|
||||||
|
'<?xml version="1.0" encoding="UTF-8"?>',
|
||||||
|
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',
|
||||||
|
...locs.map((loc) => `\t<url><loc>${escapeXml(loc)}</loc></url>`),
|
||||||
|
"</urlset>",
|
||||||
|
"",
|
||||||
|
].join("\n");
|
||||||
|
|
||||||
|
writeFileSync(join(BUILD_DIR, "sitemap.xml"), xml);
|
||||||
|
console.log(`sitemap.xml written: ${locs.length} urls (origin ${origin})`);
|
||||||
@@ -41,6 +41,10 @@
|
|||||||
);
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<meta name="robots" content="noindex, nofollow" />
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
<div class="showcase">
|
<div class="showcase">
|
||||||
<header class="showcase-head">
|
<header class="showcase-head">
|
||||||
<span class="showcase-eyebrow">KIT SHOWCASE</span>
|
<span class="showcase-eyebrow">KIT SHOWCASE</span>
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
# allow crawling everything by default
|
# allow crawling everything by default
|
||||||
User-agent: *
|
User-agent: *
|
||||||
Disallow:
|
Disallow:
|
||||||
|
|
||||||
|
# Service pages (/kit) are excluded from the index with a `noindex` meta tag,
|
||||||
|
# not with Disallow: a blocked-but-not-noindexed URL shows up in Search Console
|
||||||
|
# as an error and keeps its old indexed snippet.
|
||||||
|
|
||||||
|
Sitemap: https://easy-image-tools.com/sitemap.xml
|
||||||
Reference in New Issue
Block a user