diff --git a/.env.example b/.env.example index 263d098..866c130 100644 --- a/.env.example +++ b/.env.example @@ -24,3 +24,6 @@ WEB_PORT=3000 # The browser calls the same-origin path /api; Next proxies it server-side to the # backend at this address (the compose service name). Avoids CORS and per-env URL baking. API_PROXY_TARGET=http://api:4000 +# Public origin used for canonical/Open-Graph absolute URLs and the sitemap. +# Baked into the web build (NEXT_PUBLIC_*). Set the real domain in production. +NEXT_PUBLIC_SITE_URL=http://localhost:3000 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9f154d8..35670a8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -68,7 +68,7 @@ jobs: continue-on-error: true e2e: - name: Dynamic (integration + e2e) + name: Dynamic (integration + e2e + lighthouse) needs: static runs-on: ubuntu-latest env: @@ -78,38 +78,71 @@ jobs: steps: - uses: actions/checkout@v5 + # Node is only needed on the runner for the Lighthouse CI step (the + # integration + e2e suite itself runs in-container). Cache the tests lockfile. + - uses: actions/setup-node@v5 + with: + node-version-file: .nvmrc + cache: npm + cache-dependency-path: tests/package-lock.json + # Buildx enables the gha layer-cache backend used by Compose Bake. - uses: docker/setup-buildx-action@v4 # Compose reads .env for variable substitution; the example values are - # self-consistent and safe for the ephemeral test stack. + # self-consistent and safe for the ephemeral test stack. WEB_PORT defaults + # to 3000, so `web` is published on localhost:3000 for the Lighthouse run. - name: Create .env run: cp .env.example .env - # Build the images (Compose Bake + gha layer cache) and run the in-container - # Playwright suite against fresh db/api/web. The `tests` container's exit code - # is the suite result. Mirrors `make test`. + # Boot the stack detached. `--build` is a fast CACHED replay of the gha layer + # cache; `--wait` blocks until db/api/web healthchecks pass (api migrates + + # seeds on boot against the ephemeral test volume). + - name: Boot the dockerized stack + env: + COMPOSE_BAKE: "1" + run: docker compose ${{ env.COMPOSE_FILES }} up -d --build --wait db api web + + # In-container Playwright integration + e2e against the running stack. The + # `tests` container's exit code gates the job; its HTML report lands on the + # host via the bind mount in docker-compose.test.yml. Mirrors `make test`. - name: Run integration + e2e env: COMPOSE_BAKE: "1" - run: docker compose ${{ env.COMPOSE_FILES }} up --build --abort-on-container-exit --exit-code-from tests tests + run: docker compose ${{ env.COMPOSE_FILES }} run --build --rm tests + + # Lighthouse benchmark gate on the landing page, reusing the booted stack. + # Runs on the runner (ubuntu ships Chrome) against the published :3000. Fails + # the job if SEO / Accessibility / Best-Practices drop below threshold + # (Performance is a warning). See .lighthouserc.json. + - name: Install Lighthouse CI + working-directory: tests + run: npm ci + + - name: Lighthouse benchmark gate (landing page) + if: ${{ !cancelled() }} + working-directory: tests + run: npm run lighthouse + + - name: Upload Lighthouse report + if: ${{ !cancelled() }} + uses: actions/upload-artifact@v7 + with: + name: lighthouse-report + path: tests/lighthouse-report + if-no-files-found: ignore + retention-days: 7 - name: Dump stack logs on failure if: failure() run: docker compose ${{ env.COMPOSE_FILES }} logs --no-color - # The HTML report is written inside the tests container; copy it out before teardown. - - name: Extract Playwright report - if: ${{ !cancelled() }} - continue-on-error: true - run: docker compose ${{ env.COMPOSE_FILES }} cp tests:/app/tests/playwright-report ./playwright-report - - name: Upload Playwright report if: ${{ !cancelled() }} uses: actions/upload-artifact@v7 with: name: playwright-report - path: playwright-report + path: tests/playwright-report if-no-files-found: ignore retention-days: 7 diff --git a/.gitignore b/.gitignore index 0dc0692..feff379 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,8 @@ playwright-report/ test-results/ blob-report/ .playwright/ +lighthouse-report/ +.lighthouseci/ # logs *.log diff --git a/.lighthouserc.json b/.lighthouserc.json new file mode 100644 index 0000000..f6a2110 --- /dev/null +++ b/.lighthouserc.json @@ -0,0 +1,24 @@ +{ + "ci": { + "collect": { + "url": ["http://localhost:3000/"], + "numberOfRuns": 1, + "settings": { + "preset": "desktop", + "chromeFlags": "--no-sandbox --headless=new --disable-gpu" + } + }, + "assert": { + "assertions": { + "categories:seo": ["error", { "minScore": 0.95 }], + "categories:accessibility": ["error", { "minScore": 0.95 }], + "categories:best-practices": ["error", { "minScore": 0.9 }], + "categories:performance": ["warn", { "minScore": 0.7 }] + } + }, + "upload": { + "target": "filesystem", + "outputDir": "lighthouse-report" + } + } +} diff --git a/Makefile b/Makefile index 9f4764c..7206095 100644 --- a/Makefile +++ b/Makefile @@ -68,6 +68,15 @@ test-be: ## Run backend unit tests (engine/RTP, services) test-e2e: ## Run Playwright e2e against the running web+api containers cd tests && npm run e2e +.PHONY: lighthouse +lighthouse: env ## Boot the stack and run the Lighthouse benchmark gate on the landing page + $(COMPOSE_TEST) up -d --build --wait db api web + @set -a; . ./.env; set +a; \ + url="http://localhost:$${WEB_PORT:-3000}/"; \ + echo "Lighthouse benchmark → $$url"; \ + ( cd tests && npm ci && npx lhci autorun --config=../.lighthouserc.json --collect.url="$$url" ); \ + code=$$?; $(COMPOSE_TEST) down -v; exit $$code + ## ─── Quality gates ─────────────────────────────────────────────────────── .PHONY: lint lint: ## Lint backend + frontend diff --git a/docker-compose.test.yml b/docker-compose.test.yml index 1c95a28..1620b3e 100644 --- a/docker-compose.test.yml +++ b/docker-compose.test.yml @@ -17,6 +17,11 @@ services: API_URL: http://api:4000 WEB_URL: http://web:3000 DATABASE_URL: ${DATABASE_URL} + # Surface the Playwright HTML report + traces on the host so CI can upload them + # (the runner-side Lighthouse gate reaches `web` via the published :3000 port). + volumes: + - ./tests/playwright-report:/app/tests/playwright-report + - ./tests/test-results:/app/tests/test-results depends_on: api: condition: service_healthy diff --git a/docker-compose.yml b/docker-compose.yml index 9e07e63..a3bb894 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -55,6 +55,7 @@ services: dockerfile: frontend/Dockerfile args: API_PROXY_TARGET: ${API_PROXY_TARGET:-http://api:4000} + NEXT_PUBLIC_SITE_URL: ${NEXT_PUBLIC_SITE_URL:-http://localhost:3000} restart: unless-stopped environment: NODE_ENV: ${NODE_ENV:-production} diff --git a/docs/adr/0009-lighthouse-benchmark-gate.md b/docs/adr/0009-lighthouse-benchmark-gate.md new file mode 100644 index 0000000..2d1b819 --- /dev/null +++ b/docs/adr/0009-lighthouse-benchmark-gate.md @@ -0,0 +1,66 @@ +# ADR 0009 — Lighthouse benchmark gate (SEO / Accessibility / Best-Practices) + +- Status: Accepted +- Date: 2026-06-11 + +## Context + +CI (ADR‑0008) covered correctness — build, lint, typecheck, unit, and the in‑container +integration/e2e suite — but nothing measured the **quality of the served page**: SEO, +accessibility, and front‑end best practices were unverified and could regress silently. The +convention‑template sibling repo (`mitekk/pool-stars`) already runs a Lighthouse gate in CI; +this ADR brings poker_land to parity and makes "benchmarks" a concrete, enforced signal. + +The home page was also an SEO dead end: anonymous visitors to `/` got an invisible splash that +client‑redirected to `/login` — thin content and a flaky audit target. + +## Decision + +- **Add a Lighthouse CI gate** via [`@lhci/cli`](https://github.com/GoogleChrome/lighthouse-ci) + configured by [`.lighthouserc.json`](../../.lighthouserc.json) at the repo root, mirroring the + pool‑stars template. It audits the landing page (`/`, desktop preset) and **fails the job** + below threshold: + + | Category | Level | Min score | + |---|---|---| + | `seo` | **error** | 0.95 | + | `accessibility` | **error** | 0.95 | + | `best-practices` | **error** | 0.90 | + | `performance` | warn | 0.70 | + + Performance is a non‑blocking warning — it is the most environment‑sensitive metric on shared + CI runners; the SEO/A11y/Best‑Practices floors are the enforced contract. +- **Make `/` a real public landing.** Anonymous `/` now renders crawlable marketing content with + genuine `` CTAs (no client redirect), giving search engines indexable content + and Lighthouse a stable target. Authenticated users still get the game. +- **Ship the SEO surface** that the audit (and real crawlers) expect: full metadata + (`metadataBase`, canonical, Open Graph, Twitter), `robots.ts`, `sitemap.ts`, a web manifest, + generated icons + OG image (`next/og`, no committed binaries), and JSON‑LD structured data. +- **Run Lighthouse on the runner, keep the suite in‑container.** The `e2e` job boots the stack + detached (`up -d --wait db api web`), runs the Playwright integration + e2e suite in‑container + (`docker compose run --rm tests` — its exit code still gates the job), then runs `lhci autorun` + **on the runner** against the published `http://localhost:3000/`. The report is uploaded as the + `lighthouse-report` artifact. + +## Alternatives considered + +- **Run Lighthouse in‑container** (inside the Playwright `tests` image) to stay 100% in‑container + like ADR‑0008's suite — rejected: it requires wrangling a Chrome binary in the image and a + container‑network audit URL (`http://web:3000`), diverging the config from the template. + ubuntu‑latest already ships Chrome, so the runner path is simpler and keeps + `.lighthouserc.json` identical to pool‑stars. The Playwright suite itself stays in‑container — + this is the one deliberate exception to ADR‑0008's "host‑run" rejection, scoped to Lighthouse. +- **Benchmark `/login` instead of `/`** — smaller change, but leaves the actual home page + SEO‑empty; the public landing fixes the root problem. +- **Warn‑only thresholds** — reports scores without enforcing them; rejected in favor of hard + floors so regressions actually block, matching the convention template. + +## Consequences + +- Push/PR now enforces SEO ≥ 0.95, Accessibility ≥ 0.95, Best‑Practices ≥ 0.90 on the landing + page, with the score report attached as an artifact. +- `/` is publicly indexable with structured data, a sitemap, robots rules, and social cards. +- The `e2e` job restructured from a single `up … tests` into boot → in‑container suite → + runner‑side Lighthouse; the Playwright HTML report now reaches the host via a bind mount in + [`docker-compose.test.yml`](../../docker-compose.test.yml). Reproduce locally with + `make lighthouse`. See [`docs/ci.md`](../ci.md). diff --git a/docs/ci.md b/docs/ci.md index 4516891..a02236d 100644 --- a/docs/ci.md +++ b/docs/ci.md @@ -3,7 +3,8 @@ GitHub Actions runs the same quality gate as the local `Makefile`, automatically on every push and pull request. The workflow lives in [`.github/workflows/ci.yml`](../.github/workflows/ci.yml); the decision behind it is -recorded in [ADR‑0008](adr/0008-github-actions-ci.md). +recorded in [ADR‑0008](adr/0008-github-actions-ci.md), and the Lighthouse benchmark gate in +[ADR‑0009](adr/0009-lighthouse-benchmark-gate.md). ## Overview @@ -12,7 +13,7 @@ Two jobs, mapped onto the existing `make` targets: | Job | What it does | Local equivalent | |---|---|---| | **`static`** | build → lint → typecheck → unit tests (in‑process) | `make lint` + `make typecheck` + `make test-be` | -| **`e2e`** | build & boot the dockerized stack, run the Playwright integration + e2e suite in‑container, tear down | `make test` | +| **`e2e`** | boot the dockerized stack, run the Playwright integration + e2e suite in‑container, then the **Lighthouse benchmark gate** on the runner, tear down | `make test` + `make lighthouse` | `e2e` is **gated behind `static`** (`needs: static`) — if the cheap static checks fail, we don't spend time building and booting Docker images. @@ -53,36 +54,44 @@ Runs on `ubuntu-latest`, Node from `.nvmrc` (`engines.node >=20`): 6. `npm audit --audit-level=high` — **advisory** (`continue-on-error`): surfaces HIGH/CRITICAL CVEs without failing the run. -## `e2e` job — integration + e2e (dynamic) +## `e2e` job — integration + e2e + lighthouse (dynamic) -Runs on `ubuntu-latest`. Mirrors `make test` — the suite runs **inside a `tests` -container** (Playwright image, browsers preinstalled) against fresh `db`/`api`/`web`: +Runs on `ubuntu-latest`. The integration + e2e suite runs **inside a `tests` container** +(Playwright image, browsers preinstalled); the Lighthouse benchmark gate runs **on the runner** +(ubuntu ships Chrome) against the published `web` port. Steps: -1. **Buildx** is set up (`docker/setup-buildx-action`) to enable the layer‑cache backend. -2. `cp .env.example .env` — Compose reads `.env` for variable substitution; the example - values are self‑consistent and safe for the ephemeral test stack. -3. **Build & run:** +1. **`setup-node`** (Node from `.nvmrc`, npm cache keyed on `tests/package-lock.json`) — only the + runner‑side Lighthouse step needs Node; the suite itself is containerized. +2. **Buildx** is set up (`docker/setup-buildx-action`) to enable the layer‑cache backend. +3. `cp .env.example .env` — Compose reads `.env` for variable substitution; the example values + are self‑consistent and safe for the ephemeral test stack. `WEB_PORT` defaults to `3000`, so + `web` is published on `localhost:3000` for Lighthouse. +4. **Boot the stack (detached):** ``` COMPOSE_BAKE=1 docker compose \ -f docker-compose.yml -f docker-compose.test.yml -f docker-compose.ci.yml \ - up --build --abort-on-container-exit --exit-code-from tests tests + up -d --build --wait db api web ``` - - The three `-f` files are passed explicitly so `docker-compose.override.yml` (the dev - target + bind mounts) is **not** merged. - - `docker-compose.test.yml` swaps in an ephemeral DB and adds the `tests` runner, which - `depends_on` `api`/`web` being healthy. - - The api `runner` image **migrates + seeds on boot**, so the fresh DB is provisioned - automatically before tests start. - - `--abort-on-container-exit --exit-code-from tests` makes the `tests` container's exit - code the suite result. -4. **Always:** on failure the stack logs are dumped; the HTML report is copied out of the - `tests` container and uploaded as the **`playwright-report`** artifact; the stack is torn - down with `down -v`. - -### Reading the report - -When `e2e` fails, open the run's **Artifacts → `playwright-report`**, unzip it, and open -`index.html`. Traces are attached for first‑retry / failed specs (`trace: on-first-retry`). + - The three `-f` files are passed explicitly so `docker-compose.override.yml` (the dev target + + bind mounts) is **not** merged. + - `--wait` blocks until db/api/web healthchecks pass; the api `runner` image **migrates + + seeds on boot**, so the fresh ephemeral DB is provisioned before tests start. +5. **Integration + e2e (in‑container):** `docker compose … run --rm tests`. The `tests` + container's exit code gates the job; its HTML report lands on the host via the bind mount in + `docker-compose.test.yml`. +6. **Lighthouse benchmark gate:** `npm ci` in `tests/`, then `npm run lighthouse` + (`lhci autorun --config=../.lighthouserc.json`) audits `http://localhost:3000/`. It **fails + the job** if SEO / Accessibility / Best‑Practices fall below threshold (Performance is a + warning). See [ADR‑0009](adr/0009-lighthouse-benchmark-gate.md) and + [`.lighthouserc.json`](../.lighthouserc.json). +7. **Always:** on failure the stack logs are dumped; the **`playwright-report`** and + **`lighthouse-report`** artifacts are uploaded; the stack is torn down with `down -v`. + +### Reading the reports + +When `e2e` fails, open the run's **Artifacts**: **`playwright-report`** (unzip, open +`index.html`; traces are attached for first‑retry / failed specs) and **`lighthouse-report`** +(the per‑category scores + the specific audits that dropped below threshold). ## Caching @@ -114,6 +123,7 @@ Each CI step has a `make` / npm equivalent: | static: unit tests | `make test-be` | | static: audit | `make audit` | | e2e: full dynamic suite | `make test` | +| e2e: Lighthouse benchmark gate | `make lighthouse` | > `make test` uses `docker-compose.yml` + `docker-compose.test.yml` only — the > `docker-compose.ci.yml` cache override is CI‑specific and not needed locally. diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 48c9a02..1d3bf92 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -30,6 +30,9 @@ FROM base AS build # Baked into the standalone routes manifest at build time. ARG API_PROXY_TARGET=http://api:4000 ENV API_PROXY_TARGET=$API_PROXY_TARGET +# Public origin for canonical/OG absolute URLs + sitemap (inlined as NEXT_PUBLIC_*). +ARG NEXT_PUBLIC_SITE_URL=http://localhost:3000 +ENV NEXT_PUBLIC_SITE_URL=$NEXT_PUBLIC_SITE_URL COPY packages packages COPY frontend frontend RUN npm run build --workspace @casino/shared \ diff --git a/frontend/app/apple-icon.tsx b/frontend/app/apple-icon.tsx new file mode 100644 index 0000000..5b5037d --- /dev/null +++ b/frontend/app/apple-icon.tsx @@ -0,0 +1,33 @@ +import { ImageResponse } from 'next/og'; + +// Generated apple-touch icon (no binary asset committed). A gold crescent moon +// carved from two overlapping circles on the brand-dark tile. +export const size = { width: 180, height: 180 }; +export const contentType = 'image/png'; + +export default function AppleIcon() { + return new ImageResponse( + ( +
+
+
+
+
+
+ ), + { ...size }, + ); +} diff --git a/frontend/app/icon.svg b/frontend/app/icon.svg new file mode 100644 index 0000000..77872d1 --- /dev/null +++ b/frontend/app/icon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx index 42f20c1..5cd45d2 100644 --- a/frontend/app/layout.tsx +++ b/frontend/app/layout.tsx @@ -3,6 +3,8 @@ import { Space_Grotesk } from 'next/font/google'; import './globals.css'; import { ToastProvider } from '@/lib/toast'; import { AuthProvider } from '@/lib/auth'; +import StructuredData from '@/components/StructuredData'; +import { SITE_DESCRIPTION, SITE_NAME, SITE_TAGLINE, SITE_URL, THEME_COLOR } from '@/lib/site'; // Bind the display font to the same CSS variable the theme references. const spaceGrotesk = Space_Grotesk({ @@ -13,12 +15,38 @@ const spaceGrotesk = Space_Grotesk({ }); export const metadata: Metadata = { - title: 'Lunaland Casino 🌙', - description: 'A moonlit social casino — play, earn, progress, and redeem.', + metadataBase: new URL(SITE_URL), + title: { + default: `${SITE_NAME} — ${SITE_TAGLINE}`, + template: `%s · ${SITE_NAME}`, + }, + description: SITE_DESCRIPTION, + applicationName: SITE_NAME, + keywords: ['social casino', 'sweepstakes casino', 'free slots', 'provably fair', 'sweeps coins', 'daily bonus'], + alternates: { canonical: '/' }, + robots: { + index: true, + follow: true, + googleBot: { index: true, follow: true, 'max-image-preview': 'large' }, + }, + openGraph: { + type: 'website', + siteName: SITE_NAME, + title: `${SITE_NAME} — ${SITE_TAGLINE}`, + description: SITE_DESCRIPTION, + url: SITE_URL, + locale: 'en_US', + }, + twitter: { + card: 'summary_large_image', + title: `${SITE_NAME} — ${SITE_TAGLINE}`, + description: SITE_DESCRIPTION, + }, }; export const viewport: Viewport = { - themeColor: '#0a0612', + themeColor: THEME_COLOR, + colorScheme: 'dark', width: 'device-width', initialScale: 1, }; @@ -27,6 +55,7 @@ export default function RootLayout({ children }: { children: React.ReactNode }) return ( + {children} diff --git a/frontend/app/login/layout.tsx b/frontend/app/login/layout.tsx new file mode 100644 index 0000000..4e903fd --- /dev/null +++ b/frontend/app/login/layout.tsx @@ -0,0 +1,14 @@ +import type { Metadata } from 'next'; + +// Server layout: supplies per-route metadata for /login (the page itself is a +// client component and can't export `metadata`). Title flows into the root +// template → "Log in · Lunaland Casino". +export const metadata: Metadata = { + title: 'Log in', + description: 'Log in or create your free Lunaland Casino account — new players get a signup bonus.', + alternates: { canonical: '/login' }, +}; + +export default function LoginLayout({ children }: { children: React.ReactNode }) { + return children; +} diff --git a/frontend/app/login/page.tsx b/frontend/app/login/page.tsx index 83b731f..c0fa38f 100644 --- a/frontend/app/login/page.tsx +++ b/frontend/app/login/page.tsx @@ -110,7 +110,7 @@ export default function LoginPage() {

Lunaland Casino

-

Play under the moonlight. Win. Redeem.

+

Play under the moonlight. Win. Redeem.

{/* Card */} @@ -130,8 +130,8 @@ export default function LoginPage() { onClick={() => switchMode(m)} className={`rounded-xl px-4 py-2.5 text-sm font-semibold capitalize transition focus:outline-none focus-visible:ring-2 focus-visible:ring-luna-400 ${ mode === m - ? 'bg-luna-600/30 text-luna-200 shadow-inner shadow-luna-500/30' - : 'text-luna-400/70 hover:text-luna-300' + ? 'bg-luna-600/30 text-luna-300 shadow-inner shadow-luna-500/30' + : 'text-luna-400/80 hover:text-luna-300' }`} > {m === 'login' ? 'Log in' : 'Sign up'} @@ -154,7 +154,7 @@ export default function LoginPage() { aria-invalid={Boolean(fieldError.email)} aria-describedby={fieldError.email ? 'email-error' : undefined} placeholder="you@lunaland.gg" - className="w-full rounded-xl border border-luna-500/20 bg-base-800/60 px-4 py-3 text-luna-100 placeholder:text-luna-400/40 transition focus:border-luna-400 focus:outline-none focus:ring-2 focus:ring-luna-500/40" + className="w-full rounded-xl border border-luna-500/20 bg-base-800/60 px-4 py-3 text-luna-300 placeholder:text-luna-300/70 transition focus:border-luna-400 focus:outline-none focus:ring-2 focus:ring-luna-500/40" /> {fieldError.email && (

@@ -179,14 +179,14 @@ export default function LoginPage() { aria-invalid={Boolean(fieldError.password)} aria-describedby={fieldError.password ? 'password-error' : 'password-hint'} placeholder="••••••••" - className="w-full rounded-xl border border-luna-500/20 bg-base-800/60 px-4 py-3 text-luna-100 placeholder:text-luna-400/40 transition focus:border-luna-400 focus:outline-none focus:ring-2 focus:ring-luna-500/40" + className="w-full rounded-xl border border-luna-500/20 bg-base-800/60 px-4 py-3 text-luna-300 placeholder:text-luna-300/70 transition focus:border-luna-400 focus:outline-none focus:ring-2 focus:ring-luna-500/40" /> {fieldError.password ? (

{fieldError.password}

) : ( -

+

At least {MIN_PASSWORD} characters.

)} @@ -209,7 +209,7 @@ export default function LoginPage() { {mode === 'register' && ( -

+

New players get 10,000 LC +{' '} 2.00 SC on signup.

@@ -217,7 +217,7 @@ export default function LoginPage() {
-

+

Social casino · No purchase necessary · Sweeps Coins are promotional.

diff --git a/frontend/app/manifest.ts b/frontend/app/manifest.ts new file mode 100644 index 0000000..8406e60 --- /dev/null +++ b/frontend/app/manifest.ts @@ -0,0 +1,28 @@ +import type { MetadataRoute } from 'next'; +import { SITE_DESCRIPTION, SITE_NAME, SITE_SHORT_NAME, THEME_COLOR } from '@/lib/site'; + +// Served at /manifest.webmanifest. Enables installability + supplies app metadata. +export default function manifest(): MetadataRoute.Manifest { + return { + name: SITE_NAME, + short_name: SITE_SHORT_NAME, + description: SITE_DESCRIPTION, + start_url: '/', + display: 'standalone', + background_color: THEME_COLOR, + theme_color: THEME_COLOR, + icons: [ + { + src: '/icon.svg', + type: 'image/svg+xml', + sizes: 'any', + purpose: 'any', + }, + { + src: '/apple-icon', + type: 'image/png', + sizes: '180x180', + }, + ], + }; +} diff --git a/frontend/app/opengraph-image.tsx b/frontend/app/opengraph-image.tsx new file mode 100644 index 0000000..abe1a13 --- /dev/null +++ b/frontend/app/opengraph-image.tsx @@ -0,0 +1,38 @@ +import { ImageResponse } from 'next/og'; +import { SITE_NAME, SITE_TAGLINE } from '@/lib/site'; + +// Generated 1200×630 social card (Open Graph + Twitter). Next auto-wires it into +// og:image / twitter:image. No binary asset committed; rendered offline at build. +export const size = { width: 1200, height: 630 }; +export const contentType = 'image/png'; +export const alt = `${SITE_NAME} — ${SITE_TAGLINE}`; + +export default function OpengraphImage() { + return new ImageResponse( + ( +
+
+
+
+
+
{SITE_NAME}
+
{SITE_TAGLINE}
+
+ ), + { ...size }, + ); +} diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx index 5387e97..04dd514 100644 --- a/frontend/app/page.tsx +++ b/frontend/app/page.tsx @@ -1,9 +1,8 @@ 'use client'; -import { useEffect } from 'react'; -import { useRouter } from 'next/navigation'; import { useAuth } from '@/lib/auth'; import { ActiveCurrencyProvider } from '@/lib/currency'; +import Landing from '@/components/Landing'; import CurrencyDisplay from '@/components/CurrencyDisplay'; import VipStatus from '@/components/VipStatus'; import SlotMachine from '@/components/SlotMachine'; @@ -11,25 +10,23 @@ import DailyBonus from '@/components/DailyBonus'; import RedeemPanel from '@/components/RedeemPanel'; export default function GamePage() { - const router = useRouter(); const { user, loading, logout, refresh } = useAuth(); - // Auth gate: bounce unauthenticated visitors to /login. - useEffect(() => { - if (!loading && !user) router.replace('/login'); - }, [loading, user, router]); - - if (loading || !user) { + // Brief splash only while we check for an existing session. + if (loading) { return (
🌙
- Loading your game… + Loading…
); } + // Anonymous visitors get the public, crawlable landing (with CTAs to /login). + if (!user) return ; + return (
@@ -77,7 +74,7 @@ export default function GamePage() { -