site: dev /api/edit-find-tag/ middleware + register timelined hero
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.
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
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';
|
||||
|
||||
@@ -8,10 +10,102 @@ const EXCLUDED_PATHS = new Set([
|
||||
'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({
|
||||
|
||||
@@ -9,6 +9,7 @@ import hyperkey from '../content/reviews/hyperkey/hero.png';
|
||||
import ice from '../content/reviews/ice/ice-bar.png';
|
||||
import macMouseFix from '../content/reviews/mac-mouse-fix/studio-display.png';
|
||||
import superkey from '../content/reviews/superkey/hero.jpg';
|
||||
import timelined from '../content/reviews/timelined/hero.png';
|
||||
import tot from '../content/reviews/tot/seven-dots.png';
|
||||
|
||||
import antinote from '../content/reviews/antinote/swipe.mp4';
|
||||
@@ -34,6 +35,7 @@ export const heroes: Record<string, Hero | undefined> = {
|
||||
ice: { kind: 'image', src: ice },
|
||||
'mac-mouse-fix': { kind: 'image', src: macMouseFix },
|
||||
superkey: { kind: 'image', src: superkey },
|
||||
timelined: { kind: 'image', src: timelined },
|
||||
tot: { kind: 'image', src: tot },
|
||||
antinote: { kind: 'video', src: antinote },
|
||||
cotypist: { kind: 'video', src: cotypist },
|
||||
|
||||
Reference in New Issue
Block a user