npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

vite-plugin-builder-io-start

v0.1.4

Published

Wraps TanStack Start's Vite plugin with Builder.io CMS-driven prerendering and spec-compliant sitemap generation.

Readme

vite-plugin-builder-io-start

CI npm version license

A thin composition layer over TanStack Start's Vite plugin that drives prerendering from live Builder.io content and emits a spec-compliant sitemap.xml. It queries the Builder Content API for every published page, feeds those paths into tanstackStart's prerender.pages config, and writes the sitemap into the client build output. It is not a fork of tanstackStart — it wraps it, forwards your options through, and owns exactly two of them.

Why it exists. Two reasons. First, TanStack Start's built-in sitemap declares the https://www.sitemaps.org/schemas/sitemap/0.9 namespace URI. Namespace URIs are opaque identifiers compared by exact string equality, not URLs that get fetched — so the https:// variant is a different namespace from the one the sitemap protocol defines, and strict validators reject it. This package emits the correct http:// form. Second, a hard-coded page list drifts from the CMS the moment someone publishes a page; coupling prerendering to the live Builder content means a new page is crawlable on the next build without a code change.

Install

It only runs at build time, so it belongs in devDependencies:

npm i -D vite-plugin-builder-io-start
# or
pnpm add -D vite-plugin-builder-io-start

vite (>=7) and @tanstack/react-start are peer dependencies and are not bundled. A peer dependency means you must have it installed — it says nothing about which section it belongs in, and peers are satisfied from either. Leave them where your app already has them. A TanStack Start app keeps @tanstack/react-start in dependencies, because application code imports it at runtime; only vite-plugin-builder-io-start itself is a devDependency:

{
  "dependencies": {
    "@tanstack/react-start": "^1.168.0"        // runtime — unchanged
  },
  "devDependencies": {
    "vite": "^8.1.5",                          // build-time — unchanged
    "vite-plugin-builder-io-start": "^0.1.3"   // build-time
  }
}

Usage

// vite.config.ts
import { builderIOStart } from 'vite-plugin-builder-io-start'
import { defineConfig } from 'vite'

export default defineConfig(({ command }) => ({
  plugins: [
    builderIOStart({
      apiKey: process.env.BUILDER_API_KEY!,
      command,
      host: 'https://example.com',
    }),
  ],
}))

builderIOStart is async — the Builder fetch has to resolve before tanstackStart() is constructed, because its pages config is read at plugin-creation time rather than lazily. You do not need to await it yourself: Vite's plugins array has accepted promise entries since Vite 3 and awaits and flattens them during config resolution. Dropping the call straight into the array, as above, is the intended usage.

If you already have other async setup in your config, the awaited-and-spread form works too:

export default defineConfig(async ({ command }) => ({
  plugins: [
    ...(await builderIOStart({
      apiKey: process.env.BUILDER_API_KEY!,
      command,
      host: 'https://example.com',
    })),
  ],
}))

A fuller example

builderIOStart({
  apiKey: process.env.BUILDER_API_KEY!,
  command,
  host: 'https://example.com',
  model: 'landing-page',
  priorities: { '/': 1.0, '/pricing': 0.8 },
  defaultPriority: 0.5,
  underConstructionPaths: new Set(['/careers']),
  prerender: { concurrency: 4, retryCount: 2 },
  tanstackStartOptions: {
    srcDirectory: 'app',
  },
})

Options

| Option | Type | Default | Description | | --- | --- | --- | --- | | apiKey | string | — | Required. Builder.io Public API key (read-only). Validated unconditionally, including on serve. Never pass a Private API Key — see API keys. | | command | 'build' \| 'serve' | — | Required. Vite's resolved command. Pass through from defineConfig(({ command }) => …). | | host | string | — | Required. Absolute origin used to build sitemap <loc> entries, e.g. https://example.com. A trailing slash is stripped. | | apiBaseUrl | string | https://cdn.builder.io | Origin of the Builder Content API. For pointing integration tests at a stand-in server. apiKey is sent to whatever origin this names, so only point it at a server you control. | | model | string | 'page' | Builder.io model name to query for pages. | | priorities | Record<string, number> | {} | Per-path sitemap priority overrides. Unlisted paths fall back to defaultPriority. | | defaultPriority | number | 0.5 | Default sitemap priority for paths not listed in priorities. | | underConstructionPaths | Set<string> | new Set() | Paths to fetch from Builder but exclude from prerendering and the sitemap. | | prerender | { concurrency?, retryCount?, failOnError? } | { failOnError: true } | Forwarded to tanstackStart's prerender config. | | notFound | false \| { path?, outputPath? } | { path: '/404', outputPath: '404.html' } | Prerendered not-found document, written to the output root because that is what static hosts serve for unknown paths. Deliberately excluded from the sitemap. false emits none. | | changefreq | ChangeFreq | — | Written for every sitemap entry when set. A hint crawlers largely disregard. | | robots | { enabled?, disallow? } | { enabled: false } | Emit a robots.txt pointing at the generated sitemap. See robots.txt. | | tanstackStartOptions | Omit<TanStackStartOptions, 'prerender' \| 'pages'> | {} | Additional options merged into the config passed to tanstackStart(). Typed against the plugin's real config type, so you get autocomplete and typo rejection. prerender and pages are computed internally and omitted here so there is exactly one place to set them. |

robots.txt

A sitemap nothing points at is only half the job — crawlers discover sitemaps through the Sitemap: directive in robots.txt. Opt in:

robots: { enabled: true, disallow: ['/admin'] }

which writes, alongside sitemap.xml:

User-agent: *
Allow: /
Disallow: /admin

Sitemap: https://example.com/sitemap.xml

It is opt-in, not automatic. Many apps already ship a hand-written public/robots.txt, which Vite copies into the build output. Enabling this without noticing would discard crawler rules someone cared enough to write, so if a robots.txt is already present the build fails rather than overwriting it.

underConstructionPaths are not added to disallow automatically. It looks like the obvious default, but robots.txt is world-readable, so listing a path there announces that it exists — a URL nobody has linked becomes trivially discoverable. Add them yourself if being crawled is the bigger concern than being found.

API keys

Pass your Public API Key — the read-only one, found in Space Settings or via the command palette. Builder documents it as safe to expose, so it needs no secret handling and can be committed if that suits your setup.

Do not pass a Private API Key. It grants write access to your Space, and this package sends the key as a query parameter, where it is captured by CI logs, proxy logs, and shell history. A Private Key would otherwise appear to work, since it can read as well — which is what makes the mistake easy to miss.

This is enforced, not just documented: a key with the bpk- private-key prefix throws before any request is made, including on serve where no fetch happens at all. Public keys are bare hex and cannot carry that prefix, so a valid key is never rejected.

Behaviour notes

Dev skips the fetch, not the validation. On command === 'serve' there is no prerendering, so the Builder round trip is skipped and an empty page list is used. The apiKey check still runs — a missing key fails loudly at dev-server start rather than surfacing days later as a mysteriously empty sitemap.

Failures throw; they never degrade to partial results. A non-OK Builder response or a zero-result query throws. Shipping a build that quietly dropped pages from the crawlable surface is the kind of regression nobody notices until organic traffic has already fallen off, so this package refuses to produce one.

Builder paths are normalised before use. data.url is set by whoever edits your CMS, and it feeds two things that must agree: the path Start prerenders and the <loc> written to the sitemap. Start resolves .. segments before writing files, so an unnormalised path would advertise a URL the build never produced. Paths are canonicalised (traversal segments resolved, leading slash enforced, duplicate slashes collapsed) with query, hash, and trailing slash preserved. A value that cannot be a path on your host — an absolute or protocol-relative URL — throws rather than being coerced into a silently wrong entry.

Pagination runs to completion. The Builder Content API caps results at 100 per request. fetchPrerenderPages pages through with offset until a short page comes back, so sites over 100 pages are not silently truncated.

Only the client environment writes the sitemap. A Start build runs writeBundle once per environment (client, ssr, …), each with its own outDir. Writing from any other environment would emit a stray sitemap into the server bundle or race the client write.

No retry/backoff on the Builder fetch. Retries are a prerender concern already covered by tanstackStart's own retryCount.

Also exported

builderIOStart is the main entry point, but the pieces are exported individually if you need them:

import { fetchPrerenderPages, sitemapPlugin } from 'vite-plugin-builder-io-start'
import type { BuilderIOStartOptions, PrerenderPage } from 'vite-plugin-builder-io-start'

Scope

Deliberately narrow for a first version: no CLI, and no framework adapters beyond @tanstack/react-start.

License

MIT