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

@chiselandco/nexus

v2.2.7

Published

Self-contained project portfolio components for Next.js App Router. Includes ProjectPortfolio, ProjectPortfolioClient (with built-in filtering), ProjectDetail, SimilarProjects, ProjectMenu, ProjectMenuClient, and GalleryCarousel. Pass a clientSlug and api

Readme

@chiselandco/nexus

A suite of self-contained project portfolio components for Next.js App Router. Drop in a clientSlug and apiBase — each component fetches, caches, and renders everything it needs with zero client-side waterfall requests.

Requirements

  • Next.js 13+ (App Router)
  • React 18+

No other dependencies required.

Installation

npm install @chiselandco/nexus

Quick Start

Here is the most common full setup — a projects grid page, a detail page with similar projects, and a megamenu in the nav.

// app/projects/page.tsx
import { ProjectPortfolio } from "@chiselandco/nexus"

export default async function ProjectsPage({
  searchParams,
}: {
  searchParams: { [key: string]: string | string[] | undefined }
}) {
  return (
    <ProjectPortfolio
      clientSlug="your-client-slug"
      apiBase="https://your-api.com"
      basePath="/projects"
      searchParams={searchParams}
    />
  )
}
// app/projects/[slug]/page.tsx
import { ProjectDetail, SimilarProjects } from "@chiselandco/nexus"

export default async function ProjectPage({ params }: { params: { slug: string } }) {
  return (
    <>
      <ProjectDetail
        slug={params.slug}
        clientSlug="your-client-slug"
        apiBase="https://your-api.com"
        backPath="/projects"
      />

      {/* Hardcode the slugs you want shown — update per client request */}
      <SimilarProjects
        projectSlugs={[
          "jacob-javits-convention-center",
          "tillamook-bay-community-college",
          "lcisd-liberty-hill-high-school",
        ]}
        excludeSlug={params.slug}
        clientSlug="your-client-slug"
        apiBase="https://your-api.com"
        basePath="/projects"
      />
    </>
  )
}
// app/api/chisel-menu/route.ts
import { createMenuHandler } from "@chiselandco/nexus"

export const GET = createMenuHandler({
  clientSlug: "your-client-slug",
  apiBase: "https://your-api.com",
})
// components/Nav.tsx — "use client" component in your header
"use client"
import { ProjectMenuClient } from "@chiselandco/nexus"

export function Nav() {
  return (
    <nav>
      {/* ... other nav items ... */}
      <ProjectMenuClient
        dataUrl="/api/chisel-menu"
        basePath="/projects"
        viewAllPath="/projects"
      />
    </nav>
  )
}

Components

ProjectPortfolio

A full server-rendered project grid page. Fetches all projects for a client and renders them as responsive baseball cards (1 column on mobile, 2 on tablet, 3 on desktop). Supports URL-driven filtering via searchParams.

// app/projects/page.tsx
import { ProjectPortfolio } from "@chiselandco/nexus"

export default async function ProjectsPage({
  searchParams,
}: {
  searchParams: { [key: string]: string | string[] | undefined }
}) {
  return (
    <ProjectPortfolio
      clientSlug="your-client-slug"
      apiBase="https://your-api.com"
      basePath="/projects"
      searchParams={searchParams}
    />
  )
}

| Prop | Type | Required | Default | Description | |---|---|---|---|---| | clientSlug | string | Yes | — | Identifies which client's projects to load | | apiBase | string | Yes | — | Base URL of the projects API | | basePath | string | No | "/projects" | Base path for project detail links | | searchParams | Record<string, string \| string[] \| undefined> | No | {} | Filter params forwarded to the API — pass Next.js searchParams directly | | revalidate | number | No | 86400 | Cache revalidation period in seconds (24 hours) |

URL-driven filtering

When a user clicks a "Browse By" filter link in the menu, they land on the grid with query params in the URL. Pass searchParams from the page and ProjectPortfolio forwards them to the API automatically.

Filter URLs follow this pattern — the key matches the custom field key in the schema:

/projects?filter[type]=commercial
/projects?filter[type]=educational-facilities

When active filters are applied, a filter banner is shown above the grid with a "Clear filters" link back to basePath.


ProjectPortfolioClient

A "use client" version of the project grid. Fetches all projects once on mount (module-level cached — no re-fetch on remount) and filters them locally in memory. Use this when you need the grid inside a client component tree, or when you want to build your own custom filter UI.

// Works in any component — no RSC required
import { ProjectPortfolioClient } from "@chiselandco/nexus"

export default function ProjectsPage() {
  return (
    <ProjectPortfolioClient
      clientSlug="your-client-slug"
      apiBase="https://your-api.com"
      basePath="/projects"
    />
  )
}

With a custom filter UI

Wire up your own dropdowns, buttons, or search inputs using the filters prop. Filter changes are instant — no API call on each change, all filtering happens in memory.

"use client"
import { useState } from "react"
import { ProjectPortfolioClient } from "@chiselandco/nexus"

export default function ProjectsPage() {
  const [filters, setFilters] = useState<Record<string, string>>({})

  return (
    <>
      {/* Your own filter UI */}
      <select onChange={(e) => setFilters({ type: e.target.value })}>
        <option value="">All Types</option>
        <option value="commercial">Commercial</option>
        <option value="educational-facilities">Educational</option>
      </select>

      <ProjectPortfolioClient
        clientSlug="your-client-slug"
        apiBase="https://your-api.com"
        basePath="/projects"
        filters={filters}
      />
    </>
  )
}

| Prop | Type | Required | Default | Description | |---|---|---|---|---| | clientSlug | string | Yes | — | Identifies which client's projects to load | | apiBase | string | Yes | — | Base URL of the projects API | | basePath | string | No | "/projects" | Base path for project detail links | | filters | Record<string, string> | No | {} | Active filters keyed by custom field key — filtering is instant, no API call on change | | columns | 2 \| 3 | No | 3 | Number of columns in the project grid | | font | string | No | System font stack | Font family string applied to all text |


ProjectDetail

A full server-rendered project detail page. Fetches a single project by slug and renders a hero image, a dynamic stats bar, a "Project Overview" section with description and specs sidebar, a photo gallery, and a back link.

// app/projects/[slug]/page.tsx
import { ProjectDetail } from "@chiselandco/nexus"

export default async function ProjectPage({ params }: { params: { slug: string } }) {
  return (
    <ProjectDetail
      slug={params.slug}
      clientSlug="your-client-slug"
      apiBase="https://your-api.com"
      backPath="/projects"
      backLabel="All Projects"
    />
  )
}

Stats bar

The stats bar below the hero is driven by an explicit ordered key list. Fields are shown in this order when present in the schema and populated on the project:

| Schema key | Label shown | |---|---| | location | Location | | type (badge field) | field name from schema | | coverage | Coverage | | year-completed | Completed | | architect | Architect | | general-contractor | General Contractor |

If a field doesn't exist in the schema for a given client, or the project has no value for it, that stat is silently omitted. The column count adjusts automatically — 2 columns on mobile, 3 on tablet, up to 6 on desktop.

Project Overview specs sidebar

The "Project Overview" section renders the project description on the left and a specs sidebar on the right (amber accent border). The sidebar shows the following fields when populated, in this order:

| Schema key | Label shown | |---|---| | systems-used | Systems Used | | systems | Track Systems | | series-used | Series Used | | operation-type | Operation Type | | finishes | Finishes | | specifications | Specifications |

Each group renders values as outlined pills. Fields with no value for the current project are omitted. Both the stats bar and the specs sidebar are fully schema-driven — if a key doesn't exist in a client's schema it is simply not shown, making ProjectDetail safe to reuse across clients with wildly different field configurations.

| Prop | Type | Required | Default | Description | |---|---|---|---|---| | slug | string | Yes | — | The project slug to load | | clientSlug | string | Yes | — | The client slug that owns this project | | apiBase | string | Yes | — | Base URL of the projects API | | backPath | string | No | "/projects" | Path for the back navigation link | | backLabel | string | No | "All Projects" | Label for the back navigation link | | revalidate | number | No | 86400 | Cache revalidation period in seconds (24 hours) | | noCache | boolean | No | false | Bypasses the Next.js Data Cache and sets cache: "no-store". Useful during development or for frequently updated projects. |


GalleryCarousel

A "use client" image carousel with previous/next arrows, a counter badge, and a scrollable thumbnail strip. Used internally by ProjectDetail but can also be used standalone if you fetch your own project data.

"use client"
import { GalleryCarousel } from "@chiselandco/nexus"

// `media` is the array of image objects returned by the projects API
export function ProjectGallery({ media, title }: { media: Media[]; title: string }) {
  return (
    <GalleryCarousel
      images={media}
      projectTitle={title}
    />
  )
}

The main image and thumbnails are standardised to a 16/9 aspect ratio — no external CSS required.

| Prop | Type | Required | Default | Description | |---|---|---|---|---| | images | Media[] | Yes | — | Array of media objects from the projects API | | projectTitle | string | Yes | — | Used as the alt text fallback for the main image |


SimilarProjects

A server-rendered similar projects section. Fetches all projects for a client, filters to those matching the provided field values, excludes the current project, and renders up to 3 matching results. Designed to be placed after ProjectDetail on a project detail page.

// app/projects/[slug]/page.tsx
import { ProjectDetail, SimilarProjects } from "@chiselandco/nexus"

export default async function ProjectPage({ params }: { params: { slug: string } }) {
  return (
    <>
      <ProjectDetail
        slug={params.slug}
        clientSlug="your-client-slug"
        apiBase="https://your-api.com"
      />
      <SimilarProjects
        filters={{ type: "commercial" }}
        excludeSlug={params.slug}
        clientSlug="your-client-slug"
        apiBase="https://your-api.com"
        basePath="/projects"
      />
    </>
  )
}

Deriving filters from the current project

In most real-world cases you want to match similar projects based on the current project's own field values. Fetch the project first, then pass its field values as filters:

// app/projects/[slug]/page.tsx
import { ProjectDetail, SimilarProjects } from "@chiselandco/nexus"

async function getProject(slug: string, clientSlug: string, apiBase: string) {
  const res = await fetch(
    `${apiBase}/api/v1/clients/${clientSlug}/projects/${slug}?api_key=YOUR_API_KEY`,
    { next: { revalidate: 86400 } }
  )
  return res.ok ? (await res.json())?.data : null
}

export default async function ProjectPage({ params }: { params: { slug: string } }) {
  const project = await getProject(params.slug, "your-client-slug", "https://your-api.com")
  const projectType = project?.custom_field_values?.type ?? null

  return (
    <>
      <ProjectDetail slug={params.slug} clientSlug="your-client-slug" apiBase="https://your-api.com" />
      {projectType && (
        <SimilarProjects
          filters={{ type: projectType }}
          excludeSlug={params.slug}
          clientSlug="your-client-slug"
          apiBase="https://your-api.com"
          basePath="/projects"
        />
      )}
    </>
  )
}

Manually specifying projects (recommended for quick setup)

Pass projectSlugs to hand-pick exactly which projects appear, in the order you specify. This overrides filters entirely — useful when you want to curate the section rather than rely on field matching, or when you need a reliable result quickly without worrying about field values matching.

This is the recommended approach for most client sites. The slugs are hardcoded in the page file. If a client wants different projects shown, update the slugs and redeploy.

// app/projects/[slug]/page.tsx
import { ProjectDetail, SimilarProjects } from "@chiselandco/nexus"

export default async function ProjectPage({ params }: { params: { slug: string } }) {
  return (
    <>
      {/* All your other page content above */}
      <ProjectDetail
        slug={params.slug}
        clientSlug="your-client-slug"
        apiBase="https://your-api.com"
        backPath="/projects"
      />

      {/* Similar projects hardcoded — update slugs per client request */}
      <SimilarProjects
        projectSlugs={[
          "jacob-javits-convention-center",
          "tillamook-bay-community-college",
          "lcisd-liberty-hill-high-school",
        ]}
        excludeSlug={params.slug}
        clientSlug="your-client-slug"
        apiBase="https://your-api.com"
        basePath="/projects"
      />
    </>
  )
}

excludeSlug is still respected even in projectSlugs mode — if the current page's slug appears in the list it is automatically removed so a project never links to itself.

To update which projects appear, find the projectSlugs array in the page file and swap in the new slugs. Project slugs are visible in the URL when browsing the portfolio: /projects/jacob-javits-convention-center → slug is jacob-javits-convention-center.

Card variant

Use variant="card" to render the same baseball-card style used in ProjectPortfolio instead of the default list style:

<SimilarProjects
  filters={{ type: "commercial" }}
  excludeSlug={params.slug}
  clientSlug="your-client-slug"
  apiBase="https://your-api.com"
  basePath="/projects"
  variant="card"
/>

| Prop | Type | Required | Default | Description | |---|---|---|---|---| | clientSlug | string | Yes | — | Identifies which client's projects to load | | apiBase | string | Yes | — | Base URL of the projects API | | filters | Record<string, string> | No | {} | Key/value pairs to filter projects by custom field values. All filters must match (AND logic) | | excludeSlug | string | No | — | Slug of a project to exclude from results (e.g. the current project) | | basePath | string | No | "/projects" | Base path for project detail links | | projectSlugs | string[] | No | — | Explicit list of project slugs to show, in order. When provided, overrides filters entirely. e.g. ["jacob-javits", "tillamook-bay"] | | maxItems | number | No | 3 | Maximum number of projects to show | | title | string | No | "Similar Projects" | Heading text for the section. e.g. "Featured Projects" | | subtitle | string | No | "More Work" | Small uppercase label above the heading. e.g. "Our Work" | | variant | "list" \| "card" | No | "list" | "list" uses the border-bottom separator style; "card" renders full baseball-card style matching ProjectPortfolio | | font | string | No | System font stack | Font family string applied to all inline styles | | revalidate | number | No | 86400 | Cache revalidation period in seconds (24 hours) | | noCache | boolean | No | false | Bypasses the Next.js Data Cache and sets cache: "no-store". Useful during development or for frequently updated projects. |


ProjectMenu

A server-rendered megamenu component. Shows featured projects as compact cards on the left and "Browse By" filter links on the right. Designed to be dropped directly into a navigation dropdown.

// components/MegaMenu.tsx
import { ProjectMenu } from "@chiselandco/nexus"

// Must be a Server Component — do NOT add "use client"
export async function ProjectsMegaMenu() {
  return (
    <ProjectMenu
      clientSlug="your-client-slug"
      apiBase="https://your-api.com"
      basePath="/projects"
      subtitle="Our systems are installed in every geographic region of the U.S."
      maxProjects={6}
    />
  )
}

With a curated menu

Pass menuId to show a specific curated set of projects instead of all projects. The value should be the menu slug (not the UUID) — available slugs can be retrieved from GET /api/v1/clients/{clientSlug}/menus. The Browse By filters on the right always reflect the full schema regardless of which menu is active.

<ProjectMenu
  clientSlug="your-client-slug"
  apiBase="https://your-api.com"
  menuId="main-nav"
  basePath="/projects"
/>

| Prop | Type | Required | Default | Description | |---|---|---|---|---| | clientSlug | string | Yes | — | Identifies which client's projects to load | | apiBase | string | Yes | — | Base URL of the projects API | | menuId | string | No | — | Slug of a curated menu. When provided, fetches from /menus/{slug} instead of all projects. Filters always shown regardless. | | basePath | string | No | "/projects" | Base path for project detail links | | viewAllPath | string | No | Same as basePath | Path for the "View All Projects" link | | subtitle | string | No | — | Description paragraph shown above the project cards | | font | string | No | System font stack | Font family string applied to all inline styles | | maxProjects | number | No | 6 | Maximum number of projects to display | | revalidate | number | No | 86400 | Cache revalidation period in seconds (24 hours) | | noCache | boolean | No | false | Bypasses the Next.js Data Cache and sets cache: "no-store". Useful during development or for frequently updated projects. |


ProjectMenuClient + createMenuHandler

ProjectMenuClient is a "use client" megamenu component. Use it when your nav or header is a client component. It fetches and caches data on first mount so the API is never called twice on re-hover or remount.

There are two ways to set it up:


Option 1 — dataUrl + createMenuHandler (recommended for production)

Create one API route once. The data is server-cached for 24 hours — most users never trigger a call to the upstream API at all.

// app/api/chisel-menu/route.ts
import { createMenuHandler } from "@chiselandco/nexus"

export const GET = createMenuHandler({
  clientSlug: "your-client-slug",
  apiBase: "https://your-api.com",
})

For a curated menu, pass menuId to the handler:

// app/api/chisel-menu/route.ts
export const GET = createMenuHandler({
  clientSlug: "your-client-slug",
  apiBase: "https://your-api.com",
  menuId: "main-nav",
})

Then pass dataUrl to the component:

// components/Nav.tsx
"use client"
import { ProjectMenuClient } from "@chiselandco/nexus"

export function Nav() {
  return (
    <ProjectMenuClient
      dataUrl="/api/chisel-menu"
      basePath="/projects"
      viewAllPath="/projects"
      subtitle="Explore our portfolio of projects."
      maxProjects={6}
    />
  )
}

Option 2 — Direct fetch (quick setup / non-Next.js environments)

No API route needed. The component fetches directly from the upstream API on first mount and caches the result in memory for the session. Note: this exposes the API call to the client browser.

"use client"
import { ProjectMenuClient } from "@chiselandco/nexus"

export function Nav() {
  return (
    <ProjectMenuClient
      clientSlug="your-client-slug"
      apiBase="https://your-api.com"
      basePath="/projects"
      viewAllPath="/projects"
    />
  )
}

With a curated menu:

<ProjectMenuClient
  clientSlug="your-client-slug"
  apiBase="https://your-api.com"
  menuId="main-nav"
  basePath="/projects"
  viewAllPath="/projects"
/>

| Prop | Type | Required | Default | Description | |---|---|---|---|---| | dataUrl | string | No* | — | URL of a local API route created with createMenuHandler(). Recommended for production | | clientSlug | string | No* | — | Client slug for direct fetch mode | | apiBase | string | No* | — | API base URL for direct fetch mode | | menuId | string | No | — | Slug of a curated menu. Fetches from /menus/{slug} for projects. Filters are always shown regardless. | | noCache | boolean | No | false | Bypasses the module-level data cache and sets cache: "no-store" on all fetch calls. Useful during development or when projects update frequently. | | basePath | string | Yes | — | Base path for project detail links | | viewAllPath | string | Yes | — | Path for the "View All Projects" link | | subtitle | string | No | — | Description shown above the project cards (hidden on mobile) | | font | string | No | System font stack | Font family string | | maxProjects | number | No | 6 | Maximum number of projects to display (capped at 3 on mobile) |

*One of dataUrl or clientSlug + apiBase must be provided.


Server vs Client Components

All top-level components except ProjectMenuClient, ProjectPortfolioClient, and GalleryCarousel are async Server Components. They must be rendered in a server context:

// CORRECT
export default async function Page() {
  return <ProjectPortfolio clientSlug="..." apiBase="..." />
}

// WRONG — causes a runtime error
"use client"
export default function Page() {
  return <ProjectPortfolio clientSlug="..." apiBase="..." />
}

If your parent component uses "use client", use the client variants instead (ProjectMenuClient, ProjectPortfolioClient) or pass the server components as children from a server parent.

| Component | Type | Use when | |---|---|---| | ProjectPortfolio | Server | Page-level grid, URL-driven filters | | ProjectPortfolioClient | Client | Inside a client tree, custom filter UI | | ProjectDetail | Server | Project detail page | | GalleryCarousel | Client | Standalone image gallery | | ProjectMenu | Server | Server-rendered nav dropdown | | ProjectMenuClient | Client | Client-rendered nav/header | | SimilarProjects | Server | After ProjectDetail on detail pages |


Caching

| Component | Server Cache | Client Cache | |---|---|---| | ProjectMenu (RSC) | 24h via next.revalidate | — | | ProjectMenuClient + createMenuHandler | 24h (route handler) | Per-session module cache | | ProjectMenuClient (direct fetch) | None | Per-session module cache | | ProjectPortfolio (RSC) | 24h via next.revalidate | — | | ProjectPortfolioClient | None | Per-session module cache | | ProjectDetail (RSC) | 24h via next.revalidate | — | | SimilarProjects (RSC) | 24h via next.revalidate | — |

Bypassing the cache

| Method | Scope | Use case | |---|---|---| | ?bust=1 on /api/chisel-menu | Single request | Dev/testing after a CMS change | | CHISEL_CACHE_BYPASS=true env var | Entire deployment | Staging environments | | revalidateTag("chisel-menu-{clientSlug}") | Server cache | CMS webhook on content publish | | noCache: true on ProjectMenu | Single render | Debug during development |

// CMS webhook — invalidate server cache on publish
import { revalidateTag } from "next/cache"
revalidateTag("chisel-menu-your-client-slug")

// For a curated menu, the tag includes the menuId
revalidateTag("chisel-menu-your-client-slug-main-nav")

Publishing to npm

npm login
cd package
npm run build
npm publish --access public

To release an update, bump the version field in package/package.json then run npm publish again.