04 — Routing & account scoping¶
Routing in this app is non-obvious in two ways: most authenticated routes are nested under a dynamic _accid segment, and unprefixed URLs are aggressively rewritten by middleware to inject the active account ID. This doc explains both.
File-system routing recap¶
Nuxt 2 generates routes from pages/. The conventions used in this repo:
pages/foo.vue→/foopages/foo/index.vue→/foopages/foo/bar.vue→/foo/barpages/foo/_id.vue→/foo/:idpages/_accid/foo.vue→/:accid/foo
nuxt.config.js → router.extendRoutes adds a couple of legacy redirects:
{ name: 'dashboard-redirect', path: '/mission-control/overview', redirect: '/dashboard/overview' }
{ name: 'dashboard', path: '/mission-control', redirect: '/dashboard' }
router.linkActiveClass is 'node-active', so use that class name in CSS for active-link styling instead of the Vue Router default.
The _accid segment¶
Most authenticated routes live under pages/_accid/. That folder is a Nuxt dynamic segment — the matched value is available as route.params.accid.
pages/_accid/
├── billing/
├── brandkit/
├── chaptername/
├── contentplanner/
├── cuseditor/_id/index.vue
├── dashboard/
├── goals_objectives/
├── help_center/
├── my_library/
├── Organisationprofile/
├── payment/
├── posteditor/_id/index.vue
├── sharedposteditor/_id/index.vue
├── subjects_services/
├── templateeditor/
├── topics/
├── usereditor/_id/index.vue
├── usermediaditor/_id/index.vue
├── userprofile/
└── userSetting/
So /<accountId>/contentplanner, /<accountId>/dashboard/overview, /<accountId>/posteditor/<postId> are all real, valid routes.
A handful of editor routes also exist outside _accid (pages/posteditor/_id/, pages/cuseditor/_id/, pages/editor/_id/, etc.). These are legacy and the appendUrl middleware will redirect bare requests for them to the _accid-prefixed equivalent. Don't add new routes outside _accid unless they are intentionally global (e.g. /redirect, /callback, /payment flows that pre-date account selection).
Account-ID auto-prefixing (middleware/appendUrl.js)¶
This middleware runs on every navigation. Its job is: "if the user is logged in (has an accountId in localStorage), and the URL is one of the known account-scoped routes but is missing the accountId prefix, redirect to the prefixed version."
The list of routes that get auto-prefixed is hardcoded:
/contentplanner
/my_library
/help_center
/userSetting
/dashboard/roi
/dashboard/roadmap
/dashboard/overview
/dashboard/action-items
/dashboard/reach
/dashboard/leaderboard
/dashboard/website-traffic
/dashboard/google-reviews
/dashboard/hashtag-analytics
/dashboard/leads
/dashboard/new-connections
/billing
/topics
/cuseditor
/usereditor
/posteditor
/subjects_services
/Organisationprofile
/brandkit
/payment
/chaptername
/goals_objectives
/usermediaditor
/sharedposteditor
/templateeditor
Behaviour:
- Visiting
/contentplanner→ redirected to/<accountId>/contentplanner. - Visiting
/contentplanner/foo/bar→ no redirect (only the bare path matches). - Editor routes (
/editor/...) get a slightly different treatment: the middleware places theaccountIdat index1rather than0, allowing both/editor/<id>and/<accountId>/editor/<id>shapes. - The middleware also actively rewrites if the URL has an
accountId-shaped segment in position 0 that isn't the currentaccountId— this is part of how account switching takes effect.
Important: the middleware reads
accountIdfromlocalStorage, not cookies. IflocalStorage.getItem('accountId')is empty, the middleware short-circuits and no rewriting happens.The
localStorage.accountIdwrite does exist in the codebase — it's inmiddleware/head.js, alongside the cookie write. Buthead.jsis not wired into the global middleware chain (verify innuxt.config.js) and isn't referenced as per-page middleware anywhere. So in practice thelocalStoragevalue is set only by code paths that touch it directly (e.g. login flows,AccountSwitcher.vue). If a user lands on the app via a URL that bypasses those paths andlocalStorage.accountIdis empty,appendUrlwill silently no-op — this is the "redirect didn't happen" bug. Verify with the team whetherhead.jsshould be wired up or its logic merged intoredirect.js.
Route middleware¶
Globally, every route runs the chain declared in nuxt.config.js → router.middleware:
| Slot | File | Purpose |
|---|---|---|
auth |
middleware/redirect.js |
Post-login decision tree. Not the built-in auth middleware. |
axios |
middleware/axios.js |
Installs token / accountId / apptype interceptor; saves currentUrl. |
validUrl |
middleware/validUrl.js |
Saves localStorage.fallback-path if the route resolves cleanly. |
appendUTM |
middleware/appendUTM.js |
Decorates <a> and <form> targets with UTM params on the topics page. |
appendUrl |
middleware/appendUrl.js |
Account-ID auto-prefixing (see above). |
Pages can also opt into per-page middleware via middleware: 'auth' in their script — editor pages do this to ensure redirect.js runs even on direct loads.
Conversely, public pages can opt out of @nuxtjs/auth with auth: false in the page's script. This is the canonical way to make a page accessible to logged-out users — see pages/index.vue, pages/redirect.vue, pages/callback/index.vue, pages/privacy/index.vue, etc. Don't try to bypass the auth chain via routing or middleware tricks; use auth: false.
Heads up: the global
authmiddleware isredirect.js, so addingmiddleware: 'auth'to a page runsredirect.jsagain. That is what existing editor pages do. The naming is misleading but the behaviour is intentional.
There are three more middleware files not in the global chain:
| File | Wired in | Purpose |
|---|---|---|
middleware/sociallink.js |
Per-page on pages/index.vue, pages/_accid/userSetting/index.vue, pages/password_setup/index.vue, pages/social_accounts/index.vue, pages/callback/index.vue |
Reads fbconn cookie and dispatches sociallink + sociallinkstatus + socialLinkMe in parallel. |
middleware/socialLogin.js |
Not referenced anywhere in nuxt.config.js or pages |
Facebook-strategy OAuth handling. Likely dead code. Verify with the team before relying on or removing. |
middleware/head.js |
Not referenced anywhere | Role-based access control + localStorage.accountId sync + subscription gating. See 03-auth-and-sessions.md for the full picture. |
Adding a new authenticated page¶
The mechanical steps:
- Create your file under
pages/_accid/<your-route>/index.vue(or_id/index.vueif you need a dynamic id). - Import
<NuxtLink>targets as/<accountId>/your-route. ReadaccountIdfromthis.$auth.user.accountIdorthis.$cookies.get('accountId'). Don't hardcode. - If your page is one of the well-known unprefixed entry points (e.g. external partners deep-link to a bare
/<your-route>), add it to the list inmiddleware/appendUrl.jsso the rewrite kicks in. - Decide if the page needs the layout-default chrome or one of the special layouts (
onBoard.vuefor onboarding pages,someliNetwork.vuefor the network section,specialOffer.vuefor promotional pages,error.vue,fourOhOne.vue). Setlayout: '<name>'on the page component if non-default. - If the page should appear in the post-login
landing_urlflow, that's a backend change —landing_urlis server-driven (see03-auth-and-sessions.md).
Sharp edge: at least one page declares
layout: 'admin'but there is nolayouts/admin.vuefile. Nuxt falls back to the default layout in this case. If you see a "phantom layout" warning in the build, this is the culprit — fix it by either adding the layout or correcting the page's declaration.
Account switching¶
Selecting a different account triggers a navigation to /<newAccountId>/<currentRoute>. The appendUrl middleware detects that pathArray[0] !== currentAccountId and rewrites accordingly. The Vuex root state has isAccountSwitching and selectedAccount flags used by AccountSwitcher.vue to suppress flicker during the transition.
When you build a feature that lists accounts or switches them:
- Read the candidate list from
store.getters.getUserAccounts(root) or via the cachedapi/fetch...actions. - Use
setSelectedAccountto update the active account in the store. - Clear or refetch any account-scoped Vuex caches via
store.dispatch('api/clearApiCache')— otherwise the previous account's cached data leaks into the new one for up to 5 minutes.
Sitemap & SEO routes¶
Static landing pages and the marketing surface are handled separately under data/sitemap.js and data/stgmap.js, with a Vuex module at store/modules/sitemap.js. Most product routes are not in the sitemap (they're behind auth). If you add a public-facing page, verify with the team whether it should be sitemap-listed.