Add SEO and GEO
This commit is contained in:
@@ -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 });
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user