Blog syndication: publishing an article posts it to X and Instagram through Blotato (cover via media upload, once per article, retry button, status column)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -29,7 +29,8 @@ const legacy = require('./legacy'); // Faucet Wave / Tier One Ads bridge: welcom
|
||||
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 blog = require('./blog');
|
||||
const adminMember = require('./adminmember'); // admin member card: search, drilldown, edits (Marty, 2026-09-13) // admin-written coaching articles, server-rendered public /blog with SEO metadata (Marty, 2026-09-12)
|
||||
const adminMember = require('./adminmember');
|
||||
const syndicate = require('./syndicate'); // blog -> Blotato -> X + Instagram on publish (Marty, 2026-09-13) // admin member card: search, drilldown, edits (Marty, 2026-09-13) // 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
|
||||
@@ -363,6 +364,8 @@ async function boot() {
|
||||
blog.init({ dataDir: DATA_DIR });
|
||||
adminMember.init({ accounts, ads, chain, tank, legacy, promos, messages, dataDir: DATA_DIR });
|
||||
loadOpenTokens();
|
||||
syndicate.init({ dataDir: DATA_DIR, publicDir: PUBLIC_DIR, uploadsDir: UPLOADS_DIR });
|
||||
console.log('blog syndication:', syndicate.enabled() ? 'on (X + Instagram via Blotato)' : 'off (no blotato.key)');
|
||||
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
|
||||
@@ -1183,14 +1186,31 @@ const server = http.createServer(async (req, res) => {
|
||||
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 })) });
|
||||
return json(res, 200, { syndication: syndicate.enabled(), 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, syndicated: syndicate.statusOf(x.slug) })) });
|
||||
}
|
||||
if (p === '/api/admin/blog' && req.method === 'POST') {
|
||||
if (!isAdmin(req)) return json(res, 401, { error: 'auth' });
|
||||
const b = await readBody(req);
|
||||
const before = b.existingSlug ? await blog.get(b.existingSlug) : null;
|
||||
const r = await blog.save(b, b.existingSlug || null);
|
||||
if (r.ok && r.post.status === 'published' && (!before || before.status !== 'published') && !b.noSyndicate) {
|
||||
// first time this article goes live: push it to X + Instagram (never repeated for the same slug)
|
||||
syndicate.publish(r.post).catch(e => console.error('syndication', e.message));
|
||||
r.syndicating = syndicate.enabled();
|
||||
}
|
||||
r.syndicated = syndicate.statusOf(r.post.slug);
|
||||
return json(res, r.error ? 400 : 200, r);
|
||||
}
|
||||
if (p === '/api/admin/blog/syndicate' && req.method === 'POST') { // manual: post (or retry) a published article
|
||||
if (!isAdmin(req)) return json(res, 401, { error: 'auth' });
|
||||
const b = await readBody(req);
|
||||
const post = await blog.get(String(b.slug || ''));
|
||||
if (!post) return json(res, 404, { error: 'No such post.' });
|
||||
if (post.status !== 'published') return json(res, 400, { error: 'Publish the article first.' });
|
||||
if (!syndicate.enabled()) return json(res, 400, { error: 'No Blotato key on the server.' });
|
||||
const st = await syndicate.publish(post, { force: !!b.force });
|
||||
return json(res, 200, { ok: true, syndicated: st });
|
||||
}
|
||||
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' });
|
||||
|
||||
Reference in New Issue
Block a user