Skip to content

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/foo
  • pages/foo/index.vue/foo
  • pages/foo/bar.vue/foo/bar
  • pages/foo/_id.vue/foo/:id
  • pages/_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 the accountId at index 1 rather than 0, 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 current accountId — this is part of how account switching takes effect.

Important: the middleware reads accountId from localStorage, not cookies. If localStorage.getItem('accountId') is empty, the middleware short-circuits and no rewriting happens.

The localStorage.accountId write does exist in the codebase — it's in middleware/head.js, alongside the cookie write. But head.js is not wired into the global middleware chain (verify in nuxt.config.js) and isn't referenced as per-page middleware anywhere. So in practice the localStorage value 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 and localStorage.accountId is empty, appendUrl will silently no-op — this is the "redirect didn't happen" bug. Verify with the team whether head.js should be wired up or its logic merged into redirect.js.

Route middleware

Globally, every route runs the chain declared in nuxt.config.js → router.middleware:

middleware: ['auth', 'axios', 'validUrl', 'appendUTM', 'appendUrl']
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 auth middleware is redirect.js, so adding middleware: 'auth' to a page runs redirect.js again. 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:

  1. Create your file under pages/_accid/<your-route>/index.vue (or _id/index.vue if you need a dynamic id).
  2. Import <NuxtLink> targets as /<accountId>/your-route. Read accountId from this.$auth.user.accountId or this.$cookies.get('accountId'). Don't hardcode.
  3. 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 in middleware/appendUrl.js so the rewrite kicks in.
  4. Decide if the page needs the layout-default chrome or one of the special layouts (onBoard.vue for onboarding pages, someliNetwork.vue for the network section, specialOffer.vue for promotional pages, error.vue, fourOhOne.vue). Set layout: '<name>' on the page component if non-default.
  5. If the page should appear in the post-login landing_url flow, that's a backend change — landing_url is server-driven (see 03-auth-and-sessions.md).

Sharp edge: at least one page declares layout: 'admin' but there is no layouts/admin.vue file. 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 cached api/fetch... actions.
  • Use setSelectedAccount to 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.