Skip to content

feat(routing): move the signed-in app under /dashboard, add a public /ads marketing page - #197

Merged
ralyodio merged 1 commit into
masterfrom
worktree-dashboard-routes
Aug 17, 2026
Merged

ralyodio merged 1 commit into
masterfrom
worktree-dashboard-routes

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

What

Two changes that are really one: the top level of the site now belongs entirely to public pages, and every signed-in resource lives under /dashboard/**. That frees /ads to become a public marketing page.

Why

The top level was doing two jobs. /pricing and /about were public; /projects, /settings and /ads were the signed-in app. There was no room for a marketing page on any path the app had already claimed — and /ads is the one product here with two sides to sell and nowhere to sell them.

The move

app/(app)/* moves wholesale into app/(app)/dashboard/*. The auth gate is the (app) layout, which still wraps all of it, so nothing changes about who can reach what. All ~360 internal link references, revalidatePath calls, email links and MCP replies were rewritten to match.

next.config.ts redirects every old top-level path to its /dashboard equivalent, so bookmarks, links in already-sent report emails and URLs in past invoices keep working.

307 rather than 308 on purpose. None of these paths were ever indexed — they all sat behind a login — so there is no SEO to preserve, and a temporary redirect stays reversible instead of being cached in browsers forever if a top-level path is later wanted for a public page.

Two traps worth flagging

  • /ads/house/* is artwork in public/, not a route. Next runs redirects before the filesystem, so a blanket /ads/:path* would have 404'd every house creative. The redirect excludes it with a lookahead, and the build-verified check below covers it.
  • The proxy's auth check still listed /projects, /audits and /settings. Those are now dead prefixes; /dashboard is the whole contract.

The marketing page

/ads sells both sides of the network — advertisers and publishers — and redirects signed-in visitors to /dashboard/ads, since that is where either half is actually actionable.

Every figure on it is imported from lib/ads/pricing rather than typed in, so the page cannot quote a rate the biller has moved off. The publisher's per-click number comes from creditsToPayoutCents, the same function ad_charge_click() mirrors, so what a publisher reads there is what actually accrues.

Claims were checked against the code rather than assumed: the GitHub PR auto-installer for ad embeds is real (lib/github/install-ad.ts, wired to /api/ads/slots/[id]/install-embed), the bid-weighted lottery is live on every fill, and the six-hour click dedupe window is CLICK_DEDUPE_WINDOW_MS.

Verification

typecheck clean, 1451 tests pass, production build succeeds. Routing checked with real HTTP against the built server:

Request Result
/ads 200 — public marketing page
/ads/house/promo-rect.webp 200 — artwork intact
/ads/slots 307 → /dashboard/ads/slots
/projects/x 307 → /dashboard/projects/x
/settings/billing, /admin, /promote, /social, /autoblog, /audits/x, /analytics 307 → /dashboard/...
/dashboard/ads (signed out) 302 → /login?redirect=%2Fdashboard%2Fads
/api/ads/serve 200 — API paths untouched

Login and signup already defaulted to /dashboard and honored ?redirect, so no change was needed there.

Not included

docs/*.md and prd/ still describe the old paths. They read as point-in-time specs rather than live links, so I left them rather than rewriting history — say the word if you'd rather they track the new routes.

🤖 Generated with Claude Code

…/ads page

The top level was doing two jobs: /pricing and /about were public, while
/projects, /settings and /ads were the signed-in app. That left no room for a
marketing page on any path the app had already claimed — /ads in particular,
which is the one product here with two sides to sell and nowhere to sell them.

Split it: every authenticated resource now lives under /dashboard/**, and the
top level belongs entirely to public pages.

- app/(app)/* moves wholesale into app/(app)/dashboard/*. The auth gate is the
  (app) layout, which still wraps all of it, so nothing changes about who can
  reach what.
- next.config redirects every old top-level path to its /dashboard equivalent,
  so bookmarks, links in already-sent report emails and the URLs in past
  invoices keep working. 307 rather than 308: none of these were ever indexed
  (they sat behind a login), so there is no SEO to preserve, and a temporary
  redirect stays reversible instead of being cached in browsers forever.
- /ads is the exception at both ends. The bare path is now the public marketing
  page, and /ads/house/* is artwork in public/ — redirects run before the
  filesystem, so a blanket /ads/:path* would have 404'd every house creative.
  The redirect excludes it explicitly.
- The proxy's auth check drops the now-dead /projects, /audits and /settings
  prefixes. /dashboard is the whole contract.

The marketing page sells both sides of the network, and every figure on it is
imported from lib/ads/pricing rather than typed in — the publisher's per-click
number comes from creditsToPayoutCents, the same function ad_charge_click()
mirrors, so what a publisher reads is what actually accrues.

Verified against a production build: /ads renders publicly, /ads/house artwork
still serves 200, every legacy prefix 307s to its new home, and /dashboard/**
302s signed-out visitors to /login with the new path in ?redirect.
@github-actions

Copy link
Copy Markdown

ThreatCrush Security Scan

35 finding(s)

HIGH/CRITICAL: 5 | MEDIUM: 28 | LOW: 2

Severity Rule Location
HIGH secret-generic-credential app/(marketing)/docs/autoblog-webhook/page.tsx:145
HIGH tls-verification-disabled lib/onion.ts:47
HIGH secret-generic-credential lib/sp/platforms/facebook.ts:32
HIGH secret-generic-credential lib/sp/platforms/linkedin.ts:25
HIGH sh-remote-script-execution prober/deploy/provision.sh:30
MEDIUM js-unescaped-html-sink app/(app)/dashboard/admin/email-broadcast/EmailBroadcastForm.tsx:125
MEDIUM js-unescaped-html-sink app/(app)/dashboard/projects/[id]/autoblog/articles/[articleId]/page.tsx:214
MEDIUM js-unescaped-html-sink app/(marketing)/blog/[slug]/page.tsx:67
MEDIUM js-unescaped-html-sink app/(marketing)/blog/[slug]/page.tsx:97
MEDIUM js-unescaped-html-sink app/(marketing)/blog/[slug]/page.tsx:104
MEDIUM js-unescaped-html-sink app/(marketing)/blog/[slug]/page.tsx:110
MEDIUM js-unescaped-html-sink app/(marketing)/recent/page.tsx:186
MEDIUM js-unescaped-html-sink app/(marketing)/recent/page.tsx:190
MEDIUM js-unescaped-html-sink app/c/[project]/[slug]/page.tsx:77
MEDIUM js-unescaped-html-sink app/c/[project]/page.tsx:57
MEDIUM js-unescaped-html-sink app/careers.js/route.ts:228
MEDIUM js-unescaped-html-sink app/careers.js/route.ts:285
MEDIUM js-unescaped-html-sink app/layout.tsx:129
MEDIUM js-open-redirect app/login/form.tsx:39
MEDIUM js-unescaped-html-sink app/r/[token]/page.tsx:176
MEDIUM js-open-redirect app/signup/form.tsx:43
MEDIUM js-open-redirect components/billing/buy-credits-modal.tsx:98
MEDIUM js-unescaped-html-sink components/json-ld.tsx:8
MEDIUM js-unescaped-html-sink components/report/markdown-view.tsx:15
MEDIUM redos-nested-quantifier lib/careers/jobs.ts:139
MEDIUM js-unescaped-html-sink lib/careers/page-templates.ts:198
MEDIUM redos-nested-quantifier lib/emailMarkdown.ts:130
MEDIUM redos-nested-quantifier lib/lx/articleGen.ts:98
MEDIUM js-dynamic-code-execution tests/careers-page-templates.test.ts:21
MEDIUM js-dynamic-code-execution tests/careers-widget-script.test.ts:19
MEDIUM js-dynamic-code-execution tests/careers-widget-script.test.ts:69
MEDIUM js-dynamic-code-execution tests/contract/ad-visitor-id.test.ts:51
MEDIUM js-dynamic-code-execution tests/contract/ad-visitor-id.test.ts:52
LOW secret-generic-credential tests/contract/posthog-integration.test.ts:13
LOW secret-generic-credential tests/lead-campaign.test.ts:16

Snippets are redacted; ThreatCrush never prints matched credential material.

@ralyodio
ralyodio merged commit 8f17c92 into master Aug 17, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant