Add public getting-started onboarding route

This commit is contained in:
axiomlogicnexus 2026-06-27 23:21:47 +00:00
parent 1a7bbfae92
commit 0505a49a42
13 changed files with 299 additions and 6 deletions

View file

@ -242,6 +242,7 @@ It is **not** a claim that the simulator itself is now browser-owned.
The website now includes:
- public download center
- dedicated public `/getting-started` route for the shortest browser-to-desktop onboarding path
- plan-gated release posture
- browser-to-desktop token handoff
- protected-dashboard download gating above the public target-description page

View file

@ -63,6 +63,16 @@ into a more practical public operator reference:
operational language, and shipping/payment now carries the real digital
delivery workflow instead of generic storefront filler
A later same-family continuity packet then added a dedicated public
`/getting-started` route so the shortest complete operator path no longer has
to be reconstructed from homepage, docs, and download fragments alone. That
route now acts as the canonical public onboarding/manual surface for:
- first-session browser-to-desktop flow
- access and protected download continuity
- first launch and higher-dimensional verification order
- explicit XR/controller boundary truth
## Protected-surface continuation
The protected browser shell is no longer only one strong dashboard route plus
@ -98,6 +108,7 @@ drop in information quality.
- docs page
- pricing page
- download page
- getting-started page
- support page
- changelog page
- open-source notices page

View file

@ -1041,11 +1041,16 @@ Current audit note:
quickstart covering release-target choice, protected dashboard pairing,
first classic-cube verification, higher-dimensional verification, and the
explicit XR/controller `No-Go` boundary
- the protected dashboard now mirrors that same quickstart after sign-in so
account state, desktop-link pairing, first launch, and native-runtime
boundary truth remain visible together inside the operator shell
- this keeps the public manual professional without inventing browser
simulator parity or widening the native XR branch
- the protected dashboard now mirrors that same quickstart after sign-in so
account state, desktop-link pairing, first launch, and native-runtime
boundary truth remain visible together inside the operator shell
- a later same-family continuity packet then anchored that same quickstart on a
dedicated public `/getting-started` route so the shortest complete
browser-to-desktop onboarding/manual path is available as one canonical
crawlable page rather than only as repeated fragments across homepage, docs,
and download
- this keeps the public manual professional without inventing browser
simulator parity or widening the native XR branch
- focused validation for that continuation stayed green under:
- `npm --prefix website test -- --run src/__tests__/DashboardOverviewPage.test.tsx src/__tests__/public-marketing-pages.test.tsx`
- `2` test files passed

File diff suppressed because one or more lines are too long

View file

@ -3,6 +3,7 @@
First-party `hypertwist.app` surface for HyperTwist:
- public homepage, about, resources, pricing, support, and legal pages
- dedicated public `/getting-started` onboarding route for the first real browser-to-desktop operator journey
- public feature-atlas route for current capability and boundary truth
- browser-facing operator/account dashboard
- protected browser-access, account, and notices routes backed by live auth-health and release-manifest authority
@ -76,6 +77,9 @@ manual:
end-to-end operator path after sign-in so access, pairing, first launch, and
the current XR/controller boundary do not have to be reconstructed from
scattered cards
- that same first-session story is now also anchored at a dedicated public
`/getting-started` route so the shortest complete onboarding/manual lane is
crawlable, canonical, and easier to hand off than the broader page set alone
- the protected app shell now also carries richer operator-facing browser
boundary, account, entitlement, and notices guidance instead of treating
those routes as thin placeholders beside the main dashboard

View file

@ -12,6 +12,9 @@
<url>
<loc>https://hypertwist.app/resources</loc>
</url>
<url>
<loc>https://hypertwist.app/getting-started</loc>
</url>
<url>
<loc>https://hypertwist.app/docs</loc>
</url>

View file

@ -179,6 +179,14 @@ describe('AppRouteTree', () => {
expect(screen.getByText('Available targets')).toBeTruthy()
})
it('renders the getting-started page through the real public route tree', async () => {
renderRoute('/getting-started')
expect(screen.getByText('Getting started')).toBeTruthy()
expect(screen.getByText('Operator onboarding')).toBeTruthy()
expect(screen.getByText(/first-session quickstart, browser-to-desktop pairing flow, and the current desktop-first runtime boundary/i)).toBeTruthy()
expect(await screen.findByText('Start in the browser. Train in the desktop runtime.')).toBeTruthy()
})
it('renders the login page through the real public route tree', async () => {
renderRoute('/login?next=%2Fapp%2Fdownloads')

View file

@ -118,6 +118,7 @@ import {
DocsPage,
DownloadPage,
FeaturesPage,
GettingStartedPage,
HomeLanding,
OpenSourceNoticesPage,
PricingPage,
@ -1305,6 +1306,122 @@ describe('public marketing pages', () => {
expect(await screen.findByRole('link', { name: /open public docs portal/i })).toBeTruthy()
})
it('renders the dedicated getting-started page as a canonical onboarding route', async () => {
mockGetAuthHealth.mockResolvedValue({
ok: true,
service: 'hypertwist-auth-server',
supertokens: {
configured: true,
reachable: true,
ready: true,
apiVersion: '5.4',
error: null,
oauth: {
github: false,
google: false,
},
},
fallback: {
enabled: true,
active: false,
reason: null,
},
billing: {
statePath: '/var/lib/hypertwist/auth/hypertwist-billing-state.json',
processedEventCount: 0,
pricePlanMapConfigured: false,
productPlanMapConfigured: false,
webhookSecretConfigured: false,
},
runtime: {
mode: 'mixed',
public_origin_ready: true,
cookie_secure: true,
api_domain: 'https://hypertwist.app',
website_domain: 'https://hypertwist.app',
warnings: [],
errors: [],
},
})
mockGetReleaseManifest.mockResolvedValue({
ok: true,
manifest: {
generated_at: '2026-06-22T12:00:00.000Z',
support_email: 'hello@hypertwist.app',
public_docs_url: 'https://docs.hypertwist.app',
release_notes_url: 'https://notes.hypertwist.app',
corresponding_source_url: 'https://hypertwist.app/open-source/source.zip',
open_source_repo_url: 'https://github.com/hypertwist/hypertwist',
viewer: {
authenticated: false,
canDownload: false,
plan: null,
role: null,
accessStatus: null,
},
platforms: [
{
platform_key: 'windows',
platform: 'Windows',
subtitle: 'Primary shipping lane',
details: 'Current packaged validation is strongest on the Windows Unreal lane.',
configured: true,
channel: 'candidate',
version: '1.0.0',
build_id: 'win64-1000',
published_at: '2026-06-22T00:00:00.000Z',
file_name: 'HyperTwist-Windows.zip',
file_size_bytes: 1048576,
checksum_sha256: 'abc123',
download_url: null,
download_available: false,
validation_summary: {
lane: 'Windows Unreal packaged validation',
result: 'passed',
generated_at: '2026-06-22T01:43:08.7625247Z',
configuration: 'Development',
skip_build: true,
smoke_map_count: 2,
smoke_maps: [
{
map_url: '/Game/HyperTwistTraining/Maps/L_HyperTwist_Magic120CellTraining',
label: 'Magic120Cell dedicated-family training map',
result: 'passed',
},
{
map_url: '/Game/HyperTwistTraining/Maps/L_HyperTwist_MagicCube5DTraining',
label: 'MagicCube5D dedicated-family training map',
result: 'passed',
},
],
},
},
],
},
})
renderWithProviders(<GettingStartedPage />, ['/getting-started'])
expect(screen.getByText('The first serious HyperTwist session')).toBeTruthy()
expect(screen.getByText('Operator path')).toBeTruthy()
expect(screen.getByText('Browser account access methods')).toBeTruthy()
expect(screen.getByText('First launch and desktop setup')).toBeTruthy()
expect(screen.getByText('Simulator use today')).toBeTruthy()
expect(screen.getByText('Current control and device truth')).toBeTruthy()
expect(screen.getAllByText('Selectable control and settings roster').length).toBeGreaterThan(0)
expect(screen.getByText('Choose the right HyperTwist surface')).toBeTruthy()
expect(screen.getByText('Current packaged desktop proof')).toBeTruthy()
expect(screen.getByText('Need help on the way in?')).toBeTruthy()
expect(
screen
.getAllByRole('link', { name: 'Open download center' })
.some((link) => link.getAttribute('href') === '/download'),
).toBe(true)
expect(screen.getAllByText(/classic-wca-keyboard\/v1/i).length).toBeGreaterThan(0)
expect(document.title).toBe('Getting started with HyperTwist | HyperTwist')
expect(document.head.querySelector('meta[property="og:url"]')?.getAttribute('content')).toBe('https://hypertwist.app/getting-started')
})
it('renders the release-notes page with the latest native selector-recall packet and rollout checklist', async () => {
mockGetAuthHealth.mockResolvedValue({
ok: true,

View file

@ -33,6 +33,7 @@ describe('public route registry', () => {
it('keeps auth routes out of crawlable sitemap posture', () => {
expect(crawlablePublicRoutes.map((route) => route.path)).not.toContain('/login')
expect(crawlablePublicRoutes.map((route) => route.path)).not.toContain('/register')
expect(crawlablePublicRoutes.map((route) => route.path)).toContain('/getting-started')
})
it('carries non-empty loader metadata for every shared public-shell route', () => {
@ -48,6 +49,7 @@ describe('public route registry', () => {
expect(xml).toContain('<loc>https://hypertwist.app/</loc>')
expect(xml).toContain('<loc>https://hypertwist.app/features</loc>')
expect(xml).toContain('<loc>https://hypertwist.app/getting-started</loc>')
expect(xml).not.toContain('/login')
expect(xml).not.toContain('/register')
})

View file

@ -12,6 +12,7 @@ import {
changelogEntries,
companyNarrative,
degradedStateRecoveryTracks,
desktopFirstLaunchCards,
deploymentReadinessTracks,
digitalDeliveryCards,
featureAtlasCurrentTracks,
@ -289,6 +290,9 @@ export function HomeLanding() {
<Link className="button button--ghost" to="/app">
Open operator dashboard
</Link>
<Link className="button button--ghost" to="/getting-started">
Open getting started
</Link>
<Link className="button button--ghost" to="/features">
Explore feature atlas
</Link>
@ -665,6 +669,11 @@ export function ResourcesPage() {
<h3>Feature atlas</h3>
<p>One public page for current capability, surface boundaries, and release posture.</p>
</Link>
<Link to="/getting-started" className="feature-band__card feature-band__card--link">
<MonitorCog size={22} />
<h3>Getting started</h3>
<p>The canonical first-session path from browser access into the packaged desktop runtime.</p>
</Link>
<Link to="/docs" className="feature-band__card feature-band__card--link">
<BookOpenText size={22} />
<h3>Docs landing</h3>
@ -846,6 +855,121 @@ export function ResourcesPage() {
)
}
export function GettingStartedPage() {
const { releaseManifest, windowsValidationPlatform } = usePublicReleaseManifestView('public-getting-started')
return (
<>
<SiteMetadata
title="Getting started with HyperTwist"
description="Follow the real first-session path in HyperTwist: browser account access, protected release, desktop pairing, first launch, and current simulator boundary truth."
canonicalPath="/getting-started"
/>
<MarketingShell
eyebrow="Operator onboarding"
title="Start in the browser. Train in the desktop runtime."
lede="This page is the shortest complete operator path through the current product: resolve access, pair the installed runtime safely, verify the first session, and keep the real XR/controller boundary visible."
>
<OperatorDesktopQuickstartSection
title="The first serious HyperTwist session"
description="This quickstart stays faithful to the current product topology: browser first for identity and release posture, desktop first for the actual simulator."
/>
<StepCardSection
title="Operator path"
description="These are the current first-party working stages from public discovery through protected release access and into the native simulator."
cards={operatorManualTracks}
/>
<Section
title="Browser account access methods"
description="The onboarding path is easier to trust when the shared-auth provider lineup is visible before operators cross into the protected shell."
>
<BrowserAuthMethodsGuide />
</Section>
<Section
title="First launch and desktop setup"
description="These are the concrete checks that should happen immediately after the package is in hand."
>
<div className="card-grid">
{desktopFirstLaunchCards.map((card) => (
<article key={card.title} className="card">
<h3>{card.title}</h3>
<p>{card.description}</p>
<ul className="list top-gap">
{card.bullets.map((bullet) => (
<li key={bullet}>{bullet}</li>
))}
</ul>
</article>
))}
</div>
</Section>
<BrowserDesktopRealitySection />
<Section
title="Simulator use today"
description="This keeps onboarding anchored to the lanes that actually execute in the current native runtime."
>
<div className="card-grid">
{simulatorManualCards.map((card) => (
<article key={card.title} className="card">
<h3>{card.title}</h3>
<p>{card.description}</p>
<ul className="list top-gap">
{card.bullets.map((bullet) => (
<li key={bullet}>{bullet}</li>
))}
</ul>
</article>
))}
</div>
</Section>
<BulletCardSection
title="Current control and device truth"
description="This is the practical runtime boundary that first-session onboarding should teach directly instead of leaving as a later surprise."
cards={inputAndDevicePostureCards}
/>
<BulletCardSection
title="Selectable control and settings roster"
description="The current desktop runtime already has real profile and settings ownership, but it stays narrower than a finished XR/controller/preferences suite."
cards={controlProfileRosterCards}
/>
<SurfaceChoiceGuideSection description="Onboarding is smoother when the right next surface is explicit too: public for orientation, protected for entitled access, and desktop for simulator execution." />
<PublicReleaseDecisionGuideSection
releaseManifest={releaseManifest}
description="This onboarding lane should also tell operators whether the next honest move is protected desktop access, pricing/provisioning, browser/account continuity, or notices/source follow-through."
/>
<PublicPackagedDesktopProofSection
platform={windowsValidationPlatform}
actions={[
{ to: '/download', label: 'Open download center' },
{ to: '/app', label: 'Open operator dashboard' },
]}
/>
<SupportTopicDirectorySection
title="Need help on the way in?"
description="Use these quick routes when the next onboarding step turns into a launch, access, or rollout issue."
topics={supportTopicDirectory}
/>
<ReleaseAuthorityBundleSection
releaseManifest={releaseManifest}
description="A professional onboarding page should keep docs, release notes, corresponding source, notices, and operator support visible as part of the same release story."
/>
</MarketingShell>
</>
)
}
export function DocsPage() {
const { releaseManifest, windowsValidationPlatform } = usePublicReleaseManifestView('public-docs')

View file

@ -39,6 +39,16 @@
"footer": true,
"crawlable": true
},
{
"path": "/getting-started",
"label": "Getting Started",
"loaderTitle": "Getting started",
"loaderEyebrow": "Operator onboarding",
"loaderDescription": "Loading the first-session quickstart, browser-to-desktop pairing flow, and the current desktop-first runtime boundary.",
"nav": false,
"footer": true,
"crawlable": true
},
{
"path": "/docs",
"label": "Docs",

View file

@ -7,6 +7,7 @@ const HomeLanding = lazy(() => import('../pages/public-pages-marketing').then((m
const FeaturesPage = lazy(() => import('../pages/public-pages-features').then((m) => ({ default: m.FeaturesPage })))
const AboutPage = lazy(() => import('../pages/public-pages-marketing').then((m) => ({ default: m.AboutPage })))
const ResourcesPage = lazy(() => import('../pages/public-pages-marketing').then((m) => ({ default: m.ResourcesPage })))
const GettingStartedPage = lazy(() => import('../pages/public-pages-marketing').then((m) => ({ default: m.GettingStartedPage })))
const DocsPage = lazy(() => import('../pages/public-pages-marketing').then((m) => ({ default: m.DocsPage })))
const SupportPage = lazy(() => import('../pages/public-pages-marketing').then((m) => ({ default: m.SupportPage })))
const ChangelogPage = lazy(() => import('../pages/public-pages-marketing').then((m) => ({ default: m.ChangelogPage })))
@ -24,6 +25,7 @@ const publicRouteElements: Record<string, ReactNode> = {
'/features': <FeaturesPage />,
'/about': <AboutPage />,
'/resources': <ResourcesPage />,
'/getting-started': <GettingStartedPage />,
'/docs': <DocsPage />,
'/support': <SupportPage />,
'/changelog': <ChangelogPage />,

View file

@ -818,6 +818,7 @@ export const resourceCollections = [
title: 'Product truth',
items: [
'Feature registry-backed descriptions only',
'Dedicated getting-started route for the first real browser-to-desktop operator journey',
'Public feature atlas for the real browser-versus-desktop product split',
'Roadmap-honest separation between shipped and retained capability',
'Release notes that surface what actually landed',
@ -863,6 +864,11 @@ export const resourceCollections = [
] as const
export const changelogEntries = [
{
date: 'June 27, 2026',
title: 'A dedicated getting-started route now anchors the public onboarding story',
details: 'HyperTwist now has a crawlable public getting-started page that turns the current first-session path into one canonical manual surface: release-target choice, protected dashboard pairing, first native classic-cube verification, higher-dimensional verification, and the current XR/controller No-Go boundary all live together instead of staying spread across separate pages only.',
},
{
date: 'June 27, 2026',
title: 'Public and protected manuals now show the first real desktop-session path more directly',