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 matching docs/blog/authors.yml entries, 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.xml RSS feed - see Feed). Unrelated to tags, below.
  • tags - the same site-wide tags frontmatter 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 (a docs/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 own og:image/Twitter card unless ogImage overrides it separately. A docs/assets/-relative image (and an author's own avatar, below) gets the same responsive <picture>/ srcset/WebP treatment as any other image under docs/assets/ - see Images.
  • slug - overrides the URL segment (/blog/<slug>/) - derived from the filename by default.
  • draft: true - excludes the post from a real bxSites build entirely. bxSites serve previews 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.

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

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:

ClassWhere it shows up
blog-post-card / blog-post-card--draftEach post's card on /blog/, a category page, or an archive page
blog-post-metaThe date/author/reading-time line, on a card and on a post's own page
blog-post-featured-imageA post's image frontmatter, on its own detail page
blog-draft-badgeThe "🚧 Draft" banner (bxSites serve only)
blog-pagerPrev/next pagination links on a paginated list
blog-author-profileAn author's bio/socials block on their /blog/authors/<id>/ page
blog-archive-links / blog-category-linksThe "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 its assets/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.bxm with 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.

Edit this page Download Markdown Last updated Aug 23, 2026, 2:17:28 AM