216 lines
7.9 KiB
Markdown
216 lines
7.9 KiB
Markdown
# User Interface — Server-Rendered HTMLX
|
|
|
|
No Angular. No React. No frontend framework. The UI is a set of `.htmlx` template files that the server parses, populates with content from the embedded database, and serves as plain HTML. Real-time updates arrive via SSE and are applied with minimal client-side scripting.
|
|
|
|
## Why Not Angular
|
|
|
|
The current `writeonce-app` is an Angular 18 SPA with Tailwind, PrismJS, ngx-markdown, and FontAwesome. It works, but it's a heavy delivery mechanism for what is fundamentally a read-heavy content site:
|
|
|
|
- **~200MB of `node_modules`** for a site that renders markdown articles
|
|
- **Client-side routing** for content that doesn't need it — every article is a distinct URL, not an interactive application
|
|
- **JavaScript-dependent rendering** — content doesn't exist until Angular boots, hydrates, and fetches from the API
|
|
- **Separate build pipeline** — `npm run build` produces static assets that must be deployed to nginx independently of the API
|
|
|
|
The content is static between updates. The interactivity is limited to navigation and code highlighting. A server-rendered approach matches the actual requirements.
|
|
|
|
## HTMLX Templates
|
|
|
|
The author defines the site layout using `.htmlx` files — HTML with embedded data bindings that the server resolves at render time.
|
|
|
|
### Template Structure
|
|
|
|
```
|
|
templates/
|
|
layout.htmlx # outer shell: <html>, <head>, <body>
|
|
header.htmlx # site header, navigation
|
|
footer.htmlx # site footer
|
|
home.htmlx # homepage: article list
|
|
article.htmlx # single article view
|
|
about.htmlx # static page
|
|
contact.htmlx # static page
|
|
components/
|
|
article-card.htmlx # summary card for article listings
|
|
code-snippet.htmlx # code block with language + title
|
|
img-caption.htmlx # image with caption
|
|
section.htmlx # article section (heading + paragraphs)
|
|
```
|
|
|
|
### Template Syntax
|
|
|
|
Templates use a binding syntax that references content from the database. The server parses these bindings, resolves them against the current content, and outputs plain HTML.
|
|
|
|
```html
|
|
<!-- header.htmlx -->
|
|
<header>
|
|
<nav>
|
|
<a href="/">writeonce</a>
|
|
<a href="/about">about</a>
|
|
<a href="/contact">contact</a>
|
|
</nav>
|
|
</header>
|
|
```
|
|
|
|
```html
|
|
<!-- article.htmlx -->
|
|
<article>
|
|
<h1>{{article.title}}</h1>
|
|
<p class="meta">by {{article.author}} · {{article.tags}}</p>
|
|
|
|
{{#each article.sections}}
|
|
<section>
|
|
<h2>{{heading}}</h2>
|
|
{{#each paragraphs}}
|
|
<p>{{this}}</p>
|
|
{{/each}}
|
|
</section>
|
|
{{/each}}
|
|
|
|
{{#each article.codes}}
|
|
{{> code-snippet snippet=this}}
|
|
{{/each}}
|
|
|
|
{{#each article.images}}
|
|
{{> img-caption image=this}}
|
|
{{/each}}
|
|
</article>
|
|
```
|
|
|
|
```html
|
|
<!-- home.htmlx -->
|
|
<main>
|
|
<h1>articles</h1>
|
|
{{#each articles}}
|
|
{{> article-card article=this}}
|
|
{{/each}}
|
|
</main>
|
|
```
|
|
|
|
The `{{> partial}}` syntax includes another `.htmlx` file as a component. The server resolves these at render time — no client-side component tree.
|
|
|
|
### Content Subscription in Templates
|
|
|
|
Templates declare what data they need. The server resolves these declarations against the embedded database and subscribes the client to changes:
|
|
|
|
```html
|
|
<!-- article.htmlx -->
|
|
<!-- subscribe: article WHERE sys_title = :route_param -->
|
|
|
|
<article>
|
|
<h1>{{article.title}}</h1>
|
|
...
|
|
</article>
|
|
```
|
|
|
|
```html
|
|
<!-- home.htmlx -->
|
|
<!-- subscribe: articles WHERE published = true ORDER BY date DESC LIMIT 10 -->
|
|
|
|
<main>
|
|
{{#each articles}}
|
|
{{> article-card article=this}}
|
|
{{/each}}
|
|
</main>
|
|
```
|
|
|
|
The `<!-- subscribe: ... -->` comment is a directive to the server. It declares the query that populates the template's data context. The same query is used to register an SSE subscription for live updates (as described in [03-data.md](./03-data.md)).
|
|
|
|
## Rendering Pipeline
|
|
|
|
```
|
|
Browser request
|
|
|
|
|
v
|
|
Route match (/blog/linux-misc)
|
|
|
|
|
v
|
|
Load template (article.htmlx)
|
|
|
|
|
v
|
|
Parse subscribe directive
|
|
(article WHERE sys_title = "linux-misc")
|
|
|
|
|
v
|
|
Query embedded database (.seg index)
|
|
|
|
|
v
|
|
Resolve template bindings ({{article.title}}, etc.)
|
|
|
|
|
v
|
|
Compose with layout.htmlx + header.htmlx + footer.htmlx
|
|
|
|
|
v
|
|
Inject SSE subscription script
|
|
|
|
|
v
|
|
Send complete HTML response
|
|
```
|
|
|
|
The browser receives a fully rendered page on first load. No JavaScript framework boots. No API call fires. The content is already in the HTML.
|
|
|
|
## Live Updates via SSE
|
|
|
|
After the initial HTML is delivered, a small inline script opens an SSE connection for the page's subscription query:
|
|
|
|
```html
|
|
<script>
|
|
const source = new EventSource('/subscribe/blog/linux-misc');
|
|
source.onmessage = (event) => {
|
|
const diff = JSON.parse(event.data);
|
|
applyDiff(diff);
|
|
};
|
|
</script>
|
|
```
|
|
|
|
The `applyDiff` function is a lightweight client-side updater — it targets DOM elements by data attribute and patches their content. No virtual DOM, no reconciliation, no framework. For a content site where updates are infrequent and localized (a paragraph changed, a tag was added), direct DOM manipulation is sufficient.
|
|
|
|
```html
|
|
<h1 data-bind="article.title">Linux Misc</h1>
|
|
<p data-bind="article.sections[0].paragraphs[0]">First paragraph...</p>
|
|
```
|
|
|
|
When a diff arrives for `article.title`, the script finds the element with `data-bind="article.title"` and replaces its text content. This is the minimal client-side code the architecture requires.
|
|
|
|
## Code Highlighting
|
|
|
|
The current frontend uses PrismJS for syntax highlighting. In the server-rendered model, highlighting can happen at either layer:
|
|
|
|
**Server-side (preferred):** The server parses code blocks during template rendering and emits pre-highlighted HTML with CSS classes. The browser only needs the PrismJS CSS theme, not the JavaScript library. This eliminates client-side parsing entirely.
|
|
|
|
**Client-side (fallback):** Include PrismJS as a small script that runs on page load and on SSE update. Simpler to implement initially but adds a JavaScript dependency.
|
|
|
|
## Markdown Rendering
|
|
|
|
The current frontend uses `ngx-markdown` and `marked` to parse markdown in the browser. In the target architecture, markdown is rendered to HTML on the server during template composition. The browser never sees raw markdown.
|
|
|
|
This aligns with the content model: the JSON metadata already defines the article structure (sections, paragraphs, code snippets, images). The markdown file provides prose content. The server combines both into final HTML — the template just places the pre-rendered blocks.
|
|
|
|
## What Gets Removed
|
|
|
|
| Current (Angular) | Target (HTMLX) |
|
|
|---|---|
|
|
| `writeonce-app/` (full Angular project) | `templates/` (handful of .htmlx files) |
|
|
| `node_modules/` (~200MB) | None |
|
|
| `angular.json`, `tsconfig.json`, `karma.conf.js` | None |
|
|
| npm build pipeline | Template parsed at request time |
|
|
| Nginx static file serving | Binary serves its own HTML |
|
|
| Client-side routing | Server-side route matching |
|
|
| Client-side markdown parsing | Server-side rendering |
|
|
| Client-side code highlighting | Server-side or minimal JS |
|
|
|
|
## Styling
|
|
|
|
Templates use plain CSS. Tailwind can optionally be used as a build-time utility (generating a static CSS file), but there is no runtime CSS framework. The author writes styles in a `styles.css` file that the server serves as a static asset.
|
|
|
|
```
|
|
templates/
|
|
styles/
|
|
main.css # site-wide styles
|
|
article.css # article-specific styles
|
|
code-theme.css # syntax highlighting theme (PrismJS compatible)
|
|
```
|
|
|
|
## Template Authoring Experience
|
|
|
|
The `.htmlx` files are editable by the same author who writes articles. The template syntax is intentionally close to HTML — there's no JSX, no TypeScript, no build step. An author who knows HTML can modify the site layout.
|
|
|
|
This closes the loop on the writeonce philosophy: the author writes content (markdown + JSON) and layout (`.htmlx` + CSS) as files, and the binary turns them into a live site.
|