Build Markdown news site with local AI generation and web push
This commit is contained in:
1 parent
ad30f12116
commit
199dec8a1e
26 files changed
+2277
-1
No files matched your search
@@ -0,0 +1,65 @@
|
||||
import http from 'node:http';
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { watch } from 'node:fs';
|
||||
import { loadEnvFile } from 'node:process';
|
||||
import { build, root, readArticles } from './site.mjs';
|
||||
import { PushService, createPushHandler } from './push.mjs';
|
||||
try {loadEnvFile(path.join(root,'.env'));} catch(error) {if(error.code!=='ENOENT')throw error;}
|
||||
const port=Number(process.env.PORT || 3000);
|
||||
const siteUrl=new URL(process.env.SITE_URL || `http://localhost:${port}`).origin;
|
||||
if(!/^https?:/.test(siteUrl))throw new Error('SITE_URL must be an HTTP(S) URL.');
|
||||
let articles=await build();
|
||||
let push;
|
||||
if(process.env.PUSH_ENABLED!=='false') {
|
||||
const subject=process.env.VAPID_SUBJECT || (siteUrl.startsWith('https:')?siteUrl:'https://localhost');
|
||||
push=await new PushService({directory:path.resolve(root,process.env.PUSH_DATA_DIR || '.data/push'),subject}).init(articles);
|
||||
}
|
||||
const handlePush=createPushHandler(push,{siteUrl});
|
||||
let rebuilding=Promise.resolve();
|
||||
let timer;
|
||||
let snapshot=JSON.stringify(articles);
|
||||
function refresh() {
|
||||
rebuilding=rebuilding.then(async()=>{
|
||||
articles=await build();snapshot=JSON.stringify(articles);
|
||||
if(push)await push.sync(articles);
|
||||
}).catch(error=>console.error('Rebuild failed:',error.message));
|
||||
return rebuilding;
|
||||
}
|
||||
for(const folder of ['pages','public'])watch(path.join(root,folder),{recursive:true},()=>{clearTimeout(timer);timer=setTimeout(refresh,200);});
|
||||
// Polling also detects changes if filesystem events are missed. Delivery happens
|
||||
// only after a successful build, so a notification never precedes its page.
|
||||
let ticking=false;
|
||||
setInterval(async()=>{
|
||||
if(ticking)return;ticking=true;
|
||||
try {
|
||||
await rebuilding;
|
||||
if(JSON.stringify(await readArticles())!==snapshot)await refresh();
|
||||
if(push)await push.flush();
|
||||
}catch(error){console.error('Notification worker:',error.message);}
|
||||
finally{ticking=false;}
|
||||
},5000).unref();
|
||||
const types={'.html':'text/html; charset=utf-8','.css':'text/css; charset=utf-8','.js':'text/javascript; charset=utf-8','.svg':'image/svg+xml','.jpg':'image/jpeg','.png':'image/png','.webp':'image/webp','.webmanifest':'application/manifest+json'};
|
||||
const server=http.createServer(async(req,res)=>{
|
||||
try {
|
||||
const pathname=decodeURIComponent(new URL(req.url,'http://localhost').pathname);
|
||||
if(await handlePush(req,res,pathname))return;
|
||||
if(!['GET','HEAD'].includes(req.method)){res.writeHead(405);res.end();return;}
|
||||
await rebuilding;
|
||||
const base=path.join(root,'dist');
|
||||
let target=path.resolve(base,'.'+pathname);
|
||||
if(!target.startsWith(base+path.sep)&&target!==base){res.writeHead(403);res.end();return;}
|
||||
if((await fs.stat(target)).isDirectory()){
|
||||
if(!pathname.endsWith('/')){res.writeHead(301,{Location:pathname+'/'});res.end();return;}
|
||||
target=path.join(target,'index.html');
|
||||
}
|
||||
const content=await fs.readFile(target);
|
||||
const headers={'Content-Type':types[path.extname(target)] || 'application/octet-stream'};
|
||||
if(pathname==='/sw.js')headers['Cache-Control']='no-cache';
|
||||
res.writeHead(200,headers);res.end(req.method==='HEAD'?undefined:content);
|
||||
}catch{
|
||||
res.writeHead(404,{'Content-Type':'text/html; charset=utf-8'});
|
||||
res.end(req.method==='HEAD'?undefined:await fs.readFile(path.join(root,'dist/404.html')).catch(()=>'Page not found'));
|
||||
}
|
||||
});
|
||||
server.listen(port,'0.0.0.0',()=>console.log(`TrueNews → ${siteUrl}${push?' (push notifications enabled)':''}`));
|
||||
Reference in new issue
Block a user