Breadcrumbs for a Vue route
Never edit assets/vue/components/Breadcrumb.vue to give a page its trail. The component
holds no page name and no path. Everything a trail needs is declared in the route.
The three steps that always apply
1. Ask for a breadcrumb
Set showBreadcrumb: true in the meta of the parent route. Without it, App.vue does not mount
the component and nothing you declare below has any effect.
A route that only sometimes has a trail declares a function instead. It receives the route:
// The personal agenda hangs from no course, so it has nothing to show.
showBreadcrumb: (route) => Number(route.query?.cid || 0) > 0,
2. Declare the label
Set meta.breadcrumb on the parent and on every leaf. The value is a translation key,
never translated text.
export default {
path: "/resources/checklist/:node/",
meta: {
requiresAuth: true,
showBreadcrumb: true,
tool: "checklist",
breadcrumb: "Checklists",
},
name: "checklist",
component: () => import("../components/layout/SimpleRouterViewLayout.vue"),
redirect: { name: "ChecklistList" },
children: [
{ name: "ChecklistList", path: "", meta: { breadcrumb: "" }, component: ... },
{ name: "ChecklistCreate", path: "create", meta: { breadcrumb: "Create" }, component: ... },
{ name: "ChecklistUpdate", path: "edit/:id", meta: { breadcrumb: "Edit" }, component: ... },
],
}
breadcrumb: "" means omit this crumb, and it is what a list page usually wants: the tool
crumb already names that page. An absent property is a different thing — it triggers the
development warning and shows the raw route name.
The label can also be a function returning a key, when it depends on the request:
breadcrumb: (route) => {
const gid = Number(route.query?.gid || 0)
const cid = Number(route.query?.cid || 0)
return gid > 0 ? "Group agenda" : cid > 0 ? "Agenda" : "Personal agenda"
},
A function may also return "" to omit the crumb, but only on a route that declares
breadcrumbParents: the tool and resource branches still compare the raw meta.breadcrumb
property, so there a function is never empty. Declare it when the page's own name exists for
some requests only. AdminStatistics in assets/vue/router/admin.js is the reference — its key
comes from activeReportLabelKey (assets/vue/utils/statisticsReports.js), which names the
report in route.query.report and returns "" when there is none.
3. Register every key
A key that is new to the platform goes by hand into three files:
assets/locales/en_US.jsontranslations/messages.pottranslations/messages.en_US.po
php bin/console chamilo:update_vue_translations does not create keys. It only propagates to
the other locales the keys that en_US.json already holds. Never commit a mass regeneration of
assets/locales/*.json; a single new line is fine.
Check first whether the key exists — most tool names already do:
python3 -c "import json; d=json.load(open('assets/locales/en_US.json')); print('Checklists' in d)"
The two extra declarations
breadcrumbParents — a page that hangs from a list page
Give the leaf the fixed ancestors that always precede it. Each entry is { label, route }, where
label is a translation key and route is a Vue Router location — prefer { name: ... } over a
raw path.
meta: {
breadcrumb: "Import checklists",
breadcrumbParents: [
{ label: "Administration", route: { name: "AdminIndex" } },
{ label: "Checklists", route: { name: "ChecklistAdminList" } },
],
}
When several routes share the same ancestors, declare the lists once at the top of the file.
assets/vue/router/admin.js is the reference, with adminCrumb, classesCrumbs,
multiUrlCrumbs and accessUrlCrumbs; room.js and branch.js show the smaller shape.
A route with breadcrumbParents owns its whole trail and takes the first branch of
buildBreadcrumb, so nothing else runs for it.
The ancestors are fixed, but the leaf's own crumb is not: an empty key drops it and leaves the
ancestors standing. That is how a page hangs a report or filter name off a parent that links
back to its own bare URL, and it is the only branch where a function may return "".
breadcrumbResource — a tool that opens a resource
Declare it on the parent, when the trail has to name the thing the user opened.
// One resource at a time: list -> resource -> sub-page.
breadcrumbResource: {
trail: "self",
listRoute: "AssignmentsList",
detailRoute: "AssignmentDetail",
detailParam: "id",
}
// A chain of folders, walked up the resource-node parents.
breadcrumbResource: {
trail: "ancestors",
listRoute: "DocumentsList",
detailRoute: "DocumentsList",
}
This declaration also drives the fetch: loadResourceNodeIfNeeded loads the resource node only
for a route whose tool declares it. Nothing else has to be wired.
Do not do these
- Do not add a rule to
Breadcrumb.vue. If a trail seems to need one, the shape is probably already covered by one of the four declarations. Read the component's own builders first. A rule that names a page or a route never belongs there. Widening a contract the declarations already document is a different thing, and rare:buildDeclaredParentCrumbslearned to honour an empty key that way, becausebreadcrumb: ""was documented as "omit" everywhere yet that branch pushed the crumb regardless. - Do not match route names by substring. A rule like
route.name.includes("Page")once put a "Pages" crumb on every wiki page of every course. It was removed for that reason. - Do not pass translated text as a label. The component calls
t()on what you declare. - Do not expect a one-crumb trail to appear. The component gives the first crumb to the
PrimeVue
homeslot, and the container is hidden while the rest is empty. A page whose trail reduces to its tool shows nothing.
Verify
# Fails when a route renders a breadcrumb without declaring its label.
yarn check:breadcrumb-routes
npx eslint assets/vue/router/
yarn dev
Then open the page and read the browser console. [Breadcrumb] Route "X" has no meta.breadcrumb
means a declaration is missing. The warning is stripped from the production bundle.
Cover it
tests/playwright/features/breadcrumb.feature holds the breadcrumb scenarios. Add yours there.
Assert on CSS selectors, never on crumb text: this platform runs in many languages and the test environment is not always English.
Then I should see the ".app-breadcrumb a[href='/admin/urls']" element
An a[href] proves both that the ancestor is there and that it points at the right place. Two
traps that cost real time:
- The last crumb is a
<span>, not a link. Assertinga[href]on the current page's own crumb never matches. Assert on an ancestor. - A tool with
breadcrumbResourcepaints late. It waits for the resource-node fetch, well past Playwright's 15 s default. UseI wait up to 45 seconds for the element ... to appear.
Playwright needs BASE_URL=http://localhost inside the container, and the generated spec under
tests/playwright/.features-gen/ is tracked, so run bddgen and commit it too.