Static copy
Copied from docs/en/packages/aipo-html.md in https://github.com/poppy-team/aipo-lang (MIT). Pinned to revision ce47c4e070404fc41722ccd8e2b7b808b99062a8, blob 23e2e8c4cdb4d6de9776430f8634ee7490cea9d4. The source repository remains canonical; this copy is refreshed through a sync pull request, not live.
aipo.html is the canonical package for the Aipo programming language designed to build modern web applications, Single Page Applications (SPAs), and Server-Side Rendered (SSR) interfaces.
It features:
do ... end or { ... } blocks.render_to_string): Instant static HTML5 generation with XSS mitigation via automatic entity escaping in text bodies and attributes.fragment): Render multiple sibling elements without generating extra wrapper nodes.css): Deterministic class hashing, pseudo-classes (hover, active, focus), and responsive @media query blocks with <head> inlining (get_injected_css).mount, Cmd): The Elm Architecture pattern supporting state updates and command dispatch chains [model, cmd].In your project's aipo.toml:
[dependencies]
"aipo.html" = { path = "packages/aipo-html" }aipo.html provides tag functions that take attributes as dictionaries and child nodes via trailing closure blocks:
import aipo.html as h
let page = h.div({ "class": "container", "id": "main" }) do
h.header do
h.h1("Aipo Portal")
end
h.main do
h.section({ "class": "hero" }) do
h.p("Declarative and deterministic web development.")
h.button("Explore", { "class": "btn-primary" })
end
end
h.footer do
h.span("© 2026 Aipo Lang")
end
endSelf-closing elements (such as img, input, hr, br) are rendered without closing tags:
let avatar = h.img("avatar.png", "User Avatar", { "class": "rounded-full" })
let email_field = h.input({ "type": "email", "placeholder": "contact@aipo.dev" })
let line = h.hr()When you need to return multiple sibling nodes without an extra wrapping element:
let list_items = h.fragment do
h.li("First item")
h.li("Second item")
h.li("Third item")
endThe render_to_string function serializes a virtual node tree into safe HTML5 markup:
import aipo.html as h
let doc = h.div({ "class": "article-card" }) do
h.h2("Escaping <script> & Safe")
h.p("Audited content with entity escaping.")
h.input({ "disabled": true, "type": "text" })
end
let html = h.render_to_string(doc)h.css generates unique class hashes and isolates styling scope:
import aipo.html as h
let btn_style = h.css({
"padding": 12,
"background": "#6366f1",
"border_radius": 8,
"color": "#ffffff",
"hover": {
"background": "#4f46e5"
},
"media": {
"(min-width: 768px)": {
"padding": 20
}
}
})
let button_node = h.button("Action", { "class": btn_style })
# SSR Inlining into HTML <head>:
let css_head = h.get_injected_css()For interactive SPAs, aipo.html implements the Model-View-Update pattern supporting command chains:
import aipo.html as h
struct Model {
count
}
fn update(m, msg) {
if msg == "inc" {
return Model{ count: m.count + 1 }
} elif msg == "dec" {
return Model{ count: m.count - 1 }
} elif msg == "reset_and_double" {
return [Model{ count: 10 }, h.cmd_msg("inc")]
}
return m
}
fn view(m, dispatch) {
return h.div({ "class": "counter-box" }) do
h.h1(f"Current count: {m.count}")
h.button("+1", { "on_click": fn () { dispatch("inc") } })
h.button("-1", { "on_click": fn () { dispatch("dec") } })
end
}
let app = h.mount("#app", Model{ count: 0 }, update, view)