Blog: admin-written coaching articles, server-rendered public /blog with SEO metadata, sitemap, RSS, robots
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -28,7 +28,8 @@ const tank = require('./tank'); // holding tank: unsponsored free members, a
|
||||
const legacy = require('./legacy'); // Faucet Wave / Tier One Ads bridge: welcome-back credits for listed emails
|
||||
const traffic = require('./traffic'); // public page views by referring domain (admin Traffic tab)
|
||||
const promos = require('./promos'); // partner promo codes -> free ad credits (link ?promo=CODE or the dashboard box)
|
||||
const TRAFFIC_PAGES = new Set(['/', '/ledger', '/contract', '/shorts', '/plays', '/wallets', '/launch', '/partners', '/earning']);
|
||||
const blog = require('./blog'); // admin-written coaching articles, server-rendered public /blog with SEO metadata (Marty, 2026-09-12)
|
||||
const TRAFFIC_PAGES = new Set(['/', '/ledger', '/contract', '/shorts', '/plays', '/wallets', '/launch', '/partners', '/earning', '/blog']);
|
||||
let tankWaitCache = null; // dashboard: who is waiting for a sponsor (refreshed every minute)
|
||||
const geo = require('./geo'); // viewer country -> tier (DB-IP lite), for campaign targeting
|
||||
const burner = require('./burner'); // automatic on-chain credit burns (inert without ENGINE_KEY)
|
||||
@@ -325,6 +326,7 @@ async function boot() {
|
||||
legacy.init({ dataDir: DATA_DIR });
|
||||
traffic.init({ dataDir: DATA_DIR });
|
||||
promos.init({ dataDir: DATA_DIR });
|
||||
blog.init({ dataDir: DATA_DIR });
|
||||
setInterval(() => tankNotifyTick().catch(e => console.error('tank notify', e.message)), 15 * 60 * 1000); // new tank arrivals -> Telegram
|
||||
geo.init({ dataDir: DATA_DIR }).catch(e => console.error('geo init', e.message));
|
||||
setInterval(() => geo.refresh().catch(e => console.error('geo refresh', e.message)), 24 * 3600 * 1000); // monthly file, checked daily
|
||||
@@ -619,7 +621,7 @@ const server = http.createServer(async (req, res) => {
|
||||
const p = u.pathname;
|
||||
|
||||
// -- traffic log: public page views by referring domain (admin > Traffic)
|
||||
if (req.method === 'GET' && (TRAFFIC_PAGES.has(p) || /^\/(join|from|wall)\/[^/]+$/.test(p))) traffic.hit(p, req.headers.referer, req.headers['user-agent']);
|
||||
if (req.method === 'GET' && (TRAFFIC_PAGES.has(p) || /^\/(join|from|wall|blog)\/[^/]+$/.test(p))) traffic.hit(p.startsWith('/blog/') ? '/blog/*' : p, req.headers.referer, req.headers['user-agent']);
|
||||
// -- join links: /join/<memberId or share code> — LAST-touch cookie (Marty,
|
||||
// 2026-09-10): the link a visitor opened most recently is the sponsor shown
|
||||
// and used, and it locks the moment the account is created (accounts.ensure
|
||||
@@ -1091,6 +1093,24 @@ const server = http.createServer(async (req, res) => {
|
||||
await ads.addEarned(s.email, g.credits);
|
||||
return json(res, 200, { ok: true, credits: g.credits, code: g.code, partner: g.partner });
|
||||
}
|
||||
// -- admin: blog (list all incl. drafts, save/create, delete) (Marty, 2026-09-12)
|
||||
if (p === '/api/admin/blog' && req.method === 'GET') {
|
||||
if (!isAdmin(req)) return json(res, 401, { error: 'auth' });
|
||||
const slug = u.searchParams.get('slug');
|
||||
if (slug) { const post = await blog.get(slug); return post ? json(res, 200, { post }) : json(res, 404, { error: 'No such post.' }); }
|
||||
return json(res, 200, { posts: (await blog.listAll()).map(x => ({ slug: x.slug, title: x.title, status: x.status, tags: x.tags, publishedAt: x.publishedAt, updated: x.updated, views: x.views, excerpt: x.excerpt, cover: x.cover })) });
|
||||
}
|
||||
if (p === '/api/admin/blog' && req.method === 'POST') {
|
||||
if (!isAdmin(req)) return json(res, 401, { error: 'auth' });
|
||||
const b = await readBody(req);
|
||||
const r = await blog.save(b, b.existingSlug || null);
|
||||
return json(res, r.error ? 400 : 200, r);
|
||||
}
|
||||
if (p === '/api/admin/blog' && req.method === 'DELETE') {
|
||||
if (!isAdmin(req)) return json(res, 401, { error: 'auth' });
|
||||
const slug = u.searchParams.get('slug'); if (!slug) return json(res, 400, { error: 'slug' });
|
||||
return json(res, 200, await blog.remove(slug));
|
||||
}
|
||||
// -- admin: promo codes (create/update, switch on/off, redemptions)
|
||||
if (p === '/api/admin/promos' && req.method === 'GET') {
|
||||
if (!isAdmin(req)) return json(res, 401, { error: 'auth' });
|
||||
@@ -2145,6 +2165,27 @@ const server = http.createServer(async (req, res) => {
|
||||
if (p === '/plays') return sendFile(res, path.join(PUBLIC_DIR, 'plays.html'));
|
||||
if (p === '/partners') return sendFile(res, path.join(PUBLIC_DIR, 'partners.html')); // site-owner kit (Marty, 2026-09-12)
|
||||
if (p === '/earning') return sendFile(res, path.join(PUBLIC_DIR, 'earning.html')); // member guide: how earning works (2026-09-12)
|
||||
// -- blog: server-rendered so crawlers get real HTML + metadata (Marty, 2026-09-12)
|
||||
if (p === '/blog' || p === '/blog/' || /^\/blog\/page\/\d+$/.test(p) || /^\/blog\/tag\/[^/]+$/.test(p)) {
|
||||
const posts = await blog.listPublished();
|
||||
const pg = (m = /^\/blog\/page\/(\d+)$/.exec(p)) ? Number(m[1]) : 1;
|
||||
const tag = (m = /^\/blog\/tag\/([^/]+)$/.exec(p)) ? decodeURIComponent(m[1]).toLowerCase() : null;
|
||||
res.writeHead(200, baseHeaders({ 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'public, max-age=300' }));
|
||||
return res.end(blog.renderIndex(posts, pg, tag));
|
||||
}
|
||||
if (p === '/blog/feed.xml') { res.writeHead(200, baseHeaders({ 'Content-Type': 'application/rss+xml; charset=utf-8', 'Cache-Control': 'public, max-age=900' })); return res.end(blog.rss(await blog.listPublished())); }
|
||||
if (p === '/sitemap.xml') { res.writeHead(200, baseHeaders({ 'Content-Type': 'application/xml; charset=utf-8', 'Cache-Control': 'public, max-age=3600' })); return res.end(blog.sitemap(await blog.listPublished())); }
|
||||
if (p === '/robots.txt') { res.writeHead(200, baseHeaders({ 'Content-Type': 'text/plain; charset=utf-8', 'Cache-Control': 'public, max-age=3600' })); return res.end(blog.robots()); }
|
||||
m = /^\/blog\/([a-z0-9-]{1,80})$/.exec(p);
|
||||
if (m) {
|
||||
const post = await blog.get(m[1]);
|
||||
const preview = !!(post && post.status !== 'published' && isAdmin(req)); // admins can open a draft at its real URL
|
||||
if (!post || (post.status !== 'published' && !preview)) { res.writeHead(404, baseHeaders({ 'Content-Type': 'text/html; charset=utf-8' })); return res.end(blog.renderIndex(await blog.listPublished(), 1, null).replace('<title>', '<title>Not found | ')); }
|
||||
if (!preview && req.method === 'GET') blog.bumpViews(post.slug);
|
||||
const related = blog.relatedFor(post, await blog.listPublished());
|
||||
res.writeHead(200, baseHeaders({ 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': preview ? 'no-store' : 'public, max-age=300' }));
|
||||
return res.end(blog.renderPost(post, related));
|
||||
}
|
||||
if (p === '/wallets') return sendFile(res, path.join(PUBLIC_DIR, 'wallets.html'));
|
||||
if (p === '/launch') return sendFile(res, path.join(PUBLIC_DIR, 'launch.html'));
|
||||
if (/^\/view\/[a-f0-9]{32}$/.test(p)) return sendFile(res, path.join(PUBLIC_DIR, 'view.html'));
|
||||
|
||||
Reference in New Issue
Block a user