Blog
On this page
Blog
A blog is another by-convention feature, the same shape as
versions/i18n or the
tags index - drop posts under
docs/blog/posts/, and BX Sites builds /blog/ (paginated), a category page
per category, a year archive page per calendar year, an author page per
author, an RSS feed per category plus one for the whole blog, and a
/blog/stats/ page, with zero config required. A project with no
docs/blog/posts/ folder simply doesn't have a blog - nothing else changes.
Writing a post
Every .md file under docs/blog/posts/, at any depth, is a post -
subfolders are entirely optional and purely for your own editing
convenience. A flat folder works fine for a handful of posts; once you're
into the hundreds, filing posts under docs/blog/posts/2026/ (or
docs/blog/posts/2026/03/, or any scheme you like) keeps your editor's
file tree browsable without renaming anything or touching a frontmatter
date-prefix convention. None of it affects the built site - a post's sort
order, its year archive, and its URL (blog/<slug>/) are all derived from
frontmatter alone, never from where the file happens to live, so a post's
folder and its actual date are always free to disagree:
docs/blog/posts/
├── hello-world.md (flat is fine too)
├── 2026/
│ ├── announcing-2-0.md
│ └── 03/
│ └── a-deep-dive.md
Frontmatter, for any post regardless of where it's filed:
---
title: Announcing BoxLang 2.0
date: 2026-08-15
authors: [lmajano]
categories: [Releases]
tags: [boxlang, release]
summary: A faster runtime, a smaller footprint, and a few surprises.
image: assets/blog/boxlang-2-cover.png
---
A short intro paragraph or two.
<!-- more -->
The rest of the post - everything below the `<!-- more -->` marker is left
out of the excerpt shown on `/blog/` and category pages, but still renders
in full on the post's own page.
date(required) - any BX Sites can parse (2026-08-15, or a full date-time). Sets the post's own sort order (newest first) and its<pubDate>/article:published_time.authors- a list of ids matchingdocs/blog/authors.ymlentries, or a plain name with no matching entry (rendered as unlinked text rather than failing the build - handy for a one-off guest post).categories- a post's own taxonomy, each getting its own/blog/category/<slug>/page (and its own/blog/category/<slug>/feed.xmlRSS feed - see Feed). Unrelated totags, below.tags- the same site-widetagsfrontmatter every other page already has (see Getting Started) - a post's tags render as badges and fold into the main/tags/index alongside every other tagged page.summary- a one-line excerpt shown on/blog//category pages and in the RSS feed, used when a post has no<!-- more -->marker. Without either, BX Sites falls back to a plain-text truncation of the post's own body.image- a featured image (adocs/assets/-relative path, or a full URL) - shown at the top of the post and as a thumbnail on every list/ category card. Also becomes the post's ownog:image/Twitter card unlessogImageoverrides it separately. Adocs/assets/-relative image (and an author's ownavatar, below) gets the same responsive<picture>/srcset/WebP treatment as any other image underdocs/assets/- see Images.slug- overrides the URL segment (/blog/<slug>/) - derived from the filename by default.draft: true- excludes the post from a realbxSites buildentirely.bxSites servepreviews it anyway (with a visible "🚧 Draft" banner on the post itself and a dashed-border card wherever it's listed), so you can proofread a draft locally before it's ready - see Previewing drafts.
Every other page-level frontmatter key already documented in
Getting Started (icon, description,
ogImage, toc) works on a post too.
Featured images and other blog assets
docs/assets/blog/ is nothing special beyond an ordinary subfolder of
docs/assets/ (already copied to site/assets/ wholesale) - it's just
where this guide (and the by-convention author avatar lookup below) expects
post covers/author photos to live, so a project's own docs/assets/
doesn't get cluttered mixing blog images in with the rest of its diagrams
and icons. Nothing enforces the location - any docs/assets/** path works
in image/avatar.
Authors
docs/blog/authors.yml is optional - one entry per author id, referenced
by a post's own authors list:
lmajano:
name: Luis Majano
title: CEO, Ortus Solutions
bio: >
Founder of Ortus Solutions and creator of ColdBox, WireBox, and
BoxLang. Building developer tools since 2005.
url: https://github.com/lmajano
email: lmajano@ortussolutions.com
socials:
github: https://github.com/lmajano
twitter: https://x.com/lmajano
Only name is required. Every author referenced by at least one post gets
their own /blog/authors/<id>/ page (bio, socials, every post they've
written) - an author nobody's credited yet doesn't get a page, even if
they're in the roster.
Avatar, by convention - drop a file at
docs/assets/blog/authors/<id>.{jpg,jpeg,png,webp,svg} and it's picked up
automatically, no avatar: key needed. An explicit avatar in
authors.yml (a URL or a docs/assets/-relative path) always overrides
the by-convention lookup.
Categories, archives, pagination, and the "Blog" nav entry
Every distinct categories value across all posts gets its own
/blog/category/<slug>/ page, listing just that category's own posts.
Every calendar year with at least one post also gets its own
/blog/archive/<year>/ page (/blog/archive/2026/, /blog/archive/2025/,
...), derived entirely from each post's own date frontmatter - no folder
structure or filename convention required, so where a post's .md file
actually lives under docs/blog/posts/ (flat, or split into your own
subfolders for easier browsing while editing) never has to match its
date. The main /blog/ list gets "Browse by year"/"Browse by category"
links blocks, each with a post count per year/category, automatically once
posts span more than one year/category - a single year or category alone
isn't worth a links block, so it's left off either way.
The main /blog/ list, every category page, and every year archive page
all paginate identically - blog.postsPerPage in the site config controls
how many posts per page (default 10); page 2 onward moves to
.../page/2/, .../page/3/, etc.
A single "Blog" entry is added to the main nav automatically, once
docs/blog/posts/ has at least one non-draft post - no nav/docs/nav.json
change needed. By default it's appended last, after everything else. To put
it somewhere specific instead, add your own entry with an explicit url
(bypasses the usual path-must-match-a-real-page rule, since the blog isn't
a docs/ page) to your nav array or docs/nav.json - doing so suppresses
the auto-appended one entirely, so there's never a duplicate:
{ "nav": [
{ "path": "index.md" },
{ "title": "Blog", "url": "blog/index.html", "icon": "lucide:newspaper" },
{ "path": "about.md" }
] }
Individual posts aren't added to the nav themselves (same as the tags
index) - they're reachable from /blog/, their own category page, their
own year archive, their author's page, search, and each other's prev/next
links (posts chronologically adjacent to one another, independent of the
regular nav's own prev/next chain).
Every post's own meta line (on its card and its detail page) also shows an estimated reading time next to the date - a rough word-count / 200wpm estimate, the same ballpark figure most reading-time features use, not configurable.
Feed
/blog/feed.xml - a standard RSS 2.0 feed of the most recent posts, newest
first, written whenever the site config resolves an absolute baseURL
(same requirement as sitemap.xml) and blog.feed isn't set to false.
Every category also gets its own filtered feed at
/blog/category/<slug>/feed.xml. Both are capped to blog.feedLimit posts
(default 25) - most feed readers only care about what's new, so an
unbounded feed on a large blog just wastes bandwidth on every poll; set it
to 0 for every post, uncapped:
{ "blog": { "postsPerPage": 10, "feed": true, "feedLimit": 25 } }
Previewing drafts
draft: true keeps a post out of a real bxSites build entirely - but
bxSites serve includes it anyway, so you can read through a draft (and
click every link, check the featured image, see how it lists on /blog/)
before it's ready. A previewed draft always carries a visible "🚧 Draft"
banner - on its own detail page, and as a dashed-border card wherever it's
listed (the main /blog/ list, its own category/archive/author pages) -
so there's never any ambiguity about what's actually published. Stop
bxSites serve and run bxSites build and the same draft is gone, exactly
as if it didn't exist.
Stats
/blog/stats/ - a handful of aggregate cards about the blog as a whole:
total posts, total words written, average reading time, category/
contributor/year counts, and three "spotlight" cards (longest post, most
active category, most active author) each linked to the real page they're
about. Computed purely from the posts already loaded for this build - no
separate analytics, no tracking, nothing persisted between builds - and
always built, even for a brand-new blog with zero posts yet. Linked from
the bottom of the main /blog/ list.
SEO and social
Every post already gets everything a normal page does (<meta name="description">,
og:description, og:image+twitter:card when an image is set - see
Configuration: ogImage) plus a few
post-specific tags every built-in theme adds automatically: og:type is
"article" instead of "website", and article:published_time/
article:author (one per credited author who has a url set in
authors.yml) are included in the page <head>.
Search
Posts are indexed into the same search-index.json every other page is
(module spec section 7) - no separate blog search UI, the existing search
box already finds posts alongside docs pages.
Customizing the blog's appearance
There's no separate "blog theme" to write - every blog page (the main
/blog/ list, a category/archive/author page, /blog/stats/, and each
post's own detail page) renders through the exact same layout.bxm/
page.bxm as any other page in your site, so a blog automatically looks
like the rest of your docs, and any theme override you've already made
(see Themes) applies to it unchanged, with
no extra wiring.
The blog-specific markup itself (post cards, the date/author/reading-time
meta line, the pager, an author's profile block, the "Browse by year"/
"Browse by category" link lists) is built as plain HTML with a handful of
fixed class names, then dropped into page.contentHtml just like a
converted Markdown page:
| Class | Where it shows up |
|---|---|
blog-post-card / blog-post-card--draft | Each post's card on /blog/, a category page, or an archive page |
blog-post-meta | The date/author/reading-time line, on a card and on a post's own page |
blog-post-featured-image | A post's image frontmatter, on its own detail page |
blog-draft-badge | The "🚧 Draft" banner (bxSites serve only) |
blog-pager | Prev/next pagination links on a paginated list |
blog-author-profile | An author's bio/socials block on their /blog/authors/<id>/ page |
blog-archive-links / blog-category-links | The "Browse by year"/"Browse by category" link blocks on /blog/ |
Two ways to restyle it, same as any other page:
- A quick visual tweak - target these classes from your own
extraCss, the same way you'd customize a theme's colors. A built-in theme's own rules for these classes live in itsassets/style.css(e.g.resources/themes/bootstrap/assets/style.css) if you want a starting point to override. - Structural changes - since blog pages share
layout.bxm/page.bxmwith everything else, overriding a theme (or writing one from scratch) changes the blog's chrome (header, nav, footer, article wrapper) right along with every other page - there's no separate blog template to copy.
What you can't do is swap out the post-card/pager/author-profile markup
itself for your own - it's generated once by BlogBuilder.bx, not read
from a template file in theme/, so restyling it with CSS (above) is the
supported path rather than a per-component override.