Skip to content

Commit ccad5f8

Browse files
sametbasbugclaude
andcommitted
Mirror D1 back into the markdown archive
Yayın D1'e yazıyor ve site oradan okuyor; git'in arşiv ve kurtarma yolu olarak güncel kalması gerekiyordu. `scripts/haber-ayna.mjs` D1'i `src/content/` altına yansıtıyor: tek yönlü (D1 -> markdown) ve tam yansıma, yani D1'de olmayan haberin dosyası siliniyor. Başarı ölçütü şu: içerik değişmediyse betiği koşmak çalışma ağacını değiştirmemeli. Bu ölçüt iki ayrışma ortaya çıkardı, ikisi de biçimsel: - Arşivdeki 394 dosya sonda fazladan boş satır taşıyordu. `body_md` yazma anında zaten `trim()`leniyor, yani o boşluk D1'de saklanmıyordu. Arşiv aynanın ürettiği tek biçime oturtuldu: 396 satır silindi, tek satır eklenmedi, sayfa denkliği 586/586 korundu. - Bir dosyada açıklama tek tırnaklı YAML ile yazılmıştı (değerin içinde çift tırnak olduğu için). Ayna aynı kuralı uyguluyor artık. Ayrıca `autoGlossaryLinks` D1 şemasına eklendi (0007). Arşivin tamamında `true` olduğu için sabit yazılabilirdi, ama o zaman ileride farklı bir değer sessizce kaybolurdu; D1'i tam kaynak saymanın önündeki tek boşluk buydu. Ayna idempotent (ikinci koşuda 579 dosyanın 579'u değişmiyor) ve dişli: yerel D1'de bir başlığı bozan mutasyon tam o dosyada yakalandı. Tetikleme şimdilik elle: `npm run haber:ayna` (önce `haber:ayna:kuru`). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 4e93056 commit ccad5f8

397 files changed

Lines changed: 146 additions & 396 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎package.json‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
"news:prepare": "newsroom/.venv/bin/newsroom prepare",
2323
"news:publish": "newsroom/.venv/bin/newsroom publish --stdin",
2424
"news:status": "newsroom/.venv/bin/newsroom status",
25+
"haber:ayna": "node scripts/haber-ayna.mjs",
26+
"haber:ayna:kuru": "node scripts/haber-ayna.mjs --kuru",
2527
"test:newsroom": "newsroom/.venv/bin/python -m pytest newsroom/tests",
2628
"quality": "npm run test:newsroom && npm run build"
2729
},

‎scripts/haber-ayna.mjs‎

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
#!/usr/bin/env node
2+
/* D1 -> git aynası (write-behind).
3+
*
4+
* Yayın artık D1'e yazıyor; site oradan okuyor. Bu betik D1'i `src/content/`
5+
* altındaki markdown arşivine yansıtıyor. Amaç git'i gerçek kaynak yapmak
6+
* DEĞİL — git burada arşiv ve kurtarma yolu:
7+
* - D1 kaybolursa `worker/tools/migrate-archive.mjs` ile geri yüklenebilir,
8+
* - statik derleme (acil fren) güncel içerikle çalışır,
9+
* - içerik değişiklikleri sürüm geçmişinde okunabilir kalır.
10+
*
11+
* Ayna TAM yansımadır: D1'de olmayan haberin dosyası silinir. Tek yönlüdür,
12+
* D1'den markdown'a. Ters yön (markdown -> D1) migrate-archive'ın işi.
13+
*
14+
* Başarı ölçütü: içerik değişmediyse betiği koşmak çalışma ağacını
15+
* DEĞİŞTİRMEMELİ. Üretilen dosya elle yazılmış dosyayla bayt bayt aynı
16+
* olmalı; `git status` temiz kalmıyorsa biçimlendirme ayrışmış demektir.
17+
*
18+
* Kullanım: node scripts/haber-ayna.mjs [--yerel] [--kuru]
19+
* --yerel : uzak D1 yerine yerel veritabanını okur (testler için)
20+
* --kuru : dosya yazmaz, ne olacağını söyler
21+
*
22+
* `--yerel` kullanılırken veritabanı `worker/.wrangler/state` altından
23+
* okunuyor: denklik takımı yerel D1'i oradan dolduruyor ve wrangler'ın
24+
* varsayılan kalıcı dizini çalışılan dizine göre değiştiği için ikisi
25+
* aksi halde ayrı veritabanlarına bakıyor.
26+
*/
27+
import { execFileSync } from "node:child_process";
28+
import { readFileSync, writeFileSync, readdirSync, unlinkSync, existsSync } from "node:fs";
29+
import { join } from "node:path";
30+
31+
const ARGS = process.argv.slice(2);
32+
const YEREL = ARGS.includes("--yerel");
33+
const KURU = ARGS.includes("--kuru");
34+
const DIZIN = "src/content/equinoxHaber";
35+
const CONFIG = "wrangler.ssr.jsonc";
36+
37+
/* Gövdeler büyük olduğu için sorgu sayfalanıyor: tek seferde bütün arşivi
38+
* istemek komut çıktısını gereksiz şişiriyor. */
39+
function sorgula(sql) {
40+
const cikti = execFileSync(
41+
"npx",
42+
["wrangler", "d1", "execute", "haber", YEREL ? "--local" : "--remote",
43+
...(YEREL ? ["--persist-to", "worker/.wrangler/state"] : []),
44+
"--config", CONFIG, "--json", "--command", sql],
45+
{ encoding: "utf-8", maxBuffer: 256 * 1024 * 1024, stdio: ["ignore", "pipe", "ignore"] },
46+
);
47+
return JSON.parse(cikti.slice(cikti.indexOf("[")))[0].results;
48+
}
49+
50+
/* Tarih biçimi arşivle aynı olmak zorunda: yerel saat ve +03:00 ofseti.
51+
* Türkiye 2016'dan beri kalıcı UTC+3, yaz saati uygulaması yok. */
52+
function tarih(isoUtc) {
53+
const d = new Date(isoUtc);
54+
const y = new Date(d.getTime() + 3 * 60 * 60 * 1000).toISOString();
55+
return `${y.slice(0, 19)}+03:00`;
56+
}
57+
58+
/* YAML dizge alıntılama — arşivin kuralıyla aynı.
59+
*
60+
* Varsayılan çift tırnak; ama değerin içinde çift tırnak varsa tek tırnağa
61+
* geçiliyor (tek tırnaklı YAML'de kaçış yok, yalnız `'` iki kez yazılır).
62+
* Bu tercih keyfi değil: arşivdeki dosyalar da böyle yazılmış, ve ayna
63+
* onlarla bayt bayt aynı çıktı üretmek zorunda. */
64+
function q(s) {
65+
const v = String(s);
66+
if (v.includes('"')) return `'${v.replace(/'/g, "''")}'`;
67+
return `"${v.replace(/\\/g, "\\\\")}"`;
68+
}
69+
70+
function markdownUret(a, etiketler, kaynaklar) {
71+
const s = [];
72+
s.push("---");
73+
s.push(`title: ${q(a.title)}`);
74+
s.push(`description: ${q(a.description)}`);
75+
s.push(`pubDate: '${tarih(a.pub_date)}'`);
76+
s.push(`updatedDate: '${tarih(a.updated_date)}'`);
77+
s.push(`heroImage: ${q(a.hero_image ?? "")}`);
78+
if (a.hero_alt) s.push(`heroAlt: ${q(a.hero_alt)}`);
79+
s.push(`isDraft: ${a.is_draft ? "true" : "false"}`);
80+
s.push(`tags: [${etiketler.map(q).join(", ")}]`);
81+
s.push(`author: ${q(a.author)}`);
82+
s.push(`category: ${q(a.category)}`);
83+
s.push(`breaking: ${a.breaking ? "true" : "false"}`);
84+
if (a.editor_pick) s.push("editorPick: true");
85+
s.push("sources:");
86+
for (const k of kaynaklar) {
87+
s.push(` - name: ${q(k.name)}`);
88+
s.push(` url: ${q(k.url)}`);
89+
}
90+
s.push(`autoGlossaryLinks: ${a.auto_glossary_links ? "true" : "false"}`);
91+
s.push("---");
92+
s.push("");
93+
return s.join("\n") + "\n" + a.body_md.replace(/\s*$/, "") + "\n";
94+
}
95+
96+
const toplam = sorgula("SELECT COUNT(*) n FROM articles")[0].n;
97+
const makaleler = [];
98+
const ADIM = 60;
99+
for (let off = 0; off < toplam; off += ADIM) {
100+
makaleler.push(...sorgula(
101+
"SELECT slug,title,description,category,author,body_md,hero_image,hero_alt," +
102+
"pub_date,updated_date,is_draft,breaking,editor_pick,auto_glossary_links " +
103+
`FROM articles ORDER BY slug LIMIT ${ADIM} OFFSET ${off}`));
104+
}
105+
const etiketSatir = sorgula("SELECT slug,tag FROM article_tags ORDER BY slug,position");
106+
const kaynakSatir = sorgula("SELECT slug,name,url FROM article_sources ORDER BY slug,position");
107+
108+
const etiketler = new Map(), kaynaklar = new Map();
109+
for (const r of etiketSatir) { if (!etiketler.has(r.slug)) etiketler.set(r.slug, []); etiketler.get(r.slug).push(r.tag); }
110+
for (const r of kaynakSatir) { if (!kaynaklar.has(r.slug)) kaynaklar.set(r.slug, []); kaynaklar.get(r.slug).push(r); }
111+
112+
let yazilan = 0, degismeyen = 0, sadeceSonBosluk = 0, farkliIcerik = 0;
113+
const beklenen = new Set();
114+
for (const a of makaleler) {
115+
const yol = join(DIZIN, `${a.slug}.md`);
116+
beklenen.add(`${a.slug}.md`);
117+
const yeni = markdownUret(a, etiketler.get(a.slug) ?? [], kaynaklar.get(a.slug) ?? []);
118+
const eski = existsSync(yol) ? readFileSync(yol, "utf-8") : null;
119+
if (eski === yeni) { degismeyen++; continue; }
120+
if (eski !== null && eski.trimEnd() === yeni.trimEnd()) sadeceSonBosluk++;
121+
else if (eski !== null) { farkliIcerik++; if (farkliIcerik <= 5) console.log(` İÇERİK FARKI: ${a.slug}`); }
122+
if (!KURU) writeFileSync(yol, yeni);
123+
yazilan++;
124+
}
125+
126+
const fazla = readdirSync(DIZIN).filter((f) => f.endsWith(".md") && !beklenen.has(f));
127+
for (const f of fazla) { if (!KURU) unlinkSync(join(DIZIN, f)); console.log(` silindi: ${f}`); }
128+
129+
console.log(`\nD1: ${toplam} haber · yazılan ${yazilan} · değişmeyen ${degismeyen} · silinen ${fazla.length}${KURU ? " (KURU ÇALIŞMA)" : ""}`);
130+
if (yazilan) console.log(` yalnız son boşlukta farklı: ${sadeceSonBosluk} · gerçek içerik farkı: ${farkliIcerik}`);
131+
if (yazilan === 0 && fazla.length === 0) console.log("Ayna zaten güncel.");

‎src/content/equinoxHaber/2026nin-ilk-yarisinda-siber-saldirilar-kritik-altyapi-ve-acik-kaynak-zincirine-yayildi.md‎

Lines changed: 0 additions & 1 deletion

‎src/content/equinoxHaber/a-deal-is-a-deal-von-der-leyen-fires-back-at-trump-over-auto-tariff-threat.md‎

Lines changed: 0 additions & 1 deletion

‎src/content/equinoxHaber/ab-ajansi-sahte-ilaclarda-tehlikeli-sentetik-opioid-uyarisi-yapti.md‎

Lines changed: 0 additions & 1 deletion

‎src/content/equinoxHaber/ab-cin-ithalatina-yeni-kisitlamalari-gorusmeye-hazirlaniyor.md‎

Lines changed: 0 additions & 1 deletion

‎src/content/equinoxHaber/ab-iklim-komiseri-enerji-krizine-karsi-temiz-ve-yerli-avrupa-enerjisi-cagrisi-yapti.md‎

Lines changed: 0 additions & 1 deletion

‎src/content/equinoxHaber/ab-israilli-bakan-ben-gvire-yaptirim-konusunda-uzlasamadi.md‎

Lines changed: 0 additions & 1 deletion

‎src/content/equinoxHaber/ab-komisyonunun-covid-asi-sozlesmelerinde-daha-seffaf-olmasi-gerektigi-belirtildi.md‎

Lines changed: 0 additions & 1 deletion

‎src/content/equinoxHaber/ab-liderleri-abd-iran-ateskesini-temkinli-karsiladi.md‎

Lines changed: 0 additions & 1 deletion

0 commit comments

Comments
 (0)