Blog
2024
A simple static site generator that supports RSS.
Test harness
html
<svg xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 40 120"
style="border: 1px solid gray; pointer-events: visible; max-width: 100%; height: auto"
id="iframe-svg"
>
</svg>
Usage
- Copy the
blog.js
script into the directory you want to turn into a blog. - Run the script with
node blog.js
. - The script will generate
index.md
,rss.xml
, and a new blog post like1.md
. - When you run it again, it will regenerate and create a new file with an incrementing integer filename.
Limitations
Currently blog.js
does NOT:
- Support slugs
- Support draft
- Support blurbs in the index and feed.
- Override config on the command line (you must edit the script)
- Not generating a new file by default (need to add a 'new' command)
Test harness generating code
js
import {svg} from '/simpatico.js';
const iframeSvg = svg.elt('iframe-svg');
// NB: adjust viewbox of svg to add more rows of content
const urlPathPrefix = '/notes/';
// For now, getting this from the output of the blog.js script
const urls = [
'ai.md','app_loop.html','javascript.md','blog.md','bpm-finder.html','browser-events.html','browser-topics.html','cat-tail-theory.md','chat.md','combine.md','crypto.md','d3-draw.md','d3-rectangles.html','devx.md','distractions.md','enough.md','foobar.html','git.md','ignition.md','index.md','inspiration.md','jrest.md','last-write-wins.html','linux.md','maximalism.md','memory.html','methods.md','minimalism.md','music.md','oh-my-zsh.md','overlap-rectangles-svg-canvas.html','postgres.md','predicates.html','pwa.md','rules.md','spring.md','square.html','stree.md','stree2.md','structure.md','svg-minimal-dragging.html','tools.md','tufte-css.html','vibe.md','webaudio.md','webgl-experiment.html'
].map(url => urlPathPrefix + url);
console.log(urls.length/4);
const clickableIframe = (url, {x,y}) => `
<g transform="translate(${x} ${y})">
<rect width="10" height="10" fill="white"/>
<foreignObject id="embedded-iframe" width="500px" height="500px" transform="scale(.02)">
<iframe width="500px" height="500px" src="${url}" style="overflow:hidden" scrolling="no"></iframe>
</foreignObject>
<rect onclick="window.location='${url}'" width="10" height="10" fill-opacity="0"/>
</g>
`;
const pos = (index, cols=4, W=10, H=10) => {
const x = index % cols * W;
const y = Math.floor(index / cols) * H;
return { x, y };
}
const iframeAtIndex = (url, i) => clickableIframe(url, pos(i));
const html = urls.map(iframeAtIndex).reduce((a,b) => a + b, '');
iframeSvg.innerHTML = html;
Blog generating code
js
/// DO NOT EXECUTE - this is for node
import fs from 'fs';
import child_process from 'child_process';
// Define parameters - todo support command line override
const authorName = 'Josh';
const authorLocation= 'USA';
const urlPathPrefix = '/notes/';
const blogURL = 'https://simpatico.io' + urlPathPrefix;
const blogTitle = 'Simpatico Notes';
const blogDescription = 'Notes about Simpatico development';
const preferredEditor = '';
const currentDate = new Date().toLocaleDateString();
const noteTitle = `# ${authorName} from ${authorLocation} on ${currentDate}\n\n`;
const NOTE_FILE_PATTERN = /^([0-9]*)(?:-(?:.*))?\.md$/; //capture the number prefix, ignore stub after optional dash
const blogHeader = `# ${blogTitle}
Click [here](blog.md) to see ALL entries at once.
`;
const peek = (arr, fallback=null) => (arr && arr.length) ? arr[arr.length-1] : fallback;
const getMaxValue = (max=0, num) => (num > max) ? num : max;
const extractNoteNumber = (filename, notePattern) => +peek(filename.match(notePattern), 0);
const findGreatestNoteNumber = (fileNames, notePattern) => fileNames.map(nn => extractNoteNumber(nn, notePattern)).reduce(getMaxValue);
const generateIndexFile = (fileNames) => {
const content = fileNames.map((fileName, index) => `${index + 1}. [${fileName.replace('.md', '')}](${urlPathPrefix + fileName})`).join('\n');
fs.writeFileSync(`index.md`, blogHeader + content);
};
// TODO add a description by looking at filecontents
const generateRssFile = (fileNames) => {
const rssContent = `
<rss version="2.0">
<channel>
<title>${blogTitle}</title>
<link>${blogURL}</link>
<description>${blogDescription}</description>
${fileNames.map((fileName, index) => {
const timestamp = new Date().toUTCString(); // Get current timestamp
return `<item><title>${fileName.replace('.md', '')}</title>
<link>${blogURL + fileName}</link>
<description>${fileName}</description>
<pubDate>${timestamp}</pubDate></item>`;
}).join('\n')}
</channel>
</rss>
`;
fs.writeFileSync('rss.xml', rssContent);
};
const fileNames = fs.readdirSync('.').filter(name => name.endsWith('.md') || name.endsWith('.html'));
const noteId = findGreatestNoteNumber(fileNames, NOTE_FILE_PATTERN) + 1;
const fileName = noteId + '.md';
fs.writeFileSync(fileName, `${noteTitle}`);
// regenerate index and rss files
generateIndexFile(fileNames.concat(fileName));
generateRssFile(fileNames.concat(fileName));
// load the new blog post in an editor
if (preferredEditor) child_process.spawn(preferredEditor, [fileName]);
console.log(`created ${fileName}`);
console.log(fileNames.map(f => `'${f}'`).join(','))
Copyright SimpatiCorp 2024