Add SEO and GEO

This commit is contained in:
2026-05-09 12:52:43 -03:00
parent 321d3ccf62
commit 290cbd5bcb
5 changed files with 275 additions and 5 deletions
+49 -2
View File
@@ -1,14 +1,16 @@
import http from 'node:http';
import crypto from 'node:crypto';
import fs from 'node:fs/promises';
import { URL } from 'node:url';
import { parseMultipartUpload } from './src/multipart.js';
import { sendFile, sendJson, sendText, withSecurityHeaders } from './src/http.js';
import { sendFile, sendHtml, sendJson, sendText, withSecurityHeaders } from './src/http.js';
import { createStore } from './src/store.js';
import { validateImage } from './src/image.js';
import { normalizeToWebp } from './src/normalize.js';
import { seedDemoMemes } from './src/seed.js';
import { moderateImage } from './src/moderation.js';
import { createUploadLimiter } from './src/uploadLimits.js';
import { feedJson, llmsTxt, manifest, noIndex, publicBaseUrl, renderIndex, robotsTxt, sitemapXml } from './src/seo.js';
const PORT = Number.parseInt(process.env.PORT || '8080', 10);
const HOST = process.env.HOST || '0.0.0.0';
@@ -18,6 +20,7 @@ const UPLOAD_MAX_BYTES = 5 * 1024 * 1024;
const REQUEST_MAX_BYTES = 6 * 1024 * 1024;
const events = new Set();
const ADMIN_TOKEN = process.env.ADMIN_TOKEN || crypto.randomBytes(24).toString('hex');
const indexTemplate = await fs.readFile('./public/index.html', 'utf8');
const store = await createStore({ dataDir: DATA_DIR });
const uploadLimiter = await createUploadLimiter({ dataDir: DATA_DIR });
@@ -33,14 +36,48 @@ const server = http.createServer(async (req, res) => {
try {
const url = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`);
const baseUrl = publicBaseUrl(req);
if (req.method === 'GET' && url.pathname === '/') {
return sendFile(res, './public/index.html');
const nonce = crypto.randomBytes(16).toString('base64');
withSecurityHeaders(res, { scriptNonce: nonce });
return sendHtml(res, 200, renderIndex({
template: indexTemplate,
baseUrl,
nonce,
approvedCount: store.count('approved')
}));
}
if (req.method === 'GET' && url.pathname === '/robots.txt') {
return sendText(res, 200, robotsTxt(baseUrl));
}
if (req.method === 'GET' && url.pathname === '/llms.txt') {
return sendText(res, 200, llmsTxt(baseUrl));
}
if (req.method === 'GET' && url.pathname === '/site.webmanifest') {
return sendJson(res, 200, manifest(baseUrl));
}
if (req.method === 'GET' && url.pathname === '/feed.json') {
return sendJson(res, 200, feedJson(baseUrl, store.listForReview({ status: 'approved' }).memes));
}
if (req.method === 'GET' && url.pathname === '/sitemap.xml') {
const body = sitemapXml(baseUrl, store.listForReview({ status: 'approved' }).memes);
res.writeHead(200, {
'Content-Type': 'application/xml; charset=utf-8',
'Content-Length': String(Buffer.byteLength(body))
});
return res.end(body);
}
const adminPageMatch = url.pathname.match(/^\/admin\/([A-Za-z0-9_-]{24,128})$/);
if (req.method === 'GET' && adminPageMatch) {
if (!isAdminToken(adminPageMatch[1])) return sendText(res, 404, 'Not found');
noIndex(res);
return sendFile(res, './public/admin.html');
}
@@ -49,12 +86,14 @@ const server = http.createServer(async (req, res) => {
}
if (req.method === 'GET' && url.pathname === '/api/memes') {
noIndex(res);
const page = positiveInt(url.searchParams.get('page'), 1);
const pageSize = Math.min(positiveInt(url.searchParams.get('pageSize'), 12), PAGE_SIZE_MAX);
return sendJson(res, 200, store.list({ page, pageSize }));
}
if (req.method === 'GET' && url.pathname === '/api/status') {
noIndex(res);
return sendJson(res, 200, {
ok: true,
memeCount: store.count('approved'),
@@ -64,11 +103,13 @@ const server = http.createServer(async (req, res) => {
}
if (req.method === 'GET' && url.pathname === '/api/admin/pending') {
noIndex(res);
if (!isAdminRequest(req)) return sendJson(res, 404, { error: 'Not found' });
return sendJson(res, 200, store.listForReview({ status: 'pending' }));
}
if (req.method === 'POST' && url.pathname === '/api/admin/approve') {
noIndex(res);
if (!isAdminRequest(req)) return sendJson(res, 404, { error: 'Not found' });
const body = await readJsonBody(req, 64 * 1024);
const approved = await store.approve(safeIds(body.ids));
@@ -76,6 +117,7 @@ const server = http.createServer(async (req, res) => {
}
if (req.method === 'POST' && url.pathname === '/api/admin/delete') {
noIndex(res);
if (!isAdminRequest(req)) return sendJson(res, 404, { error: 'Not found' });
const body = await readJsonBody(req, 64 * 1024);
const deleted = await store.delete(safeIds(body.ids));
@@ -83,6 +125,7 @@ const server = http.createServer(async (req, res) => {
}
if (req.method === 'GET' && url.pathname === '/api/events') {
noIndex(res);
res.writeHead(200, {
'Content-Type': 'text/event-stream; charset=utf-8',
'Cache-Control': 'no-cache, no-transform',
@@ -95,6 +138,7 @@ const server = http.createServer(async (req, res) => {
}
if (req.method === 'POST' && url.pathname === '/api/memes') {
noIndex(res);
const contentLength = Number.parseInt(req.headers['content-length'] || '0', 10);
if (!Number.isFinite(contentLength) || contentLength <= 0) {
return sendJson(res, 411, { error: 'Missing upload size.' });
@@ -162,6 +206,7 @@ const server = http.createServer(async (req, res) => {
const adminMediaMatch = url.pathname.match(/^\/admin-media\/([A-Za-z0-9_-]{24,128})\/([a-f0-9]{64})$/);
if (req.method === 'GET' && adminMediaMatch) {
noIndex(res);
if (!isAdminToken(adminMediaMatch[1])) return sendText(res, 404, 'Not found');
const meme = store.get(adminMediaMatch[2]);
if (!meme) return sendText(res, 404, 'Not found');
@@ -174,6 +219,7 @@ const server = http.createServer(async (req, res) => {
const downloadMatch = url.pathname.match(/^\/download\/([a-f0-9]{64})$/);
if (req.method === 'GET' && downloadMatch) {
noIndex(res);
if (store.get(downloadMatch[1])?.status !== 'approved') return sendText(res, 404, 'Not found');
const updated = await store.incrementMetric(downloadMatch[1], 'downloadCount');
const meme = store.get(downloadMatch[1]);
@@ -186,6 +232,7 @@ const server = http.createServer(async (req, res) => {
}
if (req.method === 'GET' && url.pathname === '/healthz') {
noIndex(res);
return sendJson(res, 200, { ok: true });
}