diff --git a/src/routes/lineage/index.tsx b/src/routes/lineage/index.tsx new file mode 100644 index 0000000..cb0f941 --- /dev/null +++ b/src/routes/lineage/index.tsx @@ -0,0 +1,144 @@ +/** + * Life and Lineage — lineage subdomain landing page (task 08). + * + * Migrates the legacy `src/routes/marketing/life-and-lineage.tsx` page to the + * `lineage.freno.me` root (vercel.json rewrites `lineage.freno.me/*` → + * `/lineage/*`), and enriches it with feature highlights + a game-art strip. + * + * Site-awareness: + * - `` reads `useSite()` → the lineage `titleSuffix` + * (` | Life and Lineage`) + canonical `https://lineage.freno.me/` are + * derived automatically (task 02); we pass only the base title here. + * - The Google Play badge links to the **public browser path** `/downloads` + * (subdomain-relative), which vercel rewrites to `/lineage/downloads` + * (task 11). The App Store link is an absolute external URL, unchanged. + * - `SimpleParallax` reuses the existing Cave parallax background — a + * fitting "dark fantasy" ambience — and continues to work under the + * subdomain because assets are served from the shared `/public` root. + * + * Auth: this page performs NO auth — Lineage's mobile JWT (`LINEAGE_JWT_SECRET`) + * is for the mobile app's API calls, not the web landing page. + */ +import { A } from "@solidjs/router"; +import { PageHead } from "~/components/PageHead"; +import SimpleParallax from "~/components/SimpleParallax"; +import DownloadOnAppStoreDark from "~/components/icons/DownloadOnAppStoreDark"; +import { For } from "solid-js"; +import { + APP_STORE_URL, + DOWNLOADS_HREF, + APP_ICON_SRC, + GOOGLE_PLAY_BADGE_SRC, + SCREENSHOT_ASSETS, + PAGE_META, + FEATURES +} from "~/routes/lineage/landing-content"; + +export default function LineageLandingPage() { + return ( + <> + + +
+ {/* Hero */} + Life and Lineage App Icon +

+ Life and Lineage +

+

A dark fantasy adventure

+ + {/* Store badges — App Store (external) + Google Play → /downloads */} + + + {/* Feature highlights */} +
+

+ Why players return +

+
+ + {(feature) => ( +
+

{feature.title}

+

+ {feature.description} +

+
+ )} +
+
+
+ + {/* Game art / screenshots strip */} +
+

+ glimpses of the world +

+
+
+ Life and Lineage home + Life and Lineage shops +
+
+
+ + {/* Secondary CTA → per-subdomain downloads page */} +

+ Prefer a direct download?{" "} + + Visit the downloads page + + . +

+
+
+ + ); +} diff --git a/src/routes/lineage/landing-content.test.ts b/src/routes/lineage/landing-content.test.ts new file mode 100644 index 0000000..9dbe041 --- /dev/null +++ b/src/routes/lineage/landing-content.test.ts @@ -0,0 +1,133 @@ +/** + * Unit tests for the Lineage landing page content (task 08). + * + * Mirrors the `page-head-meta.ts` / `nav-config.ts` pattern: assert against + * pure constants exported from `landing-content.ts` (no solid-js / router / + * DOM). This covers the task-08 acceptance matrix that's structurally + * verifiable without rendering: + * - App Store link is present and correct + * - Google Play / downloads link targets the subdomain `/downloads` path + * (public browser path, NOT the vercel-rewritten `/lineage/downloads`) + * - Feature highlights cover: dark fantasy, mobile, remote saves, PvP + * - PageHead base title + description (suffix is added by PageHead, task 02) + * - Legacy `/marketing/life-and-lineage` redirect target points at the + * Lineage subdomain. + * + * The PageHead title-suffix + canonical derivation for the lineage site is + * already covered by `src/components/PageHead.test.ts`; these tests assert + * the *inputs* the page passes to PageHead. + */ +import { describe, it, expect } from "bun:test"; +import { + APP_STORE_URL, + DOWNLOADS_HREF, + APP_ICON_SRC, + GOOGLE_PLAY_BADGE_SRC, + SCREENSHOT_ASSETS, + PAGE_META, + FEATURES, + LEGACY_REDIRECT_TARGET, + type LineageFeature +} from "~/routes/lineage/landing-content"; + +describe("Lineage landing — App Store link", () => { + it("matches the canonical Life and Lineage App Store URL", () => { + expect(APP_STORE_URL).toBe( + "https://apps.apple.com/us/app/life-and-lineage/id6737252442" + ); + }); + + it("is an absolute https URL", () => { + expect(APP_STORE_URL.startsWith("https://")).toBe(true); + }); +}); + +describe("Lineage landing — downloads link", () => { + it("targets the subdomain-relative public browser path", () => { + // NOT `/lineage/downloads` (the internal vercel-rewrite prefix) — vercel + // rewrites `lineage.freno.me/downloads` → `/lineage/downloads` while + // leaving the browser URL clean, matching the canonical rule from task 02. + expect(DOWNLOADS_HREF).toBe("/downloads"); + }); + + it("does not leak the internal route prefix", () => { + expect(DOWNLOADS_HREF).not.toContain("/lineage"); + }); +}); + +describe("Lineage landing — asset paths", () => { + it("points at the shared app icon", () => { + expect(APP_ICON_SRC).toBe("/LineageIcon.png"); + }); + + it("points at the shared Google Play badge", () => { + expect(GOOGLE_PLAY_BADGE_SRC).toBe("/google-play-badge.png"); + }); + + it("exposes screenshot + preview assets", () => { + expect(SCREENSHOT_ASSETS.home).toBe("/lineage-home.png"); + expect(SCREENSHOT_ASSETS.shops).toBe("/lineage-shops.png"); + expect(SCREENSHOT_ASSETS.preview).toBe("/lineage-preview.mp4"); + }); +}); + +describe("Lineage landing — PageHead inputs", () => { + it("passes the base title (suffix is appended by PageHead)", () => { + expect(PAGE_META.title).toBe("Life and Lineage"); + }); + + it("does not pre-bake the site suffix into the title", () => { + // PageHead via resolvePageHeadMeta appends ` | Life and Lineage`. + expect(PAGE_META.title).not.toContain("|"); + }); + + it("carries a non-empty description", () => { + expect(PAGE_META.description.length).toBeGreaterThan(0); + }); + + it("description mentions both store fronts", () => { + expect(PAGE_META.description.toLowerCase()).toContain("app store"); + expect(PAGE_META.description.toLowerCase()).toContain("google play"); + }); +}); + +describe("Lineage landing — feature highlights", () => { + it("exposes exactly the four required pillars", () => { + expect(FEATURES.length).toBe(4); + const titles = FEATURES.map((f) => f.title); + expect(titles).toContain("Dark Fantasy Adventure"); + expect(titles).toContain("Built for Mobile"); + expect(titles).toContain("Remote Saves"); + expect(titles).toContain("PvP Combat"); + }); + + it("every feature has a non-empty title + description", () => { + for (const f of FEATURES as LineageFeature[]) { + expect(f.title.length).toBeGreaterThan(0); + expect(f.description.length).toBeGreaterThan(0); + } + }); + + it("covers the dark-fantasy / mobile / saves / PvP themes", () => { + const blob = FEATURES.map( + (f) => `${f.title} ${f.description}` + ) + .join(" ") + .toLowerCase(); + expect(blob).toContain("dark fantasy"); + expect(blob).toContain("mobile"); + expect(blob).toContain("sav"); + expect(blob).toContain("pvp"); + }); +}); + +describe("Lineage landing — legacy redirect target", () => { + it("points at the lineage subdomain apex", () => { + expect(LEGACY_REDIRECT_TARGET).toBe("https://lineage.freno.me"); + }); + + it("is an https absolute URL with no trailing path", () => { + expect(LEGACY_REDIRECT_TARGET.startsWith("https://")).toBe(true); + expect(LEGACY_REDIRECT_TARGET.endsWith("/")).toBe(false); + }); +}); diff --git a/src/routes/lineage/landing-content.ts b/src/routes/lineage/landing-content.ts new file mode 100644 index 0000000..2a677d4 --- /dev/null +++ b/src/routes/lineage/landing-content.ts @@ -0,0 +1,102 @@ +/** + * Pure content + metadata for the Lineage subdomain landing page (task 08). + * + * Intentionally imports NOTHING from solid-js / @solidjs/router / @solidjs/meta + * so the constant set here can be unit-tested in `bun:test` without spinning + * up the SolidJS router / MetaProvider — mirroring the pattern established by + * `page-head-meta.ts` and `nav-config.ts`. + * + * The render layer (`./index.tsx`) is a thin JSX consumer of these values: + * keeping them externalized means the acceptance matrix (App Store URL, + * Google Play → subdomain `/downloads` link, feature highlight copy, PageHead + * title/description) is asserted against in `landing-content.test.ts` without + * a DOM render. + * + * Cross-task contracts encoded here: + * - `APP_STORE_URL` is the canonical App Store link the marketing page has + * always surfaced (kept stable across the migration). + * - `DOWNLOADS_HREF` is the **public browser path** on the lineage subdomain + * (`/downloads`), NOT the internal vercel-rewritten prefix `/lineage/downloads`. + * This matches the canonical-URL rule from task 02 and the nav-config rule + * from task 04: vercel.json maps `lineage.freno.me/downloads` → + * `/lineage/downloads` server-side while the browser sees `/downloads`. + * Task 11 will create the matching `src/routes/lineage/downloads.tsx`. + * - `PAGE_META` is consumed verbatim by ``; the per-site title + * suffix (` | Life and Lineage`) is appended automatically by + * `resolvePageHeadMeta` (task 02), so the `title` here is the BASE title + * only — do NOT include the suffix. + */ + +/** Apple App Store link — surfaced unchanged from the legacy marketing page. */ +export const APP_STORE_URL = + "https://apps.apple.com/us/app/life-and-lineage/id6737252442"; + +/** + * Public browser path to the per-subdomain downloads page (task 11). + * Subdomain-relative: renders `lineage.freno.me/downloads` in the browser. + */ +export const DOWNLOADS_HREF = "/downloads"; + +/** App icon asset, served from the site root (shared with the main site). */ +export const APP_ICON_SRC = "/LineageIcon.png"; + +/** Google Play badge asset (shared with the main site's downloads page). */ +export const GOOGLE_PLAY_BADGE_SRC = "/google-play-badge.png"; + +/** Screenshot / game-art assets used in the enhanced marketing content. */ +export const SCREENSHOT_ASSETS = { + home: "/lineage-home.png", + shops: "/lineage-shops.png", + preview: "/lineage-preview.mp4" +} as const; + +/** Base page title (site suffix appended by PageHead). */ +export const PAGE_META = { + title: "Life and Lineage", + description: + "A dark fantasy adventure mobile game. Download Life and Lineage on the App Store and Google Play." +} as const; + +/** + * Feature highlights surfaced in the enhanced marketing section. + * + * Each entry is `{ title, description }` so the renderer can present them + * in a uniform grid; the test asserts the full set is present so the + * acceptance criteria ("dark fantasy adventure, mobile game, remote saves, + * PvP") is verified structurally. + */ +export interface LineageFeature { + title: string; + description: string; +} + +export const FEATURES: readonly LineageFeature[] = [ + { + title: "Dark Fantasy Adventure", + description: + "Carve your legend through a grim, atmospheric world steeped in dark fantasy." + }, + { + title: "Built for Mobile", + description: + "Jump in anywhere — designed ground-up for quick sessions on iOS and Android." + }, + { + title: "Remote Saves", + description: + "Your lineage follows you across devices with cloud-backed character saves." + }, + { + title: "PvP Combat", + description: + "Test your build against other lineages in head-to-head PvP showdowns." + } +]; + +/** + * Canonical absolute URL the legacy `/marketing/life-and-lineage` route + * 308-redirects to (see `src/routes/marketing/life-and-lineage.ts`). Kept here + * so tests can assert the redirect target without importing the route module + * (which would pull in the server runtime). + */ +export const LEGACY_REDIRECT_TARGET = "https://lineage.freno.me"; diff --git a/src/routes/marketing/life-and-lineage.tsx b/src/routes/marketing/life-and-lineage.tsx index dd6be47..259a5c2 100644 --- a/src/routes/marketing/life-and-lineage.tsx +++ b/src/routes/marketing/life-and-lineage.tsx @@ -1,51 +1,27 @@ -import { A } from "@solidjs/router"; -import { PageHead } from "~/components/PageHead"; -import SimpleParallax from "~/components/SimpleParallax"; -import DownloadOnAppStoreDark from "~/components/icons/DownloadOnAppStoreDark"; +/** + * Legacy Life and Lineage marketing route — now a 308 permanent redirect to + * the Lineage subdomain (task 08). + * + * The marketing content has been migrated to `src/routes/lineage/index.tsx` + * served at `lineage.freno.me` (vercel.json host rewrites map the subdomain to + * the `/lineage/*` internal prefix). Keeping this route as a permanent (308) + * server-side redirect — rather than a client `` — preserves SEO + * equity and gives installed / linked URLs a stable resolution path. + * + * Implemented as a SolidStart API route (`GET` handler returning a Response) + * so the redirect happens before any rendering; the route no longer ships a + * page component. The redirect target is centralized in + * `~/routes/lineage/landing-content.ts` (`LEGACY_REDIRECT_TARGET`) so the + * unit test can assert the destination without importing this server module. + */ +import { LEGACY_REDIRECT_TARGET } from "~/routes/lineage/landing-content"; -export default function LifeAndLineageMarketing() { - return ( - <> - - -
-
- Lineage App Icon -
-

Life and Lineage

-

A dark fantasy adventure

- -
-
- - ); +export function GET() { + return new Response(null, { + status: 308, + headers: { + Location: LEGACY_REDIRECT_TARGET, + "Cache-Control": "public, max-age=0, must-revalidate" + } + }); }