Blogging

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.

#blogging#article#typography#header#no-js

Demo

Preview

Engineering

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.

A sun over green hills
Illustration: the islands of a page, by Lin Wu.

Week notes: 43

  • Week notes

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.qwe
main.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>}
Thread
85Useful? Like it, or add to its thread below.

Thread 0 posts

On PostHeader: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.