Plugins

Breadcrumbs

Learn how to add breadcrumbs to your plugin's admin pages for better navigation and user experience.

VitNode renders dynamic, localized breadcrumbs in two places:

  • AdminCP — in the header, next to the sidebar trigger.
  • Main site — below the header, on the public pages.

They are built on Next.js Parallel Routes (the @breadcrumb slot) and work automatically — in most cases you don't have to do anything.

AdminCP

AdminCP breadcrumbs are generated from your sidebar navigation. As soon as you add navigation items and their translations, the breadcrumb reuses the same translated titles — there is nothing extra to configure.

Using the blog navigation from AdminCP Pages, visiting /admin/blog/settings renders:

Blog / Settings

Labels come from the same translation keys as the sidebar nav. URL segments that aren't part of the nav (for example a dynamic id) fall back to a humanized version of the segment (reset-passwordReset Password).

Main site

On the public site the breadcrumb is derived from the URL. Each segment is humanized and links to its path, and the home page (/) shows no breadcrumb.

/login                ->  Login
/account/settings     ->  Account / Settings

Custom breadcrumbs

Sometimes the automatic label isn't enough — you want a translated label for a public page, or a real name resolved on the server for a dynamic route (e.g. /blog/posts/[id] showing the post title instead of the id).

For that, a plugin can ship breadcrumb slots in src/routes/breadcrumb. The folder mirrors the URL and maps to the matching @breadcrumb slot:

  • routes/breadcrumb/admin → AdminCP routes (path after /admin)
  • routes/breadcrumb/main → public site (path after /)

VitNode copies these into the app automatically and keeps them in sync while you develop — exactly like the src/routes/main / src/routes/admin directories. Your slots are namespaced under your plugin, so they never collide with core or other plugins. Core already provides the generic fallback (and the home page), so you only add a slot for the specific route you want to customize.

Translated label

Render a translated label for one of your public pages. Pass a labels map of path → label; everything else still falls back to the humanized default.

plugins/{plugin_name}/src/routes/breadcrumb/main/blog/page.tsx
import { BreadcrumbMain } from '@vitnode/core/views/breadcrumb/breadcrumb-main';
import { getTranslations } from 'next-intl/server';

export default async function BreadcrumbSlot() {
  const t = await getTranslations('@vitnode/blog');

  return <BreadcrumbMain labels={{ '/blog': t('title') }} segments={['blog']} />;
}

Server-resolved name

For a dynamic route whose id is the last segment, resolve the real name on the server and pass it as overrideLastLabel — it replaces the last crumb. The folder mirrors the dynamic segment.

plugins/{plugin_name}/src/routes/breadcrumb/main/blog/posts/[id]/page.tsx
import { BreadcrumbMain } from '@vitnode/core/views/breadcrumb/breadcrumb-main';

export default async function BreadcrumbSlot({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const { id } = await params;
  const post = await getPost(id); // your data fetching

  return (
    <BreadcrumbMain
      overrideLastLabel={post.title}
      segments={['blog', 'posts', id]}
    />
  );
}

AdminCP dynamic page

In AdminCP, use BreadcrumbAdmin instead. segments are the path after /admin, and the labels for known routes come from your sidebar nav automatically — so you usually only need overrideLastLabel for the dynamic part.

plugins/{plugin_name}/src/routes/breadcrumb/admin/blog/posts/[id]/page.tsx
import { BreadcrumbAdmin } from '@vitnode/core/views/admin/layouts/breadcrumb/breadcrumb-admin';

export default async function BreadcrumbSlot({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const { id } = await params;
  const post = await getPost(id);

  return (
    <BreadcrumbAdmin
      overrideLastLabel={post.title}
      segments={['blog', 'posts', id]}
    />
  );
}

Name in the middle of the path

overrideLastLabel only replaces the last crumb. When the resolved name sits in the middle of the path — for example the post in /blog/posts/[id]/comments — use the labels prop instead, keyed by that crumb's full path. It works at any position (and the labelled crumb becomes a link). labels is available on both BreadcrumbMain and BreadcrumbAdmin.

plugins/{plugin_name}/src/routes/breadcrumb/main/blog/posts/[id]/comments/page.tsx
import { BreadcrumbMain } from '@vitnode/core/views/breadcrumb/breadcrumb-main';

export default async function BreadcrumbSlot({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const { id } = await params;
  const post = await getPost(id);

  // Blog / Posts / <post title> / Comments
  return (
    <BreadcrumbMain
      labels={{ [`/blog/posts/${id}`]: post.title }}
      segments={['blog', 'posts', id, 'comments']}
    />
  );
}

The folder under routes/breadcrumb must mirror the real route path. A slot only overrides the matching URL — every other route keeps the automatic breadcrumb.

Learn More

On this page