| ... | ... | @@ -1,339 +1,339 @@ |
| 1 | | export const blog: BlogMeta = {
|
| 2 | | title: "Marko is the coziest HTML templating language",
|
| 3 | | desc: "...todo...",
|
| 4 | | created: "2025-06-13",
|
| 5 | | draft: true,
|
| 6 | | };
|
| 7 | | export const meta = formatBlogMeta(blob);
|
| 8 | | export * as layout from "@/blog/layout.tsx";
|
| 9 | |
|
| 10 | | I've been recently playing around [Marko], and after adding limited support
|
| 11 | | for it in my website generator, [sitegen], I instantly fell in love with how
|
| 12 | | minimalistic it is in comparison to JSX, Astro components, and Svelte.
|
| 13 | |
|
| 14 | | [Marko]: https://next.markojs.com
|
| 15 | | [sitegen]: https://paperclover.dev/clo/sitegen
|
| 16 | |
|
| 17 | | ## Introduction to Marko
|
| 18 | |
|
| 19 | | If JSX was taking HTML and shoving its syntax into JavaScript, Marko is shoving
|
| 20 | | JavaScript into HTML. Attributes are JavaScript expressions.
|
| 21 | |
|
| 22 | | ```marko
|
| 23 | | <div>
|
| 24 | | // `input` is like props, but given in the top-level scope
|
| 25 | | <time datetime=input.date.toISOString()>
|
| 26 | | // Interpolation with JS template string syntax
|
| 27 | | ${formatTimeNicely(input.date)}
|
| 28 | | </time>
|
| 29 | | <div>
|
| 30 | | <a href=`/users/${input.user.id}`>${input.user.name}</a>
|
| 31 | | </div>
|
| 32 | |
|
| 33 | | // Capital letter variables for imported components
|
| 34 | | <MarkdownContent message=input.message />
|
| 35 | |
|
| 36 | | // Components also can be auto-imported by lowercase.
|
| 37 | | // This will look upwards for a `tags/` folder containing
|
| 38 | | // "custom-footer.marko", similar to how Node.js finds
|
| 39 | | // package names in all upwards `node_modules` folders.
|
| 40 | | <custom-footer />
|
| 41 | | </div>
|
| 42 | |
|
| 43 | | // ESM `import` / `export` just work as expected.
|
| 44 | | // I prefer my imports at the end, to highlight the markup.
|
| 45 | | import MarkdownContent from "./MarkdownContent.marko";
|
| 46 | | import { formatTimeNicely } from "../date-helpers.ts";
|
| 47 | | ```
|
| 48 | |
|
| 49 | | Tags with the `value` attribute have a shorthand, which is used by the built-in
|
| 50 | | `<if>` for conditional rendering.
|
| 51 | |
|
| 52 | | ```marko
|
| 53 | | // Sugar for <input value="string" />
|
| 54 | | <input="string" />
|
| 55 | |
|
| 56 | | // and it composes amazingly to the 'if' built-in
|
| 57 | | <if=input.user>
|
| 58 | | <UserProfile=input.user />
|
| 59 | | </if>
|
| 60 | | ```
|
| 61 | |
|
| 62 | | 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/>`.
|
| 63 | |
|
| 64 | | ```
|
| 65 | | <id/uniqueId />
|
| 66 | |
|
| 67 | | <input id=uniqueId type="checkbox" name="allow_trans_rights" />
|
| 68 | | <label for=uniqueId>click me!</>
|
| 69 | | // ^ oh, you can also omit the
|
| 70 | | // closing tag name if you want.
|
| 71 | | ```
|
| 72 | |
|
| 73 | | It's important that I started with the two forms of "Tag I/O": `=` for input
|
| 74 | | and `/` for output. With those building blocks, we introduce local variables
|
| 75 | | with `const`
|
| 76 | |
|
| 77 | | ```
|
| 78 | | <const/rendered = markdownToHtml(input.value) />
|
| 79 | |
|
| 80 | | // This is how you insert raw HTML to the document
|
| 81 | | <inline-html=rendered />
|
| 82 | |
|
| 83 | | // It supports all of the cozy destructuring syntax JS has
|
| 84 | | <const/{ id, name } = user />
|
| 85 | | ```
|
| 86 | |
|
| 87 | | Unlike JSX, when you pass content within a tag (`input.content` instead of
|
| 88 | | JSX's `children`), instead of it being a JSX element, it is actually a
|
| 89 | | function. This means that the `for` tag can render the content multiple times.
|
| 90 | |
|
| 91 | | ```
|
| 92 | | <ul>
|
| 93 | | <for from=1 to=10>
|
| 94 | | // Renders a new random number for each iteration.
|
| 95 | | <li>${Math.random()}</li>
|
| 96 | | </>
|
| 97 | | </ul>
|
| 98 | | ```
|
| 99 | |
|
| 100 | | Since `content` is a function, it can take arguments. This is done with `|`
|
| 101 | |
|
| 102 | | ```
|
| 103 | | <h1>my friends</h1>
|
| 104 | | <ul>
|
| 105 | | // I tend to omit the closing tag names for the built-in control
|
| 106 | | // flow tags, but I keep them for HTML tags. It's kinda like how
|
| 107 | | // in JavaScript you just write `}` to close your `if`s and loops.
|
| 108 | | //
|
| 109 | | // Anyways <for> also has 'of'
|
| 110 | | <for|item| of=user.friends>
|
| 111 | | <li class="friend">${item.name}</li>
|
| 112 | | </>
|
| 113 | |
|
| 114 | | // They support the same syntax JavaScript function params allows,
|
| 115 | | // so you can have destructuring here too, and multiple params.
|
| 116 | | <for|{ name }, index| of=user.friends>
|
| 117 | | // By the way you can also use emmet-style class and ID shorthands.
|
| 118 | | <li.friend>My #${index + 1} friend is ${name}</li>
|
| 119 | | </>
|
| 120 | | </ul>
|
| 121 | | ```
|
| 122 | |
|
| 123 | | Instead of named slots, Marko has attribute tags. These are more powerful than
|
| 124 | | slots since they are functions, and can also act as sugar for more complicated
|
| 125 | | attributes.
|
| 126 | |
|
| 127 | | ```
|
| 128 | | <Layout title="Welcome">
|
| 129 | | <@header variant="big">
|
| 130 | | <h1>the next big thing</h1>
|
| 131 | | </@header>
|
| 132 | |
|
| 133 | | <p>body text...</p>
|
| 134 | | </Layout>
|
| 135 | |
|
| 136 | | // The `input` variable inside of <Layout /> is:
|
| 137 | | //
|
| 138 | | // {
|
| 139 | | // title: "Welcome",
|
| 140 | | // header: {
|
| 141 | | // content: /* function rendering "<h1>the next big thing</h1>" */,
|
| 142 | | // variant: "big",
|
| 143 | | // },
|
| 144 | | // content: /* function rendering "<p>body text</p>" */
|
| 145 | | // }
|
| 146 | | ```
|
| 147 | |
|
| 148 | | This layout could be implemented as such:
|
| 149 | |
|
| 150 | | ```marko
|
| 151 | | <main>
|
| 152 | | <if=input.header />
|
| 153 | | <const/{ ...headerProps, content }=input.header />
|
| 154 | | <header ...headerProps>
|
| 155 | | // Instead of assigning to a variable with a capital letter,
|
| 156 | | // template interpolation works on tag names. This can also
|
| 157 | | // be a string to render the native HTML tag of that kind.
|
| 158 | | <${content} />
|
| 159 | | </header>
|
| 160 | | <hr />
|
| 161 | | </>
|
| 162 | |
|
| 163 | | <${input.content} />
|
| 164 | | </main>
|
| 165 | | ```
|
| 166 | |
|
| 167 | | The last syntax feature missing is calling a tag with parameters. That is done
|
| 168 | | just like a regular function call, with '('.
|
| 169 | |
|
| 170 | | ```
|
| 171 | | <Something(item, index) />
|
| 172 | | ```
|
| 173 | |
|
| 174 | | In fact, attributes can just be sugar over this syntax. (this technically isn't
|
| 175 | | true but it's close enough for the example)
|
| 176 | |
|
| 177 | | ```
|
| 178 | | <SpecialButton type="submit" class="red" />
|
| 179 | |
|
| 180 | | // is equal to
|
| 181 | |
|
| 182 | | <SpecialButton({ type: "submit", class: "red" }) />
|
| 183 | | ```
|
| 184 | |
|
| 185 | | All of the above is about how Marko's syntax works, and how it performs HTML
|
| 186 | | generation with components. Marko also allows interactive components, but an
|
| 187 | | explaination of that is beyond the scope of this page, mostly since I have not
|
| 188 | | used it. A brief example of it, modified from their documentation.
|
| 189 | |
|
| 190 | | ```marko
|
| 191 | | // Reactive variables with <let/> just work...
|
| 192 | | <let/basicCounter=0 />
|
| 193 | | <button onClick() { basicCounter += 1 }>${basicCounter}</button>
|
| 194 | | // ...but a counter is boring.
|
| 195 | |
|
| 196 | | <let/todos=[
|
| 197 | | { id: 0, text: "Learn Marko" },
|
| 198 | | { id: 1, text: "Make a Website" },
|
| 199 | | ]/>
|
| 200 | |
|
| 201 | | // 'by' is like React JSX's "key" property, but it's optional.
|
| 202 | | <ul><for|todo, i| of=todos by=(todo => todo.id)>
|
| 203 | | <li.todo>
|
| 204 | | // this variable remains stable even if the list
|
| 205 | | // re-orders, because 'by' was specified.
|
| 206 | | <let/done=false/>
|
| 207 | | <label>
|
| 208 | | <span>${todo.text}</span>
|
| 209 | | // ':=' creates a two-way reactive binding,
|
| 210 | | // (it passes a callback for `checkedChanged`)
|
| 211 | | <input type="checkbox" checked:=done />
|
| 212 | | </label>
|
| 213 | | <button
|
| 214 | | title="delete"
|
| 215 | | disabled=!done
|
| 216 | | onClick() {
|
| 217 | | todos = todos.toSpliced(i, 1);
|
| 218 | | }
|
| 219 | | > &times; </button>
|
| 220 | | </li>
|
| 221 | | </></ul>
|
| 222 | |
|
| 223 | | // Form example
|
| 224 | | <let/nextId=2/>
|
| 225 | | <form onSubmit(e) {
|
| 226 | | e.preventDefault();
|
| 227 | | todos = todos.concat({
|
| 228 | | id: nextId++,
|
| 229 | | // HTMLFormElement exposes all its named input
|
| 230 | | // elements as extra properties on the object.
|
| 231 | | text: e.target.text.value,
|
| 232 | | });
|
| 233 | | // And you can clear it with 'reset()'
|
| 234 | | e.target.reset();
|
| 235 | | }>
|
| 236 | | // We don't 'onChange' like a React loser. The form
|
| 237 | | // value can be read in the submit event like normal.
|
| 238 | | <input name="text" placeholder="Another Item">
|
| 239 | | <button type="submit">Add</button>
|
| 240 | | </form>
|
| 241 | | ```
|
| 242 | |
|
| 243 | | <SectionHeader updated="2025-08-11">Usage on `paperclover.net`</Section>
|
| 244 | |
|
| 245 | | Using Marko for HTML generation is quite easy. `.marko` files can be compiled
|
| 246 | | into `.js` using the `@marko/compiler` library.
|
| 247 | |
|
| 248 | | ```ts
|
| 249 | | const src = fs.readFileSync("page.marko", "utf8");
|
| 250 | | const compile = marko.compileSync(src, filepath);
|
| 251 | | fs.writeFileSync("page.js", compile.code);
|
| 252 | |
|
| 253 | | const page = require("./page.js");
|
| 254 | | console.info(page);
|
| 255 | |
|
| 256 | | import * as fs from "node:fs";
|
| 257 | | import * as marko from "@marko/compiler";
|
| 258 | | ```
|
| 259 | |
|
| 260 | | To get client side JavaScript, an option can be passed to the Marko compiler to
|
| 261 | | generate the client side code. While it is a big selling point of Marko, I do
|
| 262 | | not use any of their client side features, instead deferring to manually-written
|
| 263 | | frontend scripts. This is because that is how my website has been for years,
|
| 264 | | statically generated. And for websites like mine that are content focused, this
|
| 265 | | is the correct way to do things.
|
| 266 | |
|
| 267 | | Since I have a custom HTML generation library (built on JSX and some React-like
|
| 268 | | patterns), I have written a simple integration for it to utilize Marko
|
| 269 | | components, which is loaded by replacing the generated import to `marko/html`,
|
| 270 | | which lets me overwrite functions like `createTemplate` (to change the signature
|
| 271 | | of a component), `dynamicTag` (to allow Marko to render non-Marko components),
|
| 272 | | and `fork` (to enable async integration with the rendering framework). An
|
| 273 | | additional feature of this is I have a Node.js loader hook to allow importing
|
| 274 | | these files directly.
|
| 275 | |
|
| 276 | | ```tsx
|
| 277 | | function Page() {
|
| 278 | | const q = Question.getByDate(new Date("2025-06-07 12:12 EST"));
|
| 279 | | return <div>
|
| 280 | | <h1>example question</h1>
|
| 281 | | <QuestionRender question={q} />
|
| 282 | | </div>;
|
| 283 | | }
|
| 284 | |
|
| 285 | | // The synchronous render can be used because `Page` and `question.marko`
|
| 286 | | // do not await any promises (SQLite runs synchronously)
|
| 287 | | console.info(render.sync(<Page />).text);
|
| 288 | |
|
| 289 | | import * as render from "#engine/render";
|
| 290 | | import QuestionRender from "@/q+a/tags/question.marko";
|
| 291 | | import { Question } from "@/q+a/models/Question.ts";
|
| 292 | | ```
|
| 293 | |
|
| 294 | | Here is the `question.marko` tag used to render [questions on the clover q+a](/q+a).
|
| 295 | |
|
| 296 | | ```marko
|
| 297 | | // Renders a `Question` entry including its markdown body.
|
| 298 | | export interface Input {
|
| 299 | | question: Question;
|
| 300 | | admin?: boolean;
|
| 301 | | }
|
| 302 | |
|
| 303 | | // 2024-12-31 05:00:00 EST
|
| 304 | | export const transitionDate = 1735639200000;
|
| 305 | |
|
| 306 | | <const/{ question, admin } = input />
|
| 307 | | <const/{ id, date, text } = question/>
|
| 308 | |
|
| 309 | | <${"e-"}
|
| 310 | | f=(date > transitionDate ? true : undefined)
|
| 311 | | id=admin ? `q${id}` : undefined
|
| 312 | | >
|
| 313 | | <if=admin>
|
| 314 | | <a
|
| 315 | | style="margin-right: 0.5rem"
|
| 316 | | href=`/admin/q+a/${id}`
|
| 317 | | >[EDIT]</a>
|
| 318 | | </>
|
| 319 | | <a>
|
| 320 | | <time
|
| 321 | | datetime=formatQuestionISOTimestamp(date)
|
| 322 | | >${formatQuestionTimestamp(date)}</time>
|
| 323 | | </a>
|
| 324 | |
|
| 325 | | <CloverMarkdown ...{ text } />
|
| 326 | | </>
|
| 327 | |
|
| 328 | | // this singleton script will make all the '<time>' tags clickable.
|
| 329 | | client import "./clickable-links.client.ts";
|
| 330 | |
|
| 331 | | import type { Question } from "@/q+a/models/Question.ts";
|
| 332 | | import { formatQuestionTimestamp, formatQuestionISOTimestamp } from "@/q+a/format.ts";
|
| 333 | | import { CloverMarkdown } from "@/q+a/clover-markdown.tsx";
|
| 334 | | ```
|
| 335 | |
|
| 336 | | The integration is great, `client import` is quite a magical concept, and I've
|
| 337 | | tuned it to do the expected thing in my framework.
|
| 338 | |
|
| 339 | | import { type BlogMeta, formatBlogMeta } from '@/blog/helpers.ts';
|
| 1 | export const blog: BlogMeta = { |
| 2 | title: "Marko is the coziest HTML templating language", |
| 3 | desc: "...todo...", |
| 4 | created: "2025-06-13", |
| 5 | draft: true, |
| 6 | }; |
| 7 | export const meta = formatBlogMeta(blob); |
| 8 | export * as layout from "@/blog/layout.tsx"; |
| 9 | |
| 10 | I've been recently playing around [Marko], and after adding limited support |
| 11 | for it in my website generator, [sitegen], I instantly fell in love with how |
| 12 | minimalistic it is in comparison to JSX, Astro components, and Svelte. |
| 13 | |
| 14 | [Marko]: https://next.markojs.com |
| 15 | [sitegen]: https://paperclover.dev/clo/sitegen |
| 16 | |
| 17 | ## Introduction to Marko |
| 18 | |
| 19 | If JSX was taking HTML and shoving its syntax into JavaScript, Marko is shoving |
| 20 | JavaScript into HTML. Attributes are JavaScript expressions. |
| 21 | |
| 22 | ```marko |
| 23 | <div> |
| 24 | // `input` is like props, but given in the top-level scope |
| 25 | <time datetime=input.date.toISOString()> |
| 26 | // Interpolation with JS template string syntax |
| 27 | ${formatTimeNicely(input.date)} |
| 28 | </time> |
| 29 | <div> |
| 30 | <a href=`/users/${input.user.id}`>${input.user.name}</a> |
| 31 | </div> |
| 32 | |
| 33 | // Capital letter variables for imported components |
| 34 | <MarkdownContent message=input.message /> |
| 35 | |
| 36 | // Components also can be auto-imported by lowercase. |
| 37 | // This will look upwards for a `tags/` folder containing |
| 38 | // "custom-footer.marko", similar to how Node.js finds |
| 39 | // package names in all upwards `node_modules` folders. |
| 40 | <custom-footer /> |
| 41 | </div> |
| 42 | |
| 43 | // ESM `import` / `export` just work as expected. |
| 44 | // I prefer my imports at the end, to highlight the markup. |
| 45 | import MarkdownContent from "./MarkdownContent.marko"; |
| 46 | import { formatTimeNicely } from "../date-helpers.ts"; |
| 47 | ``` |
| 48 | |
| 49 | Tags with the `value` attribute have a shorthand, which is used by the built-in |
| 50 | `<if>` for conditional rendering. |
| 51 | |
| 52 | ```marko |
| 53 | // Sugar for <input value="string" /> |
| 54 | <input="string" /> |
| 55 | |
| 56 | // and it composes amazingly to the 'if' built-in |
| 57 | <if=input.user> |
| 58 | <UserProfile=input.user /> |
| 59 | </if> |
| 60 | ``` |
| 61 | |
| 62 | 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/>`. |
| 63 | |
| 64 | ``` |
| 65 | <id/uniqueId /> |
| 66 | |
| 67 | <input id=uniqueId type="checkbox" name="allow_trans_rights" /> |
| 68 | <label for=uniqueId>click me!</> |
| 69 | // ^ oh, you can also omit the |
| 70 | // closing tag name if you want. |
| 71 | ``` |
| 72 | |
| 73 | It's important that I started with the two forms of "Tag I/O": `=` for input |
| 74 | and `/` for output. With those building blocks, we introduce local variables |
| 75 | with `const` |
| 76 | |
| 77 | ``` |
| 78 | <const/rendered = markdownToHtml(input.value) /> |
| 79 | |
| 80 | // This is how you insert raw HTML to the document |
| 81 | <inline-html=rendered /> |
| 82 | |
| 83 | // It supports all of the cozy destructuring syntax JS has |
| 84 | <const/{ id, name } = user /> |
| 85 | ``` |
| 86 | |
| 87 | Unlike JSX, when you pass content within a tag (`input.content` instead of |
| 88 | JSX's `children`), instead of it being a JSX element, it is actually a |
| 89 | function. This means that the `for` tag can render the content multiple times. |
| 90 | |
| 91 | ``` |
| 92 | <ul> |
| 93 | <for from=1 to=10> |
| 94 | // Renders a new random number for each iteration. |
| 95 | <li>${Math.random()}</li> |
| 96 | </> |
| 97 | </ul> |
| 98 | ``` |
| 99 | |
| 100 | Since `content` is a function, it can take arguments. This is done with `|` |
| 101 | |
| 102 | ``` |
| 103 | <h1>my friends</h1> |
| 104 | <ul> |
| 105 | // I tend to omit the closing tag names for the built-in control |
| 106 | // flow tags, but I keep them for HTML tags. It's kinda like how |
| 107 | // in JavaScript you just write `}` to close your `if`s and loops. |
| 108 | // |
| 109 | // Anyways <for> also has 'of' |
| 110 | <for|item| of=user.friends> |
| 111 | <li class="friend">${item.name}</li> |
| 112 | </> |
| 113 | |
| 114 | // They support the same syntax JavaScript function params allows, |
| 115 | // so you can have destructuring here too, and multiple params. |
| 116 | <for|{ name }, index| of=user.friends> |
| 117 | // By the way you can also use emmet-style class and ID shorthands. |
| 118 | <li.friend>My #${index + 1} friend is ${name}</li> |
| 119 | </> |
| 120 | </ul> |
| 121 | ``` |
| 122 | |
| 123 | Instead of named slots, Marko has attribute tags. These are more powerful than |
| 124 | slots since they are functions, and can also act as sugar for more complicated |
| 125 | attributes. |
| 126 | |
| 127 | ``` |
| 128 | <Layout title="Welcome"> |
| 129 | <@header variant="big"> |
| 130 | <h1>the next big thing</h1> |
| 131 | </@header> |
| 132 | |
| 133 | <p>body text...</p> |
| 134 | </Layout> |
| 135 | |
| 136 | // The `input` variable inside of <Layout /> is: |
| 137 | // |
| 138 | // { |
| 139 | // title: "Welcome", |
| 140 | // header: { |
| 141 | // content: /* function rendering "<h1>the next big thing</h1>" */, |
| 142 | // variant: "big", |
| 143 | // }, |
| 144 | // content: /* function rendering "<p>body text</p>" */ |
| 145 | // } |
| 146 | ``` |
| 147 | |
| 148 | This layout could be implemented as such: |
| 149 | |
| 150 | ```marko |
| 151 | <main> |
| 152 | <if=input.header /> |
| 153 | <const/{ ...headerProps, content }=input.header /> |
| 154 | <header ...headerProps> |
| 155 | // Instead of assigning to a variable with a capital letter, |
| 156 | // template interpolation works on tag names. This can also |
| 157 | // be a string to render the native HTML tag of that kind. |
| 158 | <${content} /> |
| 159 | </header> |
| 160 | <hr /> |
| 161 | </> |
| 162 | |
| 163 | <${input.content} /> |
| 164 | </main> |
| 165 | ``` |
| 166 | |
| 167 | The last syntax feature missing is calling a tag with parameters. That is done |
| 168 | just like a regular function call, with '('. |
| 169 | |
| 170 | ``` |
| 171 | <Something(item, index) /> |
| 172 | ``` |
| 173 | |
| 174 | In fact, attributes can just be sugar over this syntax. (this technically isn't |
| 175 | true but it's close enough for the example) |
| 176 | |
| 177 | ``` |
| 178 | <SpecialButton type="submit" class="red" /> |
| 179 | |
| 180 | // is equal to |
| 181 | |
| 182 | <SpecialButton({ type: "submit", class: "red" }) /> |
| 183 | ``` |
| 184 | |
| 185 | All of the above is about how Marko's syntax works, and how it performs HTML |
| 186 | generation with components. Marko also allows interactive components, but an |
| 187 | explaination of that is beyond the scope of this page, mostly since I have not |
| 188 | used it. A brief example of it, modified from their documentation. |
| 189 | |
| 190 | ```marko |
| 191 | // Reactive variables with <let/> just work... |
| 192 | <let/basicCounter=0 /> |
| 193 | <button onClick() { basicCounter += 1 }>${basicCounter}</button> |
| 194 | // ...but a counter is boring. |
| 195 | |
| 196 | <let/todos=[ |
| 197 | { id: 0, text: "Learn Marko" }, |
| 198 | { id: 1, text: "Make a Website" }, |
| 199 | ]/> |
| 200 | |
| 201 | // 'by' is like React JSX's "key" property, but it's optional. |
| 202 | <ul><for|todo, i| of=todos by=(todo => todo.id)> |
| 203 | <li.todo> |
| 204 | // this variable remains stable even if the list |
| 205 | // re-orders, because 'by' was specified. |
| 206 | <let/done=false/> |
| 207 | <label> |
| 208 | <span>${todo.text}</span> |
| 209 | // ':=' creates a two-way reactive binding, |
| 210 | // (it passes a callback for `checkedChanged`) |
| 211 | <input type="checkbox" checked:=done /> |
| 212 | </label> |
| 213 | <button |
| 214 | title="delete" |
| 215 | disabled=!done |
| 216 | onClick() { |
| 217 | todos = todos.toSpliced(i, 1); |
| 218 | } |
| 219 | > &times; </button> |
| 220 | </li> |
| 221 | </></ul> |
| 222 | |
| 223 | // Form example |
| 224 | <let/nextId=2/> |
| 225 | <form onSubmit(e) { |
| 226 | e.preventDefault(); |
| 227 | todos = todos.concat({ |
| 228 | id: nextId++, |
| 229 | // HTMLFormElement exposes all its named input |
| 230 | // elements as extra properties on the object. |
| 231 | text: e.target.text.value, |
| 232 | }); |
| 233 | // And you can clear it with 'reset()' |
| 234 | e.target.reset(); |
| 235 | }> |
| 236 | // We don't 'onChange' like a React loser. The form |
| 237 | // value can be read in the submit event like normal. |
| 238 | <input name="text" placeholder="Another Item"> |
| 239 | <button type="submit">Add</button> |
| 240 | </form> |
| 241 | ``` |
| 242 | |
| 243 | <SectionHeader updated="2025-08-11">Usage on `paperclover.net`</Section> |
| 244 | |
| 245 | Using Marko for HTML generation is quite easy. `.marko` files can be compiled |
| 246 | into `.js` using the `@marko/compiler` library. |
| 247 | |
| 248 | ```ts |
| 249 | const src = fs.readFileSync("page.marko", "utf8"); |
| 250 | const compile = marko.compileSync(src, filepath); |
| 251 | fs.writeFileSync("page.js", compile.code); |
| 252 | |
| 253 | const page = require("./page.js"); |
| 254 | console.info(page); |
| 255 | |
| 256 | import * as fs from "node:fs"; |
| 257 | import * as marko from "@marko/compiler"; |
| 258 | ``` |
| 259 | |
| 260 | To get client side JavaScript, an option can be passed to the Marko compiler to |
| 261 | generate the client side code. While it is a big selling point of Marko, I do |
| 262 | not use any of their client side features, instead deferring to manually-written |
| 263 | frontend scripts. This is because that is how my website has been for years, |
| 264 | statically generated. And for websites like mine that are content focused, this |
| 265 | is the correct way to do things. |
| 266 | |
| 267 | Since I have a custom HTML generation library (built on JSX and some React-like |
| 268 | patterns), I have written a simple integration for it to utilize Marko |
| 269 | components, which is loaded by replacing the generated import to `marko/html`, |
| 270 | which lets me overwrite functions like `createTemplate` (to change the signature |
| 271 | of a component), `dynamicTag` (to allow Marko to render non-Marko components), |
| 272 | and `fork` (to enable async integration with the rendering framework). An |
| 273 | additional feature of this is I have a Node.js loader hook to allow importing |
| 274 | these files directly. |
| 275 | |
| 276 | ```tsx |
| 277 | function Page() { |
| 278 | const q = Question.getByDate(new Date("2025-06-07 12:12 EST")); |
| 279 | return <div> |
| 280 | <h1>example question</h1> |
| 281 | <QuestionRender question={q} /> |
| 282 | </div>; |
| 283 | } |
| 284 | |
| 285 | // The synchronous render can be used because `Page` and `question.marko` |
| 286 | // do not await any promises (SQLite runs synchronously) |
| 287 | console.info(render.sync(<Page />).text); |
| 288 | |
| 289 | import * as render from "#engine/render"; |
| 290 | import QuestionRender from "@/q+a/tags/question.marko"; |
| 291 | import { Question } from "@/q+a/models/Question.ts"; |
| 292 | ``` |
| 293 | |
| 294 | Here is the `question.marko` tag used to render [questions on the clover q+a](/q+a). |
| 295 | |
| 296 | ```marko |
| 297 | // Renders a `Question` entry including its markdown body. |
| 298 | export interface Input { |
| 299 | question: Question; |
| 300 | admin?: boolean; |
| 301 | } |
| 302 | |
| 303 | // 2024-12-31 05:00:00 EST |
| 304 | export const transitionDate = 1735639200000; |
| 305 | |
| 306 | <const/{ question, admin } = input /> |
| 307 | <const/{ id, date, text } = question/> |
| 308 | |
| 309 | <${"e-"} |
| 310 | f=(date > transitionDate ? true : undefined) |
| 311 | id=admin ? `q${id}` : undefined |
| 312 | > |
| 313 | <if=admin> |
| 314 | <a |
| 315 | style="margin-right: 0.5rem" |
| 316 | href=`/admin/q+a/${id}` |
| 317 | >[EDIT]</a> |
| 318 | </> |
| 319 | <a> |
| 320 | <time |
| 321 | datetime=formatQuestionISOTimestamp(date) |
| 322 | >${formatQuestionTimestamp(date)}</time> |
| 323 | </a> |
| 324 | |
| 325 | <CloverMarkdown ...{ text } /> |
| 326 | </> |
| 327 | |
| 328 | // this singleton script will make all the '<time>' tags clickable. |
| 329 | client import "./clickable-links.client.ts"; |
| 330 | |
| 331 | import type { Question } from "@/q+a/models/Question.ts"; |
| 332 | import { formatQuestionTimestamp, formatQuestionISOTimestamp } from "@/q+a/format.ts"; |
| 333 | import { CloverMarkdown } from "@/q+a/clover-markdown.tsx"; |
| 334 | ``` |
| 335 | |
| 336 | The integration is great, `client import` is quite a magical concept, and I've |
| 337 | tuned it to do the expected thing in my framework. |
| 338 | |
| 339 | import { type BlogMeta, formatBlogMeta } from '@/blog/helpers.ts'; |