katman-mcp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/LICENSE +13 -0
  2. package/README.md +160 -0
  3. package/dist/cli.js +71 -0
  4. package/dist/context.js +43 -0
  5. package/dist/crawlers.js +86 -0
  6. package/dist/index.js +25 -0
  7. package/dist/license.js +448 -0
  8. package/dist/meta.js +28 -0
  9. package/dist/net.js +128 -0
  10. package/dist/server.js +17 -0
  11. package/dist/state.js +81 -0
  12. package/dist/tools/account/account.js +64 -0
  13. package/dist/tools/account/cli.js +21 -0
  14. package/dist/tools/account/index.js +48 -0
  15. package/dist/tools/account/sync.js +142 -0
  16. package/dist/tools/account/upsell.js +290 -0
  17. package/dist/tools/crawl/audit/analyze.js +719 -0
  18. package/dist/tools/crawl/audit/bots.js +114 -0
  19. package/dist/tools/crawl/audit/checks.js +126 -0
  20. package/dist/tools/crawl/audit/html.js +382 -0
  21. package/dist/tools/crawl/audit/index.js +364 -0
  22. package/dist/tools/crawl/audit/model.js +1 -0
  23. package/dist/tools/crawl/audit/report.js +199 -0
  24. package/dist/tools/crawl/audit/robots.js +199 -0
  25. package/dist/tools/crawl/audit/sitemap.js +147 -0
  26. package/dist/tools/crawl/cli.js +106 -0
  27. package/dist/tools/crawl/common/advice.js +61 -0
  28. package/dist/tools/crawl/common/findings.js +57 -0
  29. package/dist/tools/crawl/common/routes.js +80 -0
  30. package/dist/tools/crawl/common/text.js +89 -0
  31. package/dist/tools/crawl/common/urls.js +64 -0
  32. package/dist/tools/crawl/index.js +226 -0
  33. package/dist/tools/crawl/indexnow.js +271 -0
  34. package/dist/tools/crawl/keywords.js +178 -0
  35. package/dist/tools/crawl/scan/framework.js +645 -0
  36. package/dist/tools/crawl/scan/index.js +175 -0
  37. package/dist/tools/crawl/scan/project.js +180 -0
  38. package/dist/tools/crawl/scan/routes.js +354 -0
  39. package/dist/tools/crawl/scan/signals.js +414 -0
  40. package/dist/tools/method/cli.js +42 -0
  41. package/dist/tools/method/core/brief.js +302 -0
  42. package/dist/tools/method/core/extract.js +435 -0
  43. package/dist/tools/method/core/generate.js +504 -0
  44. package/dist/tools/method/core/lint.js +556 -0
  45. package/dist/tools/method/core/plan.js +405 -0
  46. package/dist/tools/method/core/prompt.js +150 -0
  47. package/dist/tools/method/core/questions.js +196 -0
  48. package/dist/tools/method/core/report.js +182 -0
  49. package/dist/tools/method/core/research-run.js +148 -0
  50. package/dist/tools/method/core/research.js +448 -0
  51. package/dist/tools/method/core/snippets.js +292 -0
  52. package/dist/tools/method/core/start.js +277 -0
  53. package/dist/tools/method/data/anatomy.js +141 -0
  54. package/dist/tools/method/data/appendices.js +202 -0
  55. package/dist/tools/method/data/cloudflare.js +192 -0
  56. package/dist/tools/method/data/evidence.js +137 -0
  57. package/dist/tools/method/data/geo-jobs.js +128 -0
  58. package/dist/tools/method/data/index.js +19 -0
  59. package/dist/tools/method/data/landing.js +67 -0
  60. package/dist/tools/method/data/lessons.js +127 -0
  61. package/dist/tools/method/data/measurement.js +142 -0
  62. package/dist/tools/method/data/method.js +141 -0
  63. package/dist/tools/method/data/offsite.js +148 -0
  64. package/dist/tools/method/data/pages.js +167 -0
  65. package/dist/tools/method/data/prompts.js +633 -0
  66. package/dist/tools/method/data/publish.js +113 -0
  67. package/dist/tools/method/data/render.js +117 -0
  68. package/dist/tools/method/data/research.js +131 -0
  69. package/dist/tools/method/data/roadmap.js +105 -0
  70. package/dist/tools/method/data/traps.js +74 -0
  71. package/dist/tools/method/data/verify.js +88 -0
  72. package/dist/tools/method/data/viral-list.js +39 -0
  73. package/dist/tools/method/index.js +209 -0
  74. package/dist/tools/method/lib/calls.js +53 -0
  75. package/dist/tools/method/lib/framework.js +525 -0
  76. package/dist/tools/method/lib/l10n.js +17 -0
  77. package/dist/tools/method/lib/routes.js +118 -0
  78. package/dist/tools/method/lib/state.js +454 -0
  79. package/dist/tools/method/lib/text.js +165 -0
  80. package/dist/tools/method/mcp/prompts.js +79 -0
  81. package/dist/tools/method/mcp/resources.js +50 -0
  82. package/dist/tools/visibility/analyze.js +463 -0
  83. package/dist/tools/visibility/cli.js +86 -0
  84. package/dist/tools/visibility/estimate.js +105 -0
  85. package/dist/tools/visibility/index.js +108 -0
  86. package/dist/tools/visibility/judge.js +156 -0
  87. package/dist/tools/visibility/manual.js +275 -0
  88. package/dist/tools/visibility/models.js +277 -0
  89. package/dist/tools/visibility/monitoring.js +165 -0
  90. package/dist/tools/visibility/openrouter.js +449 -0
  91. package/dist/tools/visibility/render.js +295 -0
  92. package/dist/tools/visibility/run.js +684 -0
  93. package/dist/tools/visibility/summary.js +164 -0
  94. package/dist/types.js +3 -0
  95. package/dist/util.js +92 -0
  96. package/package.json +54 -0
  97. package/server.json +34 -0
@@ -0,0 +1,525 @@
1
+ import { t } from "./l10n.js";
2
+ import { BUILDER_RENDERING } from "../data/evidence.js";
3
+ export function fwKind(scan) {
4
+ const name = (scan?.framework.name ?? "unknown").toLowerCase();
5
+ const router = (scan?.framework.router ?? "").toLowerCase();
6
+ if (name === "next" || name.startsWith("next")) {
7
+ if (router === "pages")
8
+ return "next-pages";
9
+ if (router === "app")
10
+ return "next-app";
11
+ const files = (scan?.routes ?? []).map((r) => r.file ?? "");
12
+ if (files.some((f) => /(^|\/)pages\//.test(f)) && !files.some((f) => /(^|\/)app\//.test(f)))
13
+ return "next-pages";
14
+ return "next-app";
15
+ }
16
+ if (name.startsWith("vite") || name === "vite")
17
+ return "vite-spa";
18
+ const map = {
19
+ astro: "astro", sveltekit: "sveltekit", nuxt: "nuxt", remix: "remix", "react-router": "react-router", gatsby: "gatsby",
20
+ cra: "cra", angular: "angular", "solid-start": "solid-start", qwik: "qwik", hugo: "hugo", jekyll: "jekyll", eleventy: "eleventy",
21
+ "static-html": "static-html", "tanstack-start": "tanstack-start", "custom-server": "custom-server", vike: "vike", waku: "waku",
22
+ docusaurus: "docusaurus", vitepress: "vitepress", vuepress: "vuepress",
23
+ };
24
+ return map[name] ?? "unknown";
25
+ }
26
+ export function fwLabel(scan) {
27
+ if (!scan)
28
+ return "unknown (no scan yet)";
29
+ const f = scan.framework;
30
+ const bits = [f.name + (f.version ? ` ${f.version}` : ""), f.router ? `${f.router} router` : "", f.builder ? `built with ${f.builder}` : ""].filter(Boolean);
31
+ return `${bits.join(", ")} · rendering ${scan.rendering.verdict}`;
32
+ }
33
+ /** Public text rendered only in the browser? */
34
+ export function isCsr(scan) {
35
+ if (!scan)
36
+ return false;
37
+ if (scan.rendering.verdict === "csr")
38
+ return true;
39
+ const k = fwKind(scan);
40
+ return (k === "vite-spa" || k === "cra") && scan.rendering.verdict === "unknown";
41
+ }
42
+ /** Detect "src/" (and a monorepo prefix such as "apps/web/") in front of app/, pages/ or routes/ from the scanned route files. */
43
+ function srcPrefix(scan, dir) {
44
+ const files = (scan?.routes ?? []).map((r) => r.file ?? "");
45
+ for (const f of files) {
46
+ const i = f.indexOf(`src/${dir}/`);
47
+ if (i >= 0)
48
+ return f.slice(0, i) + "src/";
49
+ }
50
+ for (const f of files) {
51
+ const m = new RegExp(`^(.*?/)?${dir}/`).exec(f);
52
+ if (m)
53
+ return m[1] ?? "";
54
+ }
55
+ return "";
56
+ }
57
+ /** The server entry file of a custom server (the file serving "/"), when known. */
58
+ function serverEntry(scan) {
59
+ const r = (scan?.routes ?? []).find((x) => x.path === "/" && x.file) ?? (scan?.routes ?? []).find((x) => x.file);
60
+ return r?.file?.replace(/:\d+.*$/, "");
61
+ }
62
+ function runtimeOf(scan) {
63
+ return scan?.framework.router || "server";
64
+ }
65
+ function pascal(slug) {
66
+ return slug
67
+ .split(/[-/]+/)
68
+ .filter(Boolean)
69
+ .map((w) => w.charAt(0).toUpperCase() + w.slice(1))
70
+ .join("") || "Page";
71
+ }
72
+ /** Where a new public page at `urlPath` goes in this framework. */
73
+ export function pageLocation(scan, urlPath, staticDir = "") {
74
+ const slug = urlPath.replace(/^\/+|\/+$/g, "") || "index";
75
+ const k = fwKind(scan);
76
+ const files = [];
77
+ const notes = [];
78
+ switch (k) {
79
+ case "next-app": {
80
+ const p = srcPrefix(scan, "app");
81
+ files.push(`${p}app/${slug}/page.tsx`);
82
+ notes.push({
83
+ en: "Server component (no \"use client\" at the top); export `metadata` (or `generateMetadata`) reading from your single SEO source (e.g. src/seo/pages.ts). Add the URL to app/sitemap.ts.",
84
+ tr: "Sunucu bileşeni (en üstte \"use client\" yok); tek SEO kaynağınızdan (ör. src/seo/pages.ts) okuyan `metadata` (ya da `generateMetadata`) dışa aktarın. Adresi app/sitemap.ts’e ekleyin.",
85
+ });
86
+ break;
87
+ }
88
+ case "next-pages": {
89
+ const p = srcPrefix(scan, "pages");
90
+ files.push(`${p}pages/${slug}.tsx`);
91
+ notes.push({
92
+ en: "Use getStaticProps (SSG) so the text is in the HTML; set the head with next/head through your shared SEO component.",
93
+ tr: "Metin HTML’de olsun diye getStaticProps (SSG) kullanın; head’i ortak SEO bileşeninizle next/head üzerinden verin.",
94
+ });
95
+ break;
96
+ }
97
+ case "astro":
98
+ files.push(`src/pages/${slug}.astro`);
99
+ notes.push({
100
+ en: "Static by default. Pass title/description to your base layout's <head> from one SEO source; keep important text out of client:only islands. @astrojs/sitemap picks the page up if `site` is set in astro.config.",
101
+ tr: "Varsayılan olarak statik. title/açıklamayı tek SEO kaynağından temel layout’un <head>’ine geçirin; önemli metni client:only adalarına koymayın. astro.config’te `site` tanımlıysa @astrojs/sitemap sayfayı alır.",
102
+ });
103
+ break;
104
+ case "sveltekit":
105
+ files.push(`src/routes/${slug}/+page.svelte`, `src/routes/${slug}/+page.ts`);
106
+ notes.push({
107
+ en: "In +page.ts: `export const prerender = true;` (never `ssr = false` on public pages). Head tags in <svelte:head> from one SEO source.",
108
+ tr: "+page.ts içinde: `export const prerender = true;` (herkese açık sayfalarda asla `ssr = false`). Head etiketleri tek SEO kaynağından <svelte:head> içinde.",
109
+ });
110
+ break;
111
+ case "nuxt":
112
+ files.push(`pages/${slug}.vue`);
113
+ notes.push({
114
+ en: "Nuxt 4 uses app/pages/. Keep SSR on (no ssr: false / client-only for this route); prerender it with routeRules or `nuxi generate`; set tags with useSeoMeta().",
115
+ tr: "Nuxt 4 app/pages/ kullanır. SSR açık kalsın (bu yol için ssr: false / client-only yok); routeRules ya da `nuxi generate` ile ön-render edin; etiketleri useSeoMeta() ile verin.",
116
+ });
117
+ break;
118
+ case "remix":
119
+ case "react-router":
120
+ files.push(`app/routes/${slug}.tsx`);
121
+ notes.push({
122
+ en: "Export `meta` from the route; in React Router 7 register it in app/routes.ts and, for static pages, add the path to `prerender` in react-router.config.ts.",
123
+ tr: "Yoldan `meta` dışa aktarın; React Router 7’de app/routes.ts’e kaydedin ve statik sayfalar için yolu react-router.config.ts içindeki `prerender`’a ekleyin.",
124
+ });
125
+ break;
126
+ case "tanstack-start":
127
+ files.push(`src/routes/${slug}.tsx`);
128
+ notes.push({
129
+ en: "File-based route rendered on the server; set title/description with the route's `head()` from one SEO source.",
130
+ tr: "Sunucuda render edilen dosya tabanlı yol; title/açıklamayı tek SEO kaynağından yolun `head()` fonksiyonuyla verin.",
131
+ });
132
+ break;
133
+ case "gatsby":
134
+ files.push(`src/pages/${slug}.tsx`);
135
+ notes.push({
136
+ en: "Static by default; export the Gatsby Head API (`export const Head`) with title, description and canonical.",
137
+ tr: "Varsayılan olarak statik; title, açıklama ve canonical ile Gatsby Head API’yi (`export const Head`) dışa aktarın.",
138
+ });
139
+ break;
140
+ case "vite-spa":
141
+ case "cra":
142
+ files.push(`src/pages/${pascal(slug)}.tsx`, "src/App.tsx (add the <Route>)");
143
+ notes.push({
144
+ en: "WARNING: this is a single-page app. Without prerendering (P2) bots get an empty page and the new page won't count. Do P2 first, then add the route to the prerender list.",
145
+ tr: "UYARI: bu bir tek sayfa uygulama. Ön-render (P2) olmadan botlar boş sayfa görür ve yeni sayfa işe yaramaz. Önce P2’yi yapın, sonra yolu ön-render listesine ekleyin.",
146
+ });
147
+ break;
148
+ case "angular":
149
+ files.push(`src/app/pages/${slug}/${slug}.component.ts`, "src/app/app.routes.ts (add the route)");
150
+ notes.push({
151
+ en: "Use Angular SSR with prerendering (`ng add @angular/ssr`) so the route ships as HTML; set tags with the Title and Meta services.",
152
+ tr: "Yol HTML olarak gitsin diye ön-render’lı Angular SSR kullanın (`ng add @angular/ssr`); etiketleri Title ve Meta servisleriyle verin.",
153
+ });
154
+ break;
155
+ case "custom-server": {
156
+ const entry = serverEntry(scan);
157
+ const seo = scan?.files.headSource?.path;
158
+ files.push(entry ? `${entry} (add the route)` : `your ${runtimeOf(scan)} entry (add the route)`, seo ? `${seo} (title, description, canonical for the new path)` : "src/site/seo.js (one SEO module: path → title, description, canonical, og)", "src/site/pages.js (the page's text as data)");
159
+ notes.push({
160
+ en: `Custom ${runtimeOf(scan)}: add a route that returns server-rendered HTML built from one data module (the page's text), with the head tags from one SEO module (e.g. src/site/seo.js). Generate robots.txt, sitemap.xml and llms.txt in code from the same modules so they never disagree.`,
161
+ tr: `Özel ${runtimeOf(scan)}: sayfanın metnini tek bir veri modülünden kurup sunucuda render edilmiş HTML döndüren bir yol ekleyin; head etiketleri tek bir SEO modülünden (ör. src/site/seo.js) gelsin. robots.txt, sitemap.xml ve llms.txt’yi de aynı modüllerden kodla üretin ki hiç çelişmesinler.`,
162
+ });
163
+ break;
164
+ }
165
+ case "vike":
166
+ files.push(`pages/${slug}/+Page.tsx`, `pages/${slug}/+config.ts`);
167
+ notes.push({ en: "Set `title`/`description` (or a +Head) for the page and `prerender: true` for marketing pages.", tr: "Sayfa için `title`/`description` (ya da +Head) ve pazarlama sayfaları için `prerender: true` ayarlayın." });
168
+ break;
169
+ case "waku":
170
+ files.push(`src/pages/${slug}.tsx`);
171
+ notes.push({ en: "Export `getConfig` returning `{ render: 'static' }` so the page ships as HTML; set <title>/<meta> in the page.", tr: "Sayfa HTML olarak gitsin diye `{ render: 'static' }` döndüren `getConfig` dışa aktarın; <title>/<meta>’yı sayfada verin." });
172
+ break;
173
+ case "docusaurus":
174
+ files.push(`src/pages/${slug}.md`);
175
+ notes.push({ en: "Front matter: title, description; Docusaurus renders static HTML. Docs pages live under docs/.", tr: "Front matter: title, description; Docusaurus statik HTML üretir. Doküman sayfaları docs/ altında." });
176
+ break;
177
+ case "vitepress":
178
+ case "vuepress":
179
+ files.push(`docs/${slug}.md`);
180
+ notes.push({ en: "Front matter: title, description (and head for extra tags); the site is static HTML.", tr: "Front matter: title, description (ek etiketler için head); site statik HTML." });
181
+ break;
182
+ case "hugo":
183
+ files.push(`content/${slug}.md`);
184
+ notes.push({ en: "Front matter: title, description, lastmod (only when the words change).", tr: "Front matter: title, description, lastmod (yalnızca kelimeler değişince)." });
185
+ break;
186
+ case "jekyll":
187
+ files.push(`${slug}.md`);
188
+ notes.push({ en: `Front matter: title, description, permalink: /${slug}/ (jekyll-seo-tag renders the head).`, tr: `Front matter: title, description, permalink: /${slug}/ (head’i jekyll-seo-tag üretir).` });
189
+ break;
190
+ case "eleventy":
191
+ files.push(`src/${slug}.md`);
192
+ notes.push({ en: "Front matter: title, description, permalink; the base layout renders the head.", tr: "Front matter: title, description, permalink; head’i temel layout üretir." });
193
+ break;
194
+ case "static-html":
195
+ files.push(`${staticDir ? staticDir + "/" : ""}${slug}/index.html`);
196
+ notes.push({ en: "Plain HTML is already readable by every bot; copy the head block from an existing page and update it.", tr: "Düz HTML zaten her bot tarafından okunur; head bloğunu mevcut bir sayfadan kopyalayıp güncelleyin." });
197
+ break;
198
+ default:
199
+ files.push(`[FILL: the page file for ${urlPath} — run katman_scan_code so Katman knows the framework]`);
200
+ }
201
+ return { files, notes };
202
+ }
203
+ export function builderNote(scan) {
204
+ const b = (scan?.framework.builder ?? "").toLowerCase();
205
+ if (!b)
206
+ return undefined;
207
+ for (const key of Object.keys(BUILDER_RENDERING))
208
+ if (b.includes(key))
209
+ return BUILDER_RENDERING[key];
210
+ return undefined;
211
+ }
212
+ function p2Notes(scan) {
213
+ const k = fwKind(scan);
214
+ const notes = [];
215
+ const b = builderNote(scan);
216
+ if (b)
217
+ notes.push(b);
218
+ if (k === "vite-spa" || k === "cra" || scan.rendering.verdict === "csr") {
219
+ notes.push({
220
+ en: `Your public pages are rendered in the browser (${fwLabel(scan)}). Options, smallest change first:
221
+ 1. Prerender the public routes at build time and keep the app as a SPA: React Router 7 framework mode with \`ssr: false\` + \`prerender: ["/", "/pricing", …]\` in react-router.config.ts, or a prerender plugin (e.g. @prerenderer/rollup-plugin for Vite; react-snap for CRA). Build, then run the P2 curl command on every public route.
222
+ 2. Move the marketing pages to Astro on the same domain (the app stays React under /app/); Astro renders React components to HTML and ships JS only where you add client:*.
223
+ 3. Full SSR (Next.js, React Router 7 SSR, TanStack Start): the biggest change; worth it only if many public pages need live data.
224
+ Never serve bots a different page than people (cloaking): prerender the exact same content.`,
225
+ tr: `Herkese açık sayfalarınız tarayıcıda render ediliyor (${fwLabel(scan)}). Seçenekler, en küçük değişiklik önce:
226
+ 1. Herkese açık yolları derleme anında ön-render edin, uygulamayı SPA olarak bırakın: React Router 7 framework modunda react-router.config.ts içinde \`ssr: false\` + \`prerender: ["/", "/pricing", …]\`, ya da bir ön-render eklentisi (Vite için ör. @prerenderer/rollup-plugin; CRA için react-snap). Derleyin, sonra P2’deki curl komutunu her herkese açık yolda çalıştırın.
227
+ 2. Pazarlama sayfalarını aynı alan adında Astro’ya taşıyın (uygulama /app/ altında React olarak kalır); Astro React bileşenlerini HTML’e çevirir, JS’i yalnızca client:* eklediğiniz yere gönderir.
228
+ 3. Tam SSR (Next.js, React Router 7 SSR, TanStack Start): en büyük değişiklik; yalnızca çok sayıda herkese açık sayfa canlı veri istiyorsa değer.
229
+ Botlara insanlardan farklı sayfa göstermeyin (cloaking): tam olarak aynı içeriği ön-render edin.`,
230
+ });
231
+ }
232
+ else if (k === "next-app" || k === "next-pages") {
233
+ notes.push({
234
+ en: "Next.js renders on the server, but text fetched in a client component (\"use client\" + useEffect/SWR) is still missing from the HTML. Move page copy into server components or static props; use generateStaticParams for situation pages.",
235
+ tr: "Next.js sunucuda render eder, ama istemci bileşeninde çekilen metin (\"use client\" + useEffect/SWR) HTML’de yine yoktur. Sayfa metnini sunucu bileşenlerine ya da statik props’a taşıyın; durum sayfaları için generateStaticParams kullanın.",
236
+ });
237
+ }
238
+ else if (k === "custom-server") {
239
+ notes.push({
240
+ en: `Your ${runtimeOf(scan)} renders the HTML itself. Check that every public page's text is in the HTML response (not injected later by client JavaScript), and render pages from one data module so the text, the head tags, the sitemap and llms.txt come from the same source.`,
241
+ tr: `${runtimeOf(scan)} HTML’i kendisi üretiyor. Her herkese açık sayfanın metninin HTML yanıtında olduğunu (sonradan istemci JavaScript’iyle eklenmediğini) kontrol edin; sayfaları tek bir veri modülünden üretin ki metin, head etiketleri, sitemap ve llms.txt aynı kaynaktan gelsin.`,
242
+ });
243
+ }
244
+ else if (k === "tanstack-start") {
245
+ notes.push({ en: "TanStack Start renders on the server (new Lovable apps use it): keep SSR on for public routes (no `ssr: false`) and prerender static marketing pages if your host supports it.", tr: "TanStack Start sunucuda render eder (yeni Lovable uygulamaları bunu kullanır): herkese açık yollarda SSR açık kalsın (`ssr: false` yok), barındırma destekliyorsa statik pazarlama sayfalarını ön-render edin." });
246
+ }
247
+ else if (k === "vike" || k === "waku") {
248
+ notes.push({ en: k === "vike" ? "vike renders on the server; set `prerender: true` for marketing pages and never disable SSR on public routes." : "Waku: use static rendering (`getConfig` → render: 'static') for marketing pages.", tr: k === "vike" ? "vike sunucuda render eder; pazarlama sayfaları için `prerender: true` ayarlayın, herkese açık yollarda SSR’ı kapatmayın." : "Waku: pazarlama sayfaları için statik render (`getConfig` → render: 'static') kullanın." });
249
+ }
250
+ else if (k === "astro" || k === "hugo" || k === "jekyll" || k === "eleventy" || k === "static-html" || k === "gatsby" || k === "docusaurus" || k === "vitepress" || k === "vuepress") {
251
+ notes.push({
252
+ en: "This framework outputs static HTML already; check that no important text sits in client-only widgets (client:only islands, iframes, tabs filled by JS).",
253
+ tr: "Bu çatı zaten statik HTML üretiyor; önemli bir metnin yalnızca istemcide çalışan parçalarda (client:only adaları, iframe’ler, JS ile dolan sekmeler) olmadığını kontrol edin.",
254
+ });
255
+ }
256
+ else if (k === "nuxt") {
257
+ notes.push({ en: "Keep SSR on (the default); remove `ssr: false` and <ClientOnly> around public text; prerender marketing routes with routeRules.", tr: "SSR açık kalsın (varsayılan); herkese açık metnin etrafındaki `ssr: false` ve <ClientOnly>’yi kaldırın; pazarlama yollarını routeRules ile ön-render edin." });
258
+ }
259
+ else if (k === "sveltekit") {
260
+ notes.push({ en: "Remove `export const ssr = false` from public routes; add `export const prerender = true` to marketing pages.", tr: "Herkese açık yollardan `export const ssr = false`’u kaldırın; pazarlama sayfalarına `export const prerender = true` ekleyin." });
261
+ }
262
+ else if (k === "angular") {
263
+ notes.push({ en: "Add Angular SSR with prerendering (`ng add @angular/ssr`) and list the public routes to prerender.", tr: "Ön-render’lı Angular SSR ekleyin (`ng add @angular/ssr`) ve ön-render edilecek herkese açık yolları listeleyin." });
264
+ }
265
+ else if (k === "remix" || k === "react-router") {
266
+ notes.push({ en: "Server rendering is available; if the app runs in SPA mode (ssr: false), add the public paths to `prerender` in react-router.config.ts.", tr: "Sunucuda render mümkün; uygulama SPA modunda (ssr: false) çalışıyorsa herkese açık yolları react-router.config.ts içindeki `prerender`’a ekleyin." });
267
+ }
268
+ return notes;
269
+ }
270
+ function p3Notes(scan) {
271
+ switch (fwKind(scan)) {
272
+ case "next-app":
273
+ return [{ en: "App Router: one `src/seo/pages.ts` map (path → title, description, og). In each page `export const metadata` (or `generateMetadata`) reads from it; set `metadataBase` in the root layout so canonical and og:url are absolute; `alternates: { canonical, languages }` for hreflang.", tr: "App Router: tek bir `src/seo/pages.ts` haritası (yol → title, açıklama, og). Her sayfada `export const metadata` (ya da `generateMetadata`) oradan okur; canonical ve og:url mutlak olsun diye kök layout’ta `metadataBase` tanımlayın; hreflang için `alternates: { canonical, languages }`." }];
274
+ case "next-pages":
275
+ return [{ en: "Pages Router: a shared <Seo> component using next/head, fed from `src/seo/pages.ts`.", tr: "Pages Router: `src/seo/pages.ts`’ten beslenen, next/head kullanan ortak bir <Seo> bileşeni." }];
276
+ case "astro":
277
+ return [{ en: "A BaseHead.astro component in the layout, fed from `src/seo/pages.ts` (or page frontmatter).", tr: "Layout’ta `src/seo/pages.ts`’ten (ya da sayfa frontmatter’ından) beslenen bir BaseHead.astro bileşeni." }];
278
+ case "vite-spa":
279
+ case "cra":
280
+ return [{ en: "SPA: react-helmet-async tags only reach bots after prerendering (P2). Keep one `src/seo/pages.ts` map and make the prerender step write the head per route.", tr: "SPA: react-helmet-async etiketleri botlara ancak ön-render’dan (P2) sonra ulaşır. Tek bir `src/seo/pages.ts` haritası tutun ve ön-render adımı her yol için head’i yazsın." }];
281
+ case "nuxt":
282
+ return [{ en: "useSeoMeta() in each page (or app.vue) fed from one data file.", tr: "Her sayfada (ya da app.vue’da) tek veri dosyasından beslenen useSeoMeta()." }];
283
+ case "sveltekit":
284
+ return [{ en: "<svelte:head> in +layout.svelte reading page data from `src/lib/seo/pages.ts`.", tr: "+layout.svelte içinde `src/lib/seo/pages.ts`’ten sayfa verisi okuyan <svelte:head>." }];
285
+ case "remix":
286
+ case "react-router":
287
+ return [{ en: "Each route's `meta` export reads from one `app/seo/pages.ts` map.", tr: "Her yolun `meta` dışa aktarımı tek bir `app/seo/pages.ts` haritasından okur." }];
288
+ case "custom-server": {
289
+ const seo = scan.files.headSource?.path ?? "src/site/seo.js";
290
+ return [{ en: `Keep one SEO module (${seo}) that maps each path to title, description, canonical and og tags and renders <head>; every route handler calls it — no head tags written inline in handlers.`, tr: `Her yolu title, açıklama, canonical ve og etiketlerine eşleyip <head>’i üreten tek bir SEO modülü (${seo}) tutun; her yol işleyicisi onu çağırsın — işleyicilerde elle yazılmış head etiketi olmasın.` }];
291
+ }
292
+ case "tanstack-start":
293
+ return [{ en: "Each route's `head()` returns title/meta from one `src/seo/pages.ts` map.", tr: "Her yolun `head()` fonksiyonu title/meta’yı tek bir `src/seo/pages.ts` haritasından döndürür." }];
294
+ case "vike":
295
+ return [{ en: "`title`/`description` in each page's +config (or one +Head reading `src/seo/pages.ts`).", tr: "Her sayfanın +config’inde `title`/`description` (ya da `src/seo/pages.ts`’ten okuyan tek bir +Head)." }];
296
+ case "docusaurus":
297
+ case "vitepress":
298
+ case "vuepress":
299
+ return [{ en: "Front matter title/description per page; site-wide tags in the site config.", tr: "Sayfa başına front matter title/description; site geneli etiketler site yapılandırmasında." }];
300
+ default:
301
+ return [];
302
+ }
303
+ }
304
+ function p4Notes(scan, staticDir) {
305
+ const notes = [];
306
+ const k = fwKind(scan);
307
+ const sd = staticDir ? staticDir + "/" : "";
308
+ if (scan.files.robots.generated && scan.files.robots.path) {
309
+ notes.push({ en: `robots.txt is generated by ${scan.files.robots.path}: edit that file (a static ${sd}robots.txt would conflict).`, tr: `robots.txt ${scan.files.robots.path} tarafından üretiliyor: o dosyayı düzenleyin (statik ${sd}robots.txt çakışır).` });
310
+ }
311
+ if (k === "next-app")
312
+ notes.push({ en: "Next.js: app/robots.ts (MetadataRoute.Robots) and app/sitemap.ts (MetadataRoute.Sitemap) generated from the page list; lastmod from each page's content date.", tr: "Next.js: sayfa listesinden üretilen app/robots.ts (MetadataRoute.Robots) ve app/sitemap.ts (MetadataRoute.Sitemap); lastmod her sayfanın içerik tarihinden." });
313
+ else if (k === "astro")
314
+ notes.push({ en: `Astro: ${sd}robots.txt plus @astrojs/sitemap (it writes sitemap-index.xml — point the Sitemap: line at it).`, tr: `Astro: ${sd}robots.txt ve @astrojs/sitemap (sitemap-index.xml yazar — Sitemap: satırını ona yönlendirin).` });
315
+ else if (k === "sveltekit")
316
+ notes.push({ en: "SvelteKit: static/robots.txt and a prerendered src/routes/sitemap.xml/+server.ts.", tr: "SvelteKit: static/robots.txt ve ön-render edilen src/routes/sitemap.xml/+server.ts." });
317
+ else if (k === "nuxt")
318
+ notes.push({ en: "Nuxt: public/robots.txt (or @nuxtjs/robots) and @nuxtjs/sitemap.", tr: "Nuxt: public/robots.txt (ya da @nuxtjs/robots) ve @nuxtjs/sitemap." });
319
+ else if (k === "custom-server")
320
+ notes.push({ en: `Custom ${runtimeOf(scan)}: serve /robots.txt, /sitemap.xml and /llms.txt from routes generated from the same data/SEO modules as the pages (lastmod from each page's content date).`, tr: `Özel ${runtimeOf(scan)}: /robots.txt, /sitemap.xml ve /llms.txt’yi sayfalarla aynı veri/SEO modüllerinden üretilen yollardan sunun (lastmod her sayfanın içerik tarihinden).` });
321
+ else if (k === "vite-spa" || k === "cra")
322
+ notes.push({ en: `SPA: ${sd}robots.txt, and a small build script that writes ${sd}sitemap.xml from the same route list the prerender step uses.`, tr: `SPA: ${sd}robots.txt ve ön-render adımının kullandığı yol listesinden ${sd}sitemap.xml yazan küçük bir derleme betiği.` });
323
+ notes.push({ en: "`katman_generate {\"artifact\":\"robots.txt\"}` builds this project's file: one group per AI bot, app paths closed with trailing slashes, auth pages left to noindex.", tr: "`katman_generate {\"artifact\":\"robots.txt\"}` bu projenin dosyasını üretir: her yapay zekâ botuna bir grup, uygulama yolları sonda / ile kapalı, giriş sayfaları noindex’e bırakılmış." });
324
+ return notes;
325
+ }
326
+ function p5Notes(scan) {
327
+ switch (fwKind(scan)) {
328
+ case "next-app":
329
+ case "next-pages":
330
+ return [{ en: "Render `<script type=\"application/ld+json\" dangerouslySetInnerHTML={{ __html: JSON.stringify(data).replace(/</g, \"\\\\u003c\") }} />` inside the page (server-side), with data built from the same source as the visible text.", tr: "Sayfanın içinde (sunucu tarafında) görünen metinle aynı kaynaktan kurulan veriyle `<script type=\"application/ld+json\" dangerouslySetInnerHTML={{ __html: JSON.stringify(data).replace(/</g, \"\\\\u003c\") }} />` render edin." }];
331
+ case "astro":
332
+ return [{ en: "`<script type=\"application/ld+json\" set:html={JSON.stringify(data)} />` in the layout/page.", tr: "Layout/sayfada `<script type=\"application/ld+json\" set:html={JSON.stringify(data)} />`." }];
333
+ case "nuxt":
334
+ return [{ en: "useHead({ script: [{ type: 'application/ld+json', innerHTML: JSON.stringify(data) }] }).", tr: "useHead({ script: [{ type: 'application/ld+json', innerHTML: JSON.stringify(data) }] })." }];
335
+ case "vite-spa":
336
+ case "cra":
337
+ return [{ en: "SPA: JSON-LD added by JavaScript only reaches bots after prerendering (P2).", tr: "SPA: JavaScript ile eklenen JSON-LD botlara ancak ön-render’dan (P2) sonra ulaşır." }];
338
+ default:
339
+ return [];
340
+ }
341
+ }
342
+ function p8Notes(scan, staticDir) {
343
+ const sd = staticDir ? staticDir + "/" : "";
344
+ const notes = [];
345
+ if (scan.files.llms.exists)
346
+ notes.push({ en: `llms.txt already exists (${scan.files.llms.path ?? sd + "llms.txt"}): compare it with research instead of starting over.`, tr: `llms.txt zaten var (${scan.files.llms.path ?? sd + "llms.txt"}): baştan yazmak yerine araştırmayla karşılaştırın.` });
347
+ const k = fwKind(scan);
348
+ if (k === "next-app")
349
+ notes.push({ en: `Next.js: ${sd}llms.txt, or a route handler app/llms.txt/route.ts that builds the text from the same data file as the pages.`, tr: `Next.js: ${sd}llms.txt ya da metni sayfalarla aynı veri dosyasından kuran bir app/llms.txt/route.ts.` });
350
+ else if (k === "astro")
351
+ notes.push({ en: "Astro: an endpoint src/pages/llms.txt.ts (prerendered) built from the same data as the pages.", tr: "Astro: sayfalarla aynı veriden kurulan (ön-render edilen) bir src/pages/llms.txt.ts uç noktası." });
352
+ else if (k === "custom-server")
353
+ notes.push({ en: "Serve /llms.txt from a route that builds it from the same data module as the pages.", tr: "/llms.txt’yi sayfalarla aynı veri modülünden kuran bir yoldan sunun." });
354
+ notes.push({ en: "`katman_generate {\"artifact\":\"llms.txt\"}` drafts it from research (the one sentence, pricing, not-X, live situation pages).", tr: "`katman_generate {\"artifact\":\"llms.txt\"}` taslağı araştırmadan üretir (tek cümle, fiyat, “X değildir”, yayındaki durum sayfaları)." });
355
+ return notes;
356
+ }
357
+ function p9Notes(scan) {
358
+ switch (fwKind(scan)) {
359
+ case "next-app":
360
+ case "next-pages":
361
+ return [{ en: "Use next/image (width/height required, lazy by default; `priority` on the hero image).", tr: "next/image kullanın (width/height zorunlu, varsayılan lazy; ilk ekrandaki görselde `priority`)." }];
362
+ case "astro":
363
+ return [{ en: "Use <Image /> from astro:assets (sizes, formats and dimensions are generated).", tr: "astro:assets’ten <Image /> kullanın (boyutlar, formatlar ve ölçüler üretilir)." }];
364
+ case "nuxt":
365
+ return [{ en: "Use <NuxtImg> from @nuxt/image.", tr: "@nuxt/image’dan <NuxtImg> kullanın." }];
366
+ default:
367
+ return [];
368
+ }
369
+ }
370
+ function p10Notes(scan) {
371
+ const k = fwKind(scan);
372
+ const link = k.startsWith("next") ? "next/link <Link href>" : k === "vite-spa" || k === "cra" || k === "react-router" || k === "remix" ? "react-router <Link to> (renders <a href>)" : k === "nuxt" ? "<NuxtLink to>" : "<a href>";
373
+ return [{ en: `Navigate with ${link}; never a div/button with onClick + navigate(). Put the situation pages in the footer so every page links to them.`, tr: `${link} ile yönlendirin; asla onClick + navigate() olan div/button değil. Durum sayfalarını altbilgiye koyun ki her sayfa onlara link versin.` }];
374
+ }
375
+ function p11Notes(scan) {
376
+ const notes = [];
377
+ const host = (scan.hosting.provider ?? "").toLowerCase();
378
+ const k = fwKind(scan);
379
+ const spa = k === "vite-spa" || k === "cra" || scan.rendering.verdict === "csr";
380
+ if (host.includes("cloudflare"))
381
+ notes.push({ en: "Cloudflare Pages: without a top-level 404.html, Pages treats the project as a SPA and serves index.html with 200 for unknown URLs. Ship a real 404.html.", tr: "Cloudflare Pages: kökte 404.html yoksa Pages projeyi SPA sayar ve olmayan adreslere 200 ile index.html döner. Gerçek bir 404.html koyun." });
382
+ if (host.includes("netlify"))
383
+ notes.push({ en: "Netlify: a catch-all `/* /index.html 200` rule makes every unknown URL a soft 404. List the real routes instead and let the rest 404.", tr: "Netlify: `/* /index.html 200` kuralı her olmayan adresi sahte 404 yapar. Bunun yerine gerçek yolları listeleyin, gerisi 404 dönsün." });
384
+ if (host.includes("vercel"))
385
+ notes.push({ en: "Vercel: a rewrite of `/(.*)` to `/index.html` in vercel.json returns 200 for every URL. Rewrite only the real app routes; serve a 404 page for the rest.", tr: "Vercel: vercel.json’da `/(.*)` → `/index.html` yeniden yazması her adrese 200 döner. Yalnızca gerçek uygulama yollarını yeniden yazın; gerisine 404 sayfası verin." });
386
+ if (host.includes("github"))
387
+ notes.push({ en: "GitHub Pages: don't copy index.html to 404.html as an SPA trick — deep links then return 404 to bots. Prerender the real routes.", tr: "GitHub Pages: SPA hilesi olarak index.html’i 404.html’e kopyalamayın — derin linkler botlara 404 döner. Gerçek yolları ön-render edin." });
388
+ if (host.includes("firebase"))
389
+ notes.push({ en: "Firebase Hosting: a `\"source\": \"**\" → /index.html` rewrite makes soft 404s; rewrite only the app routes.", tr: "Firebase Hosting: `\"source\": \"**\" → /index.html` yeniden yazması sahte 404 üretir; yalnızca uygulama yollarını yeniden yazın." });
390
+ if (k === "next-app")
391
+ notes.push({ en: "Next.js: app/not-found.tsx + notFound() returns a real 404.", tr: "Next.js: app/not-found.tsx + notFound() gerçek 404 döner." });
392
+ if (k === "custom-server") {
393
+ notes.push({ en: `Custom ${runtimeOf(scan)}: unknown paths must get a real 404 status from the server; 301 the default host (e.g. *.workers.dev) to the main domain by checking the Host header — or turn it off (wrangler: workers_dev = false).`, tr: `Özel ${runtimeOf(scan)}: olmayan yollar sunucudan gerçek 404 almalı; varsayılan adresi (ör. *.workers.dev) Host başlığına bakarak asıl alan adına 301 ile yönlendirin — ya da kapatın (wrangler: workers_dev = false).` });
394
+ }
395
+ if (spa && !notes.length)
396
+ notes.push({ en: "SPA hosting usually serves index.html with 200 for every path. Serve real routes (prerendered files) and a 404.html with a 404 status for everything else.", tr: "SPA barındırma genelde her yola 200 ile index.html verir. Gerçek yolları (ön-render dosyaları) sunun, geri kalan her şeye 404 durumlu bir 404.html verin." });
397
+ if (scan.hosting.previewPatterns.length) {
398
+ notes.push({
399
+ en: `Preview hosts seen in this project: ${scan.hosting.previewPatterns.join(", ")}. 301 them to the main domain (middleware/edge rule checking the Host header), or send \`X-Robots-Tag: noindex\` on every non-production host, or keep them password-protected. robots.txt isn't enough.`,
400
+ tr: `Bu projede görülen önizleme adresleri: ${scan.hosting.previewPatterns.join(", ")}. Bunları asıl alan adına 301 ile yönlendirin (Host başlığına bakan middleware/edge kuralı), ya da üretim dışı her adreste \`X-Robots-Tag: noindex\` gönderin, ya da şifreli tutun. robots.txt yetmez.`,
401
+ });
402
+ }
403
+ return notes;
404
+ }
405
+ function p13Notes(scan) {
406
+ const notes = [];
407
+ const prov = scan.analytics.providers;
408
+ notes.push({
409
+ en: `Analytics in this project: ${prov.length ? prov.join(", ") : "none detected"}${scan.analytics.tracksAiChannel ? " (an AI channel is already tracked)" : ""}. \`katman_generate {"artifact":"analytics-channel-snippet"}\` adds the human filter and the AI channel rule and pushes them to your analytics.`,
410
+ tr: `Bu projedeki analitik: ${prov.length ? prov.join(", ") : "bulunamadı"}${scan.analytics.tracksAiChannel ? " (yapay zekâ kanalı zaten izleniyor)" : ""}. \`katman_generate {"artifact":"analytics-channel-snippet"}\` insan filtresini ve yapay zekâ kanal kuralını ekler ve analitiğinize gönderir.`,
411
+ });
412
+ notes.push({
413
+ en: `Bot counting: \`katman_generate {"artifact":"bot-counter-snippet","options":{"platform":"${suggestBotPlatform(scan)}"}}\`.`,
414
+ tr: `Bot sayımı: \`katman_generate {"artifact":"bot-counter-snippet","options":{"platform":"${suggestBotPlatform(scan)}"}}\`.`,
415
+ });
416
+ return notes;
417
+ }
418
+ export function suggestBotPlatform(scan) {
419
+ const host = (scan?.hosting.provider ?? "").toLowerCase();
420
+ const k = fwKind(scan);
421
+ if (k === "next-app" || k === "next-pages")
422
+ return "nextjs-middleware";
423
+ if (k === "sveltekit" && scan?.rendering.verdict !== "ssg")
424
+ return "sveltekit-hooks";
425
+ if (k === "astro" && (scan?.rendering.verdict === "ssr" || scan?.rendering.verdict === "hybrid"))
426
+ return "astro-middleware";
427
+ if (k === "custom-server")
428
+ return (scan?.framework.router ?? "").includes("express") || (scan?.framework.router ?? "").includes("node") ? "express" : "cloudflare-worker";
429
+ if (host.includes("cloudflare"))
430
+ return "cloudflare-worker";
431
+ if (host.includes("node") || host.includes("render") || host.includes("railway") || host.includes("fly"))
432
+ return "express";
433
+ return "cloudflare-worker";
434
+ }
435
+ /** Framework/host-aware notes for a prompt (empty when there's no scan). */
436
+ export function frameworkNotes(id, scan, lang, staticDir = "") {
437
+ if (!scan)
438
+ return [];
439
+ let notes = [];
440
+ switch (id) {
441
+ case "P2":
442
+ notes = p2Notes(scan);
443
+ break;
444
+ case "P3":
445
+ notes = p3Notes(scan);
446
+ break;
447
+ case "P4":
448
+ notes = p4Notes(scan, staticDir);
449
+ break;
450
+ case "P5":
451
+ notes = p5Notes(scan);
452
+ break;
453
+ case "P8":
454
+ notes = p8Notes(scan, staticDir);
455
+ break;
456
+ case "P9":
457
+ notes = p9Notes(scan);
458
+ break;
459
+ case "P10":
460
+ notes = p10Notes(scan);
461
+ break;
462
+ case "P11":
463
+ notes = p11Notes(scan);
464
+ break;
465
+ case "P13":
466
+ notes = p13Notes(scan);
467
+ break;
468
+ case "P6": {
469
+ const loc = pageLocation(scan, "/your-situation-page", staticDir);
470
+ notes = [{ en: `New page file (${fwLabel(scan)}): ${loc.files.join(" + ")}.`, tr: `Yeni sayfa dosyası (${fwLabel(scan)}): ${loc.files.join(" + ")}.` }, ...loc.notes];
471
+ if (isCsr(scan))
472
+ notes.unshift({ en: "Do P2 first: on a client-rendered site a new situation page is empty for bots.", tr: "Önce P2: istemcide render edilen sitede yeni durum sayfası botlar için boştur." });
473
+ break;
474
+ }
475
+ case "P7": {
476
+ const loc = pageLocation(scan, "/guides/tools", staticDir);
477
+ notes = [{ en: `Guide page file: ${loc.files.join(" + ")}.`, tr: `Rehber sayfası dosyası: ${loc.files.join(" + ")}.` }];
478
+ break;
479
+ }
480
+ case "P14":
481
+ notes = [{ en: "Add to package.json: \"seo-check\": \"katman-mcp audit <your site URL> --strict\" (exits 2 on critical findings) and run it in CI after every deploy.", tr: "package.json’a ekleyin: \"seo-check\": \"katman-mcp audit <site adresiniz> --strict\" (kritik bulguda 2 ile çıkar) ve her yayından sonra CI’da çalıştırın." }];
482
+ break;
483
+ default:
484
+ notes = [];
485
+ }
486
+ return notes.map((n) => t(n, lang));
487
+ }
488
+ /** File hints for plan tasks. */
489
+ export function fileHints(id, scan, staticDir) {
490
+ const sd = staticDir ? staticDir + "/" : "";
491
+ const k = fwKind(scan);
492
+ const head = scan?.files.headSource?.path;
493
+ const cfg = {
494
+ "next-app": "next.config.*", "next-pages": "next.config.*", astro: "astro.config.*", sveltekit: "svelte.config.js", nuxt: "nuxt.config.ts",
495
+ "vite-spa": "vite.config.ts", cra: "package.json", remix: "vite.config.ts", "react-router": "react-router.config.ts", angular: "angular.json",
496
+ };
497
+ if (k === "custom-server") {
498
+ const entry = serverEntry(scan) ?? "server entry";
499
+ if (id === "P2")
500
+ return [entry];
501
+ if (id === "P3" || id === "P5")
502
+ return [head ?? "src/site/seo.js (one SEO module)", entry];
503
+ if (id === "P4")
504
+ return [`${entry} (/robots.txt and /sitemap.xml routes)`];
505
+ if (id === "P8")
506
+ return [`${entry} (/llms.txt route)`];
507
+ }
508
+ switch (id) {
509
+ case "P2": return [cfg[k] ?? "framework config", k === "vite-spa" || k === "cra" ? "src/App.tsx (router)" : ""].filter(Boolean);
510
+ case "P3": return ["src/seo/pages.ts (new, single source)", head ?? "layout / head component"];
511
+ case "P4": return [scan?.files.robots.path ?? `${sd}robots.txt`, scan?.files.sitemap.path ?? `${sd}sitemap.xml (generated at build)`];
512
+ case "P5": return [head ?? "layout (Organization, WebSite)", "page templates (per-page types)"];
513
+ case "P8": return [scan?.files.llms.path ?? `${sd}llms.txt`];
514
+ case "P9": return ["components that render <img>", `${sd}images`];
515
+ case "P10": return ["header / footer / nav components"];
516
+ case "P11": {
517
+ const host = (scan?.hosting.provider ?? "").toLowerCase();
518
+ const hostFile = host.includes("vercel") ? "vercel.json" : host.includes("netlify") ? "netlify.toml or _redirects" : host.includes("cloudflare") ? `${sd}404.html / wrangler config` : "hosting config (redirects)";
519
+ return [hostFile, scan?.files.notFound.path ?? "404 page"];
520
+ }
521
+ case "P13": return ["analytics setup (layout/root)", suggestBotPlatform(scan) === "nextjs-middleware" ? "middleware.ts (proxy.ts on Next 16+)" : "server/edge entry"];
522
+ case "P14": return ["package.json (seo-check script)", "CI workflow"];
523
+ default: return [];
524
+ }
525
+ }
@@ -0,0 +1,17 @@
1
+ export function t(v, lang) {
2
+ return lang === "tr" ? v.tr : v.en;
3
+ }
4
+ export function pick(v, lang) {
5
+ return lang === "tr" ? v.tr : v.en;
6
+ }
7
+ export function toLang(x, fallback = "en") {
8
+ return x === "tr" || x === "en" ? x : fallback;
9
+ }
10
+ export const STRENGTH_LABEL = {
11
+ strong: { en: "STRONG", tr: "GÜÇLÜ" },
12
+ "medium-strong": { en: "MEDIUM-STRONG", tr: "ORTA-GÜÇLÜ" },
13
+ medium: { en: "MEDIUM", tr: "ORTA" },
14
+ weak: { en: "WEAK", tr: "ZAYIF" },
15
+ lab: { en: "STRONG (LAB)", tr: "GÜÇLÜ (LABORATUVAR)" },
16
+ announcement: { en: "ANNOUNCEMENT", tr: "DUYURU" },
17
+ };