0004: @lessjs/blog
SSG content plugin — can start after v0.8.0 core API stabilization. Zero-framework templates when compiler exists.
Draft
Source: docs/decisions/0004
<h1><code>@lessjs/blog</code> - Standalone Blog Package</h1>
<h2>Status</h2>
<p><strong>DRAFT</strong> - can start development after v0.8.0 (when core API stabilizes). The ideal zero-framework version can wait for <code>.less</code> compiler alpha (v0.11.0). Blog is an SSG content plugin at its core — it does NOT depend on Fullstack (v0.9) or ISR (v0.10).</p>
<h2>Context</h2>
<p>LessJS currently has two hardcoded blog route pages (<code>/blog/</code> + <code>/blog/less-compiler</code>) on its own docs site. These are hand-written custom element pages, not a reusable system.</p>
<p>Users need a proper blog: drop in <code>.md</code> files, get automatic listing + pagination + RSS + tags. Like VitePress's blog feature, but as a LessJS plugin.</p>
<h2>Constraints</h2>
<p>The blog system should not require Lit. It should work first as a plain SSG plugin, then gain <code>.less</code> compiler templates when the compiler exists:</p>
<ul><li>Post templates should be able to compile to vanilla Custom Elements when</li></ul>
<p><code>.less</code> exists</p>
<ul><li>SSR must be synchronous string concatenation (<code>template.innerHTML</code>)</li><li>No page-level client runtime for plain blog pages; interactive widgets remain</li></ul>
<p>islands</p>
<ul><li>The <code>.less</code> compiler is an optional future template backend, not a blocker</li></ul>
<p>for the first release</p>
<h2>Proposal</h2>
<h3><code>@lessjs/blog</code> package</h3>
<p>A Vite plugin that:</p>
<ol><li><strong>Scans</strong> a user-defined directory for <code>.md</code> files with YAML frontmatter</li><li><strong>Generates</strong> route entries at build time (like route-scanner does for <code>.ts</code></li></ol>
<p>files)</p>
<ol><li><strong>Renders</strong> listing pages, post pages, tag pages, and RSS feed during SSG</li></ol>
<h3>User experience</h3>
<pre data-language="ts"><code>// vite.config.ts
import { less } from '@lessjs/core';
import { lessBlog } from '@lessjs/blog';
export default defineConfig({
plugins: [
less(),
lessBlog({
dir: 'content/blog', // where your .md files live
title: 'My Blog',
postsPerPage: 10,
}),
],
});</code></pre>
<pre data-language="md"><code># content/blog/hello-world.md
---
title: Hello World
date: 2026-05-01
tags: [lessjs, meta]
---
This is my first post.</code></pre>
<h3>Generated routes</h3>
<table><thead><tr><th>Route</th><th>Content</th></tr></thead><tbody><tr><td><code>/blog/</code></td><td>Post listing (paginated, newest first)</td></tr><tr><td><code>/blog/hello-world</code></td><td>Individual post (rendered from .md)</td></tr><tr><td><code>/blog/page/2</code></td><td>Page 2 of listing</td></tr><tr><td><code>/blog/tags/lessjs</code></td><td>Filter by tag</td></tr><tr><td><code>/blog/feed.xml</code></td><td>RSS 2.0 / Atom feed</td></tr></tbody></table>
<h3>Plugin architecture</h3>
<p>The <code>lessBlog()</code> plugin hooks into the LessJS build pipeline:</p>
<pre data-language="text"><code>vite.config.ts
-> lessBlog() reads content/blog/*.md
extracts frontmatter -> metadata.json
registers virtual routes
-> deno task build
Phase 1: Vite SSR build
Phase 2: buildClient() (no island chunks needed for blog)
Phase 3: buildSSG() renders /blog/* pages
generates /feed.xml
-> dist/
blog/index.html
blog/hello-world/index.html
feed.xml</code></pre>
<h3>Post rendering</h3>
<p>With the <code>.less</code> compiler:</p>
<pre data-language="less"><code><!-- blog post template (built into @lessjs/blog) -->
<template>
<article>
<h1>{post.title}</h1>
<time datetime="{post.isoDate}">{post.displayDate}</time>
<div class="content">{post.html}</div>
<nav class="tags">{post.tags}</nav>
</article>
</template>
<script>
// post data injected at compile time by the plugin
post = { title: "", html: "", tags: [], isoDate: "", displayDate: "" }
</script>
<style>
.content :host { max-width: 720px; margin: 0 auto; }
time { font-size: 0.75rem; color: var(--less-text-muted); }
</style></code></pre>
<p>Without the <code>.less</code> compiler:</p>
<p>The same content can be rendered through the LessJS DSD renderer or a safe server-side HTML template helper. It should not depend on Lit for plain Markdown pages.</p>
<h3>MDX support (future)</h3>
<p>The <code>.md</code> parser should support:</p>
<ul><li><strong>Basic</strong>: Gray-matter frontmatter + marked/markdown-it</li><li><strong>Extended (v2)</strong>: MDX - embed Lit Components or <code>.less</code> components inline in</li></ul>
<p>markdown</p>
<ul><li><strong>Code blocks</strong>: Syntax highlighting at build time (not client-side)</li></ul>
<h2>Implementation order</h2>
<ol><li>v0.8.0 stabilizes core API (render-dsd split, component unification)</li><li><code>@lessjs/blog</code> development starts after v0.8.0 — SSG plugin form only</li><li>Dogfood on LessJS docs site during v0.9.0 phase</li><li><code>.less</code> compiler support is added when v0.11.0 alpha is available</li><li>LessJS docs site eats its own dogfood and replaces the current hardcoded blog</li></ol>
<p>routes</p>
<h2>Open Questions</h2>
<ol><li>Markdown parser: <code>marked</code> (lightweight) vs <code>markdown-it</code> (plugin ecosystem)</li></ol>
<p>vs custom?</p>
<ol><li>Should the blog template be customizable via</li></ol>
<p><code>lessBlog({ template: "my-template.less" })</code>?</p>
<ol><li>RSS vs Atom vs both?</li><li>Comments? (Disqus, utteranc.es, or leave it to the user)</li></ol>
<h2>Consequences</h2>
<ul><li><strong>Positive</strong>: Users get a one-line blog setup, competitive with VitePress</li><li><strong>Positive</strong>: LessJS docs site can dogfood its own blog package</li><li><strong>Positive</strong>: Zero framework runtime for plain blog pages (with <code>.less</code></li></ul>
<p>compiler)</p>
<ul><li><strong>Negative</strong>: The ideal compiler-backed architecture comes later</li><li><strong>Negative</strong>: Markdown parsing at build time adds ~100ms to Phase 1</li></ul>