${escape(a.category)}
${escape(a.title)}
${escape(a.description)}
import fs from 'node:fs/promises'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import matter from 'gray-matter'; import { marked } from 'marked'; import sanitizeHtml from 'sanitize-html'; export const root = fileURLToPath(new URL('../', import.meta.url)); const escape = value => String(value ?? '').replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); export function articleUrl(value) { const url = '/' + String(value).replace(/^\/+|\/+$/g, ''); if (!/^\/(?:[a-zA-Z0-9_-]+\/)*[a-zA-Z0-9_-]+$/.test(url) || ['/assets', '/404', '/api'].some(p => url === p || url.startsWith(p + '/'))) throw new Error(`Invalid or reserved article URL: ${value}`); return url + '/'; } export async function readArticles(directory = path.join(root, 'pages')) { const names = (await fs.readdir(directory)).filter(f => f.endsWith('.md')); const articles = await Promise.all(names.map(async name => { const {data, content} = matter(await fs.readFile(path.join(directory, name), 'utf8')); const title = data.title || content.match(/^#\s+(.+)$/m)?.[1] || name.replace(/\.md$/, '').replace(/-/g, ' '); const body = content.replace(/^#\s+.+\r?\n/, ''); const date = data.date ? new Date(data.date) : null; if (date && Number.isNaN(date.getTime())) throw new Error(`Invalid date in ${name}`); const plain = body.replace(/[#*_>`\[\]]/g, '').replace(/\s+/g, ' ').trim(); return {title: String(title), url: articleUrl(data.url || data.slug || name.replace(/\.md$/, '')), category: String(data.category || 'Stories'), description: String(data.description || plain.slice(0, 155)), author: String(data.author || 'TrueNews editorial'), date: date?.toISOString().slice(0,10) || '', image: typeof data.image === 'string' && /^(https?:\/\/|\/assets\/)/.test(data.image) ? data.image : '', minutes: Math.max(1, Math.ceil(plain.split(/\s+/).length / 220)), html: sanitizeHtml(marked.parse(body), {allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img', 'h1', 'h2']), allowedAttributes: {...sanitizeHtml.defaults.allowedAttributes, img: ['src', 'alt', 'title', 'width', 'height']}})}; })); const urls = new Set(); for (const a of articles) { if (urls.has(a.url)) throw new Error(`Duplicate article URL: ${a.url}`); urls.add(a.url); } return articles.sort((a,b) => b.date.localeCompare(a.date) || a.title.localeCompare(b.title)); } const dateLabel = date => date ? new Date(date).toLocaleDateString('en-US', {month:'short', day:'numeric', year:'numeric', timeZone:'UTC'}) : ''; const meta = a => `${escape(dateLabel(a.date))}${a.date ? ' · ' : ''}${a.minutes} min read`; const visual = (a, cls = '') => `
Get a browser notification when a new story is published. You can turn this off anytime.
${escape(a.category)}
${escape(a.description)}
THE TRUENEWS JOURNAL
Ideas, discoveries, and the people shaping what comes next.
A thoughtful take on a changing world.
Your next story starts here. Add a Markdown file to pages/.
'}KEEP EXPLORING
No stories found. Try another search or category.
${escape(a.category)}
${escape(a.description)}
The page you’re looking for doesn’t exist.
Return to the front page →