Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
.idea
dist
node_modules
.cursor
.cursorrules
.nvmrc
.progress
.vscode
.orchestrator
1 change: 0 additions & 1 deletion .nvmrc

This file was deleted.

144 changes: 119 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
| **CI** | `.github/workflows/ci.yml` | [![CI — main](https://img.shields.io/github/actions/workflow/status/ameshkin/nextcrumbs/ci.yml?branch=main\&label=ci%20\(main\))](https://github.com/ameshkin/nextcrumbs/actions/workflows/ci.yml) [![CI — dev](https://img.shields.io/github/actions/workflow/status/ameshkin/nextcrumbs/ci.yml?branch=dev\&label=ci%20\(dev\))](https://github.com/ameshkin/nextcrumbs/actions/workflows/ci.yml) |
| **npm package** | [@ameshkin/nextcrumbs](https://www.npmjs.com/package/@ameshkin/nextcrumbs) | [![npm](https://img.shields.io/npm/v/@ameshkin/nextcrumbs.svg)](https://www.npmjs.com/package/@ameshkin/nextcrumbs) |
| **Install** | `npm i @ameshkin/nextcrumbs` | [![install test](https://img.shields.io/github/actions/workflow/status/ameshkin/nextcrumbs/ci-main.yml?branch=main\&label=install%20test)](https://github.com/ameshkin/nextcrumbs/actions/workflows/ci-main.yml) |
| **Peer deps** | `@mui/material@^7`, `@mui/icons-material@^7`, `react@>=18`, `react-dom@>=18` | ![peer deps](https://img.shields.io/badge/peer_deps-MUI%207%20%7C%20Icons%207%20%7C%20React%2018-blue) |
| **Peer deps** | `@mui/material@^7`, `@mui/icons-material@^7`, `react@>=18`, `react-dom@>=18`<br/>(Optional: `react-router-dom@>=6`) | ![peer deps](https://img.shields.io/badge/peer_deps-MUI%207%20%7C%20Icons%207%20%7C%20React%2018-blue) |
| **Types** | TypeScript | ![types](https://img.shields.io/badge/types-TypeScript-blue.svg) |
| **License** | MIT | [![license](https://img.shields.io/badge/license-MIT-black.svg)](LICENSE) |
| **Bundle size** | bundlephobia | [![bundle size](https://img.shields.io/bundlephobia/minzip/%40ameshkin%2Fnextcrumbs?label=bundle%20size)](https://bundlephobia.com/package/@ameshkin/nextcrumbs) |
Expand All @@ -27,7 +27,9 @@
* [Props](#props)
* [Icon & Separator Examples](#icon--separator-examples)
* [Accessibility & SEO](#accessibility--seo)
* [Docs](#docs)
* [Dependency Checks](#dependency-checks)
* [Troubleshooting](#troubleshooting)
* [FAQ](#faq)
* [Contributing](#contributing)
* [License](#license)
Expand All @@ -39,8 +41,8 @@
* ✅ **MUI** Breadcrumbs under the hood (accessible, stable)
* ✅ **Next.js** ready via pluggable `LinkComponent` (e.g., `next/link`)
* ✅ Optional **URL → items** helper for App Router (`usePathBreadcrumbs`)
* ✅ **React Router** helper hook (`useReactRouterBreadcrumbs`)
* ✅ Built-in **schema.org/BreadcrumbList** microdata via `withSchema`
* ✅ **React Router** helper hook (`useReactRouterBreadcrumbs`) - optional dependency
* ✅ Fully typed with TypeScript

---

Expand All @@ -60,22 +62,27 @@
npm i @ameshkin/nextcrumbs
# peer deps
npm i @mui/material @mui/icons-material react react-dom
# optional for React Router examples
# optional: only needed if using useReactRouterBreadcrumbs hook
npm i react-router-dom
```

> **Note for Next.js projects:** If you're using Next.js and don't need React Router, you can either:
> - Install `react-router-dom` as a dev dependency: `npm i -D react-router-dom`
> - Or configure webpack to ignore it (see [Troubleshooting](#troubleshooting))

---

## Quick Start (Next.js)

```tsx
"use client";
import Link from "next/link";
import { Breadcrumbs } from "@ameshkin/nextcrumbs";
import { NextCrumb } from "@ameshkin/nextcrumbs";
// Also available as: import { Breadcrumbs } from "@ameshkin/nextcrumbs";

export default function HeaderTrail() {
return (
<Breadcrumbs
<NextCrumb
LinkComponent={Link}
items={[
{ label: "Dashboard", href: "/" },
Expand Down Expand Up @@ -103,6 +110,7 @@ export default function AutoTrail() {
const pathname = usePathname();
const items = usePathBreadcrumbs(pathname, {
baseHref: "/",
rootLabel: "Home",
labelMap: { new: "Create" },
exclude: ["_private"]
});
Expand All @@ -119,8 +127,7 @@ See full guide: [`docs/react-router.md`](./docs/react-router.md)

```tsx
import { MemoryRouter, Routes, Route } from "react-router-dom";
import { Breadcrumbs } from "@ameshkin/nextcrumbs";
import useReactRouterBreadcrumbs from "@ameshkin/nextcrumbs";
import { Breadcrumbs, useReactRouterBreadcrumbs } from "@ameshkin/nextcrumbs";

function Trail() {
const items = useReactRouterBreadcrumbs({
Expand All @@ -142,7 +149,7 @@ export default function Demo() {
}
```

> `react-router-dom` is an optional peer. Install it only if you use the router hook.
> **Important:** `react-router-dom` is an **optional peer dependency**. You only need to install it if you use the `useReactRouterBreadcrumbs` hook. The package uses lazy loading, so importing other parts of the library won't require `react-router-dom` to be installed.

---

Expand All @@ -156,17 +163,18 @@ export default function Demo() {
| `LinkComponent` | `React.ElementType` | `@mui/material/Link` | Custom link component (e.g., Next.js `Link`). |
| `muiProps` | `Omit<MUIBreadcrumbsProps,"children">` | — | Props passed through to MUI `<Breadcrumbs />`. |
| `separatorIcon` | `React.ReactNode` | `ChevronRightIcon` | Icon/node placed between items. |
| `homeLabel` | `string` | `"Dashboard"` | If a crumb’s label matches, it can receive a Home icon. |
| `withSchema` | `boolean` | `true` | Adds `schema.org/BreadcrumbList` microdata to the markup. |
| `homeLabel` | `string` | `"Home"` | If a crumb's label matches, it can receive a Home icon. |

### `usePathBreadcrumbs(pathname, options?)`

| Option | Type | Default | Description |
| ---------- | ----------------------- | ------- | ---------------------------------- |
| `baseHref` | `string` | `"/"` | Root href for the first crumb. |
| `labelMap` | `Record<string,string>` | `{}` | Override labels by URL segment. |
| `exclude` | `string[]` | `[]` | Skip specific segments. |
| `decode` | `boolean` | `true` | `decodeURIComponent` each segment. |
| Option | Type | Default | Description |
| -------------- | ----------------------- | ------------ | ---------------------------------- |
| `baseHref` | `string` | `"/"` | Root href for the first crumb. |
| `labelMap` | `Record<string,string>` | `{}` | Override labels by URL segment. |
| `exclude` | `string[]` | `[]` | Skip specific segments. |
| `decode` | `boolean` | `true` | `decodeURIComponent` each segment. |
| `rootLabel` | `string` | `"Dashboard"`| Label for the root/home breadcrumb. |
| `transformLabel` | `(segment: string) => string` | — | Custom label formatter function. |

### `useReactRouterBreadcrumbs(options?)`

Expand Down Expand Up @@ -203,34 +211,120 @@ import { Breadcrumbs } from "@ameshkin/nextcrumbs";

## Accessibility & SEO

* Uses MUI’s accessible `<Breadcrumbs aria-label="breadcrumbs">`.
* **SEO microdata is enabled by default** via `withSchema`. Set `withSchema={false}` to disable.
* Uses MUI's accessible `<Breadcrumbs aria-label="breadcrumbs">`.
* Minimal DOM footprint; sensible defaults for current page vs. links.
* Follows ARIA best practices for breadcrumb navigation.

### JSON-LD Structured Data

For SEO, you can add JSON-LD structured data using the built-in utilities:

```tsx
import { Breadcrumbs, BreadcrumbJsonLd, breadcrumbsToJsonLd } from "@ameshkin/nextcrumbs";

function MyPage() {
const items = [
{ label: "Home", href: "/" },
{ label: "Products", href: "/products" },
{ label: "New Product" }
];

return (
<>
<BreadcrumbJsonLd crumbs={breadcrumbsToJsonLd(items)} origin="https://example.com" />
<Breadcrumbs items={items} />
</>
);
}
```

Or use the `toJsonLd` function for custom JSON-LD generation:

```tsx
import { toJsonLd } from "@ameshkin/nextcrumbs";

const jsonLd = toJsonLd(
[{ name: "Home", href: "/" }, { name: "Products" }],
{ origin: "https://example.com" }
);
```

> See [`docs/vanilla-json.md`](./docs/vanilla-json.md) for more examples.

> Prefer **page-level JSON-LD** for static SEO markup. See: [`docs/vanilla-json.md`](./docs/vanilla-json.md)
---

## Docs

- **Next.js App Router guide**: `docs/next-router.md`
Deep dive into `usePathBreadcrumbs` with Next App Router, theming, and testing tips.

- **React Router guide**: `docs/react-router.md`
How to use `useReactRouterBreadcrumbs`, wire up `<Link>` for SPA navigation, and test with RTL.

- **Vanilla React + JSON-LD**: `docs/vanilla-json.md`
Using `<Breadcrumbs />` without any router and adding SEO JSON-LD at the page level.

- **GitLab / subpath exports**: `docs/gitlab.md`
How to consume `@ameshkin/nextcrumbs` (and its subpath exports) in GitLab and other CI/CD setups with good tree shaking.

- **Export & API changes**: `docs/changes.md`
Notes on the current export layout, subpath exports, and dependency cleanup.

---

## Dependency Checks

```bash
npm ls @mui/material @mui/icons-material react react-dom
# optional router
# optional router (only needed if using useReactRouterBreadcrumbs)
npm ls react-router-dom
```

## Troubleshooting

### Webpack Error: "Can't resolve 'react-router-dom'"

If you're using Next.js (or another webpack-based bundler) and see this error even though you're not using the React Router hook, you have two options:

**Option 1: Install as dev dependency (Recommended)**
```bash
npm install --save-dev react-router-dom
```

**Option 2: Configure webpack to ignore it**

For Next.js, add this to your `next.config.js`:

```js
/** @type {import('next').NextConfig} */
const nextConfig = {
webpack: (config) => {
config.externals = config.externals || {}
config.externals['react-router-dom'] = 'commonjs react-router-dom'
return config
}
}

module.exports = nextConfig
```

> **Note:** The `useReactRouterBreadcrumbs` hook uses lazy loading, so `react-router-dom` is only accessed when the hook is actually called. However, webpack may still try to resolve it during static analysis. The solutions above prevent webpack from failing when the module isn't installed.

---

## FAQ

**Does it work with React Router?**
Yes. Use `useReactRouterBreadcrumbs()` or pass your router’s `<Link>` via `LinkComponent`.
Yes. Use `useReactRouterBreadcrumbs()` or pass your router's `<Link>` via `LinkComponent`. Note that `react-router-dom` is optional and only needed if you use the hook.

**Do I need to install react-router-dom if I'm using Next.js?**
No, you only need it if you're using the `useReactRouterBreadcrumbs` hook. If webpack complains about it, install it as a dev dependency or configure webpack to ignore it (see [Troubleshooting](#troubleshooting)).

**How do I change the last crumbs style?**
The last item usually has no `href`. Style it as the current page using your theme or conditional props.
**How do I change the last crumb's style?**
The last item usually has no `href`. Style it as the current page using your theme or conditional props via the `currentSx` prop.

**Can I disable icons entirely?**
Yes—dont pass `icon`, and set `homeLabel` to a non-matching value.
Yes—don't pass `icon` in your items, and set `homeLabel` to a non-matching value if you don't want the home icon.

---

Expand Down
29 changes: 29 additions & 0 deletions docs/changes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
## Docs & API Changes

### Export layout & tree shaking

- **New:** multiple ESM entrypoints are now built:
- `src/index.ts`
- `src/Breadcrumbs.tsx`
- `src/hooks/usePathBreadcrumbs.ts`
- `src/hooks/useReactRouterBreadcrumbs.ts`
- `src/utils/jsonld.ts`
- **New:** subpath exports in `package.json`:
- `@ameshkin/nextcrumbs/breadcrumbs`
- `@ameshkin/nextcrumbs/hooks/usePathBreadcrumbs`
- `@ameshkin/nextcrumbs/hooks/useReactRouterBreadcrumbs`
- `@ameshkin/nextcrumbs/utils/jsonld`
- **Unchanged (backward compatible):**
- Top-level exports from `@ameshkin/nextcrumbs` still include:
- `Breadcrumbs`, `NextCrumb`
- `usePathBreadcrumbs`
- `useReactRouterBreadcrumbs` and `useRRBreadcrumbs`
- `toJsonLd`, `BreadcrumbJsonLd`, `breadcrumbsToJsonLd`
- `BreadcrumbsProps`, `BreadcrumbItem`, `PathCrumbOptions`, `PathBreadcrumb`, `ReactRouterBreadcrumbsOptions`, `JsonLdBreadcrumb`, `JsonLdOptions`

### Dependency cleanup

- **Removed:** runtime dependency on `@ameshkin/orchestrator` from `@ameshkin/nextcrumbs`.
- This avoids install issues in external projects and keeps the public package focused on breadcrumbs only.


53 changes: 53 additions & 0 deletions docs/gitlab.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
## Using `@ameshkin/nextcrumbs` in GitLab projects

This package is published as a modern ESM bundle with tree-shakable entrypoints and subpath exports.

### Install

```bash
npm install @ameshkin/nextcrumbs
```

### Top-level (backward-compatible) imports

These continue to work and are the recommended default:

```ts
import {
Breadcrumbs,
NextCrumb, // alias of Breadcrumbs
usePathBreadcrumbs,
useReactRouterBreadcrumbs,
toJsonLd,
BreadcrumbJsonLd,
breadcrumbsToJsonLd,
} from "@ameshkin/nextcrumbs";
```

### Tree-shakable subpath imports

For GitLab (and other) projects that prefer explicit per-feature imports, you can use the new subpath exports:

```ts
// Component only
import { Breadcrumbs } from "@ameshkin/nextcrumbs/breadcrumbs";

// Next.js App Router hook
import { usePathBreadcrumbs } from "@ameshkin/nextcrumbs/hooks/usePathBreadcrumbs";

// React Router hook
import useReactRouterBreadcrumbs, {
ReactRouterBreadcrumbsOptions,
} from "@ameshkin/nextcrumbs/hooks/useReactRouterBreadcrumbs";

// JSON-LD helpers
import {
toJsonLd,
BreadcrumbJsonLd,
breadcrumbsToJsonLd,
} from "@ameshkin/nextcrumbs/utils/jsonld";
```

All of these entrypoints are pure ESM and are safe for bundlers that perform tree shaking.


3 changes: 1 addition & 2 deletions docs/next-router.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,8 +105,7 @@ type PathOptions = {
## Accessibility & SEO

* `<Breadcrumbs aria-label="breadcrumbs">` for screen readers
* `withSchema` is enabled by default to add `schema.org/BreadcrumbList`
* For JSON-LD, add page-level `<script type="application/ld+json">` in your layout if you prefer static SEO markup
* For JSON-LD, add page-level `<script type="application/ld+json">` in your layout if you prefer static SEO markup (see [`docs/vanilla-json.md`](./vanilla-json.md))

## Theming

Expand Down
12 changes: 4 additions & 8 deletions docs/react-router.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@ Use `useReactRouterBreadcrumbs` to derive breadcrumb items from the current loca

```tsx
import { MemoryRouter, Routes, Route } from "react-router-dom"
import { Breadcrumbs } from "@ameshkin/nextcrumbs"
import useReactRouterBreadcrumbs from "@ameshkin/nextcrumbs"
import { Breadcrumbs, useReactRouterBreadcrumbs } from "@ameshkin/nextcrumbs"

function Trail() {
const items = useReactRouterBreadcrumbs({ rootLabel: "Home" })
Expand All @@ -31,8 +30,7 @@ Pass React Router’s `<Link>` as `LinkComponent` so breadcrumb links use client

```tsx
import { Link as RouterLink, BrowserRouter, Routes, Route } from "react-router-dom"
import { Breadcrumbs } from "@ameshkin/nextcrumbs"
import useReactRouterBreadcrumbs from "@ameshkin/nextcrumbs"
import { Breadcrumbs, useReactRouterBreadcrumbs } from "@ameshkin/nextcrumbs"

function Trail() {
const items = useReactRouterBreadcrumbs({ rootLabel: "Home" })
Expand Down Expand Up @@ -77,8 +75,7 @@ type ReactRouterBreadcrumbsOptions = {

```tsx
import { Link as RouterLink } from "react-router-dom"
import { Breadcrumbs } from "@ameshkin/nextcrumbs"
import useReactRouterBreadcrumbs from "@ameshkin/nextcrumbs"
import { Breadcrumbs, useReactRouterBreadcrumbs } from "@ameshkin/nextcrumbs"

export default function Trail() {
const items = useReactRouterBreadcrumbs({
Expand Down Expand Up @@ -106,8 +103,7 @@ When testing with Vitest/RTL, wrap your component with a memory or browser route
```tsx
import { render, screen } from "@testing-library/react"
import { MemoryRouter, Routes, Route } from "react-router-dom"
import { Breadcrumbs } from "@ameshkin/nextcrumbs"
import useReactRouterBreadcrumbs from "@ameshkin/nextcrumbs"
import { Breadcrumbs, useReactRouterBreadcrumbs } from "@ameshkin/nextcrumbs"

function Trail() {
const items = useReactRouterBreadcrumbs({ rootLabel: "Home" })
Expand Down
Loading