PostHeader
The top of a post: category, title and standfirst, the authors with their pictures, published and updated dates, reading time, tags and a cover.
Demo
Islands, resumed: how a page wakes up where the server left it
The browser takes the server's elements as its own instead of drawing them again. Here is what that means for forms, scrolling and images.
Week notes: 43
Get it
It needs theme.qwe, as every component does; or take the whole kit, quillo-ui.qwe. Each component brings its own styles to the pages that show it.
$ curl -O https://quillolang.org/components/ui/theme.qwe$ curl -O https://quillolang.org/components/ui/post_header.qwemain.qwe
import ui/theme.qwe use theme_cssuse ui/post_header.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>Example
The code of the preview above.
demos/post_header.qwe
let post_header_demo_cover = `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 800 400'%3E%3Crect width='800' height='400' fill='%23264a6e'/%3E%3Ccircle cx='620' cy='110' r='54' fill='%23f3b54a'/%3E%3Cpath d='M0 300 Q120 250 240 290 T480 280 T800 270 V400 H0z' fill='%2318794e'/%3E%3Cpath d='M0 340 Q160 310 320 340 T800 330 V400 H0z' fill='%230f5133'/%3E%3C/svg%3E`view PostHeaderDemo() = <div.demo-stack style="gap: 40px"> <PostHeader id="post-header-demo" title="Islands, resumed: how a page wakes up where the server left it" subtitle="The browser takes the server's elements as its own instead of drawing them again. Here is what that means for forms, scrolling and images." category="Engineering" category_href="#post_header-engineering" authors={[{ name: "Ada Lovelace", href: "#post_header-ada" }, { name: "Grace Hopper" }, { name: "Margaret Hamilton", href: "#post_header-margaret" }]} date="2026-10-14" updated="2026-10-20T08:15:00Z" words={1850} tags={["Quillo", "Browsers", "Deep dive"]} tag_href="#post_header-tag-" image={post_header_demo_cover} image_alt="A sun over green hills" caption="Illustration: the islands of a page, by Lin Wu."/> <PostHeader id="post-header-demo-short" title="Week notes: 43" author="Ben Okafor" date="2026-10-24" updated="2026-10-24" read_time="2 min read" align="center" level={2} tags={["Week notes"]}/> </div>Source
ui/post_header.qwe
// PostHeader: the top of a blog post: its category, the title, a line under it,// who wrote it (with their pictures, or their initials), when it was published and// last updated, how long it takes to read, its tags and a cover picture with a caption.//// <PostHeader title="Islands, resumed" subtitle="How a page wakes up where the server left it."// category="Engineering" category_href="/blog/engineering"// authors={[{ name: "Ada Lovelace", href: "/authors/ada" }]}// date="2026-10-14" updated="2026-10-20" words={1850} tags={["Quillo", "Browsers"]}// tag_href="/blog/tag/" image="/img/islands.jpg" image_alt="Islands seen from a plane"/>//// title: the post's title, its <h1> (level 2 makes it an <h2>). subtitle: a line under it,// the standfirst. category: a small label over the title, a link with category_href.// authors: a list of { name, href?, src? } (src a picture, else the initials); or author,// author_href and author_src for one. date: when it was published, an ISO date or// date-time, shown as "Oct 14, 2026" in a <time>; updated: when it last changed (shown// only when it is another day). read_time: "6 min read" as it should read; or words, the// count, read at wpm words a minute (230) and rounded up. tags: short words; tag_href// makes each a link to that address plus the tag in small letters (rel="tag"). image,// image_alt, caption: the cover, what it shows, and a line under it (credit). align:// "start" (default) or "center". locale: how dates are written ("en-US"). id: the// <header>'s id; its title gets id + "-title".import ui/theme.qwe use ui_clslet post_header_css = css`.ui-bhead { display: grid; gap: 14px; min-width: 0; margin: 0; font: 15px/1.5 var(--ui-font); color: var(--ui-fg); }.ui-bhead-center { justify-items: center; text-align: center; }.ui-bhead-kicker { margin: 0; font-size: 13px; font-weight: 650; letter-spacing: .06em; text-transform: uppercase; color: var(--ui-accent); }.ui-bhead-kicker a { color: inherit; text-decoration: none; border-radius: 3px; }.ui-bhead-kicker a:hover { text-decoration: underline; }.ui-bhead-kicker a:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-bhead .ui-bhead-title { margin: 0; max-width: 24ch; font-size: clamp(28px, 5vw, 42px); font-weight: 750; line-height: 1.12; letter-spacing: -.02em; text-wrap: balance; }.ui-bhead-dek { margin: 0; max-width: 60ch; color: var(--ui-muted); font-size: 19px; line-height: 1.45; text-wrap: pretty; }.ui-bhead-byline { display: flex; align-items: center; gap: 12px; margin-top: 4px; }.ui-bhead-faces { display: flex; flex: none; }.ui-bhead-face { display: inline-grid; place-items: center; width: 40px; height: 40px; overflow: hidden; border: 2px solid var(--ui-bg); border-radius: 50%; background: var(--ui-accent-soft); color: var(--ui-accent); font-size: 14px; font-weight: 650; }.ui-bhead-face + .ui-bhead-face { margin-left: -12px; }.ui-bhead-face img { width: 100%; height: 100%; object-fit: cover; }.ui-bhead-who { display: grid; gap: 1px; min-width: 0; text-align: start; }.ui-bhead-by { margin: 0; font-weight: 600; }.ui-bhead-by a { color: inherit; text-decoration: none; border-radius: 3px; }.ui-bhead-by a:hover { color: var(--ui-accent); text-decoration: underline; }.ui-bhead-by a:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-bhead-when { display: flex; flex-wrap: wrap; gap: 0 8px; margin: 0; color: var(--ui-muted); font-size: 14px; }.ui-bhead-when > * + *::before { content: "·"; margin-right: 8px; }.ui-bhead-tags { display: flex; flex-wrap: wrap; gap: 6px; margin: 0; padding: 0; list-style: none; }.ui-bhead-center .ui-bhead-tags, .ui-bhead-center .ui-bhead-byline { justify-content: center; }.ui-bhead-tag { display: inline-block; padding: 2px 10px; border: 1px solid var(--ui-border); border-radius: 999px; color: var(--ui-muted); font-size: 13px; text-decoration: none; }a.ui-bhead-tag:hover { border-color: var(--ui-accent); color: var(--ui-accent); }a.ui-bhead-tag:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-bhead-cover { display: grid; gap: 8px; width: 100%; margin: 10px 0 0; }.ui-bhead-cover img { display: block; width: 100%; aspect-ratio: 2 / 1; object-fit: cover; border-radius: var(--ui-radius); background: var(--ui-subtle); }.ui-bhead-cover figcaption { color: var(--ui-muted); font-size: 13px; text-align: start; }`// An ISO date or date-time as the locale writes a date ("Oct 14, 2026"); other text as it is.fn post_header_date(d: str, locale: str) -> str = date.valid(d) ? date.format(slice(d, 0, 10), { locale, style: "medium" }) : d// The initials of a name: "Ada Lovelace" is "AL".fn post_header_initials(name: str) -> str = split(trim(name), " ") | filter((w) => w != "") | take(2) | map((w) => upper(slice(w, 0, 1))) | join("")// Names as a sentence says them: "Ada", "Ada and Ben", "Ada, Ben and Cy".fn post_header_and(items: list) { let n = len(items) for i in range(n) => [i == 0 ? null : (i == n - 1 ? " and " : ", "), items[i]]}view PostHeader(title: str, subtitle: str? = null, category: str? = null, category_href: str? = null, authors: list? = null, author: str? = null, author_href: str? = null, author_src: str? = null, date: str? = null, updated: str? = null, read_time: str? = null, words: int? = null, wpm: int = 230, tags: list? = null, tag_href: str? = null, image: str? = null, image_alt: str = "", caption: str? = null, level: int = 1, align: str = "start", locale: str = "en-US", id: str? = null, class: str? = null) { style global post_header_css let people = authors ?? (author != null ? [{ name: author, href: author_href, src: author_src }] : []) let minutes = words != null ? max([1, ceil(words / wpm)]) : null let reading = read_time ?? (minutes != null ? `${minutes} min read` : null) let changed = updated != null && (date == null || slice(updated, 0, 10) != slice(date, 0, 10)) let title_id = id != null ? `${id}-title` : null let heading = level == 2 ? <h2.ui-bhead-title id={title_id}>{title}</h2> : <h1.ui-bhead-title id={title_id}>{title}</h1> <header id={id} class={ui_cls(["ui-bhead", align == "center" && "ui-bhead-center", class])}> {category != null && <p.ui-bhead-kicker>{category_href != null ? <a href={category_href}>{category}</a> : category}</p>} {heading} {subtitle != null && <p.ui-bhead-dek>{subtitle}</p>} {(len(people) > 0 || date != null || reading != null) && <div.ui-bhead-byline> {len(people) > 0 && <span.ui-bhead-faces aria-hidden="true"> {for p in take(people, 3) => <span.ui-bhead-face> {p.src != null ? <img src={p.src} alt="" loading="lazy"/> : post_header_initials(p.name)} </span>} </span>} <div.ui-bhead-who> {len(people) > 0 && <p.ui-bhead-by>By {post_header_and(people | map((p) => p.href != null ? <a href={p.href} rel="author">{p.name}</a> : <span>{p.name}</span>))}</p>} {(date != null || reading != null) && <p.ui-bhead-when> {date != null && <span>Published <time datetime={date}>{post_header_date(date, locale)}</time></span>} {changed && <span.ui-bhead-updated>Updated <time datetime={updated}>{post_header_date(updated, locale)}</time></span>} {reading != null && <span.ui-bhead-read>{reading}</span>} </p>} </div> </div>} {tags != null && len(tags) > 0 && <ul.ui-bhead-tags aria-label="Tags"> {for t in tags => <li>{tag_href != null ? <a.ui-bhead-tag href={`${tag_href}${url_encode(lower(t))}`} rel="tag">{t}</a> : <span.ui-bhead-tag>{t}</span>}</li>} </ul>} {image != null && <figure.ui-bhead-cover> <img src={image} alt={image_alt}/> {caption != null && <figcaption>{caption}</figcaption>} </figure>} </header>}Related
SidenoteNotes in the margin beside the line they belong to, numbered or not; on a narrow page they fold, and the number opens each under its line.8 views · 7 downloadsPostUpdatesThe note that a post changed since it came out: Updated Oct 28, how many changes, and each one newest first, with corrections marked.8 views · 9 downloadsReaderSettingsA post's text the way each reader likes it: size, serif or sans, line length, line spacing and light or dark, kept for their next visit.9 views · 6 downloadsArticleHeaderThe top of a story: its label and section, the headline, the dek, every author grouped by role, published and updated times, and the reading time.8 views · 8 downloadsAuthorBioThe box at the end of a post about its author: picture or initials, name and role, a short bio, links elsewhere, their post count and a follow link.8 views · 9 downloadsBlogrollThe blogs a writer recommends, in groups: name, a line about each, its latest post and how long ago, its feed, and the list as OPML.9 views · 8 downloads
Thread
Thread 0 posts
On PostHeader: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.