An HTML app is a web page with data-kedge bindings. Kedge compiles its reads, writes, and schema at deploy time and renders requests against the app's local database replica. HTML apps also integrate with app authentication, account controls, and ownership-aware writes.

a guestbook

~/guestbook-html$ show index.html
── index.html
<!doctype html>
<title>Guestbook</title>

<form data-kedge="messages">
  <label>Message <input name="text" required maxlength="280"></label>
  <button>post</button>
</form>

<ul data-kedge="messages?$order=-created_at">
  <template><li>{{text}}</li></template>
  <li data-when=":empty">No messages yet.</li>
</ul>

~/guestbook-html$ kedge up

The form creates messages records. The list renders its <template> once per record. Native input constraints are enforced on the server. Bound regions update live; the form remains a normal POST and redirect without JavaScript.

HTML and Markdown

Markdown directives are shorthand for the same HTML:

~/guestbook-markdown$ show index.md
── index.md
# Guestbook

:::form{bind="messages"}
:input[Message]{name=text required maxlength=280}
:button[post]
:::

:::each{bind="messages?$order=-created_at" as=ul}
{{text}}
::empty[No messages yet.]
:::

~/guestbook-markdown$ kedge up

Markdown lowers to HTML before compilation. Raw HTML remains available in a Markdown file. The HTML app reference lists both forms.

Kedger News

~/kedge-news$ show index.md
── index.md
---
title: Kedger News
style: /style.css
---

# [⚓︎](/) [Kedger News](/) :account[login]{provider=github next="/" score="me/votes?author=$viewer&$via=story:stories,comment:comments&$tally"}

:::details[submit]{.submit}
:::form{bind="stories"}
:input[title]{name=title required maxlength=120}
:input[url]{name=url type=url required placeholder="https://…"}
:button[submit]
:::
:::

:::each{bind="stories?$order=-created_at" as=ol .stories}
:button[▲]{bind="me/votes/self?story={{id}}&$toggle" when="!:mine" .vote aria-label="upvote"}
[{{title}}]({{url}}) _({{url | host}})_

:value[{{count}} points]{bind="me/votes?story={{id}}&$tally"}
by :author[{{author.name}}] {{created_at | ago}} |
:value[{{count}} comments]{bind="comments?story={{id}}&$count" href="/item/{{id}}"}

::empty[No stories yet.]
:::

_served from {{$dc.metro}}_

~/kedge-news$ kedge up

Kedger News includes story submission, discussions, private votes, public scores, ownership controls, GitHub login, and shared styling.

how it works

The sections below break index.md and item/[id].md into the bindings for stories, votes, discussions, and identity.

create stories

:::form{bind="stories"}
:input[title]{name=title required maxlength=120}
:input[url]{name=url type=url required placeholder="https://…"}
:button[submit]
:::

The form creates a record and infers title and url from its controls.

render stories

:::each{bind="stories?$order=-created_at" as=ol}
[{{title}}]({{url}}) _({{url | host}})_
by :author[{{author.name}}] {{created_at | ago}}
::empty[No stories yet.]
:::

The binding orders records; interpolations and formatters render each row.

tally private votes

:button[▲]{bind="me/votes/self?story={{id}}&$toggle" when="!:mine"}
:value[{{count}} points]{bind="me/votes?story={{id}}&$tally"}

me/votes is private to the current author. $tally exposes only the count.

bind a discussion route

item/[id].md serves /item/:id:

:::record{bind="stories/{{$url.id}}"}
# {{title}}
:::

:::form{bind="comments?story={{$url.id}}"}
:textarea[]{name=text required maxlength=2000 aria-label=comment}
:button[add comment]
:::

{{$url.id}} selects the story and is sealed into each new comment.

attach identity

:account[login]{provider=github next="/"}
:button[delete]{bind="comments/{{id}}?$delete" when=":mine"}

The account control supplies login and logout. :mine gates the control; the server enforces ownership. Use app authentication to protect a route before it reaches the page.

escape hatches

Use a SQLite view for a read beyond the binding grammar. Use a handler for transactions or domain logic. Both use the shared database.

Routes and assets follow the site model.