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 upThe 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.
Submitting applies in the page immediately: the new row renders locally, marked pending until the server confirms it. Offline, writes queue in the browser and replay on reconnect, and visited pages reload from a local cache. The reference lists the limits.
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 upMarkdown 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 item/[id].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}}_
── item/[id].md
---
title: Kedger News
style: /style.css
---
# [⚓︎](/) [Kedger News](/) [stories](/) :account[login]{provider=github next="/item/{{$url.id}}" score="me/votes?author=$viewer&$via=story:stories,comment:comments&$tally"}
:::record{bind="stories/{{$url.id}}" .story}
: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="#comments"}
:::
:::form{bind="comments?story={{$url.id}}" .submit .comment-form}
:textarea[]{name=text required maxlength=2000 aria-label=comment}
:button[add comment]
:::
:::each{bind="comments?story={{$url.id}}&$order=created_at" as=ol #comments .comments}
:button[▲]{bind="me/votes/self?comment={{id}}&$toggle" when="!:mine" .vote aria-label="upvote comment"}
:value[{{count}} points]{bind="me/votes?comment={{id}}&$tally" when=":mine"}
by :author[{{author.name}}] · {{created_at | ago}}
:button[delete]{bind="comments/{{id}}?$delete" when=":mine" .text-action}
{{text}}
::empty[No comments yet.]
:::
~/kedge-news$ kedge upKedger 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.
agents
An agent is a member of the app declared in the page. It runs when records arrive, on a schedule, or when a viewer finds a record stale; it reads only what the page grants and writes only the fields it is given, through the same validators as a form. Its answer appears in the page as it is written.
<template data-kedge="comments" data-agent="Helper"
data-reads="dishes" data-creates="comments">
Answer questions about this potluck from the dish list. If a comment is not a
question for you, write nothing.
</template>
The answer is an ordinary record, so {{author.name}} says Helper, live
regions update when it lands, and the owner can delete it like any other row.
The reference lists the attributes, the
privacy bound, and the per-app budget.
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.