refactor: remove blog and markdown test pages, update navigation links

- Deleted blog.html and associated blog post files (markdown-test, testddd, node-graphs, welcome).
- Updated index.html to use dynamic navigation links from navLinks.js.
- Enhanced MarkdownRenderer to include new heading pin elements.
- Implemented SVG bezier splines for blog post navigation.
- Adjusted CSS styles for blog layout and new elements.
This commit is contained in:
2026-05-10 23:29:02 +10:00
parent fabbb0539a
commit ba36cdd05b
13 changed files with 446 additions and 340 deletions

View File

@@ -3,6 +3,7 @@ const path = require('path');
const esbuild = require('esbuild');
const { execSync } = require('child_process');
const { StaticBlogGenerator } = require('./tools/StaticBlogGenerator');
const { renderNavLinkItems } = require('./tools/navLinks');
/**
* Parse YAML front matter from markdown content
@@ -38,13 +39,14 @@ function parseMarkdownWithFrontmatter(content) {
/**
* Process blog markdown files and generate blogs.json, also update graph.json
* @param {string} buildDir - Path to the build output directory
*/
async function processBlogPosts() {
async function processBlogPosts(buildDir) {
const blogDir = path.join(__dirname, 'website', 'data', 'blog');
const outputPath = path.join(__dirname, 'website', 'data', 'blogs.json');
const graphPath = path.join(__dirname, 'website', 'data', 'graph.json');
const outputPath = path.join(buildDir, 'data', 'blogs.json');
const graphPath = path.join(buildDir, 'data', 'graph.json');
const staticBlogGenerator = new StaticBlogGenerator({
siteRootDir: path.join(__dirname, 'website')
siteRootDir: buildDir
});
try {
@@ -323,6 +325,11 @@ async function updateHtmlForBundle() {
html = html.replace(/src="\/scripts\/blogPage\.js"/g, 'src="/scripts/blogPage.bundle.js"');
html = html.replace(
/[ \t]*<!-- NAV_LINKS -->/g,
renderNavLinkItems(' ')
);
await fs.writeFile(filePath, html);
}
@@ -357,8 +364,9 @@ async function updateHtmlForBundle() {
// Run the build
async function build() {
await processBlogPosts();
const buildDir = path.join(__dirname, 'build');
await copyWebsiteFiles();
await processBlogPosts(buildDir);
await bundleJavaScript();
await minifyCSS();
await updateHtmlForBundle();

View File

@@ -2,6 +2,7 @@
const fs = require('fs').promises;
const path = require('path');
const { renderNavLinkItems } = require('./navLinks');
/**
* Generates static blog pages and an index page from markdown post data.
@@ -32,10 +33,13 @@ class StaticBlogGenerator {
const indexHtml = this.renderIndexPage(posts);
await fs.writeFile(this.blogIndexPath, indexHtml, 'utf-8');
for (const post of posts) {
for (let i = 0; i < posts.length; i++) {
const post = posts[i];
const prev = posts[i + 1] || null;
const next = posts[i - 1] || null;
const postDir = path.join(this.blogRootDir, post.slug);
await fs.mkdir(postDir, { recursive: true });
const postHtml = this.renderPostPage(post);
const postHtml = this.renderPostPage(post, prev, next);
await fs.writeFile(path.join(postDir, 'index.html'), postHtml, 'utf-8');
}
}
@@ -82,9 +86,11 @@ class StaticBlogGenerator {
/**
* @param {{slug: string, title: string, date: string | null, author: string | null, tags?: string[], content: string}} post
* @param {{slug: string, title: string, date: string | null} | null} prev - Older post (lower index)
* @param {{slug: string, title: string, date: string | null} | null} next - Newer post (higher index)
* @returns {string}
*/
renderPostPage(post) {
renderPostPage(post, prev = null, next = null) {
const safeTitle = this.escapeHtml(post.title || post.slug);
const dateLabel = post.date ? this.formatDate(post.date) : null;
const authorLabel = post.author ? this.escapeHtml(post.author) : null;
@@ -100,18 +106,58 @@ class StaticBlogGenerator {
const markdownJson = JSON.stringify(post.content || '').replace(/<\//g, '<\\/');
const prevPeek = prev
? [
`<div class="blog-post-peek-wrapper blog-post-peek-wrapper--prev">`,
`<a class="blog-post-peek blog-post-peek--prev" href="/blog/${encodeURIComponent(prev.slug)}/" aria-label="Older post: ${this.escapeHtml(prev.title)}">`,
` <div class="blog-post-peek-header">`,
` <span class="blog-post-peek-direction">← older</span>`,
` <span class="blog-post-peek-pin" data-peek-pin="prev-out"></span>`,
` </div>`,
` <div class="blog-post-peek-body">`,
` <span class="blog-post-peek-title">${this.escapeHtml(prev.title)}</span>`,
prev.date ? ` <span class="blog-post-peek-date">${this.formatDate(prev.date)}</span>` : '',
` </div>`,
`</a>`,
`</div>`,
].join('\n')
: '<div class="blog-post-peek-wrapper blog-post-peek-wrapper--prev"></div>';
const nextPeek = next
? [
`<div class="blog-post-peek-wrapper blog-post-peek-wrapper--next">`,
`<a class="blog-post-peek blog-post-peek--next" href="/blog/${encodeURIComponent(next.slug)}/" aria-label="Newer post: ${this.escapeHtml(next.title)}">`,
` <div class="blog-post-peek-header">`,
` <span class="blog-post-peek-pin" data-peek-pin="next-in"></span>`,
` <span class="blog-post-peek-direction" style="text-align:right">newer →</span>`,
` </div>`,
` <div class="blog-post-peek-body">`,
` <span class="blog-post-peek-title">${this.escapeHtml(next.title)}</span>`,
next.date ? ` <span class="blog-post-peek-date">${this.formatDate(next.date)}</span>` : '',
` </div>`,
`</a>`,
`</div>`,
].join('\n')
: '<div class="blog-post-peek-wrapper blog-post-peek-wrapper--next"></div>';
const content = [
'<main class="blog-shell">',
this.renderBrandBar(),
'<div class="blog-post-layout">',
prevPeek,
'<article class="blog-card" aria-labelledby="blog-post-title">',
' <header class="blog-post-header">',
` <h1 id="blog-post-title" class="blog-post-title">${safeTitle}</h1>`,
metaBits.length > 0 ? ` <p class="blog-post-meta">${metaBits.join('<span aria-hidden="true">-</span>')}</p>` : '',
tagsHtml,
' </header>',
'<div class="md-hr" role="separator"><span class="md-hr-line"></span><span class="md-hr-arrow"></span></div>',
' <section class="blog-post-content" data-blog-post-content></section>',
` <script id="blogPostMarkdown" type="application/json">${markdownJson}</script>`,
'</article>',
nextPeek,
'<svg class="blog-post-splines" aria-hidden="true"></svg>',
'</div>',
'</main>'
].join('\n');
@@ -166,13 +212,7 @@ class StaticBlogGenerator {
* @returns {string}
*/
renderHeaderLinks() {
return [
' <a href="/blog.html" class="quick-link">blog</a>',
' <a href="/docs/" class="quick-link">docs</a>',
' <a href="https://github.com/litruv" target="_blank" rel="noopener" class="quick-link">github</a>',
' <a href="https://bsky.app/profile/lit.mates.dev" target="_blank" rel="noopener" class="quick-link">bluesky</a>',
' <a href="/materials" class="quick-link">materials</a>'
].join('\n');
return renderNavLinkItems(' ');
}
/**

32
tools/navLinks.js Normal file
View File

@@ -0,0 +1,32 @@
'use strict';
/**
* @typedef {Object} NavLink
* @property {string} label
* @property {string} href
* @property {boolean} [external]
*/
/** @type {NavLink[]} */
const NAV_LINKS = [
{ label: 'blog', href: '/blog.html' },
{ label: 'docs', href: '/docs/' },
{ label: 'github', href: 'https://github.com/litruv', external: true },
{ label: 'bluesky', href: 'https://bsky.app/profile/lit.mates.dev', external: true },
{ label: 'materials', href: '/materials' },
];
/**
* Renders nav link `<a>` elements as an HTML string.
*
* @param {string} [indent=' '] - Leading whitespace for each line.
* @returns {string}
*/
function renderNavLinkItems(indent = ' ') {
return NAV_LINKS.map(({ label, href, external }) => {
const attrs = external ? ' target="_blank" rel="noopener"' : '';
return `${indent}<a href="${href}"${attrs} class="quick-link">${label}</a>`;
}).join('\n');
}
module.exports = { NAV_LINKS, renderNavLinkItems };

View File

@@ -1,76 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<base href="/">
<title>Blog</title>
<meta name="description" content="Lit.ruv.wtf blog posts.">
<link rel="canonical" href="https://lit.ruv.wtf/blog.html">
<link rel="icon" type="image/png" sizes="32x32" href="/logos/32px.png">
<link rel="icon" type="image/png" sizes="64x64" href="/logos/64px.png">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-tomorrow.min.css">
<link rel="stylesheet" href="/styles/main.css">
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js"></script>
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-javascript.min.js"></script>
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-python.min.js"></script>
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-typescript.min.js"></script>
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-jsx.min.js"></script>
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-css.min.js"></script>
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-json.min.js"></script>
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-bash.min.js"></script>
</head>
<body class="blog-page">
<nav class="quick-links" aria-label="Primary">
<a href="/blog.html" class="quick-link">blog</a>
<a href="/docs/" class="quick-link">docs</a>
<a href="https://github.com/litruv" target="_blank" rel="noopener" class="quick-link">github</a>
<a href="https://bsky.app/profile/lit.mates.dev" target="_blank" rel="noopener" class="quick-link">bluesky</a>
<a href="/materials" class="quick-link">materials</a>
</nav>
<header class="blog-scroll-topbar" data-blog-scroll-topbar>
<a class="blog-scroll-logo-link" href="/" aria-label="Back to main site">
<img src="/logos/LogoFull.svg" alt="lit.ruv.wtf" class="blog-scroll-logo" />
</a>
<nav class="blog-scroll-links" aria-label="Scrolled navigation">
<a href="/blog.html" class="quick-link">blog</a>
<a href="/docs/" class="quick-link">docs</a>
<a href="https://github.com/litruv" target="_blank" rel="noopener" class="quick-link">github</a>
<a href="https://bsky.app/profile/lit.mates.dev" target="_blank" rel="noopener" class="quick-link">bluesky</a>
<a href="/materials" class="quick-link">materials</a>
</nav>
</header>
<main class="blog-shell">
<a class="blog-logo-link" href="/" aria-label="Back to main site">
<img src="/logos/LogoFull.svg" alt="lit.ruv.wtf" class="blog-logo" />
</a>
<section class="blog-card" aria-labelledby="blog-index-title">
<header class="blog-post-header">
<h1 id="blog-index-title" class="blog-post-title">Blog</h1>
<p class="blog-index-subtitle">Direct links to every post.</p>
</header>
<div class="blog-index-list"><article class="blog-index-item">
<h2 class="blog-index-item-title"><a href="/blog/markdown-test/">Markdown Formatting Test</a></h2>
<p class="blog-index-item-meta">September 11, 2026 <span aria-hidden="true">-</span> Test Author</p>
<div class="blog-index-tags"><span class="blog-tag">test</span><span class="blog-tag">formatting</span><span class="blog-tag">markdown</span></div>
</article>
<article class="blog-index-item">
<h2 class="blog-index-item-title"><a href="/blog/testddd/">test d d d d</a></h2>
<p class="blog-index-item-meta">May 10, 2026 <span aria-hidden="true">-</span> it me</p>
</article>
<article class="blog-index-item">
<h2 class="blog-index-item-title"><a href="/blog/welcome/">Welcome to My Blog</a></h2>
<p class="blog-index-item-meta">May 10, 2026 <span aria-hidden="true">-</span> Max Litruv Boonzaayer</p>
<div class="blog-index-tags"><span class="blog-tag">welcome</span><span class="blog-tag">meta</span><span class="blog-tag">introduction</span></div>
</article>
<article class="blog-index-item">
<h2 class="blog-index-item-title"><a href="/blog/node-graphs/">Building Interactive Node Graphs</a></h2>
<p class="blog-index-item-meta">April 15, 2026 <span aria-hidden="true">-</span> Max Litruv Boonzaayer</p>
<div class="blog-index-tags"><span class="blog-tag">javascript</span><span class="blog-tag">graph-systems</span><span class="blog-tag">tutorial</span></div>
</article></div>
</section>
</main>
<script type="module" src="/scripts/blogPage.js"></script>
</body>
</html>

View File

@@ -1,59 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<base href="/">
<title>Markdown Formatting Test - Blog</title>
<meta name="description" content="Lit.ruv.wtf blog posts.">
<link rel="canonical" href="https://lit.ruv.wtf/blog/markdown-test/">
<link rel="icon" type="image/png" sizes="32x32" href="/logos/32px.png">
<link rel="icon" type="image/png" sizes="64x64" href="/logos/64px.png">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-tomorrow.min.css">
<link rel="stylesheet" href="/styles/main.css">
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js"></script>
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-javascript.min.js"></script>
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-python.min.js"></script>
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-typescript.min.js"></script>
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-jsx.min.js"></script>
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-css.min.js"></script>
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-json.min.js"></script>
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-bash.min.js"></script>
</head>
<body class="blog-page">
<nav class="quick-links" aria-label="Primary">
<a href="/blog.html" class="quick-link">blog</a>
<a href="/docs/" class="quick-link">docs</a>
<a href="https://github.com/litruv" target="_blank" rel="noopener" class="quick-link">github</a>
<a href="https://bsky.app/profile/lit.mates.dev" target="_blank" rel="noopener" class="quick-link">bluesky</a>
<a href="/materials" class="quick-link">materials</a>
</nav>
<header class="blog-scroll-topbar" data-blog-scroll-topbar>
<a class="blog-scroll-logo-link" href="/" aria-label="Back to main site">
<img src="/logos/LogoFull.svg" alt="lit.ruv.wtf" class="blog-scroll-logo" />
</a>
<nav class="blog-scroll-links" aria-label="Scrolled navigation">
<a href="/blog.html" class="quick-link">blog</a>
<a href="/docs/" class="quick-link">docs</a>
<a href="https://github.com/litruv" target="_blank" rel="noopener" class="quick-link">github</a>
<a href="https://bsky.app/profile/lit.mates.dev" target="_blank" rel="noopener" class="quick-link">bluesky</a>
<a href="/materials" class="quick-link">materials</a>
</nav>
</header>
<main class="blog-shell">
<a class="blog-logo-link" href="/" aria-label="Back to main site">
<img src="/logos/LogoFull.svg" alt="lit.ruv.wtf" class="blog-logo" />
</a>
<article class="blog-card" aria-labelledby="blog-post-title">
<header class="blog-post-header">
<h1 id="blog-post-title" class="blog-post-title">Markdown Formatting Test</h1>
<p class="blog-post-meta"><span>September 11, 2026</span><span aria-hidden="true">-</span><span>Test Author</span></p>
<div class="blog-post-tags"><span class="blog-tag">test</span><span class="blog-tag">formatting</span><span class="blog-tag">markdown</span></div>
</header>
<section class="blog-post-content" data-blog-post-content></section>
<script id="blogPostMarkdown" type="application/json">"# Heading 1 test 123\n\n## Heading 2\n\n### Heading 3\n\n#### Heading 4\n\n##### Heading 5\n\n###### Heading 6\n\n## Text Formatting\n\nThis is **bold text** and this is *italic text* and this is ***bold italic text***.\n\nHere's some `inline code` in a sentence.\n\n## Links\n\nHere's a [regular link](https://example.com) and here's a [YouTube link](https://youtube.com/watch?v=test).\n\n## Lists\n\nUnordered list:\n\n* First item\n\n* Second item\n\n* Third item\n\nOrdered list:\n\n1. First step\n2. Second step\n3. Third step\n\n## Code Blocks\n\nStandard JavaScript:\n\n```javascript\nconst hello = \"world\";\nconsole.log(hello);\n\nfunction test() {\n return true;\n}\n```\n\nPython with syntax highlighting:\n\n```python\ndef hello_world():\n print(\"Hello, World!\")\n return True\n```\n\nCode with max height (200px):\n\n```javascript\n// This is a long code block that will scroll\nconst data = [1, 2, 3, 4, 5];\n\nfunction processData(arr) {\n return arr.map(x => x * 2);\n}\n\nconsole.log(processData(data));\n\n// Adding more lines to demonstrate scrolling\nfor (let i = 0; i < 10; i++) {\n console.log(`Iteration ${i}`);\n}\n\n// Even more content\nconst obj = {\n name: \"Test\",\n value: 42,\n nested: {\n deep: true\n }\n};\n```\n\n## Blockquotes\n\n> This is a blockquote.\n> It can span multiple lines.\n> And continues here.\n\n## Horizontal Rule\n\n***\n\n## Images\n\n![1.00](data/blog/media/test.gif \"Caption Test\")\n\n![0.43](data/blog/media/paste-1778405550240-ddf8h.png \"Gobby\")\n\n## Mixed Content\n\n<br />\n\nYou can mix **bold**, *italic*, and `code` in the same paragraph. Here's a [link with](https://example.com) **[bold](https://example.com)** [text](https://example.com) too.\n\n### Nested Lists\n\n* Top level itemsss\n\n* Another top level\n\n * Nested item (if supported)\n\n * Another nested\n\n1. Numbered item\n2. Another numbered\n\n * Mixed with bullets\n\n * More bullets\n\n## Special Characters\n\nTesting special chars: < > & \" '\n\n## Long Paragraph\n\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\n\nDuis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\n"</script>
</article>
</main>
<script type="module" src="/scripts/blogPage.js"></script>
</body>
</html>

View File

@@ -1,59 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<base href="/">
<title>Building Interactive Node Graphs - Blog</title>
<meta name="description" content="Lit.ruv.wtf blog posts.">
<link rel="canonical" href="https://lit.ruv.wtf/blog/node-graphs/">
<link rel="icon" type="image/png" sizes="32x32" href="/logos/32px.png">
<link rel="icon" type="image/png" sizes="64x64" href="/logos/64px.png">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-tomorrow.min.css">
<link rel="stylesheet" href="/styles/main.css">
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js"></script>
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-javascript.min.js"></script>
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-python.min.js"></script>
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-typescript.min.js"></script>
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-jsx.min.js"></script>
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-css.min.js"></script>
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-json.min.js"></script>
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-bash.min.js"></script>
</head>
<body class="blog-page">
<nav class="quick-links" aria-label="Primary">
<a href="/blog.html" class="quick-link">blog</a>
<a href="/docs/" class="quick-link">docs</a>
<a href="https://github.com/litruv" target="_blank" rel="noopener" class="quick-link">github</a>
<a href="https://bsky.app/profile/lit.mates.dev" target="_blank" rel="noopener" class="quick-link">bluesky</a>
<a href="/materials" class="quick-link">materials</a>
</nav>
<header class="blog-scroll-topbar" data-blog-scroll-topbar>
<a class="blog-scroll-logo-link" href="/" aria-label="Back to main site">
<img src="/logos/LogoFull.svg" alt="lit.ruv.wtf" class="blog-scroll-logo" />
</a>
<nav class="blog-scroll-links" aria-label="Scrolled navigation">
<a href="/blog.html" class="quick-link">blog</a>
<a href="/docs/" class="quick-link">docs</a>
<a href="https://github.com/litruv" target="_blank" rel="noopener" class="quick-link">github</a>
<a href="https://bsky.app/profile/lit.mates.dev" target="_blank" rel="noopener" class="quick-link">bluesky</a>
<a href="/materials" class="quick-link">materials</a>
</nav>
</header>
<main class="blog-shell">
<a class="blog-logo-link" href="/" aria-label="Back to main site">
<img src="/logos/LogoFull.svg" alt="lit.ruv.wtf" class="blog-logo" />
</a>
<article class="blog-card" aria-labelledby="blog-post-title">
<header class="blog-post-header">
<h1 id="blog-post-title" class="blog-post-title">Building Interactive Node Graphs</h1>
<p class="blog-post-meta"><span>April 15, 2026</span><span aria-hidden="true">-</span><span>Max Litruv Boonzaayer</span></p>
<div class="blog-post-tags"><span class="blog-tag">javascript</span><span class="blog-tag">graph-systems</span><span class="blog-tag">tutorial</span></div>
</header>
<section class="blog-post-content" data-blog-post-content></section>
<script id="blogPostMarkdown" type="application/json">"\r\n# Building Interactive Node Graphs\r\n\r\nIn this post, I'll share some insights on building interactive node-based graph systems for the web.\r\n\r\n## Why Node Graphs?\r\n\r\nNode graphs are powerful visual programming tools that make complex logic easier to understand and manipulate:\r\n\r\n- **Visual Clarity**: See the flow of data and execution at a glance\r\n- **Modularity**: Each node is a self-contained unit\r\n- **Flexibility**: Easy to add new node types and behaviors\r\n\r\n## The Architecture\r\n\r\nThe system is built on several key classes:\r\n\r\n### NodeBase\r\nThe abstract base class that all nodes inherit from. It defines the blueprint interface:\r\n\r\n```javascript\r\nclass NodeBase {\r\n static NodeType = \"\";\r\n static BlueprintPure_GetDefaultPins() { }\r\n BlueprintNativeEvent_OnRender(article, graphNode, renderCtx) { }\r\n async BlueprintCallable_Execute(ctx) { }\r\n}\r\n```\r\n\r\n### Node Registry\r\nAutomatically registers node types and creates instances based on type strings.\r\n\r\n### Execution Context\r\nHandles the graph traversal and execution flow when nodes are triggered.\r\n\r\n## Creating Custom Nodes\r\n\r\nTo create a new node type:\r\n\r\n1. Extend `NodeBase`\r\n2. Set a unique `NodeType`\r\n3. Define pins with `BlueprintPure_GetDefaultPins()`\r\n4. Implement rendering in `BlueprintNativeEvent_OnRender()`\r\n5. Implement logic in `BlueprintCallable_Execute()`\r\n\r\nThat's the blueprint system in a nutshell!\r\n"</script>
</article>
</main>
<script type="module" src="/scripts/blogPage.js"></script>
</body>
</html>

View File

@@ -1,59 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<base href="/">
<title>test d d d d - Blog</title>
<meta name="description" content="Lit.ruv.wtf blog posts.">
<link rel="canonical" href="https://lit.ruv.wtf/blog/testddd/">
<link rel="icon" type="image/png" sizes="32x32" href="/logos/32px.png">
<link rel="icon" type="image/png" sizes="64x64" href="/logos/64px.png">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-tomorrow.min.css">
<link rel="stylesheet" href="/styles/main.css">
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js"></script>
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-javascript.min.js"></script>
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-python.min.js"></script>
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-typescript.min.js"></script>
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-jsx.min.js"></script>
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-css.min.js"></script>
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-json.min.js"></script>
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-bash.min.js"></script>
</head>
<body class="blog-page">
<nav class="quick-links" aria-label="Primary">
<a href="/blog.html" class="quick-link">blog</a>
<a href="/docs/" class="quick-link">docs</a>
<a href="https://github.com/litruv" target="_blank" rel="noopener" class="quick-link">github</a>
<a href="https://bsky.app/profile/lit.mates.dev" target="_blank" rel="noopener" class="quick-link">bluesky</a>
<a href="/materials" class="quick-link">materials</a>
</nav>
<header class="blog-scroll-topbar" data-blog-scroll-topbar>
<a class="blog-scroll-logo-link" href="/" aria-label="Back to main site">
<img src="/logos/LogoFull.svg" alt="lit.ruv.wtf" class="blog-scroll-logo" />
</a>
<nav class="blog-scroll-links" aria-label="Scrolled navigation">
<a href="/blog.html" class="quick-link">blog</a>
<a href="/docs/" class="quick-link">docs</a>
<a href="https://github.com/litruv" target="_blank" rel="noopener" class="quick-link">github</a>
<a href="https://bsky.app/profile/lit.mates.dev" target="_blank" rel="noopener" class="quick-link">bluesky</a>
<a href="/materials" class="quick-link">materials</a>
</nav>
</header>
<main class="blog-shell">
<a class="blog-logo-link" href="/" aria-label="Back to main site">
<img src="/logos/LogoFull.svg" alt="lit.ruv.wtf" class="blog-logo" />
</a>
<article class="blog-card" aria-labelledby="blog-post-title">
<header class="blog-post-header">
<h1 id="blog-post-title" class="blog-post-title">test d d d d</h1>
<p class="blog-post-meta"><span>May 10, 2026</span><span aria-hidden="true">-</span><span>it me</span></p>
</header>
<section class="blog-post-content" data-blog-post-content></section>
<script id="blogPostMarkdown" type="application/json">"asdffdsaasdffdsa\n\n| <br /> | <br /> | <br /> | <br /> |\n| :----- | :----- | :----- | -----: |\n| <br /> | <br /> | <br /> | <br /> |\n| <br /> | <br /> | <br /> | <br /> |\n\n<br />\n\nthis is a test post, should't publish i guess, but who's reading lmao\n"</script>
</article>
</main>
<script type="module" src="/scripts/blogPage.js"></script>
</body>
</html>

View File

@@ -1,59 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<base href="/">
<title>Welcome to My Blog - Blog</title>
<meta name="description" content="Lit.ruv.wtf blog posts.">
<link rel="canonical" href="https://lit.ruv.wtf/blog/welcome/">
<link rel="icon" type="image/png" sizes="32x32" href="/logos/32px.png">
<link rel="icon" type="image/png" sizes="64x64" href="/logos/64px.png">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-tomorrow.min.css">
<link rel="stylesheet" href="/styles/main.css">
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js"></script>
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-javascript.min.js"></script>
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-python.min.js"></script>
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-typescript.min.js"></script>
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-jsx.min.js"></script>
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-css.min.js"></script>
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-json.min.js"></script>
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-bash.min.js"></script>
</head>
<body class="blog-page">
<nav class="quick-links" aria-label="Primary">
<a href="/blog.html" class="quick-link">blog</a>
<a href="/docs/" class="quick-link">docs</a>
<a href="https://github.com/litruv" target="_blank" rel="noopener" class="quick-link">github</a>
<a href="https://bsky.app/profile/lit.mates.dev" target="_blank" rel="noopener" class="quick-link">bluesky</a>
<a href="/materials" class="quick-link">materials</a>
</nav>
<header class="blog-scroll-topbar" data-blog-scroll-topbar>
<a class="blog-scroll-logo-link" href="/" aria-label="Back to main site">
<img src="/logos/LogoFull.svg" alt="lit.ruv.wtf" class="blog-scroll-logo" />
</a>
<nav class="blog-scroll-links" aria-label="Scrolled navigation">
<a href="/blog.html" class="quick-link">blog</a>
<a href="/docs/" class="quick-link">docs</a>
<a href="https://github.com/litruv" target="_blank" rel="noopener" class="quick-link">github</a>
<a href="https://bsky.app/profile/lit.mates.dev" target="_blank" rel="noopener" class="quick-link">bluesky</a>
<a href="/materials" class="quick-link">materials</a>
</nav>
</header>
<main class="blog-shell">
<a class="blog-logo-link" href="/" aria-label="Back to main site">
<img src="/logos/LogoFull.svg" alt="lit.ruv.wtf" class="blog-logo" />
</a>
<article class="blog-card" aria-labelledby="blog-post-title">
<header class="blog-post-header">
<h1 id="blog-post-title" class="blog-post-title">Welcome to My Blog</h1>
<p class="blog-post-meta"><span>May 10, 2026</span><span aria-hidden="true">-</span><span>Max Litruv Boonzaayer</span></p>
<div class="blog-post-tags"><span class="blog-tag">welcome</span><span class="blog-tag">meta</span><span class="blog-tag">introduction</span></div>
</header>
<section class="blog-post-content" data-blog-post-content></section>
<script id="blogPostMarkdown" type="application/json">"# Welcome to My Blog\n\nThis is my first blog post! I'm excited to share my thoughts and experiences with you.\n\n## What This Blog Is About\n\nThis blog is built using a **custom node-based graph system** where each blog post is represented as a node in an interactive visual graph. Pretty cool, right?\n\n### Features\n\n* 📝 Markdown support with YAML front matter\n\n* 📅 Date-based organization\n\n* 🏷️ Tag system for categorization\n\n* 🎨 Interactive node-based visualization\n\n* 📁 Media support in `data/blog/media/`\n\n## Technical Details\n\nThe build system automatically:\n\n1. Scans the `data/blog/` directory for `.md` files\n2. Parses YAML front matter for metadata\n3. Generates a `blogs.json` file\n4. Makes posts available to the BlogPostNode\n\nYou can reference images like this:\n![Example](data/blog/media/example.png)\n\n## Code Examples\n\nHere's some example code:\n\n```javascript\nconst greeting = \"Hello, World!\";\r\nconsole.log(greeting);\n```\n\n## Conclusion\n\nStay tuned for more posts! This is just the beginning of an exciting journey.\n"</script>
</article>
</main>
<script type="module" src="/scripts/blogPage.js"></script>
</body>
</html>

View File

@@ -50,11 +50,7 @@
<body>
<div id="workspaceCanvas" class="workspace-canvas">
<nav class="quick-links">
<a href="/blog.html" class="quick-link">blog</a>
<a href="/docs/" class="quick-link">docs</a>
<a href="https://github.com/litruv" target="_blank" rel="noopener" class="quick-link">github</a>
<a href="https://bsky.app/profile/lit.mates.dev" target="_blank" rel="noopener" class="quick-link">bluesky</a>
<a href="/materials" class="quick-link">materials</a>
<!-- NAV_LINKS -->
</nav>
<svg id="connectionLayer" class="connection-layer"></svg>

View File

@@ -97,14 +97,17 @@ export class MarkdownRenderer {
// Horizontal rule
if (/^[-*_]{3,}$/.test(block)) {
return `<hr class="md-hr" />`;
return `<div class="md-hr" role="separator"><span class="md-hr-line"></span><span class="md-hr-arrow"></span></div>`;
}
// Headings
const headingMatch = block.match(/^(#{1,6})\s+(.+)$/m);
if (headingMatch && block.split("\n").length === 1) {
const level = headingMatch[1].length;
return `<h${level} class="md-h${level}">${MarkdownRenderer.#renderInline(headingMatch[2])}</h${level}>`;
const pin = level === 1 ? '<span class="md-h1-pin" aria-hidden="true"></span>'
: level === 2 ? '<span class="md-h2-pin" aria-hidden="true"></span>'
: '';
return `<h${level} class="md-h${level}">${pin}${MarkdownRenderer.#renderInline(headingMatch[2])}</h${level}>`;
}
// Tables

View File

@@ -95,6 +95,106 @@ function setupScrollTopBar() {
}
}
/**
* Draws SVG bezier splines from the peek-card exec pins to the main blog card.
*
* @returns {void}
*/
function setupPeekSplines() {
const svg = document.querySelector(".blog-post-splines");
const layout = document.querySelector(".blog-post-layout");
const card = document.querySelector(".blog-card");
if (!svg || !layout || !card) return;
const WIRE_COLOR = "#ffffff";
const WIRE_OPACITY = "0.35";
/**
* Creates an SVG path element styled as an exec wire.
*
* @returns {SVGPathElement}
*/
function makePath() {
const p = document.createElementNS("http://www.w3.org/2000/svg", "path");
p.setAttribute("fill", "none");
p.setAttribute("stroke", WIRE_COLOR);
p.setAttribute("stroke-width", "2.5");
p.setAttribute("stroke-linecap", "round");
p.style.opacity = WIRE_OPACITY;
return p;
}
/**
* Returns the center of an element relative to the layout container.
*
* @param {Element} el
* @returns {{x: number, y: number}}
*/
function centerOf(el) {
const er = el.getBoundingClientRect();
const lr = layout.getBoundingClientRect();
return {
x: er.left - lr.left + er.width / 2,
y: er.top - lr.top + er.height / 2,
};
}
/**
* Returns the midpoint of a card's left or right edge relative to the layout.
*
* @param {Element} el
* @param {'left'|'right'} side
* @returns {{x: number, y: number}}
*/
function edgeOf(el, side) {
const er = el.getBoundingClientRect();
const lr = layout.getBoundingClientRect();
return {
x: side === 'left' ? er.left - lr.left : er.right - lr.left,
y: er.top - lr.top + 158,
};
}
const pathPrev = makePath();
const pathNext = makePath();
svg.appendChild(pathPrev);
svg.appendChild(pathNext);
/** @returns {void} */
function draw() {
const lr = layout.getBoundingClientRect();
svg.setAttribute("width", String(lr.width));
svg.setAttribute("height", String(lr.height));
const prevPin = document.querySelector("[data-peek-pin='prev-out']");
const nextPin = document.querySelector("[data-peek-pin='next-in']");
const cardLeft = edgeOf(card, "left");
const cardRight = edgeOf(card, "right");
if (prevPin) {
const s = centerOf(prevPin);
const e = cardLeft;
const cp = Math.max(60, Math.abs(e.x - s.x) * 0.5);
pathPrev.setAttribute("d", `M ${s.x} ${s.y} C ${s.x + cp} ${s.y} ${e.x - cp} ${e.y} ${e.x} ${e.y}`);
} else {
pathPrev.setAttribute("d", "");
}
if (nextPin) {
const s = cardRight;
const e = centerOf(nextPin);
const cp = Math.max(60, Math.abs(e.x - s.x) * 0.5);
pathNext.setAttribute("d", `M ${s.x} ${s.y} C ${s.x + cp} ${s.y} ${e.x - cp} ${e.y} ${e.x} ${e.y}`);
} else {
pathNext.setAttribute("d", "");
}
}
draw();
window.addEventListener("resize", draw, { passive: true });
window.addEventListener("scroll", draw, { passive: true });
}
/**
* Bootstraps static blog page behaviors.
*
@@ -103,6 +203,7 @@ function setupScrollTopBar() {
function initializeBlogPage() {
renderStaticBlogPost();
setupScrollTopBar();
setupPeekSplines();
}
if (document.readyState === "loading") {

View File

@@ -691,7 +691,7 @@ body.blog-page {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
justify-content: flex-start;
padding: 110px 16px 36px;
}
@@ -718,15 +718,150 @@ body.blog-page {
border: 1px solid rgba(205, 214, 244, 0.14);
border-radius: 16px;
box-shadow: 0 20px 70px rgba(0, 0, 0, 0.35);
padding: 22px 24px 28px;
padding: 22px 24px 28px 48px;
}
.blog-post-header {
border-bottom: 1px solid rgba(205, 214, 244, 0.12);
padding-bottom: 14px;
margin-bottom: 16px;
margin-bottom: 0;
}
/* ─── Post layout with prev/next peek cards ───────────────────────────────────── */
.blog-post-layout {
width: min(900px, 100%);
margin: 0 auto;
position: relative;
overflow: visible;
isolation: isolate;
}
.blog-post-peek-wrapper {
position: absolute;
top: 0;
width: 160px;
height: 0;
pointer-events: none;
overflow: visible;
}
.blog-post-peek-wrapper--prev {
right: calc(100% + 100px);
}
.blog-post-peek-wrapper--next {
left: calc(100% + 100px);
}
.blog-post-peek {
width: 160px;
background: var(--node-bg);
border: 1px solid var(--node-border);
border-radius: 8px;
box-shadow: 0 8px 18px rgba(0, 0, 0, 0.35);
display: flex;
flex-direction: column;
overflow: hidden;
opacity: 0.7;
transition: opacity 0.2s ease, border-color 0.2s ease;
text-decoration: none;
position: sticky;
top: 80px;
pointer-events: auto;
margin-top: 121px;
}
.blog-post-peek:hover {
opacity: 1;
border-color: rgba(250, 179, 135, 0.35);
}
.blog-post-peek--empty {
pointer-events: none;
background: transparent;
border-color: transparent;
box-shadow: none;
}
.blog-post-peek-header {
background: linear-gradient(90deg, var(--ctp-base), var(--ctp-surface0));
border-bottom: 1px solid var(--node-border);
padding: 6px 8px;
display: flex;
align-items: center;
gap: 6px;
}
.blog-post-peek-direction {
font-size: 0.65rem;
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--ctp-overlay2);
flex: 1;
}
.blog-post-peek-body {
padding: 8px;
display: flex;
flex-direction: column;
gap: 4px;
}
.blog-post-peek-title {
font-size: 0.78rem;
font-weight: 600;
color: var(--ctp-text);
line-height: 1.3;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
.blog-post-peek-date {
font-size: 0.68rem;
color: var(--ctp-subtext0);
}
/* exec pin dot on peek cards */
.blog-post-peek-pin {
width: 14px;
height: 14px;
flex-shrink: 0;
background: #ffffff;
clip-path: polygon(0% 0%, 60% 0%, 100% 50%, 60% 100%, 0% 100%);
}
.blog-post-peek--next {
margin-top: 221px;
}
.blog-post-peek--next .blog-post-peek-pin {
}
/* spline SVG overlay */
.blog-post-splines {
position: absolute;
inset: 0;
pointer-events: none;
overflow: visible;
z-index: 10;
}
.blog-post-layout > .blog-card {
position: relative;
}
@media (max-width: 1100px) {
.blog-post-peek {
display: none;
}
.blog-post-splines {
display: none;
}
}
.blog-post-title {
margin: 0;
color: var(--ctp-text);
@@ -771,6 +906,57 @@ body.blog-page {
user-select: text;
}
.blog-post-content .md-hr,
.blog-card > .md-hr {
border: none;
margin: 2em -8px;
width: calc(100% + 16px);
display: flex;
align-items: center;
gap: 0;
height: 14px;
position: relative;
}
.blog-card > .md-hr {
margin: 14px -8px 16px;
width: calc(100% + 16px);
}
.blog-post-content .md-hr::before,
.blog-card > .md-hr::before {
content: '';
display: block;
flex-shrink: 0;
width: 14px;
height: 14px;
background: var(--ctp-text);
clip-path: polygon(0% 0%, 60% 0%, 100% 50%, 60% 100%, 0% 100%);
}
.blog-post-content .md-hr::after,
.blog-card > .md-hr::after {
content: none;
}
.md-hr-line {
display: block;
flex: 1;
height: 3px;
background: var(--ctp-text);
opacity: 0.4;
}
.md-hr-arrow {
display: block;
flex-shrink: 0;
width: 14px;
height: 14px;
background: var(--ctp-text);
clip-path: polygon(0% 0%, 60% 0%, 100% 50%, 60% 100%, 0% 100%);
opacity: 0.4;
}
.blog-post-content .md-h1,
.blog-post-content .md-h2,
.blog-post-content .md-h3,
@@ -783,8 +969,60 @@ body.blog-page {
line-height: 1.3;
}
.blog-post-content .md-h1 { font-size: 1.9rem; }
.blog-post-content .md-h2 { font-size: 1.55rem; }
.blog-post-content .md-h1 {
font-size: 1.9rem;
position: relative;
}
.md-h1-pin {
display: inline-flex;
flex-shrink: 0;
width: 18px;
height: 18px;
border-radius: 50%;
border: 2px solid var(--node-border);
background: var(--ctp-surface0);
color: #3ee581;
position: absolute;
left: -37px;
top: 50%;
transform: translateY(-50%);
}
.md-h1-pin::after {
content: '';
position: absolute;
inset: 3px;
border-radius: 50%;
background: currentColor;
}
.blog-post-content .md-h2 {
font-size: 1.55rem;
position: relative;
}
.md-h2-pin {
display: inline-flex;
flex-shrink: 0;
width: 18px;
height: 18px;
border-radius: 50%;
border: 2px solid var(--node-border);
background: var(--ctp-surface0);
color: #ff66ff;
position: absolute;
left: -37px;
top: 50%;
transform: translateY(-50%);
}
.md-h2-pin::after {
content: '';
position: absolute;
inset: 3px;
border-radius: 50%;
background: currentColor;
}
.blog-post-content .md-h3 { font-size: 1.3rem; }
.blog-post-content .md-h4 { font-size: 1.1rem; }