Add SEO and GEO
This commit is contained in:
+12
-2
@@ -13,15 +13,16 @@ const TYPES = new Map([
|
||||
['.ico', 'image/x-icon']
|
||||
]);
|
||||
|
||||
export function withSecurityHeaders(res) {
|
||||
export function withSecurityHeaders(res, options = {}) {
|
||||
res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
|
||||
res.setHeader('Referrer-Policy', 'same-origin');
|
||||
res.setHeader('X-Content-Type-Options', 'nosniff');
|
||||
res.setHeader('X-Frame-Options', 'DENY');
|
||||
res.setHeader('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');
|
||||
const scriptSrc = options.scriptNonce ? `script-src 'self' 'nonce-${options.scriptNonce}'; ` : '';
|
||||
res.setHeader(
|
||||
'Content-Security-Policy',
|
||||
"default-src 'self'; style-src 'self' https://fonts.googleapis.com; font-src https://fonts.gstatic.com; img-src 'self' blob:; connect-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'; form-action 'self'"
|
||||
`default-src 'self'; ${scriptSrc}style-src 'self' https://fonts.googleapis.com; font-src https://fonts.gstatic.com; img-src 'self' blob:; connect-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'; form-action 'self'`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -43,6 +44,15 @@ export function sendText(res, statusCode, message) {
|
||||
res.end(body);
|
||||
}
|
||||
|
||||
export function sendHtml(res, statusCode, html) {
|
||||
const body = Buffer.from(html);
|
||||
res.writeHead(statusCode, {
|
||||
'Content-Type': 'text/html; charset=utf-8',
|
||||
'Content-Length': String(body.length)
|
||||
});
|
||||
res.end(body);
|
||||
}
|
||||
|
||||
export function sendFile(res, filePath, options = {}) {
|
||||
const resolved = options.absolute ? path.resolve(filePath) : path.resolve(filePath);
|
||||
if (!options.absolute && !resolved.startsWith(PUBLIC_ROOT + path.sep) && resolved !== path.join(PUBLIC_ROOT, 'index.html')) {
|
||||
|
||||
+182
@@ -0,0 +1,182 @@
|
||||
const SITE_NAME = 'The Meme Protocol';
|
||||
const SITE_DESCRIPTION = 'A live, moderated meme stream for The Meme Protocol: square WebP memes, MEME_CONSENSUS_SCORE ranking, and community review.';
|
||||
const REPO_URL = 'https://git.yoonect.com/Nautilus/bitsforfree';
|
||||
|
||||
export function publicBaseUrl(req) {
|
||||
if (process.env.SITE_URL) return cleanBase(process.env.SITE_URL);
|
||||
const host = process.env.TRUST_PROXY === 'true'
|
||||
? req.headers['x-forwarded-host'] || req.headers.host
|
||||
: req.headers.host;
|
||||
const proto = process.env.TRUST_PROXY === 'true'
|
||||
? req.headers['x-forwarded-proto'] || 'https'
|
||||
: 'http';
|
||||
return cleanBase(`${proto}://${host || 'localhost:8080'}`);
|
||||
}
|
||||
|
||||
export function renderIndex({ template, baseUrl, nonce, approvedCount }) {
|
||||
const canonical = `${baseUrl}/`;
|
||||
const image = `${baseUrl}/assets/yoonect-logo.png`;
|
||||
const jsonLd = {
|
||||
'@context': 'https://schema.org',
|
||||
'@graph': [
|
||||
{
|
||||
'@type': 'WebSite',
|
||||
'@id': `${canonical}#website`,
|
||||
name: SITE_NAME,
|
||||
url: canonical,
|
||||
description: SITE_DESCRIPTION,
|
||||
inLanguage: 'en'
|
||||
},
|
||||
{
|
||||
'@type': 'CollectionPage',
|
||||
'@id': `${canonical}#collection`,
|
||||
name: SITE_NAME,
|
||||
url: canonical,
|
||||
description: SITE_DESCRIPTION,
|
||||
isPartOf: { '@id': `${canonical}#website` },
|
||||
about: ['memes', 'internet culture', 'image gallery', 'community moderation'],
|
||||
numberOfItems: approvedCount
|
||||
},
|
||||
{
|
||||
'@type': 'SoftwareApplication',
|
||||
name: SITE_NAME,
|
||||
applicationCategory: 'MultimediaApplication',
|
||||
operatingSystem: 'Web',
|
||||
url: canonical,
|
||||
codeRepository: REPO_URL,
|
||||
offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' }
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
return template
|
||||
.replaceAll('__SEO_TITLE__', escapeHtml(SITE_NAME))
|
||||
.replaceAll('__SEO_DESCRIPTION__', escapeHtml(SITE_DESCRIPTION))
|
||||
.replaceAll('__SEO_CANONICAL__', canonical)
|
||||
.replaceAll('__SEO_IMAGE__', image)
|
||||
.replaceAll('__SEO_JSON_LD__', escapeJsonScript(JSON.stringify(jsonLd)))
|
||||
.replaceAll('__CSP_NONCE__', nonce);
|
||||
}
|
||||
|
||||
export function robotsTxt(baseUrl) {
|
||||
return [
|
||||
'User-agent: *',
|
||||
'Allow: /',
|
||||
'Disallow: /admin/',
|
||||
'Disallow: /admin-media/',
|
||||
'Disallow: /api/',
|
||||
'Disallow: /download/',
|
||||
'',
|
||||
`Sitemap: ${baseUrl}/sitemap.xml`,
|
||||
''
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
export function llmsTxt(baseUrl) {
|
||||
return [
|
||||
'# The Meme Protocol',
|
||||
'',
|
||||
'> A live, moderated meme gallery. Uploads are square PNG/JPEG inputs normalized to metadata-stripped WebP, scored with MEME_CONSENSUS_SCORE, and published only after AI or admin approval.',
|
||||
'',
|
||||
'Important URLs:',
|
||||
`- Site: ${baseUrl}/`,
|
||||
`- JSON feed: ${baseUrl}/feed.json`,
|
||||
`- Sitemap: ${baseUrl}/sitemap.xml`,
|
||||
`- Source: ${REPO_URL}`,
|
||||
'',
|
||||
'Crawler guidance:',
|
||||
'- Public approved meme images are available under /media/<sha256>.',
|
||||
'- 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.',
|
||||
''
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
export function manifest(baseUrl) {
|
||||
return {
|
||||
name: SITE_NAME,
|
||||
short_name: 'Meme Protocol',
|
||||
description: SITE_DESCRIPTION,
|
||||
start_url: '/',
|
||||
scope: '/',
|
||||
display: 'standalone',
|
||||
background_color: '#050505',
|
||||
theme_color: '#00ff41',
|
||||
icons: [
|
||||
{
|
||||
src: `${baseUrl}/assets/yoonect-logo.png`,
|
||||
sizes: '1447x712',
|
||||
type: 'image/png',
|
||||
purpose: 'any'
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
export function feedJson(baseUrl, memes) {
|
||||
return {
|
||||
version: 'https://jsonfeed.org/version/1.1',
|
||||
title: SITE_NAME,
|
||||
home_page_url: `${baseUrl}/`,
|
||||
feed_url: `${baseUrl}/feed.json`,
|
||||
description: SITE_DESCRIPTION,
|
||||
items: memes.slice(0, 50).map((meme) => ({
|
||||
id: meme.id,
|
||||
url: `${baseUrl}/media/${meme.id}`,
|
||||
image: `${baseUrl}/media/${meme.id}`,
|
||||
title: `Meme ${shortId(meme.id)}`,
|
||||
content_text: `Approved meme with MEME_CONSENSUS_SCORE ${meme.moderationScore}/100.`,
|
||||
date_published: meme.createdAt
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
export function sitemapXml(baseUrl, memes) {
|
||||
const images = memes.slice(0, 1000).map((meme) => [
|
||||
' <image:image>',
|
||||
` <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>`,
|
||||
' </image:image>'
|
||||
].join('\n')).join('\n');
|
||||
|
||||
return [
|
||||
'<?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">',
|
||||
' <url>',
|
||||
` <loc>${xmlEscape(`${baseUrl}/`)}</loc>`,
|
||||
' <changefreq>hourly</changefreq>',
|
||||
' <priority>1.0</priority>',
|
||||
images,
|
||||
' </url>',
|
||||
'</urlset>',
|
||||
''
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
export function noIndex(res) {
|
||||
res.setHeader('X-Robots-Tag', 'noindex, nofollow, noarchive');
|
||||
}
|
||||
|
||||
function cleanBase(value) {
|
||||
return String(value).replace(/\/+$/, '');
|
||||
}
|
||||
|
||||
function shortId(id) {
|
||||
return `0x${id.slice(0, 4).toUpperCase()}...${id.slice(-4).toUpperCase()}`;
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value)
|
||||
.replaceAll('&', '&')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
.replaceAll('"', '"');
|
||||
}
|
||||
|
||||
function xmlEscape(value) {
|
||||
return escapeHtml(value).replaceAll("'", ''');
|
||||
}
|
||||
|
||||
function escapeJsonScript(value) {
|
||||
return value.replaceAll('<', '\\u003c').replaceAll('>', '\\u003e').replaceAll('&', '\\u0026');
|
||||
}
|
||||
Reference in New Issue
Block a user