feat: dynamic page regeneration #24
2 changed files with 426 additions and 0 deletions
87
src/blog/pages/webdev/deranged-typescript.md
Normal file
87
src/blog/pages/webdev/deranged-typescript.md
Normal file
|
@ -0,0 +1,87 @@
|
|||
- imports at the bottom
|
||||
- order your file by importance.
|
||||
- 'G' to jump to imports, etc
|
||||
- prefer namespace imports
|
||||
- easier to type and refactor. easier to read.
|
||||
- large files are okay
|
||||
- all files are their own library
|
||||
- split files up by making components modular, not by "oh it's too big"
|
||||
- engine/render.ts is a standalone library, in order to split JSX, Suspense,
|
||||
and Marko out, the main file was made modular.
|
||||
- lowercase
|
||||
- name objects ultra-concisely
|
||||
- filenames are often one word describing what they contain
|
||||
- avoid useless descriptors like "utils", "helpers", and "data"
|
||||
- examples
|
||||
- async.ts contains all the async library functions.
|
||||
- watch.ts contains the file watcher and watch-reload mode.
|
||||
- render.*, Io
|
||||
- be ultra-concise in comments
|
||||
- no "discarded" variables, embrace `void x`
|
||||
- makes code more readable
|
||||
- note how i want to write a lint for this
|
||||
- note the one proposal i want about void
|
||||
- push the ts inference engine (as const, ReturnType, etc)
|
||||
- reduces how much you repeat yourself making it easier to refactor things
|
||||
- use the code as the source of truth
|
||||
- push the ts inference engine (generics)
|
||||
- do not implement crazy things with the TS engine, instead use generic input
|
||||
types, and then use regular control to narrow and transform the return type.
|
||||
source of truth is your code.
|
||||
- UNWRAP, ASSERT utility globals are amazing
|
||||
- ban postfix '!'
|
||||
- stripped for production frontend builds
|
||||
- destructure often
|
||||
- use the one example from work lol
|
||||
- package.json "imports" are amazing
|
||||
- remapping
|
||||
- implementation switching
|
||||
- testing
|
||||
- embrace the web and node.js APIs
|
||||
- sitegen relies on so many node features that bun and deno fail to run it.
|
||||
- overlay modules are great
|
||||
- avoid dependencies
|
||||
- once you build your own mini standard library you win
|
||||
- talk about regrets with mdx
|
||||
|
||||
## imports at the bottom
|
||||
|
||||
Here is an abridged version of my website's `backend.ts`. When reading it from
|
||||
top to bottom it is immediately obvious that it is a Hono web server.
|
||||
|
||||
```ts
|
||||
// This is the main file for paperclover.net's server.
|
||||
const app = new Hono();
|
||||
const logHttp = console.scoped("http", { color: "magenta" });
|
||||
|
||||
// Middleware
|
||||
app.use(...);
|
||||
...
|
||||
|
||||
// Backends
|
||||
app.route("", require("./q+a/backend.ts").app);
|
||||
...
|
||||
|
||||
export default app;
|
||||
|
||||
...
|
||||
|
||||
import { type Context, Hono, type Next } from "#hono";
|
||||
import { logger } from "hono/logger";
|
||||
import { trimTrailingSlash } from "hono/trailing-slash";
|
||||
import * as assets from "#sitegen/assets";
|
||||
import * as admin from "./admin.ts";
|
||||
import * as console from "@paperclover/console";
|
||||
```
|
||||
|
||||
Since `import`/`export` statements are hoisted like `var` and `function`, the
|
||||
position of these statements within the file does not matter. The imported
|
||||
modules have to be loaded first before this file can start. With this, I've
|
||||
found it nicer to sort the file by _importance_ rather than by arbitrary rules
|
||||
dictated by how C-style `#include`s worked.
|
||||
|
||||
Start with a documentation comment, then the most important
|
||||
functions/variables/types, sort the file by importance. Imports are not really
|
||||
important since you very quickly get to know where common namespaces come from.
|
||||
And since they're at the bottom, you can just press `G` in Vim or `CMD+Down` on
|
||||
the Mac to scroll to the end of the file.
|
339
src/blog/pages/webdev/marko-intro.markodown
Normal file
339
src/blog/pages/webdev/marko-intro.markodown
Normal file
|
@ -0,0 +1,339 @@
|
|||
export const blog: BlogMeta = {
|
||||
title: "Marko is the coziest HTML templating language",
|
||||
desc: "...todo...",
|
||||
created: "2025-06-13",
|
||||
draft: true,
|
||||
};
|
||||
export const meta = formatBlogMeta(blob);
|
||||
export * as layout from "@/blog/layout.tsx";
|
||||
|
||||
I've been recently playing around [Marko], and after adding limited support
|
||||
for it in my website generator, [sitegen], I instantly fell in love with how
|
||||
minimalistic it is in comparison to JSX, Astro components, and Svelte.
|
||||
|
||||
[Marko]: https://next.markojs.com
|
||||
[sitegen]: https://paperclover.dev/clo/sitegen
|
||||
|
||||
## Introduction to Marko
|
||||
|
||||
If JSX was taking HTML and shoving its syntax into JavaScript, Marko is shoving
|
||||
JavaScript into HTML. Attributes are JavaScript expressions.
|
||||
|
||||
```marko
|
||||
<div>
|
||||
// `input` is like props, but given in the top-level scope
|
||||
<time datetime=input.date.toISOString()>
|
||||
// Interpolation with JS template string syntax
|
||||
${formatTimeNicely(input.date)}
|
||||
</time>
|
||||
<div>
|
||||
<a href=`/users/${input.user.id}`>${input.user.name}</a>
|
||||
</div>
|
||||
|
||||
// Capital letter variables for imported components
|
||||
<MarkdownContent message=input.message />
|
||||
|
||||
// Components also can be auto-imported by lowercase.
|
||||
// This will look upwards for a `tags/` folder containing
|
||||
// "custom-footer.marko", similar to how Node.js finds
|
||||
// package names in all upwards `node_modules` folders.
|
||||
<custom-footer />
|
||||
</div>
|
||||
|
||||
// ESM `import` / `export` just work as expected.
|
||||
// I prefer my imports at the end, to highlight the markup.
|
||||
import MarkdownContent from "./MarkdownContent.marko";
|
||||
import { formatTimeNicely } from "../date-helpers.ts";
|
||||
```
|
||||
|
||||
Tags with the `value` attribute have a shorthand, which is used by the built-in
|
||||
`<if>` for conditional rendering.
|
||||
|
||||
```marko
|
||||
// Sugar for <input value="string" />
|
||||
<input="string" />
|
||||
|
||||
// and it composes amazingly to the 'if' built-in
|
||||
<if=input.user>
|
||||
<UserProfile=input.user />
|
||||
</if>
|
||||
```
|
||||
|
||||
Tags can also return values into the scope for use in the template using `/`, such as `<id>` for unique ID generation. This is available to components that `<return=output/>`.
|
||||
|
||||
```
|
||||
<id/uniqueId />
|
||||
|
||||
<input id=uniqueId type="checkbox" name="allow_trans_rights" />
|
||||
<label for=uniqueId>click me!</>
|
||||
// ^ oh, you can also omit the
|
||||
// closing tag name if you want.
|
||||
```
|
||||
|
||||
It's important that I started with the two forms of "Tag I/O": `=` for input
|
||||
and `/` for output. With those building blocks, we introduce local variables
|
||||
with `const`
|
||||
|
||||
```
|
||||
<const/rendered = markdownToHtml(input.value) />
|
||||
|
||||
// This is how you insert raw HTML to the document
|
||||
<inline-html=rendered />
|
||||
|
||||
// It supports all of the cozy destructuring syntax JS has
|
||||
<const/{ id, name } = user />
|
||||
```
|
||||
|
||||
Unlike JSX, when you pass content within a tag (`input.content` instead of
|
||||
JSX's `children`), instead of it being a JSX element, it is actually a
|
||||
function. This means that the `for` tag can render the content multiple times.
|
||||
|
||||
```
|
||||
<ul>
|
||||
<for from=1 to=10>
|
||||
// Renders a new random number for each iteration.
|
||||
<li>${Math.random()}</li>
|
||||
</>
|
||||
</ul>
|
||||
```
|
||||
|
||||
Since `content` is a function, it can take arguments. This is done with `|`
|
||||
|
||||
```
|
||||
<h1>my friends</h1>
|
||||
<ul>
|
||||
// I tend to omit the closing tag names for the built-in control
|
||||
// flow tags, but I keep them for HTML tags. It's kinda like how
|
||||
// in JavaScript you just write `}` to close your `if`s and loops.
|
||||
//
|
||||
// Anyways <for> also has 'of'
|
||||
<for|item| of=user.friends>
|
||||
<li class="friend">${item.name}</li>
|
||||
</>
|
||||
|
||||
// They support the same syntax JavaScript function params allows,
|
||||
// so you can have destructuring here too, and multiple params.
|
||||
<for|{ name }, index| of=user.friends>
|
||||
// By the way you can also use emmet-style class and ID shorthands.
|
||||
<li.friend>My #${index + 1} friend is ${name}</li>
|
||||
</>
|
||||
</ul>
|
||||
```
|
||||
|
||||
Instead of named slots, Marko has attribute tags. These are more powerful than
|
||||
slots since they are functions, and can also act as sugar for more complicated
|
||||
attributes.
|
||||
|
||||
```
|
||||
<Layout title="Welcome">
|
||||
<@header variant="big">
|
||||
<h1>the next big thing</h1>
|
||||
</@header>
|
||||
|
||||
<p>body text...</p>
|
||||
</Layout>
|
||||
|
||||
// The `input` variable inside of <Layout /> is:
|
||||
//
|
||||
// {
|
||||
// title: "Welcome",
|
||||
// header: {
|
||||
// content: /* function rendering "<h1>the next big thing</h1>" */,
|
||||
// variant: "big",
|
||||
// },
|
||||
// content: /* function rendering "<p>body text</p>" */
|
||||
// }
|
||||
```
|
||||
|
||||
This layout could be implemented as such:
|
||||
|
||||
```marko
|
||||
<main>
|
||||
<if=input.header />
|
||||
<const/{ ...headerProps, content }=input.header />
|
||||
<header ...headerProps>
|
||||
// Instead of assigning to a variable with a capital letter,
|
||||
// template interpolation works on tag names. This can also
|
||||
// be a string to render the native HTML tag of that kind.
|
||||
<${content} />
|
||||
</header>
|
||||
<hr />
|
||||
</>
|
||||
|
||||
<${input.content} />
|
||||
</main>
|
||||
```
|
||||
|
||||
The last syntax feature missing is calling a tag with parameters. That is done
|
||||
just like a regular function call, with '('.
|
||||
|
||||
```
|
||||
<Something(item, index) />
|
||||
```
|
||||
|
||||
In fact, attributes can just be sugar over this syntax. (this technically isn't
|
||||
true but it's close enough for the example)
|
||||
|
||||
```
|
||||
<SpecialButton type="submit" class="red" />
|
||||
|
||||
// is equal to
|
||||
|
||||
<SpecialButton({ type: "submit", class: "red" }) />
|
||||
```
|
||||
|
||||
All of the above is about how Marko's syntax works, and how it performs HTML
|
||||
generation with components. Marko also allows interactive components, but an
|
||||
explaination of that is beyond the scope of this page, mostly since I have not
|
||||
used it. A brief example of it, modified from their documentation.
|
||||
|
||||
```marko
|
||||
// Reactive variables with <let/> just work...
|
||||
<let/basicCounter=0 />
|
||||
<button onClick() { basicCounter += 1 }>${basicCounter}</button>
|
||||
// ...but a counter is boring.
|
||||
|
||||
<let/todos=[
|
||||
{ id: 0, text: "Learn Marko" },
|
||||
{ id: 1, text: "Make a Website" },
|
||||
]/>
|
||||
|
||||
// 'by' is like React JSX's "key" property, but it's optional.
|
||||
<ul><for|todo, i| of=todos by=(todo => todo.id)>
|
||||
<li.todo>
|
||||
// this variable remains stable even if the list
|
||||
// re-orders, because 'by' was specified.
|
||||
<let/done=false/>
|
||||
<label>
|
||||
<span>${todo.text}</span>
|
||||
// ':=' creates a two-way reactive binding,
|
||||
// (it passes a callback for `checkedChanged`)
|
||||
<input type="checkbox" checked:=done />
|
||||
</label>
|
||||
<button
|
||||
title="delete"
|
||||
disabled=!done
|
||||
onClick() {
|
||||
todos = todos.toSpliced(i, 1);
|
||||
}
|
||||
> × </button>
|
||||
</li>
|
||||
</></ul>
|
||||
|
||||
// Form example
|
||||
<let/nextId=2/>
|
||||
<form onSubmit(e) {
|
||||
e.preventDefault();
|
||||
todos = todos.concat({
|
||||
id: nextId++,
|
||||
// HTMLFormElement exposes all its named input
|
||||
// elements as extra properties on the object.
|
||||
text: e.target.text.value,
|
||||
});
|
||||
// And you can clear it with 'reset()'
|
||||
e.target.reset();
|
||||
}>
|
||||
// We don't 'onChange' like a React loser. The form
|
||||
// value can be read in the submit event like normal.
|
||||
<input name="text" placeholder="Another Item">
|
||||
<button type="submit">Add</button>
|
||||
</form>
|
||||
```
|
||||
|
||||
<SectionHeader updated="2025-08-11">Usage on `paperclover.net`</Section>
|
||||
|
||||
Using Marko for HTML generation is quite easy. `.marko` files can be compiled
|
||||
into `.js` using the `@marko/compiler` library.
|
||||
|
||||
```ts
|
||||
const src = fs.readFileSync("page.marko", "utf8");
|
||||
const compile = marko.compileSync(src, filepath);
|
||||
fs.writeFileSync("page.js", compile.code);
|
||||
|
||||
const page = require("./page.js");
|
||||
console.info(page);
|
||||
|
||||
import * as fs from "node:fs";
|
||||
import * as marko from "@marko/compiler";
|
||||
```
|
||||
|
||||
To get client side JavaScript, an option can be passed to the Marko compiler to
|
||||
generate the client side code. While it is a big selling point of Marko, I do
|
||||
not use any of their client side features, instead deferring to manually-written
|
||||
frontend scripts. This is because that is how my website has been for years,
|
||||
statically generated. And for websites like mine that are content focused, this
|
||||
is the correct way to do things.
|
||||
|
||||
Since I have a custom HTML generation library (built on JSX and some React-like
|
||||
patterns), I have written a simple integration for it to utilize Marko
|
||||
components, which is loaded by replacing the generated import to `marko/html`,
|
||||
which lets me overwrite functions like `createTemplate` (to change the signature
|
||||
of a component), `dynamicTag` (to allow Marko to render non-Marko components),
|
||||
and `fork` (to enable async integration with the rendering framework). An
|
||||
additional feature of this is I have a Node.js loader hook to allow importing
|
||||
these files directly.
|
||||
|
||||
```tsx
|
||||
function Page() {
|
||||
const q = Question.getByDate(new Date("2025-06-07 12:12 EST"));
|
||||
return <div>
|
||||
<h1>example question</h1>
|
||||
<QuestionRender question={q} />
|
||||
</div>;
|
||||
}
|
||||
|
||||
// The synchronous render can be used because `Page` and `question.marko`
|
||||
// do not await any promises (SQLite runs synchronously)
|
||||
console.info(render.sync(<Page />).text);
|
||||
|
||||
import * as render from "#engine/render";
|
||||
import QuestionRender from "@/q+a/tags/question.marko";
|
||||
import { Question } from "@/q+a/models/Question.ts";
|
||||
```
|
||||
|
||||
Here is the `question.marko` tag used to render [questions on the clover q+a](/q+a).
|
||||
|
||||
```marko
|
||||
// Renders a `Question` entry including its markdown body.
|
||||
export interface Input {
|
||||
question: Question;
|
||||
admin?: boolean;
|
||||
}
|
||||
|
||||
// 2024-12-31 05:00:00 EST
|
||||
export const transitionDate = 1735639200000;
|
||||
|
||||
<const/{ question, admin } = input />
|
||||
<const/{ id, date, text } = question/>
|
||||
|
||||
<${"e-"}
|
||||
f=(date > transitionDate ? true : undefined)
|
||||
id=admin ? `q${id}` : undefined
|
||||
>
|
||||
<if=admin>
|
||||
<a
|
||||
style="margin-right: 0.5rem"
|
||||
href=`/admin/q+a/${id}`
|
||||
>[EDIT]</a>
|
||||
</>
|
||||
<a>
|
||||
<time
|
||||
datetime=formatQuestionISOTimestamp(date)
|
||||
>${formatQuestionTimestamp(date)}</time>
|
||||
</a>
|
||||
|
||||
<CloverMarkdown ...{ text } />
|
||||
</>
|
||||
|
||||
// this singleton script will make all the '<time>' tags clickable.
|
||||
client import "./clickable-links.client.ts";
|
||||
|
||||
import type { Question } from "@/q+a/models/Question.ts";
|
||||
import { formatQuestionTimestamp, formatQuestionISOTimestamp } from "@/q+a/format.ts";
|
||||
import { CloverMarkdown } from "@/q+a/clover-markdown.tsx";
|
||||
```
|
||||
|
||||
The integration is great, `client import` is quite a magical concept, and I've
|
||||
tuned it to do the expected thing in my framework.
|
||||
|
||||
import { type BlogMeta, formatBlogMeta } from '@/blog/helpers.ts';
|
Loading…
Reference in a new issue