Skip to content
txbrendan/ui

Blog Page

beta

A whole blog index, built from our website components: a floating navbar, a page title, every post with the newest drawn wide and a row of tags to narrow them, a closing band, and a footer.

Preview

The blog. What we ship, and how we work.

Product news, guides, and notes from the team.

  • Product, 4 min read

    Shared folders are here

    Put files in one folder that the whole team can open, and choose who can change them.

    Priya Raman
  • Engineering, 9 min read

    Making sync faster on large teams

    What we changed so a team of a thousand sees edits in under a second.

    Hana Novak
  • Company, 2 min read

    We opened a second office

    More room for the team, and a place to meet the people we work with.

    Marcus Bell
  • Engineering, 6 min read

    How to plan a release

    The steps a team can follow from the first list of changes to the day it ships.

    Tomás Okafor

Plan your team’s week in one place.

Free for one team. No card needed.

Install

  1. 1. Add the registry to components.json, once per app. Without it, the shadcn CLI looks up @txbrendan in a public list that txbrendan/ui does not control.

    npx shadcn@latest registry add "@txbrendan=https://txbrendan.dev/r/{name}.json"
  2. 2. Add the theme, once per app. It replaces your color tokens with ones checked for contrast.

    npx shadcn@latest add @txbrendan/theme
  3. 3. Add the template. It also adds every component it uses. Its own files go in your components folder, and they are yours to edit.

    npx shadcn@latest add @txbrendan/blog-page

Use it for

  • The list of posts on a product site's blog. Render it as the whole page, from your app's page file.
  • Write every word in the props: one object for the navbar, the header, the posts, the closing band, and the footer.
  • Pass the posts newest first, each with an `href` to its own page. Post Page draws that page.
  • Give each post one `tag`, so the row of tags can narrow the list. Set `posts.filter` to false to hide the row.
  • Pick a post count that fills the rows: the newest drawn wide, then rows of three, like 7 or 10.
  • Pass your router's link once in `renderLink`. The navbar, the posts, the closing band, and the footer all use it. From a server page, render the page from a client file, since a server page cannot pass a function.
  • Set `embedded` when you show the page inside another page, like a gallery, so it has no second main.

Avoid it for

  • Hundreds of posts on one page. Show the newest, and split the rest across pages.
  • A tag on only one post. Each tag should narrow the list to a few.

Props

  • navbar

    The Navbar's props. It floats as a pill once the page scrolls, and folds into a menu below 42rem.

    NavbarProps

  • header

    The headline at the top of the page. It renders as the page's one h1.

    HeadlineProps

  • posts

    Required. Post Grid's props, like `posts`. `featured` and `filter` default to true here. Each post's title is an h2, right under the page's h1.

    PostGridProps

  • cta

    The closing band above the footer.

    CallToActionBandProps

  • footer

    The Footer's props.

    FooterProps

  • renderLink

    Draws every link in the navbar, the posts, the closing band, and the footer, unless a part sets its own.

    (props: NavbarLinkProps) => ReactNode · default a plain <a>

  • embedded

    Renders the content in a div, not a main, for a preview inside another page.

    boolean · default false

  • className

    Extra classes on the root, which holds the container query.

    string

Accessibility

  • The content sits in a main landmark, between the navbar's nav and the footer, so skip links and screen readers find it.
  • The header renders the page's one h1, and each post's title is an h2 under it.
  • Each part keeps its own accessibility, listed on its page.

Examples

A short blog index

<BlogPage
  navbar={{ brand: "Workspace", links: [{ href: "/blog", label: "Blog", current: true }] }}
  header={{ title: "The blog.", muted: "What we ship, and how we work." }}
  posts={{
    posts: [
      { href: "/blog/shared-folders", title: "Shared folders are here", tag: "Product", date: "2026-10-28" },
      { href: "/blog/plan-a-release", title: "How to plan a release", tag: "Guides", date: "2026-10-12" },
    ],
  }}
  footer={{
    brand: "Workspace",
    columns: [{ title: "Company", links: [{ href: "/blog", label: "Blog" }] }],
    legal: "© 2026 Workspace",
  }}
/>