From 664902e7ae144fd2bc94aa35f066e620becb88db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20Faruk=20Demirel?= Date: Fri, 20 Mar 2026 12:15:26 +0300 Subject: [PATCH 1/4] refactor: use language config for scripts --- package.json | 2 +- scripts/generate-og.mjs | 36 ++----- scripts/update-data.mjs | 203 ++++++++++++++++++------------------- src/data/en/og-labels.json | 8 ++ src/data/tr/og-labels.json | 8 ++ 5 files changed, 125 insertions(+), 132 deletions(-) create mode 100644 src/data/en/og-labels.json create mode 100644 src/data/tr/og-labels.json diff --git a/package.json b/package.json index 85e4257..23ea344 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "prepare": "husky", "check": "astro check", "generate:og": "node scripts/generate-og.mjs", - "generate:data": "node scripts/update-data.mjs --lang=tr && node scripts/update-data.mjs --lang=en" + "generate:data": "node scripts/update-data.mjs" }, "dependencies": { "@astrojs/partytown": "^2.1.5", diff --git a/scripts/generate-og.mjs b/scripts/generate-og.mjs index a4e9fe3..ed9ee94 100644 --- a/scripts/generate-og.mjs +++ b/scripts/generate-og.mjs @@ -2,6 +2,7 @@ import { Resvg } from '@resvg/resvg-js' import satori from 'satori' import fs from 'fs' import path from 'path' +import { SUPPORTED_LANGS } from '../src/config.mjs' const fontRegular = await fetch( 'https://fonts.gstatic.com/s/inter/v20/UcCO3FwrK3iLTeHuS_nVMrMxCp50SjIw2boKoduKmMEVuLyfMZg.ttf', @@ -16,27 +17,7 @@ const logoDark = fs.readFileSync(path.join(repoRoot, 'src/assets/logo-dark.png') const DEFAULT_COLORS = { bg: '#18181b', text: '#e5e7eb' } -const LANG_TEXTS = { - tr: { - explore: 'Makaleleri Keşfet', - category: 'KATEGORİ', - article: 'MAKALE', - tagline: 'Sinir ağları, transformerlar, pekiştirmeli öğrenme ve daha fazlası', - description: 'Makine öğrenimi kavramlarının net açıklamaları', - suffix: 'ML Kavramları Açıklandı', - }, - en: { - explore: 'Explore Articles', - category: 'CATEGORY', - article: 'ARTICLE', - tagline: 'Neural networks, transformers, reinforcement learning & more', - description: 'Clear explanations of machine learning concepts', - suffix: 'ML Concepts Explained', - }, -} - -async function makeSvg(title, color, isArticle, lang = 'en', logo) { - const texts = LANG_TEXTS[lang] || LANG_TEXTS.en +async function makeSvg(title, color, isArticle, texts, logo) { const bg = color || DEFAULT_COLORS.bg const svg = await satori( { @@ -177,21 +158,22 @@ async function makeSvg(title, color, isArticle, lang = 'en', logo) { const outDir = path.join(repoRoot, 'public', 'og') fs.mkdirSync(outDir, { recursive: true }) -const SUPPORTED_LANGS = ['tr', 'en'] - for (const lang of SUPPORTED_LANGS) { const langOutDir = path.join(outDir, lang) fs.mkdirSync(langOutDir, { recursive: true }) const dataDir = path.join(repoRoot, 'src', 'data', lang) - let categories = [] - let articles = [] + let categories + let articles + let langTexts try { categories = JSON.parse(fs.readFileSync(path.join(dataDir, 'categories.json'), 'utf8')) articles = JSON.parse(fs.readFileSync(path.join(dataDir, 'articles.json'), 'utf8')) + langTexts = JSON.parse(fs.readFileSync(path.join(dataDir, 'og-labels.json'), 'utf8')) } catch { console.log(`No data found for language: ${lang}`) + continue } const categoryColorMap = {} @@ -199,7 +181,7 @@ for (const lang of SUPPORTED_LANGS) { categoryColorMap[cat.name] = cat.color || null } - const svgIndex = await makeSvg('4byte.dev', null, false, lang, logoDark) + const svgIndex = await makeSvg('4byte.dev', null, false, langTexts, logoDark) const resvgIndex = new Resvg(svgIndex, { fitTo: { mode: 'width', value: 1200 } }) fs.writeFileSync(path.join(langOutDir, 'index.png'), Buffer.from(resvgIndex.render().asPng())) console.log(`Generated public/og/${lang}/index.png`) @@ -209,7 +191,7 @@ for (const lang of SUPPORTED_LANGS) { } else { for (const { slug, title, category } of articles) { const color = categoryColorMap[category] || null - const svgArticle = await makeSvg(title, color, true, lang, logoDark) + const svgArticle = await makeSvg(title, color, true, langTexts, logoDark) const resvgArticle = new Resvg(svgArticle, { fitTo: { mode: 'width', value: 1200 } }) fs.writeFileSync(path.join(langOutDir, `${slug}.png`), Buffer.from(resvgArticle.render().asPng())) console.log(`Generated public/og/${lang}/${slug}.png`) diff --git a/scripts/update-data.mjs b/scripts/update-data.mjs index c9e9039..74ce3ea 100755 --- a/scripts/update-data.mjs +++ b/scripts/update-data.mjs @@ -3,123 +3,118 @@ import { promises as fs } from 'fs' import path from 'path' import matter from 'gray-matter' import slugify from 'slugify' +import { SUPPORTED_LANGS } from '../src/config.mjs' const readJSON = async (file) => JSON.parse(await fs.readFile(file, 'utf8')) const writeJSON = async (file, data) => await fs.writeFile(file, JSON.stringify(data, null, 2), 'utf8') -const langArg = process.argv.find((arg) => arg.startsWith('--lang=')) -const targetLang = langArg ? langArg.split('=')[1] : 'tr' - -const SUPPORTED_LANGS = ['tr', 'en'] -if (!SUPPORTED_LANGS.includes(targetLang)) { - console.error(`Unsupported language: ${targetLang}. Supported: ${SUPPORTED_LANGS.join(', ')}`) - process.exit(1) -} - ;(async () => { const __dirname = new URL('.', import.meta.url).pathname const repoRoot = path.resolve(__dirname, '..') - const contentDir = path.join(repoRoot, 'src/content/articles', targetLang) - const tagsPath = path.join(repoRoot, 'src/data', targetLang, 'tags.json') - const categoriesPath = path.join(repoRoot, 'src/data', targetLang, 'categories.json') - const articlesPath = path.join(repoRoot, 'src/data', targetLang, 'articles.json') - - const tags = (await readJSON(tagsPath)) ?? [] - const categories = (await readJSON(categoriesPath)) ?? [] - - tags.forEach((t) => (t.articleCount = 0)) - categories.forEach((c) => (c.articleCount = 0)) - - const addedTags = new Set() - const addedCategories = new Set() - - let mdFiles = [] - try { - mdFiles = (await fs.readdir(contentDir)).filter((f) => f.endsWith('.md')) - } catch { - console.log(`No content directory for language '${targetLang}': ${contentDir}`) - console.log('Skipping article generation.') - } - const articles = [] - - for (const file of mdFiles) { - const filePath = path.join(contentDir, file) - const raw = await fs.readFile(filePath, 'utf8') - const { data } = matter(raw) - - const title = data.title?.trim() ?? path.parse(file).name - const slug = data.slug?.trim() ?? slugify(title, { lower: true }) - - const fileTags = Array.isArray(data.tags) ? data.tags : [] - for (const tagName of fileTags) { - const existing = tags.find((t) => t.name === tagName) - if (existing) { - existing.articleCount = (existing.articleCount ?? 0) + 1 - } else { - tags.push({ - id: String(tags.length + 1), - name: tagName, - slug: slugify(tagName, { lower: true }), - articleCount: 1, - description: '', - }) - addedTags.add(tagName) - } - } + for (const lang of SUPPORTED_LANGS) { + const contentDir = path.join(repoRoot, 'src/content/articles', lang) + const tagsPath = path.join(repoRoot, 'src/data', lang, 'tags.json') + const categoriesPath = path.join(repoRoot, 'src/data', lang, 'categories.json') + const articlesPath = path.join(repoRoot, 'src/data', lang, 'articles.json') - const catName = data.category?.trim() - if (catName) { - const existing = categories.find((c) => c.name === catName) - if (existing) { - existing.articleCount = (existing.articleCount ?? 0) + 1 - } else { - categories.push({ - id: String(categories.length + 1), - name: catName, - slug: slugify(catName, { lower: true }), - description: '', - color: '', - articleCount: 1, - }) - addedCategories.add(catName) - } - } + const tags = (await readJSON(tagsPath)) ?? [] + const categories = (await readJSON(categoriesPath)) ?? [] - articles.push({ title, slug, category: data.category?.trim() ?? '' }) - } + tags.forEach((t) => (t.articleCount = 0)) + categories.forEach((c) => (c.articleCount = 0)) - const filteredTags = tags.filter((t) => (t.articleCount ?? 0) > 0) - const filteredCategories = categories.filter((c) => (c.articleCount ?? 0) > 0) - - await writeJSON(tagsPath, filteredTags) - await writeJSON(categoriesPath, filteredCategories) - await writeJSON(articlesPath, articles) - - console.log(`Updated data for language: ${targetLang}`) - console.log(` Articles: ${articles.length}`) - console.log(` Categories: ${filteredCategories.length}`) - console.log(` Tags: ${filteredTags.length}`) - - if (process.env.GITHUB_EVENT_PATH) { - const event = JSON.parse(await fs.readFile(process.env.GITHUB_EVENT_PATH, 'utf8')) - const prNumber = event.pull_request?.number - if (prNumber) { - const lines = [] - if (addedTags.size) { - lines.push(`## New tags added for ${targetLang} (please add descriptions)`) - for (const t of addedTags) lines.push(`- \`${t}\``) - lines.push('') + const addedTags = new Set() + const addedCategories = new Set() + + let mdFiles = [] + try { + mdFiles = (await fs.readdir(contentDir)).filter((f) => f.endsWith('.md')) + } catch { + console.log(`No content directory for language '${lang}': ${contentDir}`) + console.log('Skipping article generation.') + } + + const articles = [] + + for (const file of mdFiles) { + const filePath = path.join(contentDir, file) + const raw = await fs.readFile(filePath, 'utf8') + const { data } = matter(raw) + + const title = data.title?.trim() ?? path.parse(file).name + const slug = data.slug?.trim() ?? slugify(title, { lower: true }) + + const fileTags = Array.isArray(data.tags) ? data.tags : [] + for (const tagName of fileTags) { + const existing = tags.find((t) => t.name === tagName) + if (existing) { + existing.articleCount = (existing.articleCount ?? 0) + 1 + } else { + tags.push({ + id: String(tags.length + 1), + name: tagName, + slug: slugify(tagName, { lower: true }), + articleCount: 1, + description: '', + }) + addedTags.add(tagName) + } } - if (addedCategories.size) { - lines.push(`## New categories added for ${targetLang} (please add descriptions)`) - for (const c of addedCategories) lines.push(`- \`${c}\``) - lines.push('') + + const catName = data.category?.trim() + if (catName) { + const existing = categories.find((c) => c.name === catName) + if (existing) { + existing.articleCount = (existing.articleCount ?? 0) + 1 + } else { + categories.push({ + id: String(categories.length + 1), + name: catName, + slug: slugify(catName, { lower: true }), + description: '', + color: '', + articleCount: 1, + }) + addedCategories.add(catName) + } } - if (lines.length) { - const comment = lines.join('\n') - const { execSync } = await import('child_process') - execSync(`gh pr comment ${prNumber} --body "${comment.replace(/"/g, '\\"')}"`) + + articles.push({ title, slug, category: data.category?.trim() ?? '' }) + } + + const filteredTags = tags.filter((t) => (t.articleCount ?? 0) > 0) + const filteredCategories = categories.filter((c) => (c.articleCount ?? 0) > 0) + + await writeJSON(tagsPath, filteredTags) + await writeJSON(categoriesPath, filteredCategories) + await writeJSON(articlesPath, articles) + + console.log(`Updated data for language: ${lang}`) + console.log(` Articles: ${articles.length}`) + console.log(` Categories: ${filteredCategories.length}`) + console.log(` Tags: ${filteredTags.length}`) + + if (process.env.GITHUB_EVENT_PATH) { + const event = JSON.parse(await fs.readFile(process.env.GITHUB_EVENT_PATH, 'utf8')) + const prNumber = event.pull_request?.number + if (prNumber) { + const lines = [] + if (addedTags.size) { + lines.push(`## New tags added for ${lang} (please add descriptions)`) + for (const t of addedTags) lines.push(`- \`${t}\``) + lines.push('') + } + if (addedCategories.size) { + lines.push(`## New categories added for ${lang} (please add descriptions)`) + for (const c of addedCategories) lines.push(`- \`${c}\``) + lines.push('') + } + if (lines.length) { + const comment = lines.join('\n') + const { execSync } = await import('child_process') + execSync(`gh pr comment ${prNumber} --body "${comment.replace(/"/g, '\\"')}"`) + } } } } diff --git a/src/data/en/og-labels.json b/src/data/en/og-labels.json new file mode 100644 index 0000000..1f0344d --- /dev/null +++ b/src/data/en/og-labels.json @@ -0,0 +1,8 @@ +{ + "explore": "Explore Articles", + "category": "CATEGORY", + "article": "ARTICLE", + "tagline": "Neural networks, transformers, reinforcement learning & more", + "description": "Clear explanations of machine learning concepts", + "suffix": "ML Concepts Explained" +} diff --git a/src/data/tr/og-labels.json b/src/data/tr/og-labels.json new file mode 100644 index 0000000..007c25a --- /dev/null +++ b/src/data/tr/og-labels.json @@ -0,0 +1,8 @@ +{ + "explore": "Makaleleri Keşfet", + "category": "KATEGORİ", + "article": "MAKALE", + "tagline": "Sinir ağları, transformerlar, pekiştirmeli öğrenme ve daha fazlası", + "description": "Makine öğrenimi kavramlarının net açıklamaları", + "suffix": "ML Kavramları Açıklandı" +} From c391eb89d1bada19eb19b727f00ff80fc3c408eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20Faruk=20Demirel?= Date: Fri, 20 Mar 2026 12:19:58 +0300 Subject: [PATCH 2/4] chore: remove new line from end of data file --- scripts/update-data.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/update-data.mjs b/scripts/update-data.mjs index 74ce3ea..33c294f 100755 --- a/scripts/update-data.mjs +++ b/scripts/update-data.mjs @@ -6,7 +6,7 @@ import slugify from 'slugify' import { SUPPORTED_LANGS } from '../src/config.mjs' const readJSON = async (file) => JSON.parse(await fs.readFile(file, 'utf8')) -const writeJSON = async (file, data) => await fs.writeFile(file, JSON.stringify(data, null, 2), 'utf8') +const writeJSON = async (file, data) => await fs.writeFile(file, JSON.stringify(data, null, 2) + '\n', 'utf8') ;(async () => { const __dirname = new URL('.', import.meta.url).pathname From ec7b654854043f36c182926db6d08c589c534861 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20Faruk=20Demirel?= Date: Fri, 20 Mar 2026 12:29:47 +0300 Subject: [PATCH 3/4] refactor: extract shared Tag and Category types to content config file --- src/components/Sidebar.astro | 14 +------------- src/content.config.ts | 13 +++++++++++++ src/pages/categories/[slug].astro | 8 +------- src/pages/categories/index.astro | 8 +------- src/pages/en/categories/[slug].astro | 8 +------- src/pages/en/categories/index.astro | 8 +------- src/pages/en/tags/[slug].astro | 7 +------ src/pages/en/tags/index.astro | 7 +------ src/pages/tags/[slug].astro | 7 +------ src/pages/tags/index.astro | 7 +------ 10 files changed, 22 insertions(+), 65 deletions(-) diff --git a/src/components/Sidebar.astro b/src/components/Sidebar.astro index 79216ba..293e771 100644 --- a/src/components/Sidebar.astro +++ b/src/components/Sidebar.astro @@ -6,6 +6,7 @@ import categoriesEn from '../data/en/categories.json' import tagsEn from '../data/en/tags.json' import LanguageSelector from './LanguageSelector.astro' import { t } from '../i18n/index.ts' +import type { Category, Tag } from '../content.config' interface Props { lang: string @@ -15,19 +16,6 @@ interface Props { const { lang, currentPath, labels } = Astro.props -interface Category { - slug: string - name: string - description: string - articleCount: number -} - -interface Tag { - slug: string - name: string - articleCount: number -} - const categories: Category[] = lang === 'en' ? categoriesEn : categoriesTr const tags: Tag[] = lang === 'en' ? tagsEn : tagsTr diff --git a/src/content.config.ts b/src/content.config.ts index 3326f10..3db7d5b 100644 --- a/src/content.config.ts +++ b/src/content.config.ts @@ -1,6 +1,19 @@ import { defineCollection, z } from 'astro:content' import { glob } from 'astro/loaders' +export interface Tag { + slug: string + name: string + articleCount: number +} + +export interface Category { + slug: string + name: string + description: string + articleCount: number +} + const articles = defineCollection({ loader: glob({ pattern: '**/*{.md,.mdx}', base: './src/content/articles' }), schema: z.object({ diff --git a/src/pages/categories/[slug].astro b/src/pages/categories/[slug].astro index 4ec7007..ba69e67 100644 --- a/src/pages/categories/[slug].astro +++ b/src/pages/categories/[slug].astro @@ -4,13 +4,7 @@ import { getCollection } from 'astro:content' import categoriesTr from '../../data/tr/categories.json' import categoriesEn from '../../data/en/categories.json' import { getLangFromUrl, t } from '../../i18n/index.ts' - -interface Category { - slug: string - name: string - description: string - articleCount: number -} +import type { Category } from '../../content.config' const lang = getLangFromUrl(Astro.url) const labels = await import(`../../data/${lang}/labels.json`).then((m) => m.default) diff --git a/src/pages/categories/index.astro b/src/pages/categories/index.astro index ccafb89..f2a487c 100644 --- a/src/pages/categories/index.astro +++ b/src/pages/categories/index.astro @@ -3,13 +3,7 @@ import DocsLayout from '../../layouts/DocsLayout.astro' import categoriesTr from '../../data/tr/categories.json' import categoriesEn from '../../data/en/categories.json' import { getLangFromUrl, t } from '../../i18n/index.ts' - -interface Category { - slug: string - name: string - description: string - articleCount: number -} +import type { Category } from '../../content.config' const lang = getLangFromUrl(Astro.url) const categories: Category[] = lang === 'en' ? categoriesEn : categoriesTr diff --git a/src/pages/en/categories/[slug].astro b/src/pages/en/categories/[slug].astro index fc0cb89..198431c 100644 --- a/src/pages/en/categories/[slug].astro +++ b/src/pages/en/categories/[slug].astro @@ -3,13 +3,7 @@ import DocsLayout from '../../../layouts/DocsLayout.astro' import { getCollection } from 'astro:content' import categoriesEn from '../../../data/en/categories.json' import { t } from '../../../i18n/index.ts' - -interface Category { - slug: string - name: string - description: string - articleCount: number -} +import type { Category } from '../../../content.config' const lang = 'en' const labels = await import(`../../../data/${lang}/labels.json`).then((m) => m.default) diff --git a/src/pages/en/categories/index.astro b/src/pages/en/categories/index.astro index 06afa19..2e13a24 100644 --- a/src/pages/en/categories/index.astro +++ b/src/pages/en/categories/index.astro @@ -2,13 +2,7 @@ import DocsLayout from '../../../layouts/DocsLayout.astro' import categoriesEn from '../../../data/en/categories.json' import { t } from '../../../i18n/index.ts' - -interface Category { - slug: string - name: string - description: string - articleCount: number -} +import type { Category } from '../../../content.config' const lang = 'en' const labels = await import(`../../../data/${lang}/labels.json`).then((m) => m.default) diff --git a/src/pages/en/tags/[slug].astro b/src/pages/en/tags/[slug].astro index 95e61d7..4cfb46b 100644 --- a/src/pages/en/tags/[slug].astro +++ b/src/pages/en/tags/[slug].astro @@ -3,12 +3,7 @@ import DocsLayout from '../../../layouts/DocsLayout.astro' import { getCollection } from 'astro:content' import tagsEn from '../../../data/en/tags.json' import { t } from '../../../i18n/index.ts' - -interface Tag { - slug: string - name: string - articleCount: number -} +import type { Tag } from '../../../content.config' const lang = 'en' const labels = await import(`../../../data/${lang}/labels.json`).then((m) => m.default) diff --git a/src/pages/en/tags/index.astro b/src/pages/en/tags/index.astro index e389569..a0797e8 100644 --- a/src/pages/en/tags/index.astro +++ b/src/pages/en/tags/index.astro @@ -2,12 +2,7 @@ import DocsLayout from '../../../layouts/DocsLayout.astro' import tagsEn from '../../../data/en/tags.json' import { t } from '../../../i18n/index.ts' - -interface Tag { - slug: string - name: string - articleCount: number -} +import type { Tag } from '../../../content.config' const lang = 'en' const labels = await import(`../../../data/${lang}/labels.json`).then((m) => m.default) diff --git a/src/pages/tags/[slug].astro b/src/pages/tags/[slug].astro index 8bf8c0f..176c672 100644 --- a/src/pages/tags/[slug].astro +++ b/src/pages/tags/[slug].astro @@ -4,12 +4,7 @@ import { getCollection } from 'astro:content' import tagsTr from '../../data/tr/tags.json' import tagsEn from '../../data/en/tags.json' import { getLangFromUrl, t } from '../../i18n/index.ts' - -interface Tag { - slug: string - name: string - articleCount: number -} +import type { Tag } from '../../content.config' const lang = getLangFromUrl(Astro.url) const labels = await import(`../../data/${lang}/labels.json`).then((m) => m.default) diff --git a/src/pages/tags/index.astro b/src/pages/tags/index.astro index be6b13f..4c185a4 100644 --- a/src/pages/tags/index.astro +++ b/src/pages/tags/index.astro @@ -3,12 +3,7 @@ import DocsLayout from '../../layouts/DocsLayout.astro' import tagsTr from '../../data/tr/tags.json' import tagsEn from '../../data/en/tags.json' import { getLangFromUrl, t } from '../../i18n/index.ts' - -interface Tag { - slug: string - name: string - articleCount: number -} +import type { Tag } from '../../content.config' const lang = getLangFromUrl(Astro.url) const tags: Tag[] = lang === 'en' ? tagsEn : tagsTr From 13a425b0e25644037ca50e8bb2b25183955393fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96mer=20Faruk=20Demirel?= <62325045+omerfdmrl@users.noreply.github.com> Date: Fri, 20 Mar 2026 12:35:24 +0300 Subject: [PATCH 4/4] Potential fix for code scanning alert no. 3: Incomplete string escaping or encoding Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- scripts/update-data.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/update-data.mjs b/scripts/update-data.mjs index 33c294f..64c4b30 100755 --- a/scripts/update-data.mjs +++ b/scripts/update-data.mjs @@ -112,8 +112,8 @@ const writeJSON = async (file, data) => await fs.writeFile(file, JSON.stringify( } if (lines.length) { const comment = lines.join('\n') - const { execSync } = await import('child_process') - execSync(`gh pr comment ${prNumber} --body "${comment.replace(/"/g, '\\"')}"`) + const { execFileSync } = await import('child_process') + execFileSync('gh', ['pr', 'comment', String(prNumber), '--body', comment]) } } }