Apply Biome formatting across the workspace

Fixes the formatting deltas from the initial Astro/Strapi scaffold
and brings the legacy generate-og-images/generate-llms/validate-post
scripts in line with Biome's style. No behavioral changes.
This commit is contained in:
2026-08-30 17:59:30 +03:00
parent 8be678d488
commit cae62d4a66
14 changed files with 332 additions and 236 deletions
+11 -9
View File
@@ -1,24 +1,26 @@
import type { Core } from '@strapi/strapi'; import type { Core } from "@strapi/strapi";
const config = ({ env }: Core.Config.Shared.ConfigParams): Core.Config.Admin => ({ const config = ({
env,
}: Core.Config.Shared.ConfigParams): Core.Config.Admin => ({
auth: { auth: {
secret: env('ADMIN_JWT_SECRET')!, secret: env("ADMIN_JWT_SECRET")!,
}, },
apiToken: { apiToken: {
salt: env('API_TOKEN_SALT')!, salt: env("API_TOKEN_SALT")!,
}, },
transfer: { transfer: {
token: { token: {
salt: env('TRANSFER_TOKEN_SALT')!, salt: env("TRANSFER_TOKEN_SALT")!,
}, },
}, },
secrets: { secrets: {
encryptionKey: env('ENCRYPTION_KEY')!, encryptionKey: env("ENCRYPTION_KEY")!,
}, },
flags: { flags: {
nps: env.bool('FLAG_NPS', true), nps: env.bool("FLAG_NPS", true),
promoteEE: env.bool('FLAG_PROMOTE_EE', true), promoteEE: env.bool("FLAG_PROMOTE_EE", true),
docLinks: env.bool('FLAG_DOC_LINKS', true), docLinks: env.bool("FLAG_DOC_LINKS", true),
}, },
}); });
+1 -1
View File
@@ -1,4 +1,4 @@
import type { Core } from '@strapi/strapi'; import type { Core } from "@strapi/strapi";
const config: Core.Config.Api = { const config: Core.Config.Api = {
rest: { rest: {
+62 -40
View File
@@ -1,61 +1,83 @@
import path from 'path'; import { isDatabaseClientKind } from "@strapi/database";
import type { Core } from '@strapi/strapi'; import type { Core } from "@strapi/strapi";
import { isDatabaseClientKind } from '@strapi/database'; import path from "path";
const config = ({ env }: Core.Config.Shared.ConfigParams): Core.Config.Database => { const config = ({
const client = env('DATABASE_CLIENT', 'sqlite'); env,
}: Core.Config.Shared.ConfigParams): Core.Config.Database => {
const client = env("DATABASE_CLIENT", "sqlite");
if (!isDatabaseClientKind(client)) { if (!isDatabaseClientKind(client)) {
throw new Error( throw new Error(
`Unsupported DATABASE_CLIENT: ${client}. Use "postgres", "mysql", or "sqlite".` `Unsupported DATABASE_CLIENT: ${client}. Use "postgres", "mysql", or "sqlite".`,
); );
} }
const connections: Record<Core.Config.Database.ClientKind, Core.Config.Database['connection']> = { const connections: Record<
Core.Config.Database.ClientKind,
Core.Config.Database["connection"]
> = {
mysql: { mysql: {
client: 'mysql', client: "mysql",
connection: { connection: {
host: env('DATABASE_HOST', 'localhost'), host: env("DATABASE_HOST", "localhost"),
port: env.int('DATABASE_PORT', 3306), port: env.int("DATABASE_PORT", 3306),
database: env('DATABASE_NAME', 'strapi'), database: env("DATABASE_NAME", "strapi"),
user: env('DATABASE_USERNAME', 'strapi'), user: env("DATABASE_USERNAME", "strapi"),
password: env('DATABASE_PASSWORD', 'strapi'), password: env("DATABASE_PASSWORD", "strapi"),
ssl: env.bool('DATABASE_SSL', false) && { ssl: env.bool("DATABASE_SSL", false) && {
key: env('DATABASE_SSL_KEY', undefined), key: env("DATABASE_SSL_KEY", undefined),
cert: env('DATABASE_SSL_CERT', undefined), cert: env("DATABASE_SSL_CERT", undefined),
ca: env('DATABASE_SSL_CA', undefined), ca: env("DATABASE_SSL_CA", undefined),
capath: env('DATABASE_SSL_CAPATH', undefined), capath: env("DATABASE_SSL_CAPATH", undefined),
cipher: env('DATABASE_SSL_CIPHER', undefined), cipher: env("DATABASE_SSL_CIPHER", undefined),
rejectUnauthorized: env.bool('DATABASE_SSL_REJECT_UNAUTHORIZED', true), rejectUnauthorized: env.bool(
"DATABASE_SSL_REJECT_UNAUTHORIZED",
true,
),
}, },
}, },
pool: { min: env.int('DATABASE_POOL_MIN', 2), max: env.int('DATABASE_POOL_MAX', 10) }, pool: {
min: env.int("DATABASE_POOL_MIN", 2),
max: env.int("DATABASE_POOL_MAX", 10),
},
}, },
postgres: { postgres: {
client: 'postgres', client: "postgres",
connection: { connection: {
connectionString: env('DATABASE_URL'), connectionString: env("DATABASE_URL"),
host: env('DATABASE_HOST', 'localhost'), host: env("DATABASE_HOST", "localhost"),
port: env.int('DATABASE_PORT', 5432), port: env.int("DATABASE_PORT", 5432),
database: env('DATABASE_NAME', 'strapi'), database: env("DATABASE_NAME", "strapi"),
user: env('DATABASE_USERNAME', 'strapi'), user: env("DATABASE_USERNAME", "strapi"),
password: env('DATABASE_PASSWORD', 'strapi'), password: env("DATABASE_PASSWORD", "strapi"),
ssl: env.bool('DATABASE_SSL', false) && { ssl: env.bool("DATABASE_SSL", false) && {
key: env('DATABASE_SSL_KEY', undefined), key: env("DATABASE_SSL_KEY", undefined),
cert: env('DATABASE_SSL_CERT', undefined), cert: env("DATABASE_SSL_CERT", undefined),
ca: env('DATABASE_SSL_CA', undefined), ca: env("DATABASE_SSL_CA", undefined),
capath: env('DATABASE_SSL_CAPATH', undefined), capath: env("DATABASE_SSL_CAPATH", undefined),
cipher: env('DATABASE_SSL_CIPHER', undefined), cipher: env("DATABASE_SSL_CIPHER", undefined),
rejectUnauthorized: env.bool('DATABASE_SSL_REJECT_UNAUTHORIZED', true), rejectUnauthorized: env.bool(
"DATABASE_SSL_REJECT_UNAUTHORIZED",
true,
),
}, },
schema: env('DATABASE_SCHEMA', 'public'), schema: env("DATABASE_SCHEMA", "public"),
},
pool: {
min: env.int("DATABASE_POOL_MIN", 2),
max: env.int("DATABASE_POOL_MAX", 10),
}, },
pool: { min: env.int('DATABASE_POOL_MIN', 2), max: env.int('DATABASE_POOL_MAX', 10) },
}, },
sqlite: { sqlite: {
client: 'sqlite', client: "sqlite",
connection: { connection: {
filename: path.join(__dirname, '..', '..', env('DATABASE_FILENAME', '.tmp/data.db')), filename: path.join(
__dirname,
"..",
"..",
env("DATABASE_FILENAME", ".tmp/data.db"),
),
}, },
useNullAsDefault: true, useNullAsDefault: true,
}, },
@@ -64,7 +86,7 @@ const config = ({ env }: Core.Config.Shared.ConfigParams): Core.Config.Database
return { return {
connection: { connection: {
...connections[client], ...connections[client],
acquireConnectionTimeout: env.int('DATABASE_CONNECTION_TIMEOUT', 60000), acquireConnectionTimeout: env.int("DATABASE_CONNECTION_TIMEOUT", 60000),
}, },
}; };
}; };
+11 -11
View File
@@ -1,16 +1,16 @@
import type { Core } from '@strapi/strapi'; import type { Core } from "@strapi/strapi";
const config: Core.Config.Middlewares = [ const config: Core.Config.Middlewares = [
'strapi::logger', "strapi::logger",
'strapi::errors', "strapi::errors",
'strapi::security', "strapi::security",
'strapi::cors', "strapi::cors",
'strapi::poweredBy', "strapi::poweredBy",
'strapi::query', "strapi::query",
'strapi::body', "strapi::body",
'strapi::session', "strapi::session",
'strapi::favicon', "strapi::favicon",
'strapi::public', "strapi::public",
]; ];
export default config; export default config;
+23 -21
View File
@@ -1,32 +1,34 @@
import type { Core } from '@strapi/strapi'; import type { Core } from "@strapi/strapi";
const allowedMediaTypes = [ const allowedMediaTypes = [
'image/*', "image/*",
'video/*', "video/*",
'audio/*', "audio/*",
'application/pdf', "application/pdf",
'application/msword', "application/msword",
'application/vnd.openxmlformats-officedocument.*', "application/vnd.openxmlformats-officedocument.*",
'text/plain', "text/plain",
'text/csv', "text/csv",
]; ];
const deniedTypes = [ const deniedTypes = [
'image/svg+xml', "image/svg+xml",
'application/vnd.microsoft.portable-executable', "application/vnd.microsoft.portable-executable",
'application/x-msdownload', "application/x-msdownload",
'application/x-msdos-program', "application/x-msdos-program",
'application/x-executable', "application/x-executable",
'application/x-dosexec', "application/x-dosexec",
'application/x-sh', "application/x-sh",
'text/x-shellscript', "text/x-shellscript",
'application/x-mach-binary', "application/x-mach-binary",
]; ];
const config = ({ env }: Core.Config.Shared.ConfigParams): Core.Config.Plugin => ({ const config = ({
'users-permissions': { env,
}: Core.Config.Shared.ConfigParams): Core.Config.Plugin => ({
"users-permissions": {
config: { config: {
jwtManagement: 'refresh', jwtManagement: "refresh",
sessions: { sessions: {
httpOnly: true, httpOnly: true,
}, },
+8 -6
View File
@@ -1,13 +1,15 @@
import type { Core } from '@strapi/strapi'; import type { Core } from "@strapi/strapi";
const config = ({ env }: Core.Config.Shared.ConfigParams): Core.Config.Server => ({ const config = ({
host: env('HOST', '0.0.0.0'), env,
port: env.int('PORT', 1337), }: Core.Config.Shared.ConfigParams): Core.Config.Server => ({
host: env("HOST", "0.0.0.0"),
port: env.int("PORT", 1337),
app: { app: {
keys: env.array('APP_KEYS')!, keys: env.array("APP_KEYS")!,
}, },
webhooks: { webhooks: {
populateRelations: env.bool('WEBHOOKS_POPULATE_RELATIONS', false), populateRelations: env.bool("WEBHOOKS_POPULATE_RELATIONS", false),
}, },
}); });
+1 -1
View File
@@ -1,4 +1,4 @@
import type { StrapiApp } from '@strapi/strapi/admin'; import type { StrapiApp } from "@strapi/strapi/admin";
export default { export default {
config: { config: {
+2 -2
View File
@@ -1,11 +1,11 @@
import { mergeConfig, type UserConfig } from 'vite'; import { mergeConfig, type UserConfig } from "vite";
export default (config: UserConfig) => { export default (config: UserConfig) => {
// Important: always return the modified config // Important: always return the modified config
return mergeConfig(config, { return mergeConfig(config, {
resolve: { resolve: {
alias: { alias: {
'@': '/src', "@": "/src",
}, },
}, },
}); });
+1 -1
View File
@@ -1,5 +1,5 @@
// @ts-check // @ts-check
import { defineConfig } from 'astro/config'; import { defineConfig } from "astro/config";
// https://astro.build/config // https://astro.build/config
export default defineConfig({}); export default defineConfig({});
+1 -1
View File
@@ -17,4 +17,4 @@
"allowScripts": { "allowScripts": {
"esbuild": true "esbuild": true
} }
} }
+7 -1
View File
@@ -7,7 +7,13 @@
}, },
"files": { "files": {
"ignoreUnknown": true, "ignoreUnknown": true,
"includes": ["apps/**", "scripts/**", "!apps/**/dist/**", "!apps/**/.strapi/**", "!apps/**/public/**"] "includes": [
"apps/**",
"scripts/**",
"!apps/**/dist",
"!apps/**/.strapi",
"!apps/**/public"
]
}, },
"formatter": { "formatter": {
"enabled": true, "enabled": true,
+41 -35
View File
@@ -1,13 +1,15 @@
import fs from 'node:fs'; import fs from "node:fs";
import path from 'node:path'; import path from "node:path";
const root = process.cwd(); const root = process.cwd();
const config = fs.readFileSync(path.join(root, 'zola.toml'), 'utf8'); const config = fs.readFileSync(path.join(root, "zola.toml"), "utf8");
const postsDir = path.join(root, 'content', 'posts'); const postsDir = path.join(root, "content", "posts");
const output = path.join(root, 'static', 'llms.txt'); const output = path.join(root, "static", "llms.txt");
function tomlString(source, key) { function tomlString(source, key) {
const match = source.match(new RegExp(`^${key}\\s*=\\s*("(?:[^"\\\\]|\\\\.)*")`, 'm')); const match = source.match(
new RegExp(`^${key}\\s*=\\s*("(?:[^"\\\\]|\\\\.)*")`, "m"),
);
return match ? JSON.parse(match[1]) : undefined; return match ? JSON.parse(match[1]) : undefined;
} }
@@ -19,31 +21,32 @@ function markdownFiles(directory) {
return fs.readdirSync(directory, { withFileTypes: true }).flatMap((entry) => { return fs.readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
const location = path.join(directory, entry.name); const location = path.join(directory, entry.name);
if (entry.isDirectory()) return markdownFiles(location); if (entry.isDirectory()) return markdownFiles(location);
return entry.isFile() && entry.name.endsWith('.md') ? [location] : []; return entry.isFile() && entry.name.endsWith(".md") ? [location] : [];
}); });
} }
function postUrl(file, metadata, baseUrl) { function postUrl(file, metadata, baseUrl) {
const relative = path.relative(postsDir, file).replaceAll(path.sep, '/'); const relative = path.relative(postsDir, file).replaceAll(path.sep, "/");
const defaultSlug = relative.endsWith('/index.md') const defaultSlug = relative.endsWith("/index.md")
? relative.slice(0, -'/index.md'.length) ? relative.slice(0, -"/index.md".length)
: relative.slice(0, -'.md'.length); : relative.slice(0, -".md".length);
const slug = tomlString(metadata, 'slug') ?? defaultSlug; const slug = tomlString(metadata, "slug") ?? defaultSlug;
return `${baseUrl}/posts/${slug}/`; return `${baseUrl}/posts/${slug}/`;
} }
const baseUrl = tomlString(config, 'base_url')?.replace(/\/$/, ''); const baseUrl = tomlString(config, "base_url")?.replace(/\/$/, "");
const siteTitle = new URL(baseUrl).hostname; const siteTitle = new URL(baseUrl).hostname;
const summary = tomlString(config, 'llms_description') ?? tomlString(config, 'description'); const summary =
tomlString(config, "llms_description") ?? tomlString(config, "description");
const posts = markdownFiles(postsDir) const posts = markdownFiles(postsDir)
.filter((file) => path.basename(file) !== '_index.md') .filter((file) => path.basename(file) !== "_index.md")
.map((file) => { .map((file) => {
const metadata = frontMatter(fs.readFileSync(file, 'utf8')); const metadata = frontMatter(fs.readFileSync(file, "utf8"));
if (!metadata || /^draft\s*=\s*true$/m.test(metadata)) return null; if (!metadata || /^draft\s*=\s*true$/m.test(metadata)) return null;
const title = tomlString(metadata, 'title'); const title = tomlString(metadata, "title");
const description = tomlString(metadata, 'description'); const description = tomlString(metadata, "description");
const date = metadata.match(/^date\s*=\s*(\d{4}-\d{2}-\d{2})/m)?.[1]; const date = metadata.match(/^date\s*=\s*(\d{4}-\d{2}-\d{2})/m)?.[1];
if (!title || !date) return null; if (!title || !date) return null;
@@ -54,32 +57,35 @@ const posts = markdownFiles(postsDir)
const lines = [ const lines = [
`# ${siteTitle}`, `# ${siteTitle}`,
'', "",
`> ${summary}`, `> ${summary}`,
'', "",
'This site contains articles in English, often with commands, code samples, worked explanations, and material aimed at readers learning by doing.', "This site contains articles in English, often with commands, code samples, worked explanations, and material aimed at readers learning by doing.",
'', "",
'## Main pages', "## Main pages",
'', "",
`- [Home](${baseUrl}/): Introduction and latest articles.`, `- [Home](${baseUrl}/): Introduction and latest articles.`,
`- [About](${baseUrl}/about/): About the author and contact information.`, `- [About](${baseUrl}/about/): About the author and contact information.`,
`- [Posts](${baseUrl}/posts/): All published articles.`, `- [Posts](${baseUrl}/posts/): All published articles.`,
`- [Tags](${baseUrl}/tags/): Articles grouped by topic.`, `- [Tags](${baseUrl}/tags/): Articles grouped by topic.`,
`- [Archive](${baseUrl}/archive/): Chronological article archive.`, `- [Archive](${baseUrl}/archive/): Chronological article archive.`,
'', "",
'## Published articles', "## Published articles",
'', "",
...posts.map(({ title, description, url }) => ...posts.map(
`- [${title}](${url})${description ? `: ${description.replace(/\s+/g, ' ').trim()}` : ''}`, ({ title, description, url }) =>
`- [${title}](${url})${description ? `: ${description.replace(/\s+/g, " ").trim()}` : ""}`,
), ),
'', "",
'## Feeds and discovery', "## Feeds and discovery",
'', "",
`- [RSS feed](${baseUrl}/rss.xml): Full site feed in RSS format.`, `- [RSS feed](${baseUrl}/rss.xml): Full site feed in RSS format.`,
`- [JSON feed](${baseUrl}/feed.json): Full site feed in JSON format.`, `- [JSON feed](${baseUrl}/feed.json): Full site feed in JSON format.`,
`- [Sitemap](${baseUrl}/sitemap.xml): Machine-readable index of public pages.`, `- [Sitemap](${baseUrl}/sitemap.xml): Machine-readable index of public pages.`,
'', "",
]; ];
fs.writeFileSync(output, lines.join('\n')); fs.writeFileSync(output, lines.join("\n"));
console.log(`Generated ${path.relative(root, output)} with ${posts.length} published articles.`); console.log(
`Generated ${path.relative(root, output)} with ${posts.length} published articles.`,
);
+136 -89
View File
@@ -1,30 +1,36 @@
import fs from 'node:fs/promises'; import fs from "node:fs/promises";
import path from 'node:path'; import path from "node:path";
import satori from 'satori'; import { Resvg } from "@resvg/resvg-js";
import { Resvg } from '@resvg/resvg-js'; import satori from "satori";
import { decompress } from 'wawoff2'; import { decompress } from "wawoff2";
const root = process.cwd(); const root = process.cwd();
const postsDir = path.join(root, 'content', 'posts'); const postsDir = path.join(root, "content", "posts");
const outDir = path.join(root, 'static', 'images', 'og'); const outDir = path.join(root, "static", "images", "og");
const COLORS = { const COLORS = {
bg: '#131311', bg: "#131311",
text: '#e9e7de', text: "#e9e7de",
muted: '#8b8a80', muted: "#8b8a80",
accent: '#ff5a46', accent: "#ff5a46",
border: 'rgba(233, 231, 222, 0.16)', border: "rgba(233, 231, 222, 0.16)",
}; };
function tomlString(source, key) { function tomlString(source, key) {
const match = source.match(new RegExp(`^${key}\\s*=\\s*("(?:[^"\\\\]|\\\\.)*")`, 'm')); const match = source.match(
new RegExp(`^${key}\\s*=\\s*("(?:[^"\\\\]|\\\\.)*")`, "m"),
);
return match ? JSON.parse(match[1]) : undefined; return match ? JSON.parse(match[1]) : undefined;
} }
function tomlStringArray(source, key) { function tomlStringArray(source, key) {
const match = source.match(new RegExp(`^${key}\\s*=\\s*\\[([^\\]]*)\\]`, 'm')); const match = source.match(
new RegExp(`^${key}\\s*=\\s*\\[([^\\]]*)\\]`, "m"),
);
if (!match) return []; if (!match) return [];
return [...match[1].matchAll(/"((?:[^"\\]|\\.)*)"/g)].map((m) => JSON.parse(`"${m[1]}"`)); return [...match[1].matchAll(/"((?:[^"\\]|\\.)*)"/g)].map((m) =>
JSON.parse(`"${m[1]}"`),
);
} }
function frontMatter(source) { function frontMatter(source) {
@@ -32,7 +38,9 @@ function frontMatter(source) {
} }
function taxonomies(source) { function taxonomies(source) {
return source.match(/^\[taxonomies\]\s*\n([\s\S]*?)(?:\n\[|\n?$)/m)?.[1] ?? ''; return (
source.match(/^\[taxonomies\]\s*\n([\s\S]*?)(?:\n\[|\n?$)/m)?.[1] ?? ""
);
} }
function markdownFiles(directory) { function markdownFiles(directory) {
@@ -41,103 +49,117 @@ function markdownFiles(directory) {
entries.map((entry) => { entries.map((entry) => {
const location = path.join(directory, entry.name); const location = path.join(directory, entry.name);
if (entry.isDirectory()) return markdownFiles(location); if (entry.isDirectory()) return markdownFiles(location);
return entry.isFile() && entry.name.endsWith('.md') ? [location] : []; return entry.isFile() && entry.name.endsWith(".md") ? [location] : [];
}), }),
).then((lists) => lists.flat()), ).then((lists) => lists.flat()),
); );
} }
function slugFor(file, metadata) { function slugFor(file, metadata) {
const relative = path.relative(postsDir, file).replaceAll(path.sep, '/'); const relative = path.relative(postsDir, file).replaceAll(path.sep, "/");
const defaultSlug = relative.endsWith('/index.md') const defaultSlug = relative.endsWith("/index.md")
? relative.slice(0, -'/index.md'.length) ? relative.slice(0, -"/index.md".length)
: relative.slice(0, -'.md'.length); : relative.slice(0, -".md".length);
return tomlString(metadata, 'slug') ?? defaultSlug; return tomlString(metadata, "slug") ?? defaultSlug;
} }
async function loadFont(relPath) { async function loadFont(relPath) {
const woff2 = await fs.readFile(path.join(root, 'static', relPath)); const woff2 = await fs.readFile(path.join(root, "static", relPath));
const ttf = await decompress(woff2); const ttf = await decompress(woff2);
return Buffer.from(ttf); return Buffer.from(ttf);
} }
function h(type, props, ...children) { function h(type, props, ...children) {
return { type, props: { ...props, children: children.flat().filter((c) => c !== null && c !== undefined) } }; return {
type,
props: {
...props,
children: children.flat().filter((c) => c !== null && c !== undefined),
},
};
} }
function formatDate(date) { function formatDate(date) {
return new Date(`${date}T00:00:00Z`).toLocaleDateString('en-US', { return new Date(`${date}T00:00:00Z`).toLocaleDateString("en-US", {
year: 'numeric', year: "numeric",
month: 'long', month: "long",
day: 'numeric', day: "numeric",
timeZone: 'UTC', timeZone: "UTC",
}); });
} }
function card({ title, date, tags }) { function card({ title, date, tags }) {
const stampTag = tags[0]; const stampTag = tags[0];
const footerTags = tags.length > 4 ? `${tags.slice(0, 4).join(' · ')} ⋯` : tags.join(' · '); const footerTags =
tags.length > 4 ? `${tags.slice(0, 4).join(" · ")} ⋯` : tags.join(" · ");
const titleSize = title.length > 70 ? 46 : title.length > 45 ? 54 : 64; const titleSize = title.length > 70 ? 46 : title.length > 45 ? 54 : 64;
return h( return h(
'div', "div",
{ {
style: { style: {
width: '1200px', width: "1200px",
height: '630px', height: "630px",
display: 'flex', display: "flex",
backgroundColor: COLORS.bg, backgroundColor: COLORS.bg,
color: COLORS.text, color: COLORS.text,
fontFamily: 'IBM Plex Serif', fontFamily: "IBM Plex Serif",
}, },
}, },
h( h(
'div', "div",
{ {
style: { style: {
flex: 1, flex: 1,
margin: '40px', margin: "40px",
border: `1px solid ${COLORS.border}`, border: `1px solid ${COLORS.border}`,
borderRadius: '6px', borderRadius: "6px",
padding: '48px 56px', padding: "48px 56px",
display: 'flex', display: "flex",
flexDirection: 'column', flexDirection: "column",
justifyContent: 'space-between', justifyContent: "space-between",
}, },
}, },
h( h(
'div', "div",
{ style: { display: 'flex', flexDirection: 'row', justifyContent: 'space-between', alignItems: 'flex-start' } }, {
style: {
display: "flex",
flexDirection: "row",
justifyContent: "space-between",
alignItems: "flex-start",
},
},
h( h(
'div', "div",
{ {
style: { style: {
display: 'flex', display: "flex",
fontFamily: 'JetBrains Mono', fontFamily: "JetBrains Mono",
fontWeight: 400, fontWeight: 400,
fontSize: '22px', fontSize: "22px",
color: COLORS.muted, color: COLORS.muted,
letterSpacing: '2px', letterSpacing: "2px",
}, },
}, },
'ANDWATI.COM · CASE FILE', "ANDWATI.COM · CASE FILE",
), ),
stampTag stampTag
? h( ? h(
'div', "div",
{ {
style: { style: {
display: 'flex', display: "flex",
border: `2px solid ${COLORS.accent}`, border: `2px solid ${COLORS.accent}`,
borderRadius: '4px', borderRadius: "4px",
padding: '8px 16px', padding: "8px 16px",
color: COLORS.accent, color: COLORS.accent,
fontFamily: 'JetBrains Mono', fontFamily: "JetBrains Mono",
fontWeight: 700, fontWeight: 700,
fontSize: '20px', fontSize: "20px",
letterSpacing: '2px', letterSpacing: "2px",
textTransform: 'uppercase', textTransform: "uppercase",
transform: 'rotate(3deg)', transform: "rotate(3deg)",
}, },
}, },
stampTag, stampTag,
@@ -145,57 +167,57 @@ function card({ title, date, tags }) {
: null, : null,
), ),
h( h(
'div', "div",
{ {
style: { style: {
display: 'flex', display: "flex",
fontFamily: 'IBM Plex Serif', fontFamily: "IBM Plex Serif",
fontWeight: 700, fontWeight: 700,
fontSize: `${titleSize}px`, fontSize: `${titleSize}px`,
lineHeight: 1.15, lineHeight: 1.15,
letterSpacing: '-1px', letterSpacing: "-1px",
color: COLORS.text, color: COLORS.text,
overflow: 'hidden', overflow: "hidden",
}, },
}, },
title, title,
), ),
h( h(
'div', "div",
{ {
style: { style: {
display: 'flex', display: "flex",
flexDirection: 'row', flexDirection: "row",
justifyContent: 'space-between', justifyContent: "space-between",
alignItems: 'center', alignItems: "center",
borderTop: `1px solid ${COLORS.border}`, borderTop: `1px solid ${COLORS.border}`,
paddingTop: '24px', paddingTop: "24px",
}, },
}, },
h( h(
'div', "div",
{ {
style: { style: {
display: 'flex', display: "flex",
fontFamily: 'JetBrains Mono', fontFamily: "JetBrains Mono",
fontWeight: 400, fontWeight: 400,
fontSize: '20px', fontSize: "20px",
color: COLORS.muted, color: COLORS.muted,
letterSpacing: '1px', letterSpacing: "1px",
}, },
}, },
formatDate(date), formatDate(date),
), ),
h( h(
'div', "div",
{ {
style: { style: {
display: 'flex', display: "flex",
fontFamily: 'JetBrains Mono', fontFamily: "JetBrains Mono",
fontWeight: 400, fontWeight: 400,
fontSize: '20px', fontSize: "20px",
color: COLORS.muted, color: COLORS.muted,
letterSpacing: '1px', letterSpacing: "1px",
}, },
}, },
footerTags, footerTags,
@@ -206,32 +228,57 @@ function card({ title, date, tags }) {
} }
const fonts = [ const fonts = [
{ name: 'IBM Plex Serif', data: await loadFont('fonts/ibm-plex-serif/ibm-plex-serif-v20-latin-700.woff2'), weight: 700, style: 'normal' }, {
{ name: 'JetBrains Mono', data: await loadFont('fonts/jetbrains-mono/JetBrainsMono-Regular.woff2'), weight: 400, style: 'normal' }, name: "IBM Plex Serif",
{ name: 'JetBrains Mono', data: await loadFont('fonts/jetbrains-mono/JetBrainsMono-Bold.woff2'), weight: 700, style: 'normal' }, data: await loadFont(
"fonts/ibm-plex-serif/ibm-plex-serif-v20-latin-700.woff2",
),
weight: 700,
style: "normal",
},
{
name: "JetBrains Mono",
data: await loadFont("fonts/jetbrains-mono/JetBrainsMono-Regular.woff2"),
weight: 400,
style: "normal",
},
{
name: "JetBrains Mono",
data: await loadFont("fonts/jetbrains-mono/JetBrainsMono-Bold.woff2"),
weight: 700,
style: "normal",
},
]; ];
const files = (await markdownFiles(postsDir)).filter((file) => path.basename(file) !== '_index.md'); const files = (await markdownFiles(postsDir)).filter(
(file) => path.basename(file) !== "_index.md",
);
await fs.mkdir(outDir, { recursive: true }); await fs.mkdir(outDir, { recursive: true });
let count = 0; let count = 0;
for (const file of files) { for (const file of files) {
const source = await fs.readFile(file, 'utf8'); const source = await fs.readFile(file, "utf8");
const metadata = frontMatter(source); const metadata = frontMatter(source);
if (!metadata || /^draft\s*=\s*true$/m.test(metadata)) continue; if (!metadata || /^draft\s*=\s*true$/m.test(metadata)) continue;
const title = tomlString(metadata, 'title'); const title = tomlString(metadata, "title");
const date = metadata.match(/^date\s*=\s*(\d{4}-\d{2}-\d{2})/m)?.[1]; const date = metadata.match(/^date\s*=\s*(\d{4}-\d{2}-\d{2})/m)?.[1];
if (!title || !date) continue; if (!title || !date) continue;
const tags = tomlStringArray(taxonomies(metadata), 'tags'); const tags = tomlStringArray(taxonomies(metadata), "tags");
const slug = slugFor(file, metadata); const slug = slugFor(file, metadata);
const svg = await satori(card({ title, date, tags }), { width: 1200, height: 630, fonts }); const svg = await satori(card({ title, date, tags }), {
width: 1200,
height: 630,
fonts,
});
const png = new Resvg(svg).render().asPng(); const png = new Resvg(svg).render().asPng();
await fs.writeFile(path.join(outDir, `${slug}.png`), png); await fs.writeFile(path.join(outDir, `${slug}.png`), png);
count += 1; count += 1;
} }
console.log(`Generated ${count} OG image${count === 1 ? '' : 's'} in ${path.relative(root, outDir)}/`); console.log(
`Generated ${count} OG image${count === 1 ? "" : "s"} in ${path.relative(root, outDir)}/`,
);
+27 -18
View File
@@ -1,16 +1,19 @@
import fs from 'node:fs'; import { execSync } from "node:child_process";
import path from 'node:path'; import fs from "node:fs";
import { execSync } from 'node:child_process'; import path from "node:path";
// Get staged files from git // Get staged files from git
const stagedFiles = execSync('git diff --cached --name-only --diff-filter=ACMR') const stagedFiles = execSync("git diff --cached --name-only --diff-filter=ACMR")
.toString() .toString()
.split('\n') .split("\n")
.filter(Boolean); .filter(Boolean);
// Filter to only new or modified posts // Filter to only new or modified posts
const postFiles = stagedFiles.filter( const postFiles = stagedFiles.filter(
(file) => file.startsWith('content/posts/') && file.endsWith('.md') && !file.endsWith('_index.md'), (file) =>
file.startsWith("content/posts/") &&
file.endsWith(".md") &&
!file.endsWith("_index.md"),
); );
if (postFiles.length === 0) { if (postFiles.length === 0) {
@@ -20,21 +23,21 @@ if (postFiles.length === 0) {
let hasError = false; let hasError = false;
for (const file of postFiles) { for (const file of postFiles) {
const basename = path.basename(file, '.md'); const basename = path.basename(file, ".md");
// 1. Slug validation (kebab-case: lowercase letters, numbers, and hyphens) // 1. Slug validation (kebab-case: lowercase letters, numbers, and hyphens)
if (!/^[a-z0-9\-]+$/.test(basename)) { if (!/^[a-z0-9-]+$/.test(basename)) {
console.error(`[ERROR] in ${file}:`); console.error(`[ERROR] in ${file}:`);
console.error(` Filename must be kebab-case (e.g. 'my-cool-post.md').`); console.error(` Filename must be kebab-case (e.g. 'my-cool-post.md').`);
hasError = true; hasError = true;
} }
// 2. Front-matter validation // 2. Front-matter validation
const content = fs.readFileSync(file, 'utf8'); const content = fs.readFileSync(file, "utf8");
// Zola uses +++ for TOML front-matter // Zola uses +++ for TOML front-matter
const frontMatterMatch = content.match(/^\+\+\+([\s\S]*?)\+\+\+/); const frontMatterMatch = content.match(/^\+\+\+([\s\S]*?)\+\+\+/);
if (!frontMatterMatch) { if (!frontMatterMatch) {
console.error(`[ERROR] in ${file}:`); console.error(`[ERROR] in ${file}:`);
console.error(` Missing TOML front-matter (must be enclosed in +++).`); console.error(` Missing TOML front-matter (must be enclosed in +++).`);
@@ -43,26 +46,32 @@ for (const file of postFiles) {
} }
const frontMatter = frontMatterMatch[1]; const frontMatter = frontMatterMatch[1];
const hasTitle = /^title\s*=\s*".+"/m.test(frontMatter); const hasTitle = /^title\s*=\s*".+"/m.test(frontMatter);
const hasDate = /^date\s*=\s*\d{4}-\d{2}-\d{2}/m.test(frontMatter); const hasDate = /^date\s*=\s*\d{4}-\d{2}-\d{2}/m.test(frontMatter);
if (!hasTitle) { if (!hasTitle) {
console.error(`[ERROR] in ${file}:`); console.error(`[ERROR] in ${file}:`);
console.error(` Missing 'title' field in front-matter (e.g. title = "My Post").`); console.error(
` Missing 'title' field in front-matter (e.g. title = "My Post").`,
);
hasError = true; hasError = true;
} }
if (!hasDate) { if (!hasDate) {
console.error(`[ERROR] in ${file}:`); console.error(`[ERROR] in ${file}:`);
console.error(` Missing 'date' field in front-matter (e.g. date = 2024-01-01).`); console.error(
` Missing 'date' field in front-matter (e.g. date = 2024-01-01).`,
);
hasError = true; hasError = true;
} }
} }
if (hasError) { if (hasError) {
console.error('\n[FATAL] Commit rejected due to validation errors. Please fix the issues above and try again.\n'); console.error(
"\n[FATAL] Commit rejected due to validation errors. Please fix the issues above and try again.\n",
);
process.exit(1); process.exit(1);
} else { } else {
console.log('[SUCCESS] Post validation passed!'); console.log("[SUCCESS] Post validation passed!");
} }