caliper uidocs

Blog post template

Block

One article at a reading measure: a progress line under the header, an outline that marks where you are, linkable headings, the author and three related posts.

Installs components/ui/blog-post-template.tsxDepends on @radix-ui/react-slot, lucide-reactMotion none in component source
pnpm dlx shadcn@latest add https://caliperui.site/r/blog-post-template.json
Skip to content

Field notes7 min read

How Ostra Freight closed September in a day and a half

Their finance team of three used to lose the first week of every month to bank matching. Here is what changed, in their words and their numbers.

Inês DuarteCustomer team, Lisbon
ostra.halden.app/reports/close-time

Days to close, last nine months

Ostra's close time by month, from their Halden reports.

Where the week went

Ostra books around 2,400 bank lines a month across four currencies. Before Halden, two people matched them by hand against invoices in NetSuite, starting on the first working day.

“We weren’t reviewing anything,” says Marta, their controller. “We were searching. By the time the matching was done, there was no time left to look at what the numbers meant.”

Rules that learn from corrections

The team started with the 40 vendors that make up most of their volume. Each correction they made became a rule, and by the second month Halden matched 93% of lines before anyone opened the close.

“The first month felt like training a colleague. The second, the colleague did the work.”
  • Payouts from Stripe and Adyen match by payout ID.
  • Fuel cards match by statement date and total.
  • Anything over €10,000 always waits for a person.

A close that is a review

What is left on the first morning is a short list: lines that differ by more than a rounding amount, new vendors, and anything flagged by the €10,000 rule. In September that list was 61 lines long.

Marta’s team cleared it before lunch on the second day, and the board pack went out that afternoon. Read how review works in the docs.

What they are doing next

With the time back, Ostra is moving its intercompany eliminations into Halden in November, and plans to close Q4 in a single day.

See a sample close

When to use #

Use it for

  • A company blog, engineering notes or a long announcement.
  • Articles long enough to need an outline.

Skip it when

  • A list of posts; use the blog grid.
  • Product documentation; use the docs template.

Installation #

terminal
pnpm dlx shadcn@latest add https://caliperui.site/r/blog-post-template.json

Adds its 9 files to components/ui/, each with its own stylesheet, and installs their packages. No account needed. The first part also adds Caliper UI’s tokens; import them once as in Installation.

Usage #

This is the code behind the preview above.

example.tsx
import { BlogPostTemplate } from "@/components/ui/blog-post-template"

const pages = ["blog-post-template", "about-template", "contact-template", "legal-template", "error-template", "profile-page"];

const related = (stem: string, ...extra: string[]) => [...pages.filter((item) => item !== stem).slice(0, 2), ...extra];

const nav = (current: string) =>
  [
    { label: "Product", href: "#product" },
    { label: "Customers", href: "#customers" },
    { label: "Blog", href: "#blog" },
    { label: "About", href: "#about" },
    { label: "Contact", href: "#contact" },
  ].map((link) => ({ ...link, current: link.label.toLowerCase() === current }));

const signIn = { label: "Sign in", href: "#" };

const frame = "h-[720px] w-full overflow-auto rounded-[14px] shadow-[0_0_0_1px_var(--border)]";

function CloseChart() {
  const days = [9.4, 8.8, 8.1, 7.2, 6.4, 5.1, 3.9, 2.6, 1.5];
  return (
    <div className="grid gap-3 p-5">
      <p className="m-0 text-[13px] text-[var(--k-muted)]">Days to close, last nine months</p>
      <svg viewBox="0 0 360 120" role="img" aria-label="Days to close fell from 9.4 in January to 1.5 in September" className="h-auto w-full">
        {days.map((value, index) => (
          <g key={index}>
            <rect x={index * 40 + 6} y={120 - value * 11} width={26} height={value * 11} rx={4} fill={index === days.length - 1 ? "var(--k-hi)" : "var(--k-line-strong)"} />
          </g>
        ))}
      </svg>
    </div>
  );
}

export function BlogPostDemo() {
  return (
    <div className={frame}>
      <BlogPostTemplate
        brand="Halden"
        nav={nav("blog")}
        signIn={signIn}
        primary={heroes.primary!}
        post={{
          category: "Field notes",
          title: "How Ostra Freight closed September in a day and a half",
          lead: "Their finance team of three used to lose the first week of every month to bank matching. Here is what changed, in their words and their numbers.",
          date: "2026-10-07",
          readingMinutes: 7,
          url: "https://halden.app/blog/ostra-close",
          author: { name: "Inês Duarte", role: "Customer team, Lisbon", href: "#ines", bio: "Inês helps finance teams move their close onto Halden. Before that she ran month-end at a logistics company for six years." },
          cover: <CloseChart />,
          coverTitle: "ostra.halden.app/reports/close-time",
          coverCaption: "Ostra's close time by month, from their Halden reports.",
          sections: [
            {
              id: "before",
              title: "Where the week went",
              body: (
                <>
                  <p>Ostra books around 2,400 bank lines a month across four currencies. Before Halden, two people matched them by hand against invoices in NetSuite, starting on the first working day.</p>
                  <p>“We weren’t reviewing anything,” says Marta, their controller. “We were searching. By the time the matching was done, there was no time left to look at what the numbers meant.”</p>
                </>
              ),
            },
            {
              id: "rules",
              title: "Rules that learn from corrections",
              body: (
                <>
                  <p>The team started with the 40 vendors that make up most of their volume. Each correction they made became a rule, and by the second month Halden matched <strong>93%</strong> of lines before anyone opened the close.</p>
                  <blockquote>“The first month felt like training a colleague. The second, the colleague did the work.”</blockquote>
                  <ul>
                    <li>Payouts from Stripe and Adyen match by payout ID.</li>
                    <li>Fuel cards match by statement date and total.</li>
                    <li>Anything over €10,000 always waits for a person.</li>
                  </ul>
                </>
              ),
            },
            {
              id: "review",
              title: "A close that is a review",
              body: (
                <>
                  <p>What is left on the first morning is a short list: lines that differ by more than a rounding amount, new vendors, and anything flagged by the €10,000 rule. In September that list was 61 lines long.</p>
                  <p>Marta’s team cleared it before lunch on the second day, and the board pack went out that afternoon. Read how review works in <a href="#docs">the docs</a>.</p>
                </>
              ),
            },
            {
              id: "next",
              title: "What they are doing next",
              body: <p>With the time back, Ostra is moving its intercompany eliminations into Halden in November, and plans to close Q4 in a single day.</p>,
            },
          ],
        }}
        next={{ label: "See a sample close", href: "#" }}
        related={[
          { title: "The month-end checklist we give every new customer", href: "#a", category: "Guides", date: "2026-09-22", excerpt: "Fourteen steps, who owns each, and what Halden does for you." },
          { title: "Two-way NetSuite sync is here", href: "#b", category: "Product", date: "2026-09-09", excerpt: "Entries posted in either place appear in both within a minute." },
          { title: "Why we never auto-post large entries", href: "#c", category: "Field notes", date: "2026-08-28", excerpt: "A rule we will not let customers turn off, and the story behind it." },
        ]}
        footer={footers}
      />
    </div>
  );
}

API reference #

BlogPostTemplate

component
PropTypeDefaultDescription
brand *string—
navreadonly BlockNavLink[]—
signInBlockAction—
primaryBlockAction—
post *BlogArticle—
relatedreadonly BlogArticleRelated[][]
nextBlockAction—A closing action under the article, e.g. "Try the close checklist".
footerTemplateFooter—
classNamestring—

Accessibility #

It is built on native elements and keeps aria-hidden, aria-labelledby and aria-label in step with state.

No custom key handling; Tab and the native activation keys do the work.

Motion #

It declares no animation in its component source. Any transition comes from its stylesheet, which uses the duration tokens that collapse to instant under prefers-reduced-motion.