Deep-link public topic handoffs into protected routes

This commit is contained in:
axiomlogicnexus 2026-06-24 22:08:02 +00:00
parent 4a5c0f7a7c
commit 556ad2edf2
6 changed files with 49 additions and 8 deletions

View file

@ -268,6 +268,12 @@ Latest same-day browser/distribution continuity follow-up still on `2026-06-24`:
- operator access
- studio rollout
- each topic now carries explicit next steps, manual/route focus, and quick actions so the public web surface reflects the same browser/package/runtime/compliance separation the hardened routes already enforce
- those topic actions now also preserve exact protected destinations through auth:
- protected downloads
- protected notices
- protected browser-access
- protected dashboard
- the public topic cards therefore no longer dead-end on broad public guidance when the real next step is signed-in protected follow-through
- the new shared callout posture explicitly separates:
- what still works
- what stays intentionally withheld

View file

@ -884,8 +884,9 @@ describe('public marketing pages', () => {
const selectedLaneSection = selectedLaneHeading.closest('section')
expect(selectedLaneSection).toBeTruthy()
expect(within(selectedLaneSection as HTMLElement).getByText('Confirm public launch status, operator/studio checkout posture, and support contact first.')).toBeTruthy()
expect(within(selectedLaneSection as HTMLElement).getByRole('link', { name: 'Sign in for protected release lane' }).getAttribute('href')).toBe('/login?next=%2Fapp%2Fdownloads%3Fplatform%3Dwindows')
expect(within(selectedLaneSection as HTMLElement).getByRole('link', { name: 'Sign in for protected notices' }).getAttribute('href')).toBe('/login?next=%2Fapp%2Fnotices')
expect(within(selectedLaneSection as HTMLElement).getByRole('link', { name: 'Open pricing' }).getAttribute('href')).toBe('/pricing')
expect(within(selectedLaneSection as HTMLElement).getByRole('link', { name: 'Open download center' }).getAttribute('href')).toBe('/download')
expect(screen.getByText('If the desktop app is primary, why keep the web version?')).toBeTruthy()
expect(screen.getByText('Is VR/controller support already fully finished?')).toBeTruthy()
expect(screen.getByText('Do higher-dimensional selector choices persist between sessions?')).toBeTruthy()

View file

@ -1,6 +1,8 @@
import { describe, expect, it } from 'vitest'
import {
buildLoginPath,
buildProtectedDownloadPath,
buildRegisterPath,
buildSupportPath,
isExternalHref,
normalizeDownloadPlatform,
@ -25,4 +27,10 @@ describe('site route helpers', () => {
expect(isExternalHref('https://checkout.paddle.com')).toBe(true)
expect(isExternalHref('/support?topic=operator-access')).toBe(false)
})
it('builds auth-preserving login and register links for protected destinations', () => {
expect(buildLoginPath('/app/downloads?platform=windows')).toBe('/login?next=%2Fapp%2Fdownloads%3Fplatform%3Dwindows')
expect(buildRegisterPath('/app/notices')).toBe('/register?next=%2Fapp%2Fnotices')
expect(buildLoginPath('//evil.example')).toBe('/login?next=%2Fapp')
})
})

View file

@ -16,7 +16,12 @@ import {
releaseNotesUrl,
} from '../site-config'
import { deliverySurfaceCards } from '../site-data'
import { isExternalHref } from '../site-routes'
import {
buildLoginPath,
buildProtectedDownloadPath,
buildRegisterPath,
isExternalHref,
} from '../site-routes'
export type SupportTopicGuide = {
title: string
@ -45,9 +50,9 @@ export const supportTopicGuidance: Record<string, SupportTopicGuide> = {
'The public manual explains the browser-versus-desktop product split that launch copy must preserve.',
],
actions: [
{ label: 'Sign in for protected release lane', to: buildLoginPath(buildProtectedDownloadPath('windows')) },
{ label: 'Sign in for protected notices', to: buildLoginPath('/app/notices') },
{ label: 'Open pricing', to: '/pricing' },
{ label: 'Open download center', to: '/download' },
{ label: 'Review notices', to: '/open-source-notices' },
],
},
'operator-access': {
@ -65,9 +70,9 @@ export const supportTopicGuidance: Record<string, SupportTopicGuide> = {
'The public manual explains when the browser dashboard owns the next step and when the native runtime takes over.',
],
actions: [
{ label: 'Open sign-in', to: '/login' },
{ label: 'Open download posture', to: '/download' },
{ label: 'Open public manual', to: '/docs' },
{ label: 'Sign in for protected downloads', to: buildLoginPath(buildProtectedDownloadPath('windows')) },
{ label: 'Create account for desktop access', to: buildRegisterPath(buildProtectedDownloadPath('windows')) },
{ label: 'Open protected dashboard', to: buildLoginPath('/app') },
],
},
'studio-rollout': {
@ -85,9 +90,10 @@ export const supportTopicGuidance: Record<string, SupportTopicGuide> = {
'Resources summarize higher-dimensional runtime lanes and operator playbooks for wider team review.',
],
actions: [
{ label: 'Sign in for protected notices', to: buildLoginPath('/app/notices') },
{ label: 'Sign in for protected browser access', to: buildLoginPath('/app/browser-access') },
{ label: 'Open release notes', to: '/changelog' },
{ label: 'Open resources', to: '/resources' },
{ label: 'Review notices', to: '/open-source-notices' },
],
},
}

View file

@ -762,6 +762,11 @@ export const resourceCollections = [
] as const
export const changelogEntries = [
{
date: 'June 24, 2026',
title: 'Public support topics now preserve exact protected destinations through auth',
details: 'Support-topic quick routes no longer stop at generic public guidance. Launch-readiness, operator-access, and studio-rollout actions now deep-link into the correct protected dashboard, download, browser-access, or notices destination through auth-preserving next-target links.',
},
{
date: 'June 24, 2026',
title: 'Support-topic routing now spans support, docs, and resources with the same authority split',

View file

@ -1,3 +1,5 @@
import { normalizeNextPath } from './auth/next-path'
export type DownloadPlatformKey = 'windows' | 'macos' | 'linux'
const DOWNLOAD_PLATFORM_LABELS: Record<DownloadPlatformKey, string> = {
@ -43,6 +45,19 @@ export function buildSupportPath(topic?: string): string {
return `/support?${params.toString()}`
}
function buildAuthPath(mode: 'login' | 'register', nextPath: string | null | undefined, fallback = '/app') {
const normalizedNextPath = normalizeNextPath(nextPath, fallback)
return `/${mode}?next=${encodeURIComponent(normalizedNextPath)}`
}
export function buildLoginPath(nextPath?: string | null, fallback?: string): string {
return buildAuthPath('login', nextPath, fallback)
}
export function buildRegisterPath(nextPath?: string | null, fallback?: string): string {
return buildAuthPath('register', nextPath, fallback)
}
export function isExternalHref(value: string): boolean {
return /^[a-z][a-z0-9+.-]*:/i.test(value)
}