XML Style

This commit is contained in:
2026-05-09 13:16:52 -03:00
parent 290cbd5bcb
commit 908084c394
4 changed files with 34 additions and 17 deletions
+1 -1
View File
@@ -39,7 +39,7 @@ data/
The app serves crawler and answer-engine metadata without adding visible page copy: The app serves crawler and answer-engine metadata without adding visible page copy:
- `/robots.txt` - `/robots.txt`
- `/sitemap.xml` with approved meme image entries - `/sitemap.xml` with the home page and approved meme URLs
- `/feed.json` - `/feed.json`
- `/llms.txt` - `/llms.txt`
- `/site.webmanifest` - `/site.webmanifest`
+13 -2
View File
@@ -19,6 +19,13 @@ const PAGE_SIZE_MAX = 48;
const UPLOAD_MAX_BYTES = 5 * 1024 * 1024; const UPLOAD_MAX_BYTES = 5 * 1024 * 1024;
const REQUEST_MAX_BYTES = 6 * 1024 * 1024; const REQUEST_MAX_BYTES = 6 * 1024 * 1024;
const events = new Set(); const events = new Set();
const DISCOVERY_ROUTES = new Set([
'/robots.txt',
'/llms.txt',
'/site.webmanifest',
'/feed.json',
'/sitemap.xml'
]);
const ADMIN_TOKEN = process.env.ADMIN_TOKEN || crypto.randomBytes(24).toString('hex'); const ADMIN_TOKEN = process.env.ADMIN_TOKEN || crypto.randomBytes(24).toString('hex');
const indexTemplate = await fs.readFile('./public/index.html', 'utf8'); const indexTemplate = await fs.readFile('./public/index.html', 'utf8');
@@ -32,10 +39,9 @@ if (!process.env.ADMIN_TOKEN) {
} }
const server = http.createServer(async (req, res) => { const server = http.createServer(async (req, res) => {
withSecurityHeaders(res);
try { try {
const url = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`); const url = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`);
withSecurityHeaders(res, { contentSecurityPolicy: !isDiscoveryRoute(req, url) });
const baseUrl = publicBaseUrl(req); const baseUrl = publicBaseUrl(req);
if (req.method === 'GET' && url.pathname === '/') { if (req.method === 'GET' && url.pathname === '/') {
@@ -253,6 +259,11 @@ function positiveInt(value, fallback) {
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback; return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
} }
function isDiscoveryRoute(req, url) {
if (req.method !== 'GET') return false;
return DISCOVERY_ROUTES.has(url.pathname);
}
function downloadName(meme) { function downloadName(meme) {
return `meme-protocol-${meme.id.slice(0, 12)}.${meme.ext}`; return `meme-protocol-${meme.id.slice(0, 12)}.${meme.ext}`;
} }
+2
View File
@@ -19,6 +19,8 @@ export function withSecurityHeaders(res, options = {}) {
res.setHeader('X-Content-Type-Options', 'nosniff'); res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-Frame-Options', 'DENY'); res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('Permissions-Policy', 'camera=(), microphone=(), geolocation=()'); res.setHeader('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');
if (options.contentSecurityPolicy === false) return;
const scriptSrc = options.scriptNonce ? `script-src 'self' 'nonce-${options.scriptNonce}'; ` : ''; const scriptSrc = options.scriptNonce ? `script-src 'self' 'nonce-${options.scriptNonce}'; ` : '';
res.setHeader( res.setHeader(
'Content-Security-Policy', 'Content-Security-Policy',
+18 -14
View File
@@ -87,7 +87,7 @@ export function llmsTxt(baseUrl) {
'Crawler guidance:', 'Crawler guidance:',
'- Public approved meme images are available under /media/<sha256>.', '- Public approved meme images are available under /media/<sha256>.',
'- Admin, review, upload, and API mutation routes are not public knowledge sources.', '- Admin, review, upload, and API mutation routes are not public knowledge sources.',
'- The site is intentionally visual and sparse; use metadata, sitemap image entries, and JSON feed for machine summaries.', '- The site is intentionally visual and sparse; use metadata, sitemap URLs, and JSON feed for machine summaries.',
'' ''
].join('\n'); ].join('\n');
} }
@@ -132,22 +132,26 @@ export function feedJson(baseUrl, memes) {
} }
export function sitemapXml(baseUrl, memes) { export function sitemapXml(baseUrl, memes) {
const images = memes.slice(0, 1000).map((meme) => [ const latest = memes[0]?.createdAt || new Date().toISOString();
' <image:image>', const urls = [
` <image:loc>${xmlEscape(`${baseUrl}/media/${meme.id}`)}</image:loc>`, [
` <image:caption>${xmlEscape(`Meme ${shortId(meme.id)} with MEME_CONSENSUS_SCORE ${meme.moderationScore}/100`)}</image:caption>`, ' <url>',
' </image:image>' ` <loc>${xmlEscape(`${baseUrl}/`)}</loc>`,
].join('\n')).join('\n'); ` <lastmod>${xmlEscape(latest)}</lastmod>`,
' </url>'
].join('\n'),
...memes.slice(0, 1000).map((meme) => [
' <url>',
` <loc>${xmlEscape(`${baseUrl}/media/${meme.id}`)}</loc>`,
` <lastmod>${xmlEscape(meme.createdAt)}</lastmod>`,
' </url>'
].join('\n'))
].join('\n');
return [ return [
'<?xml version="1.0" encoding="UTF-8"?>', '<?xml version="1.0" encoding="UTF-8"?>',
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1">', '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',
' <url>', urls,
` <loc>${xmlEscape(`${baseUrl}/`)}</loc>`,
' <changefreq>hourly</changefreq>',
' <priority>1.0</priority>',
images,
' </url>',
'</urlset>', '</urlset>',
'' ''
].join('\n'); ].join('\n');