Adds the Vite-middleware backend that the refactored FindCard tag picker POSTs to (writes editorialTags into find frontmatter). Without it the in-card tag toggles 404 in dev. Also registers timelined's hero in the heroes map so it shows up on the home grid.
117 lines
3.9 KiB
JavaScript
117 lines
3.9 KiB
JavaScript
import { defineConfig } from 'astro/config';
|
|
import { readFile, writeFile, stat } from 'node:fs/promises';
|
|
import path from 'node:path';
|
|
import mdx from '@astrojs/mdx';
|
|
import sitemap from '@astrojs/sitemap';
|
|
|
|
const EXCLUDED_PATHS = new Set([
|
|
'https://unique.rzen.dev/grid/',
|
|
'https://unique.rzen.dev/index3/',
|
|
'https://unique.rzen.dev/404/',
|
|
]);
|
|
|
|
const FIND_ROOT = path.resolve('src/content/find');
|
|
const VALID_SLUG = /^[a-z0-9][a-z0-9-]*$/;
|
|
const VALID_TAG = /^[a-z0-9][a-z0-9-]{0,40}$/i;
|
|
|
|
function readBody(req) {
|
|
return new Promise((resolve, reject) => {
|
|
let data = '';
|
|
req.setEncoding('utf8');
|
|
req.on('data', (c) => { data += c; if (data.length > 8192) reject(new Error('body too large')); });
|
|
req.on('end', () => resolve(data));
|
|
req.on('error', reject);
|
|
});
|
|
}
|
|
|
|
function send(res, status, payload) {
|
|
res.statusCode = status;
|
|
res.setHeader('content-type', 'application/json');
|
|
res.end(JSON.stringify(payload));
|
|
}
|
|
|
|
function parseEditorialTags(fm) {
|
|
const inline = fm.match(/^editorialTags:\s*\[([^\]]*)\]\s*$/m);
|
|
if (inline) {
|
|
return inline[1]
|
|
.split(',')
|
|
.map((s) => s.trim().replace(/^["']|["']$/g, ''))
|
|
.filter(Boolean);
|
|
}
|
|
const list = fm.match(/^editorialTags:\s*\n((?:[ \t]+-[^\n]*\n?)+)/m);
|
|
if (list) {
|
|
return list[1]
|
|
.split('\n')
|
|
.map((line) => line.replace(/^[ \t]+-[ \t]*/, '').trim().replace(/^["']|["']$/g, ''))
|
|
.filter(Boolean);
|
|
}
|
|
if (/^editorialTags:\s*\[\s*\]\s*$/m.test(fm) || /^editorialTags:\s*$/m.test(fm)) return [];
|
|
return null; // field absent
|
|
}
|
|
|
|
function stripEditorialTags(fm) {
|
|
return fm.replace(/^editorialTags:[^\n]*(?:\n[ \t]+-[^\n]*)*\n?/m, '');
|
|
}
|
|
|
|
function formatEditorialTags(tags) {
|
|
return `editorialTags: [${tags.map((t) => JSON.stringify(t)).join(', ')}]`;
|
|
}
|
|
|
|
const editFindTagApi = {
|
|
name: 'edit-find-tag-dev-api',
|
|
apply: 'serve',
|
|
configureServer(server) {
|
|
server.middlewares.use('/api/edit-find-tag', async (req, res, next) => {
|
|
if (req.method !== 'POST') return next();
|
|
try {
|
|
const raw = await readBody(req);
|
|
const { slug, tag, mode } = JSON.parse(raw || '{}');
|
|
if (typeof slug !== 'string' || !VALID_SLUG.test(slug)) return send(res, 400, { error: 'invalid slug' });
|
|
if (typeof tag !== 'string' || !VALID_TAG.test(tag)) return send(res, 400, { error: 'invalid tag' });
|
|
|
|
const filePath = path.join(FIND_ROOT, slug, 'index.mdx');
|
|
if (!filePath.startsWith(FIND_ROOT + path.sep)) return send(res, 400, { error: 'path escape' });
|
|
try { await stat(filePath); } catch { return send(res, 404, { error: 'find not found' }); }
|
|
|
|
const text = await readFile(filePath, 'utf8');
|
|
const m = text.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
|
|
if (!m) return send(res, 422, { error: 'no frontmatter' });
|
|
const fm = m[1];
|
|
const body = m[2];
|
|
|
|
const current = parseEditorialTags(fm) ?? [];
|
|
const has = current.includes(tag);
|
|
let next;
|
|
if (mode === 'add') next = has ? current : [...current, tag];
|
|
else if (mode === 'remove') next = current.filter((t) => t !== tag);
|
|
else next = has ? current.filter((t) => t !== tag) : [...current, tag]; // toggle
|
|
|
|
const stripped = stripEditorialTags(fm).replace(/\n+$/, '');
|
|
const newFm = next.length
|
|
? `${stripped}\n${formatEditorialTags(next)}`
|
|
: stripped;
|
|
const out = `---\n${newFm}\n---\n${body}`;
|
|
await writeFile(filePath, out, 'utf8');
|
|
|
|
return send(res, 200, { slug, tag, editorialTags: next });
|
|
} catch (err) {
|
|
return send(res, 500, { error: String(err && err.message ? err.message : err) });
|
|
}
|
|
});
|
|
},
|
|
};
|
|
|
|
export default defineConfig({
|
|
site: 'https://unique.rzen.dev',
|
|
trailingSlash: 'always',
|
|
build: { format: 'directory' },
|
|
vite: { plugins: [editFindTagApi] },
|
|
integrations: [
|
|
mdx(),
|
|
sitemap({
|
|
filter: (page) => !EXCLUDED_PATHS.has(page),
|
|
changefreq: 'weekly',
|
|
}),
|
|
],
|
|
});
|