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