Skip to content

URL Hierarchy Refactor

Problem

Navigation URLs break the parent-child chain halfway through the hierarchy. The first two levels are nested correctly, but from creative groups onward the URL restarts with a new root:

Brand:          /workspaces/211/brands/
Campaign:       /workspaces/211/brands/1066/campaigns             -- nested under brand
Group:          /workspaces/211/campaigns/50113/creative-groups    -- jumps to campaign as root
Creative list:  /workspaces/211/creative-groups/50132/creatives    -- jumps to group as root
Builder:        /creatives/1066-50113-50132-50790/build#design     -- flat, no hierarchy at all

This makes it impossible to understand where you are in the hierarchy just by reading the URL. You lose the brand context as soon as you navigate to groups, and all parent context once you open the builder.

Proposed URL structure

A fully nested hierarchy where every level is visible:

Brand:          /workspaces/211/brands/1066
Campaigns:      /workspaces/211/brands/1066/campaigns
Campaign:       /workspaces/211/brands/1066/campaigns/50113
Groups:         /workspaces/211/brands/1066/campaigns/50113/groups
Group:          /workspaces/211/brands/1066/campaigns/50113/groups/50132
Creatives:      /workspaces/211/brands/1066/campaigns/50113/groups/50132/creatives
Builder:        /workspaces/211/brands/1066/campaigns/50113/groups/50132/creatives/50790/build

The builder URL no longer needs the compound ID format (1066-50113-50132-50790) because every segment is explicit in the path.

Benefits

  • Readable context: you can always tell where you are in the hierarchy from the URL alone
  • Shareable links: someone receiving a link immediately sees the full path (brand, campaign, group, creative)
  • Breadcrumbs: breadcrumb components can derive their structure directly from the URL segments instead of needing separate lookups
  • Browser history: back/forward navigation follows the natural hierarchy
  • Consistency: one pattern for the entire app, not three different URL schemes

Requirements

  • Backwards compatible: old URLs must still work. Either redirect (301) or support both patterns during a transition period.
  • No broken flows: reporting links, build pipeline callbacks, login redirects, deep links from external tools (Linear, Slack, etc.) must all continue to work.
  • Route guards: permission checks currently rely on route params. New routes need the same guard logic.

Scope of change

This is a significant refactor that touches:

  • Vue Router (router.ts): new route definitions with nested params
  • Navigation components: sidebar, breadcrumbs, page headers that read route params
  • API calls: some pages fetch data based on route params (e.g. campaign page fetches by campaignId from URL). These should still work since the IDs are the same, just extracted from different URL positions.
  • Reporting: any links that point back to campaigns, groups, or creatives
  • Build pipeline: if any callbacks or webhooks reference creative URLs
  • External integrations: Linear issues, Slack messages, bookmarks that use old URLs

Migration strategy

  1. Define new routes alongside old ones
  2. Old routes redirect to new equivalents (301)
  3. Update all internal router.push() / router-link calls to use new route names
  4. Keep old route definitions as redirects for at least one release cycle
  5. Remove old routes once confirmed no external systems depend on them

Priority

Low-medium. The current URLs work, they're just not ideal. This is a quality-of-life improvement for navigation and debugging, not a bug fix. Best done as a dedicated project when there's time, not squeezed in alongside other work.

Internal documentation