mirror of
https://github.com/andwati/andwati.com.git
synced 2026-10-18 09:22:53 +00:00
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:
@@ -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,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
@@ -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),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
@@ -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,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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,4 +1,4 @@
|
|||||||
import type { StrapiApp } from '@strapi/strapi/admin';
|
import type { StrapiApp } from "@strapi/strapi/admin";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
config: {
|
config: {
|
||||||
|
|||||||
@@ -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,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({});
|
||||||
|
|||||||
@@ -17,4 +17,4 @@
|
|||||||
"allowScripts": {
|
"allowScripts": {
|
||||||
"esbuild": true
|
"esbuild": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-1
@@ -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
@@ -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
@@ -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
@@ -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!");
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user