Session G (night2): Phase 6 — landing first-paint + content engine + OG

6.1 FIRST-PAINT ROOT CAUSE: the landing blocked its ENTIRE render on
    Supabase auth init ('loading || user') — anonymous visitors stared at
    'LOADING THE SLATE' for the whole auth roundtrip (~3-4s). Now a
    synchronous localStorage session check gates the suppression: only
    visitors who actually hold a session (and will redirect) wait;
    anonymous traffic paints the hero immediately. Full RSC conversion of
    the hero is deferred and logged — the blocker itself is dead.
    Proof Strip rules: top-3 by grade whatever they are; 'TONIGHT'S TOP
    SIGNALS' only with >=1 A-tier, else 'TONIGHT'S BOARD'; nothing graded
    yet → yesterday's SETTLED reads with outcome chips (misses included).
6.2 Content routes: /api/content/top-signals/:sport,
    /streak-watch/:sport (the zero-grade daily format off the aggregator),
    /daily-report/:sport — built but self-flagging do_not_post until the
    record clears n>=20. Flag, don't fake.
6.3 Per-player OG images: app/player/[name]/opengraph-image.tsx (Node
    runtime per the S53 rule) + server layout generateMetadata — every
    shared player link unfurls as an intelligence card.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Kev
2026-07-11 02:26:45 -04:00
parent f110bd63f1
commit 1b4f2772d6
6 changed files with 285 additions and 9 deletions
+22 -4
View File
@@ -1,6 +1,6 @@
'use client';
import { useEffect } from 'react';
import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import { useAuth } from '@/contexts/AuthContext';
import Hero from '@/components/Hero';
@@ -25,17 +25,35 @@ import Pricing from '@/components/Pricing';
import FAQ from '@/components/FAQ';
// Footer is mounted globally in the root layout (Session 34) — no per-page import.
// Session 60 (6.1) — the 34s "LOADING THE SLATE" first paint was THIS
// page blocking its entire render on Supabase auth initialization
// (`loading || user`), even for anonymous visitors who were never going to
// redirect. Synchronous localStorage check instead: only a visitor who
// actually HAS a stored session (and will redirect to /dashboard) sees the
// suppressed render; anonymous traffic paints the hero immediately.
function hasStoredSession(): boolean {
if (typeof window === 'undefined') return false;
try {
for (let i = 0; i < window.localStorage.length; i += 1) {
const k = window.localStorage.key(i) || '';
if (/^sb-.*-auth-token$/.test(k) || k === 'sb-token') return true;
}
} catch { /* storage blocked → treat as anonymous */ }
return false;
}
export default function Home() {
const { user, loading } = useAuth();
const router = useRouter();
const [maybeSignedIn] = useState<boolean>(() => hasStoredSession());
useEffect(() => {
if (!loading && user) router.replace('/dashboard');
}, [user, loading, router]);
// While we know the user is signed in we suppress the marketing
// render to avoid a flicker before the redirect lands.
if (loading || user) {
// Suppress the marketing render ONLY for visitors with a stored session
// (they're about to redirect) — anonymous first paint is instant.
if (user || (loading && maybeSignedIn)) {
return (
<section style={{ minHeight: '80vh', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<p className="mono" style={{ color: 'var(--text-tertiary)', fontSize: 13, letterSpacing: '0.08em', textTransform: 'uppercase' }}>
+24
View File
@@ -0,0 +1,24 @@
import type { Metadata } from 'next';
/**
* Session 60 (night2/G, 6.3) — server metadata for the player dossier.
* The page itself stays a client component; this layout carries the
* per-player title/description, and opengraph-image.tsx (same segment)
* renders the shareable intelligence card.
*/
export async function generateMetadata({ params }: { params: Promise<{ name: string }> }): Promise<Metadata> {
const { name } = await params;
const player = decodeURIComponent(name || '').slice(0, 60);
const title = `${player} — Player Intelligence | VYNDR`;
const description = `${player}: archetype DNA, active streaks, prop DNA, last-10 log, and VYNDR's settled record — every number with matchup context.`;
return {
title,
description,
openGraph: { title, description },
twitter: { card: 'summary_large_image', title, description },
};
}
export default function PlayerLayout({ children }: { children: React.ReactNode }) {
return children;
}
@@ -0,0 +1,53 @@
import { ImageResponse } from 'next/og';
// Session 60 (night2/G, 6.3) — every shared player link unfurls as an
// intelligence card. Self-hosted standalone build → Node runtime (NOT edge;
// Session-53 rule — next/og breaks under edge off Vercel).
export const alt = 'VYNDR Player Intelligence';
export const size = { width: 1200, height: 630 };
export const contentType = 'image/png';
export default async function Image({ params }: { params: Promise<{ name: string }> }) {
const { name } = await params;
const player = decodeURIComponent(name || '').slice(0, 40) || 'Player';
return new ImageResponse(
(
<div
style={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
width: '100%',
height: '100%',
background: '#06060B',
color: '#E8E8F0',
padding: 72,
fontFamily: 'monospace',
}}
>
<div style={{ display: 'flex', position: 'absolute', top: 0, left: 0, right: 0, height: 6, background: '#00D4A0' }} />
<div style={{ display: 'flex', flexDirection: 'column' }}>
<div style={{ display: 'flex', fontSize: 26, letterSpacing: '0.22em', color: '#00D4A0', fontWeight: 700 }}>
PLAYER INTELLIGENCE
</div>
<div style={{ display: 'flex', fontSize: 84, fontWeight: 900, letterSpacing: '-0.02em', marginTop: 18, color: '#FFFFFF' }}>
{player}
</div>
<div style={{ display: 'flex', fontSize: 28, color: '#7A7A8E', marginTop: 20 }}>
Archetype DNA · Active streaks · Prop DNA · Settled record
</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<div style={{ display: 'flex', fontSize: 44, fontWeight: 900, letterSpacing: '0.08em' }}>
<span style={{ color: '#FFFFFF' }}>VYND</span>
<span style={{ color: '#00D4A0' }}>R</span>
</div>
<div style={{ display: 'flex', fontSize: 22, color: '#4A4A5E', letterSpacing: '0.12em' }}>
EVERY NUMBER WITH CONTEXT
</div>
</div>
</div>
),
{ ...size },
);
}