289 lines
8.4 KiB
Text
289 lines
8.4 KiB
Text
export const blog: BlogMeta = {
|
|
title: "Marko is the coziest HTML templating language",
|
|
desc: "...todo...",
|
|
date: "2025-06-13",
|
|
draft: true,
|
|
};
|
|
export const meta = formatBlogMeta(blob);
|
|
|
|
I've been recently playing around [Marko][1], and after adding limited support
|
|
for it in my website generator, [sitegen][2], I instantly fell in love with how
|
|
minimalistic it is in comparison to JSX, Astro components, and Svelte.
|
|
|
|
## Introduction
|
|
|
|
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>
|
|
```
|
|
|
|
## Usage on `paperclover.net`
|
|
|
|
TODO: document a lot of feedback, how i embedded Marko
|
|
|
|
My website uses statically generated HTML. That is why I have not needed to use
|
|
reactive variables. My generator doesn't even try compiling components
|
|
client-side.
|
|
|
|
Here is the actual component 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";
|
|
```
|
|
|
|
import { type BlogMeta, formatBlogMeta } from '@/blog/helpers.ts';
|