Add public getting-started onboarding route
This commit is contained in:
parent
1a7bbfae92
commit
0505a49a42
13 changed files with 299 additions and 6 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 />,
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue