XML Style
This commit is contained in:
@@ -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`
|
||||||
|
|||||||
@@ -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}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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');
|
||||||
|
|||||||
Reference in New Issue
Block a user