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.
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
── 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 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.
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.