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.
- package/LICENSE +13 -0
- package/README.md +160 -0
- package/dist/cli.js +71 -0
- package/dist/context.js +43 -0
- package/dist/crawlers.js +86 -0
- package/dist/index.js +25 -0
- package/dist/license.js +448 -0
- package/dist/meta.js +28 -0
- package/dist/net.js +128 -0
- package/dist/server.js +17 -0
- package/dist/state.js +81 -0
- package/dist/tools/account/account.js +64 -0
- package/dist/tools/account/cli.js +21 -0
- package/dist/tools/account/index.js +48 -0
- package/dist/tools/account/sync.js +142 -0
- package/dist/tools/account/upsell.js +290 -0
- package/dist/tools/crawl/audit/analyze.js +719 -0
- package/dist/tools/crawl/audit/bots.js +114 -0
- package/dist/tools/crawl/audit/checks.js +126 -0
- package/dist/tools/crawl/audit/html.js +382 -0
- package/dist/tools/crawl/audit/index.js +364 -0
- package/dist/tools/crawl/audit/model.js +1 -0
- package/dist/tools/crawl/audit/report.js +199 -0
- package/dist/tools/crawl/audit/robots.js +199 -0
- package/dist/tools/crawl/audit/sitemap.js +147 -0
- package/dist/tools/crawl/cli.js +106 -0
- package/dist/tools/crawl/common/advice.js +61 -0
- package/dist/tools/crawl/common/findings.js +57 -0
- package/dist/tools/crawl/common/routes.js +80 -0
- package/dist/tools/crawl/common/text.js +89 -0
- package/dist/tools/crawl/common/urls.js +64 -0
- package/dist/tools/crawl/index.js +226 -0
- package/dist/tools/crawl/indexnow.js +271 -0
- package/dist/tools/crawl/keywords.js +178 -0
- package/dist/tools/crawl/scan/framework.js +645 -0
- package/dist/tools/crawl/scan/index.js +175 -0
- package/dist/tools/crawl/scan/project.js +180 -0
- package/dist/tools/crawl/scan/routes.js +354 -0
- package/dist/tools/crawl/scan/signals.js +414 -0
- package/dist/tools/method/cli.js +42 -0
- package/dist/tools/method/core/brief.js +302 -0
- package/dist/tools/method/core/extract.js +435 -0
- package/dist/tools/method/core/generate.js +504 -0
- package/dist/tools/method/core/lint.js +556 -0
- package/dist/tools/method/core/plan.js +405 -0
- package/dist/tools/method/core/prompt.js +150 -0
- package/dist/tools/method/core/questions.js +196 -0
- package/dist/tools/method/core/report.js +182 -0
- package/dist/tools/method/core/research-run.js +148 -0
- package/dist/tools/method/core/research.js +448 -0
- package/dist/tools/method/core/snippets.js +292 -0
- package/dist/tools/method/core/start.js +277 -0
- package/dist/tools/method/data/anatomy.js +141 -0
- package/dist/tools/method/data/appendices.js +202 -0
- package/dist/tools/method/data/cloudflare.js +192 -0
- package/dist/tools/method/data/evidence.js +137 -0
- package/dist/tools/method/data/geo-jobs.js +128 -0
- package/dist/tools/method/data/index.js +19 -0
- package/dist/tools/method/data/landing.js +67 -0
- package/dist/tools/method/data/lessons.js +127 -0
- package/dist/tools/method/data/measurement.js +142 -0
- package/dist/tools/method/data/method.js +141 -0
- package/dist/tools/method/data/offsite.js +148 -0
- package/dist/tools/method/data/pages.js +167 -0
- package/dist/tools/method/data/prompts.js +633 -0
- package/dist/tools/method/data/publish.js +113 -0
- package/dist/tools/method/data/render.js +117 -0
- package/dist/tools/method/data/research.js +131 -0
- package/dist/tools/method/data/roadmap.js +105 -0
- package/dist/tools/method/data/traps.js +74 -0
- package/dist/tools/method/data/verify.js +88 -0
- package/dist/tools/method/data/viral-list.js +39 -0
- package/dist/tools/method/index.js +209 -0
- package/dist/tools/method/lib/calls.js +53 -0
- package/dist/tools/method/lib/framework.js +525 -0
- package/dist/tools/method/lib/l10n.js +17 -0
- package/dist/tools/method/lib/routes.js +118 -0
- package/dist/tools/method/lib/state.js +454 -0
- package/dist/tools/method/lib/text.js +165 -0
- package/dist/tools/method/mcp/prompts.js +79 -0
- package/dist/tools/method/mcp/resources.js +50 -0
- package/dist/tools/visibility/analyze.js +463 -0
- package/dist/tools/visibility/cli.js +86 -0
- package/dist/tools/visibility/estimate.js +105 -0
- package/dist/tools/visibility/index.js +108 -0
- package/dist/tools/visibility/judge.js +156 -0
- package/dist/tools/visibility/manual.js +275 -0
- package/dist/tools/visibility/models.js +277 -0
- package/dist/tools/visibility/monitoring.js +165 -0
- package/dist/tools/visibility/openrouter.js +449 -0
- package/dist/tools/visibility/render.js +295 -0
- package/dist/tools/visibility/run.js +684 -0
- package/dist/tools/visibility/summary.js +164 -0
- package/dist/types.js +3 -0
- package/dist/util.js +92 -0
- package/package.json +54 -0
- package/server.json +34 -0
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import { calls } from "../lib/calls.js";
|
|
2
|
+
import { fwLabel, isCsr, pageLocation } from "../lib/framework.js";
|
|
3
|
+
import { t } from "../lib/l10n.js";
|
|
4
|
+
import { findPage, knownPublicPaths } from "../lib/routes.js";
|
|
5
|
+
import { loadConfigSafe, loadLatestAudit, loadResearchSafe, loadScan, staticDirFor } from "../lib/state.js";
|
|
6
|
+
import { FILL, brandSentence, hasNotX, isFilled, slugify } from "../lib/text.js";
|
|
7
|
+
import { ANATOMY } from "../data/anatomy.js";
|
|
8
|
+
import { fmtCallShort } from "./plan.js";
|
|
9
|
+
import { pickSituation, siteUrlOf, situationUrl } from "./prompt.js";
|
|
10
|
+
export const BRIEF_TYPES = ["situation", "comparison", "pricing", "faq", "about", "single-question", "glossary", "tool", "home"];
|
|
11
|
+
function cap(s) {
|
|
12
|
+
return s ? s.charAt(0).toUpperCase() + s.slice(1) : s;
|
|
13
|
+
}
|
|
14
|
+
function titleWithBrand(core, brand, notes, lang) {
|
|
15
|
+
const c = cap(core.replace(/[.?!]+$/, ""));
|
|
16
|
+
if (!brand)
|
|
17
|
+
return c;
|
|
18
|
+
const full = `${c} — ${brand}`;
|
|
19
|
+
if (full.length <= 60)
|
|
20
|
+
return full;
|
|
21
|
+
notes.push(lang === "tr"
|
|
22
|
+
? `“${full}” ${full.length} karakter: 60’ı aşınca sondaki marka düşer (kitap kuralı). Durumu daha kısa yazın ya da markasız kullanın.`
|
|
23
|
+
: `“${full}” is ${full.length} characters: over 60, the brand suffix drops (book rule). Shorten the situation wording or use it without the brand.`);
|
|
24
|
+
return c.length <= 60 ? c : `${c.slice(0, 57).replace(/\s+\S*$/, "")}…`;
|
|
25
|
+
}
|
|
26
|
+
export function buildBrief(type, c) {
|
|
27
|
+
const lang = c.lang;
|
|
28
|
+
const L = (en, tr) => (lang === "tr" ? tr : en);
|
|
29
|
+
const F = (hint) => `${FILL} ${hint}`;
|
|
30
|
+
const r = c.research;
|
|
31
|
+
const brand = isFilled(r?.brand.name) ? r.brand.name.trim() : undefined;
|
|
32
|
+
const category = isFilled(r?.brand.category) ? r.brand.category.trim() : undefined;
|
|
33
|
+
const sentence = brandSentence(r);
|
|
34
|
+
const pricing = isFilled(r?.brand.pricing) ? r.brand.pricing.trim() : undefined;
|
|
35
|
+
const notX = hasNotX(r?.brand.notX) ? r.brand.notX.trim() : undefined;
|
|
36
|
+
const year = (c.now ?? new Date()).getUTCFullYear();
|
|
37
|
+
const known = knownPublicPaths(c.scan, c.audit);
|
|
38
|
+
const primaryTr = (r?.site.languages[0] ?? "en").toLowerCase().startsWith("tr");
|
|
39
|
+
const notes = [];
|
|
40
|
+
const proof = (r?.proof ?? []).filter((p) => isFilled(p.source)).map((p) => `${p.claim}${p.value ? `: ${p.value}` : ""} — ${p.source}${p.date ? `, ${p.date}` : ""}`);
|
|
41
|
+
const proofOr = proof.length ? proof : [F(L("a verifiable number with its source and date — or leave the block out; never invent one", "kaynağı ve tarihi olan doğrulanabilir bir sayı — yoksa bloğu çıkarın; asla uydurmayın"))];
|
|
42
|
+
const alternatives = (r?.competitors ?? []).map((x) => `${x.name}: ${isFilled(x.forWhom) ? x.forWhom : F(L("who it fits (from its own site)", "kime uygun (kendi sitesinden)"))} (${isFilled(x.url) ? x.url : F("url")}, ${L("checked", "kontrol")} ${isFilled(x.checkedAt) ? x.checkedAt : F("YYYY-MM-DD")})`);
|
|
43
|
+
const pricingPath = findPage(known, "pricing")?.path ?? (primaryTr ? "/fiyatlandirma" : "/pricing");
|
|
44
|
+
const guidePath = findPage(known, "comparison")?.path ?? (category ? (primaryTr ? `/rehber/${slugify(category, 4)}-araclari` : `/guides/${slugify(category, 4)}-tools`) : undefined);
|
|
45
|
+
const faqPath = findPage(known, "faq")?.path ?? (primaryTr ? "/sss" : "/faq");
|
|
46
|
+
const aboutPath = findPage(known, "about")?.path ?? (primaryTr ? "/hakkimizda" : "/about");
|
|
47
|
+
const b = {
|
|
48
|
+
type, url: "", urlSuggested: false, title: "", h1: "", firstSentence: "", action: "", proof: proofOr, notFor: [], alternatives: [], faq: [], related: [],
|
|
49
|
+
blocks: [], jsonLd: [], files: [], fileNotes: [], notes,
|
|
50
|
+
};
|
|
51
|
+
const genericFaq = [
|
|
52
|
+
pricing ? L(`How much does it cost? (answer: ${pricing})`, `Ne kadar tutuyor? (cevap: ${pricing})`) : L("How much does it cost? (answer from research pricing)", "Ne kadar tutuyor? (cevap araştırmadaki fiyattan)"),
|
|
53
|
+
L("Do I need an account to try it?", "Denemek için hesap gerekiyor mu?"),
|
|
54
|
+
L("What happens to my files / data?", "Dosyalarıma / verilerime ne oluyor?"),
|
|
55
|
+
L("How long does it take?", "Ne kadar sürüyor?"),
|
|
56
|
+
L("What if the result isn't what I wanted?", "Sonuç istediğim gibi olmazsa ne olur?"),
|
|
57
|
+
];
|
|
58
|
+
const anatomy = ANATOMY.map((a) => `${a.n}. ${a.tags.length ? `[${a.tags.join(" ")}] ` : ""}**${t(a.name, lang)}** — ${t(a.rule, lang)}`);
|
|
59
|
+
const sitewide = L("Sitewide (layout): Organization (not-X in disambiguatingDescription) + WebSite — `katman_generate` organization-jsonld / website-jsonld", "Bütün sitede (layout): Organization (“X değildir” disambiguatingDescription’da) + WebSite — `katman_generate` organization-jsonld / website-jsonld");
|
|
60
|
+
const crumbs = L("BreadcrumbList (Home → this page) — `katman_generate` breadcrumb-jsonld", "BreadcrumbList (Ana sayfa → bu sayfa) — `katman_generate` breadcrumb-jsonld");
|
|
61
|
+
const faqLd = L("FAQPage with exactly the questions and answers visible on the page — `katman_generate` faq-jsonld", "Sayfada görünen soru-cevapların aynısıyla FAQPage — `katman_generate` faq-jsonld");
|
|
62
|
+
switch (type) {
|
|
63
|
+
case "situation": {
|
|
64
|
+
const s = pickSituation(r, c.situationId, c.scan, c.audit);
|
|
65
|
+
if (c.situationId && !s)
|
|
66
|
+
return { error: `Situation "${c.situationId}" not found. Known: ${(r?.situations ?? []).map((x) => x.id).join(", ") || "none"}.` };
|
|
67
|
+
const sentenceText = s?.sentence ?? c.topic ?? F(L("the customer's sentence (katman_research)", "müşterinin cümlesi (katman_research)"));
|
|
68
|
+
if (s)
|
|
69
|
+
b.situation = { id: s.id, sentence: s.sentence };
|
|
70
|
+
b.url = s ? situationUrl(s, r?.site.languages) : c.topic ? `/${slugify(c.topic)}` : "/" + F("words-from-the-sentence");
|
|
71
|
+
b.urlSuggested = !s?.url;
|
|
72
|
+
b.title = titleWithBrand(sentenceText, brand, notes, lang);
|
|
73
|
+
b.h1 = sentenceText;
|
|
74
|
+
b.firstSentence = sentence
|
|
75
|
+
? L(`Tailor the one sentence to this situation, keeping its four parts: “${sentence}”`, `Tek cümleyi bu duruma uyarlayın, dört parçasını koruyun: “${sentence}”`)
|
|
76
|
+
: F(L("brings → we do → they get. fear is unnecessary. (fill the 4-part sentence in research)", "getirir → yaparız → eline geçer. korku gereksiz. (4 parçalı cümleyi araştırmada doldurun)"));
|
|
77
|
+
b.action = F(L("one clear action usable in the first minute — a file drop box, a trial, or the price", "ilk dakikada kullanılabilen tek net eylem — dosya kutusu, deneme ya da fiyat"));
|
|
78
|
+
b.notFor = [F(L("who should use something else — 2–3 bullets", "kim başka bir şey kullanmalı — 2–3 madde"))];
|
|
79
|
+
b.alternatives = alternatives.length ? alternatives : [F(L("competitors from research, with who each one fits", "araştırmadaki rakipler, her birinin kime uygun olduğuyla"))];
|
|
80
|
+
const kw = (r?.keywords ?? []).filter((k) => k.page && b.url && k.page === b.url).map((k) => cap(k.term) + "?");
|
|
81
|
+
b.faq = [...kw, ...genericFaq].slice(0, 8);
|
|
82
|
+
b.related = [
|
|
83
|
+
...(r?.situations ?? []).filter((x) => x.id !== s?.id).slice(0, 3).map((x) => `${situationUrl(x, r?.site.languages)} (“${x.sentence}”)`),
|
|
84
|
+
pricingPath,
|
|
85
|
+
...(guidePath ? [guidePath] : []),
|
|
86
|
+
].slice(0, 5);
|
|
87
|
+
b.blocks = anatomy;
|
|
88
|
+
b.jsonLd = [
|
|
89
|
+
L("Service — or SoftwareApplication / WebApplication for a product; offers only if the price is visible on this page (`katman_generate` software-jsonld)", "Service — ürün için SoftwareApplication / WebApplication; fiyat yalnızca bu sayfada görünüyorsa offers (`katman_generate` software-jsonld)"),
|
|
90
|
+
faqLd,
|
|
91
|
+
crumbs,
|
|
92
|
+
sitewide,
|
|
93
|
+
];
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
case "comparison": {
|
|
97
|
+
const topic = c.topic ?? category ?? F(L("category", "kategori"));
|
|
98
|
+
b.topic = topic;
|
|
99
|
+
b.url = c.topic ? (primaryTr ? `/rehber/${slugify(c.topic, 5)}` : `/guides/${slugify(c.topic, 5)}`) : guidePath ?? "/guides/" + F("tools-for-x");
|
|
100
|
+
b.urlSuggested = true;
|
|
101
|
+
b.title = titleWithBrand(L(`${cap(topic)} tools: an honest comparison (${year})`, `${topic} için araçlar: dürüst karşılaştırma (${year})`), undefined, notes, lang);
|
|
102
|
+
b.h1 = L(`Which ${topic} tools are there?`, `${topic} için hangi araçlar var?`);
|
|
103
|
+
b.firstSentence = F(L("a 2–3 sentence short answer: which tool for which situation", "2–3 cümlelik kısa cevap: hangi durumda hangi araç"));
|
|
104
|
+
b.action = L("Link each tool to its own site (nofollow is fine); our own entry links to the relevant situation page.", "Her aracı kendi sitesine linkleyin (nofollow olabilir); bizim satırımız ilgili durum sayfasına linklesin.");
|
|
105
|
+
b.alternatives = [
|
|
106
|
+
...(alternatives.length ? alternatives : [F(L("at least 3 competitors from research", "araştırmadan en az 3 rakip"))]),
|
|
107
|
+
`${brand ?? F(L("our brand", "markamız"))}: ${L("who it fits", "kime uygun")}: ${FILL} · ${L("what it doesn't do", "neyi yapmıyor")}: ${FILL} ${L("(not first in the list)", "(listenin başında değil)")}`,
|
|
108
|
+
];
|
|
109
|
+
b.notFor = [L("For every tool, ours included: “who it fits” and “what it doesn't do”.", "Bizimki dahil her araç için: “kime uygun” ve “neyi yapmıyor”.")];
|
|
110
|
+
b.faq = [L("Which one is easiest for beginners?", "Yeni başlayanlar için hangisi daha kolay?"), L("Which one is cheapest?", "Hangisi en ucuz?"), L("Which one works without an account?", "Hangisi hesapsız çalışıyor?")].map((q) => `${q} ${L("(answer only from the sourced facts above)", "(yalnızca yukarıdaki kaynaklı bilgilerden cevaplayın)")}`);
|
|
111
|
+
b.related = (r?.situations ?? []).slice(0, 4).map((x) => situationUrl(x, r?.site.languages)).concat(pricingPath).slice(0, 5);
|
|
112
|
+
b.blocks = [
|
|
113
|
+
L("Short answer at the top (2–3 sentences).", "En üstte kısa cevap (2–3 cümle)."),
|
|
114
|
+
L("One section per tool: what it does, price, who it fits, what it doesn't do, source link + date checked.", "Her araç için bir bölüm: ne yapar, fiyat, kime uygun, neyi yapmıyor, kaynak linki + kontrol tarihi."),
|
|
115
|
+
L("Don't put us first; write our gaps too. Self-ranked lists backfire (the brand was left out 69% of the time when its own list was cited).", "Bizi başa koymayın; eksiğimizi de yazın. Kendini birinci yazan listeler ters tepiyor (kendi listesi alıntılandığında marka %69 oranında dışarıda kaldı)."),
|
|
116
|
+
L("A comparison table in plain HTML.", "Düz HTML karşılaştırma tablosu."),
|
|
117
|
+
L("Visible update date and author.", "Görünür güncelleme tarihi ve yazar."),
|
|
118
|
+
];
|
|
119
|
+
b.jsonLd = [L("Article (headline, datePublished, dateModified, author)", "Article (headline, datePublished, dateModified, author)"), L("ItemList with the tools in the same order as on the page", "Sayfadaki sırayla araçları içeren ItemList"), crumbs, sitewide];
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
case "pricing": {
|
|
123
|
+
b.url = pricingPath;
|
|
124
|
+
b.title = titleWithBrand(L(`${brand ?? "[FILL]"} pricing: plans and prices`, `${brand ?? "[DOLDUR]"} fiyatları: planlar`), undefined, notes, lang);
|
|
125
|
+
b.h1 = L(`${brand ?? F("brand")} pricing`, `${brand ?? F("marka")} fiyatları`);
|
|
126
|
+
b.firstSentence = pricing ?? F(L("the pricing exactly as in research (and llms.txt)", "araştırmadaki (ve llms.txt’deki) fiyatın aynısı"));
|
|
127
|
+
b.action = L("The plan buttons (checkout), matching the visitor's intent.", "Ziyaretçinin niyetine uygun plan düğmeleri (ödeme).");
|
|
128
|
+
b.blocks = [
|
|
129
|
+
L("Prices in plain HTML text (no JS-only toggles).", "Fiyatlar düz HTML metinde (yalnızca JS ile değişen anahtarlar yok)."),
|
|
130
|
+
L("What each plan includes and its limits.", "Her planın içeriği ve sınırları."),
|
|
131
|
+
L("Trial, cancellation, refunds — as they really are.", "Deneme, iptal, iade — gerçekte nasılsa."),
|
|
132
|
+
L("Don't call a paid product “free”; no fake urgency.", "Ücretli ürüne “ücretsiz” demeyin; sahte aciliyet yok."),
|
|
133
|
+
L("FAQ about billing.", "Faturalandırma SSS’si."),
|
|
134
|
+
];
|
|
135
|
+
b.faq = [L("Can I cancel any time?", "İstediğim zaman iptal edebilir miyim?"), L("Is there a trial?", "Deneme var mı?"), L("What happens when I hit the limit?", "Sınıra gelince ne olur?"), L("Do you offer refunds?", "İade var mı?"), L("Which payment methods do you accept?", "Hangi ödeme yöntemleri kabul ediliyor?")];
|
|
136
|
+
b.related = (r?.situations ?? []).slice(0, 3).map((x) => situationUrl(x, r?.site.languages)).concat(faqPath);
|
|
137
|
+
b.jsonLd = [L("SoftwareApplication / Service with offers exactly as shown on this page (`katman_generate` software-jsonld)", "Bu sayfada görünen fiyatlarla birebir offers içeren SoftwareApplication / Service (`katman_generate` software-jsonld)"), faqLd, crumbs, sitewide];
|
|
138
|
+
notes.push(L("The same prices must appear in llms.txt and schema; the linter compares them.", "Aynı fiyatlar llms.txt’de ve schema’da olmalı; denetleyici karşılaştırır."));
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
case "faq": {
|
|
142
|
+
b.url = faqPath;
|
|
143
|
+
b.title = titleWithBrand(L(`${brand ?? "[FILL]"} FAQ`, `${brand ?? "[DOLDUR]"} SSS`), undefined, notes, lang);
|
|
144
|
+
b.h1 = L(`Questions about ${brand ?? F("brand")}`, `${brand ?? F("marka")} hakkında sorular`);
|
|
145
|
+
b.firstSentence = sentence ?? F(L("the one sentence", "tek cümle"));
|
|
146
|
+
b.faq = [
|
|
147
|
+
L(`What is ${brand ?? "[FILL]"}? (answer: the one sentence)`, `${brand ?? "[DOLDUR]"} nedir? (cevap: tek cümle)`),
|
|
148
|
+
...(notX ? [L(`Is it the same as … ? (answer: ${notX})`, `… ile aynı mı? (cevap: ${notX})`)] : []),
|
|
149
|
+
pricing ? L(`How much does it cost? (answer: ${pricing})`, `Ne kadar tutuyor? (cevap: ${pricing})`) : L("How much does it cost?", "Ne kadar tutuyor?"),
|
|
150
|
+
...(r?.situations ?? []).slice(0, 3).map((x) => L(`Can it help if “${x.sentence}”? (link ${situationUrl(x, r?.site.languages)})`, `“${x.sentence}” durumunda işe yarar mı? (link ${situationUrl(x, r?.site.languages)})`)),
|
|
151
|
+
F(L("the real questions from support e-mails and sales calls", "destek e-postaları ve satış görüşmelerinden gerçek sorular")),
|
|
152
|
+
];
|
|
153
|
+
b.blocks = [L("Every answer's first sentence is the answer.", "Her cevabın ilk cümlesi cevaptır."), L("Answers link to the page that covers the topic.", "Cevaplar konuyu anlatan sayfaya link verir."), L("Plain HTML (no answers hidden behind JS-only accordions; <details> is fine).", "Düz HTML (yalnızca JS ile açılan akordeonlarda gizli cevap yok; <details> olur).")];
|
|
154
|
+
b.related = [pricingPath, aboutPath, ...(r?.situations ?? []).slice(0, 3).map((x) => situationUrl(x, r?.site.languages))];
|
|
155
|
+
b.jsonLd = [faqLd, crumbs, sitewide];
|
|
156
|
+
notes.push(L("Google retired the FAQ rich result in May 2026; write the FAQ for assistants, not for a Google box.", "Google SSS zengin sonucunu Mayıs 2026’da kaldırdı; SSS’yi Google kutusu için değil asistanlar için yazın."));
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
case "about": {
|
|
160
|
+
b.url = aboutPath;
|
|
161
|
+
b.title = titleWithBrand(L(`About ${brand ?? "[FILL]"}`, `${brand ?? "[DOLDUR]"} hakkında`), category, notes, lang);
|
|
162
|
+
b.h1 = L(`About ${brand ?? F("brand")}`, `${brand ?? F("marka")} hakkında`);
|
|
163
|
+
b.firstSentence = sentence ?? F(L("the one sentence", "tek cümle"));
|
|
164
|
+
b.blocks = [
|
|
165
|
+
L(`Who builds it: ${isFilled(r?.brand.company) ? r.brand.company : "[FILL: company]"}${isFilled(r?.brand.country) ? ` (${r.brand.country})` : ""}.`, `Kim yapıyor: ${isFilled(r?.brand.company) ? r.brand.company : "[DOLDUR: şirket]"}${isFilled(r?.brand.country) ? ` (${r.brand.country})` : ""}.`),
|
|
166
|
+
notX ? L(`The not-X note, word for word: “${notX}”`, `“X değildir” notu, aynen: “${notX}”`) : L("Not-X note: only if the brand gets confused with something (ask ChatGPT “What is <brand>?”).", "“X değildir” notu: yalnızca marka bir şeyle karışıyorsa (ChatGPT’ye “<marka> nedir?” diye sorun)."),
|
|
167
|
+
L(`Social accounts: ${(r?.brand.socials ?? []).join(", ") || "[FILL]"}`, `Sosyal hesaplar: ${(r?.brand.socials ?? []).join(", ") || "[DOLDUR]"}`),
|
|
168
|
+
L("A contact e-mail in plain text (not obfuscated).", "Düz metin iletişim e-postası (gizlenmemiş)."),
|
|
169
|
+
L("Proof with sources; say plainly when a test was small.", "Kaynaklı kanıt; test küçükse açıkça söyleyin."),
|
|
170
|
+
];
|
|
171
|
+
b.jsonLd = [L("AboutPage", "AboutPage"), L("Organization with alternateName, disambiguatingDescription (the not-X note) and sameAs (socials)", "alternateName, disambiguatingDescription (“X değildir” notu) ve sameAs (sosyal hesaplar) ile Organization"), crumbs];
|
|
172
|
+
b.related = [pricingPath, faqPath];
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
case "single-question": {
|
|
176
|
+
const q = c.topic ?? F(L("the one question (from Google autocomplete: katman_keywords)", "tek soru (Google otomatik tamamlamasından: katman_keywords)"));
|
|
177
|
+
b.topic = q;
|
|
178
|
+
b.url = c.topic ? `/${slugify(c.topic)}` : "/" + F("words-from-the-question");
|
|
179
|
+
b.urlSuggested = true;
|
|
180
|
+
b.title = titleWithBrand(q, brand, notes, lang);
|
|
181
|
+
b.h1 = q;
|
|
182
|
+
b.firstSentence = F(L("the direct answer in one sentence, quotable on its own", "tek başına alıntılanabilen, tek cümlelik doğrudan cevap"));
|
|
183
|
+
b.blocks = [
|
|
184
|
+
L("The answer in the first 1–3 sentences (key claim within the first ~200 characters).", "Cevap ilk 1–3 cümlede (asıl iddia ilk ~200 karakterde)."),
|
|
185
|
+
L("Steps or details in plain HTML.", "Adımlar ya da ayrıntılar düz HTML’de."),
|
|
186
|
+
L("A link to the situation page that solves it with the product.", "Ürünle çözen durum sayfasına link."),
|
|
187
|
+
L("Sources and dates for any number.", "Her sayı için kaynak ve tarih."),
|
|
188
|
+
L("Updated date and author.", "Güncelleme tarihi ve yazar."),
|
|
189
|
+
];
|
|
190
|
+
b.related = (r?.situations ?? []).slice(0, 3).map((x) => situationUrl(x, r?.site.languages)).concat(guidePath ? [guidePath] : []);
|
|
191
|
+
b.jsonLd = [L("Article (headline = the question, datePublished, dateModified, author)", "Article (headline = soru, datePublished, dateModified, author)"), L("FAQPage only if there's a visible FAQ", "Yalnızca görünür SSS varsa FAQPage"), crumbs, sitewide];
|
|
192
|
+
notes.push(L("Publish 5–10 new pages a week at most; if a template's “crawled – not indexed” rate passes 30%, merge or noindex. Mass-produced pages count as scaled content abuse.", "Haftada en fazla 5–10 yeni sayfa; bir şablonun “tarandı, dizine eklenmedi” oranı %30’u geçerse birleştirin ya da noindex yapın. Seri üretilmiş sayfalar ölçekli içerik istismarı sayılır."));
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
case "glossary": {
|
|
196
|
+
b.url = findPage(known, "glossary")?.path ?? (primaryTr ? "/sozluk" : "/glossary");
|
|
197
|
+
b.title = titleWithBrand(L(`${category ?? "[FILL]"} glossary`, `${category ?? "[DOLDUR]"} sözlüğü`), brand, notes, lang);
|
|
198
|
+
b.h1 = L(`${category ?? F("category")} glossary`, `${category ?? F("kategori")} sözlüğü`);
|
|
199
|
+
b.firstSentence = L("One line on what the glossary covers; every term's first sentence is its definition.", "Sözlüğün neyi kapsadığına dair bir satır; her terimin ilk cümlesi tanımıdır.");
|
|
200
|
+
b.blocks = [L("One <h2> or <dt> per term; the definition first, then a link to the page that uses it.", "Her terim için bir <h2> ya da <dt>; önce tanım, sonra onu kullanan sayfaya link."), L("Link the glossary from the footer (a glossary with no links in doesn't get indexed).", "Sözlüğe altbilgiden link verin (hiç link almayan sözlük dizine girmez).")];
|
|
201
|
+
b.jsonLd = [L("DefinedTermSet with one DefinedTerm per visible term", "Görünen her terim için bir DefinedTerm içeren DefinedTermSet"), crumbs];
|
|
202
|
+
b.related = [faqPath, ...(guidePath ? [guidePath] : [])];
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
case "tool": {
|
|
206
|
+
const topic = c.topic ?? F(L("the tool, e.g. a calculator or a finder", "araç, ör. bir hesaplayıcı ya da bulucu"));
|
|
207
|
+
b.topic = topic;
|
|
208
|
+
b.url = c.topic ? `/tools/${slugify(c.topic, 5)}` : "/tools/" + F("tool-name");
|
|
209
|
+
b.urlSuggested = true;
|
|
210
|
+
b.title = titleWithBrand(topic, brand, notes, lang);
|
|
211
|
+
b.h1 = F(L("what the tool does, in the customer's words", "aracın ne yaptığı, müşterinin kelimeleriyle"));
|
|
212
|
+
b.firstSentence = F(L("what it does and what you get, in one sentence", "ne yaptığı ve ne elde edildiği, tek cümlede"));
|
|
213
|
+
b.action = L("The tool itself, usable without an account.", "Aracın kendisi, hesap olmadan kullanılabilir.");
|
|
214
|
+
b.blocks = [
|
|
215
|
+
L("The tool (it may need JavaScript).", "Araç (JavaScript gerekebilir)."),
|
|
216
|
+
L("An 80+ word written explanation in plain HTML: bots don't run the tool (the sample site's quiz page showed bots 27 words until an 86-word section was added).", "Düz HTML’de 80+ kelimelik yazılı açıklama: botlar aracı çalıştırmaz (örnek sitenin test sayfası 86 kelimelik bölüm eklenene kadar botlara 27 kelime gösteriyordu)."),
|
|
217
|
+
L("How it works: 3–5 steps.", "Nasıl çalışır: 3–5 adım."),
|
|
218
|
+
L("A link to the product's matching situation page.", "Ürünün ilgili durum sayfasına link."),
|
|
219
|
+
L("FAQ 5–8.", "SSS 5–8."),
|
|
220
|
+
];
|
|
221
|
+
b.faq = genericFaq;
|
|
222
|
+
b.related = (r?.situations ?? []).slice(0, 3).map((x) => situationUrl(x, r?.site.languages)).concat(pricingPath);
|
|
223
|
+
b.jsonLd = [L("WebApplication (name, description, url; offers only if a price is shown)", "WebApplication (name, description, url; fiyat yalnızca görünüyorsa offers)"), faqLd, crumbs, sitewide];
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
case "home": {
|
|
227
|
+
b.url = "/";
|
|
228
|
+
b.title = brand && category ? `${brand} — ${category}` : F(L("brand — category (the home title states the category)", "marka — kategori (ana sayfa title’ı kategoriyi söyler)"));
|
|
229
|
+
if (b.title.length > 60)
|
|
230
|
+
notes.push(L(`Title is ${b.title.length} characters: shorten the category wording.`, `Title ${b.title.length} karakter: kategori ifadesini kısaltın.`));
|
|
231
|
+
b.h1 = F(L("the main promise in the customer's words (often the top situation)", "müşterinin kelimeleriyle ana vaat (çoğunlukla en önemli durum)"));
|
|
232
|
+
b.firstSentence = sentence ? L(`The one sentence, word for word: “${sentence}”`, `Tek cümle, aynen: “${sentence}”`) : F(L("the one sentence", "tek cümle"));
|
|
233
|
+
b.action = F(L("the main action (try it / pricing)", "ana eylem (dene / fiyat)"));
|
|
234
|
+
b.blocks = [
|
|
235
|
+
L("The one sentence in the first paragraph and the meta description, word for word.", "Tek cümle ilk paragrafta ve meta açıklamada, aynen."),
|
|
236
|
+
L("Links to every situation page (text links, not only cards with onClick).", "Her durum sayfasına link (yalnızca onClick’li kartlar değil, metinli linkler)."),
|
|
237
|
+
L("Links to pricing, the comparison guide, FAQ, About.", "Fiyat, karşılaştırma rehberi, SSS ve Hakkımızda’ya link."),
|
|
238
|
+
L("Proof with a source and date.", "Kaynaklı ve tarihli kanıt."),
|
|
239
|
+
notX ? L(`A short not-X line: “${notX}”`, `Kısa bir “X değildir” satırı: “${notX}”`) : L("A not-X line only if the brand gets confused with something.", "“X değildir” satırı yalnızca marka bir şeyle karışıyorsa."),
|
|
240
|
+
];
|
|
241
|
+
b.related = [...(r?.situations ?? []).map((x) => situationUrl(x, r?.site.languages)).slice(0, 8), pricingPath, ...(guidePath ? [guidePath] : []), faqPath, aboutPath];
|
|
242
|
+
b.jsonLd = [L("Organization (alternateName, disambiguatingDescription, sameAs, logo)", "Organization (alternateName, disambiguatingDescription, sameAs, logo)"), L("WebSite", "WebSite"), L("SoftwareApplication / WebApplication if the product lives here (offers only as shown on the site)", "Ürün buradaysa SoftwareApplication / WebApplication (offers yalnızca sitede görünen)")];
|
|
243
|
+
notes.push(L("A vague home page is a common mistake: most AI referrals land on the home page, and ChatGPT names you as a brand.", "Belirsiz bir ana sayfa sık yapılan bir hata: yapay zekâdan gelenlerin çoğu ana sayfaya iner ve ChatGPT sizi marka olarak anar."));
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
const loc = pageLocation(c.scan, b.url.includes("[FILL") ? "/new-page" : b.url, c.staticDir ?? "");
|
|
248
|
+
b.files = loc.files;
|
|
249
|
+
b.fileNotes = loc.notes.map((n) => t(n, lang));
|
|
250
|
+
if (isCsr(c.scan))
|
|
251
|
+
notes.unshift(L("This site renders in the browser: do P2 first or the new page will be empty for bots.", "Bu site tarayıcıda render ediliyor: önce P2’yi yapın, yoksa yeni sayfa botlar için boş olur."));
|
|
252
|
+
return { brief: b };
|
|
253
|
+
}
|
|
254
|
+
export function renderBriefMd(b, c) {
|
|
255
|
+
const lang = c.lang;
|
|
256
|
+
const L = (en, tr) => (lang === "tr" ? tr : en);
|
|
257
|
+
const list = (xs) => (xs.length ? xs.map((x) => ` - ${x}`).join("\n") : " - –");
|
|
258
|
+
const out = [];
|
|
259
|
+
out.push(`# ${L("Page brief", "Sayfa brifi")}: ${b.type}${b.situation ? ` — “${b.situation.sentence}”` : b.topic ? ` — ${b.topic}` : ""}`);
|
|
260
|
+
out.push(L("Book ch. 9. Fill every [FILL] from research or ask the user; never invent proof, prices or competitor facts.", "Kitap, 9. bölüm. Her [DOLDUR]’u araştırmadan doldurun ya da kullanıcıya sorun; kanıt, fiyat ya da rakip bilgisi uydurmayın."));
|
|
261
|
+
out.push([
|
|
262
|
+
b.situation ? `- **${L("Situation", "Durum")}:** \`${b.situation.id}\` ${b.situation.sentence}` : "",
|
|
263
|
+
`- **URL:** ${b.url}${b.urlSuggested ? L(" (suggested — same words as the H1; lowercase, hyphens)", " (öneri — H1 ile aynı kelimeler; küçük harf, tire)") : ""}`,
|
|
264
|
+
`- **Title** (${b.title.length} ${L("chars", "karakter")}): ${b.title}`,
|
|
265
|
+
`- **H1:** ${b.h1}`,
|
|
266
|
+
`- **${L("First sentence", "İlk cümle")}:** ${b.firstSentence}`,
|
|
267
|
+
b.action ? `- **${L("Action", "Eylem")}:** ${b.action}` : "",
|
|
268
|
+
`- **${L("Proof", "Kanıt")}:**\n${list(b.proof)}`,
|
|
269
|
+
b.notFor.length ? `- **${L("Not for", "Kimler için değil")}:**\n${list(b.notFor)}` : "",
|
|
270
|
+
b.alternatives.length ? `- **${L("Alternatives", "Alternatifler")}:**\n${list(b.alternatives)}` : "",
|
|
271
|
+
b.faq.length ? `- **${L("FAQ (5–8, in the customer's words; keep only what customers really ask)", "SSS (5–8, müşterinin ağzından; yalnızca gerçekten sorulanları tutun)")}:**\n${list(b.faq)}` : "",
|
|
272
|
+
b.related.length ? `- **${L("Related pages (text links)", "İlgili sayfalar (metinli link)")}:**\n${list(b.related)}` : "",
|
|
273
|
+
]
|
|
274
|
+
.filter(Boolean)
|
|
275
|
+
.join("\n"));
|
|
276
|
+
out.push(`## ${b.type === "situation" ? L("The 11 blocks, in order", "11 blok, sırayla") : L("Blocks", "Bloklar")}\n${b.blocks.map((x) => (/^\d+\./.test(x) ? x : `- ${x}`)).join("\n")}`);
|
|
277
|
+
out.push(`## ${L("JSON-LD to include (only information visible on the page)", "Eklenecek JSON-LD (yalnızca sayfada görünen bilgi)")}\n${b.jsonLd.map((x) => `- ${x}`).join("\n")}`);
|
|
278
|
+
out.push(`## ${L("Where the file goes", "Dosya nereye")} (${fwLabel(c.scan)})\n${b.files.map((f) => `- \`${f}\``).join("\n")}${b.fileNotes.length ? `\n${b.fileNotes.map((n) => `- ${n}`).join("\n")}` : ""}`);
|
|
279
|
+
if (b.notes.length)
|
|
280
|
+
out.push(`## ${L("Notes", "Notlar")}\n${b.notes.map((n) => `- ${n}`).join("\n")}`);
|
|
281
|
+
const next = [];
|
|
282
|
+
if (b.type === "situation")
|
|
283
|
+
next.push(fmtCallShort(calls.prompt(c.root, "P6", b.situation?.id)));
|
|
284
|
+
if (b.type === "comparison")
|
|
285
|
+
next.push(fmtCallShort(calls.prompt(c.root, "P7")));
|
|
286
|
+
next.push(fmtCallShort(calls.lint(c.root, { file: b.files[0] ?? "[FILL]", page_type: b.type, ...(b.situation ? { situation_id: b.situation.id } : {}) })));
|
|
287
|
+
const site = siteUrlOf(c.research, c.config);
|
|
288
|
+
if (site && !b.url.includes("[FILL"))
|
|
289
|
+
next.push(`${L("after deploy", "yayından sonra")}: ${fmtCallShort(calls.auditUrls(c.root, [`${site}${b.url}`]))}`);
|
|
290
|
+
out.push(`## ${L("Then", "Sonra")}\n${next.map((n) => `- ${n}`).join("\n")}`);
|
|
291
|
+
return out.join("\n\n");
|
|
292
|
+
}
|
|
293
|
+
export async function runBrief(root, args) {
|
|
294
|
+
const [research, config, scan, loaded] = await Promise.all([loadResearchSafe(root), loadConfigSafe(root), loadScan(root), loadLatestAudit(root)]);
|
|
295
|
+
const lang = args.lang ?? config.lang ?? "en";
|
|
296
|
+
const ctx = { root, lang, research, config, scan, audit: loaded?.audit, staticDir: staticDirFor(root, scan).dir, situationId: args.situationId, topic: args.topic };
|
|
297
|
+
const { brief, error } = buildBrief(args.type, ctx);
|
|
298
|
+
if (!brief)
|
|
299
|
+
return { text: error ?? "Couldn't build the brief.", isError: true };
|
|
300
|
+
const pre = research ? "" : `No research yet: most fields are [FILL]. Start with ${fmtCallShort(calls.researchTemplate(root))}.\n\n`;
|
|
301
|
+
return { text: pre + renderBriefMd(brief, ctx), structured: brief };
|
|
302
|
+
}
|