Files
bitsforfree/src/seo.js
T
2026-05-09 13:16:52 -03:00

187 lines
5.4 KiB
JavaScript

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 URLs, 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 latest = memes[0]?.createdAt || new Date().toISOString();
const urls = [
[
' <url>',
` <loc>${xmlEscape(`${baseUrl}/`)}</loc>`,
` <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 [
'<?xml version="1.0" encoding="UTF-8"?>',
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',
urls,
'</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('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;');
}
function xmlEscape(value) {
return escapeHtml(value).replaceAll("'", '&apos;');
}
function escapeJsonScript(value) {
return value.replaceAll('<', '\\u003c').replaceAll('>', '\\u003e').replaceAll('&', '\\u0026');
}