187 lines
5.4 KiB
JavaScript
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('&', '&')
|
|
.replaceAll('<', '<')
|
|
.replaceAll('>', '>')
|
|
.replaceAll('"', '"');
|
|
}
|
|
|
|
function xmlEscape(value) {
|
|
return escapeHtml(value).replaceAll("'", ''');
|
|
}
|
|
|
|
function escapeJsonScript(value) {
|
|
return value.replaceAll('<', '\\u003c').replaceAll('>', '\\u003e').replaceAll('&', '\\u0026');
|
|
}
|