Session 53: Social preview fix — OG meta tags + image (2246 tests)

The link preview still showed engineer-speak ("Bayesian intelligence", "kill
conditions") because the OG meta tags were never updated after the S44 landing
cleanup.

- layout.tsx: de-jargoned the openGraph + main description + titles → "Pre-graded
  player props with proprietary archetypes. Correlation-aware Parlay Lab.
  Real-time line movement tracking. Built in Detroit." (Used "proprietary
  archetypes" not "45" — codebase has 41.)
- NEW app/opengraph-image.tsx (+ twitter-image.tsx re-export): dynamically
  generated 1200x630 card — VYNDR wordmark, "The books have every advantage. /
  We built this to give it back." + feature row. Node runtime (not edge —
  self-hosted standalone). Both routes prerender to a real PNG.
- Removed the stale /og-image.png metadata ref so the file convention owns the
  image (no duplicate og:image tag).

Backend 2239 -> 2246 tests (+7), 193 suites. Web build clean (exit 0).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kev
2026-06-19 14:25:05 -04:00
parent cdedecf55b
commit b012da13f8
7 changed files with 151 additions and 11 deletions
+21 -3
View File
@@ -1,11 +1,29 @@
# VYNDR — Build State
## Last Updated
2026-06-18
2026-06-19
## Current Phase
SHIP BUILD v52.0 — Push-to-Book "Coming Soon" teaser + infrastructure
verification (snapshot status probe, scheduler logs, delta pipeline confirmed).
SHIP BUILD v53.0 — Social preview fix: clean OG/Twitter meta + dynamically
generated 1200x630 OG image.
## Session 53 (2026-06-19) — SHIPPED ✅ SOCIAL PREVIEW FIX
Backend 2239 → **2246 tests** (+7), 193 suites. Web build clean (exit 0).
- **OG/Twitter copy de-jargoned** (`layout.tsx`): the openGraph description had
"Bayesian intelligence / kill conditions"; the main description had "xG
regression / penalty taker". Replaced with "Pre-graded player props with
proprietary archetypes. Correlation-aware Parlay Lab. Real-time line movement
tracking. Built in Detroit." Title → "VYNDR — The edge the books don't want you
to have". NOTE: did NOT write "45 archetypes" (the spec said 45, but the codebase
has 41 — used "proprietary archetypes" to stay accurate).
- **Dynamic OG image** (`app/opengraph-image.tsx` + `twitter-image.tsx` re-export):
1200x630, #06060B bg, VYNDR wordmark (green R), "The books have every advantage.
/ We built this to give it back." + feature row. Node runtime (NOT edge — this
is a self-hosted standalone build). Both routes prerender to a real PNG at build.
- The stale static `/og-image.png` reference was removed from metadata so the
file-based convention owns the image (no duplicate/conflicting og:image tag).
## Session 52 (2026-06-19) — SHIPPED ✅ TEASER + INFRA VERIFICATION
+13
View File
@@ -593,6 +593,19 @@ snapshot, locked to the line, and read from cache.
- **Push-to-Book is a TEASER only** — "BOOK IT ⟶" (StatStrip) + "PUSH-TO-BOOK ·
COMING SOON" (GradeResultCard) advertise an unbuilt feature. No backend.
## Social Preview / OG Image (Session 53 — non-obvious)
- The OG/Twitter image is **dynamically generated** by the file-based convention
`web/src/app/opengraph-image.tsx` (Twitter re-exports it). Do NOT re-add an
`images: ['/og-image.png']` to the `layout.tsx` metadata — that emits a second,
conflicting `og:image` tag. The legacy static `/og-image.png` still exists in
`public/` but is unreferenced.
- It uses **Node runtime, NOT `runtime = 'edge'`** — this is a self-hosted
`output: 'standalone'` build; edge runtime isn't available off Vercel and breaks
`next/og` here. Don't copy the edge example from Next docs.
- Social copy is intentionally jargon-free (no "Bayesian"/"kill conditions"/"xG
regression"). "kill conditions" is still legit IN-APP product copy (help/pricing/
scan) — only the social meta was cleaned.
## Active Skills
- vyndr-voice (all user-facing output)
- prop-analysis (grading methodology)
+50
View File
@@ -0,0 +1,50 @@
// Session 53 — social preview: clean OG/Twitter copy + generated OG image.
const fs = require('fs');
const path = require('path');
const APP = path.join(__dirname, '..', '..', 'web', 'src', 'app');
const read = (rel) => fs.readFileSync(path.join(APP, rel), 'utf8');
describe('Open Graph / Twitter metadata (layout)', () => {
const src = read('layout.tsx');
it('drops the engineer-speak from the social copy', () => {
const og = src.slice(src.indexOf('openGraph'));
expect(og).not.toMatch(/Bayesian/i);
expect(og).not.toMatch(/kill condition/i);
expect(og).not.toMatch(/point-biserial/i);
expect(og).not.toMatch(/xG regression/i);
});
it('uses the new user-facing copy', () => {
expect(src).toContain("The edge the books don't want you to have");
expect(src).toContain('Pre-graded player props');
expect(src).toContain('Parlay Lab');
expect(src).toContain('Built in Detroit');
});
it('keeps the large Twitter card + creator', () => {
expect(src).toContain("card: 'summary_large_image'");
expect(src).toContain("creator: '@getvyndr'");
});
it('lets the file-based convention own the image (no stale /og-image.png ref)', () => {
expect(src).not.toContain('/og-image.png');
});
});
describe('generated OG image', () => {
const og = read('opengraph-image.tsx');
it('is a 1200x630 PNG with the new tagline', () => {
expect(og).toContain('width: 1200, height: 630');
expect(og).toContain("contentType = 'image/png'");
expect(og).toContain('We built this to give it back.');
expect(og).toContain('BUILT IN DETROIT');
});
it('does NOT use the edge runtime (self-hosted standalone)', () => {
expect(og).not.toContain("runtime = 'edge'");
});
it('twitter-image reuses the same card', () => {
expect(read('twitter-image.tsx')).toContain("from './opengraph-image'");
});
});
+1 -1
View File
File diff suppressed because one or more lines are too long
+8 -7
View File
@@ -46,7 +46,7 @@ export const metadata: Metadata = {
template: '%s · VYNDR',
},
description:
"Grade your props across every sport with intelligence the books don't want you to have. NBA, MLB, WNBA, and soccer today — NFL and more through 2026. World Cup 2026 intelligence: xG regression, altitude, referee, penalty taker. Built in Detroit.",
'Pre-graded player props with proprietary archetypes. Correlation-aware Parlay Lab. Real-time line movement tracking. NBA, MLB, WNBA, and soccer. Built in Detroit.',
applicationName: 'VYNDR',
authors: [{ name: 'VYNDR', url: 'https://vyndr.app' }],
manifest: '/manifest.json',
@@ -62,20 +62,21 @@ export const metadata: Metadata = {
'prop betting tools',
],
openGraph: {
title: "VYNDR — Intelligence the books don't want you to have",
title: "VYNDR — The edge the books don't want you to have",
description:
'Read player props with Bayesian intelligence. See the factors. Know the kill conditions. Take the edge back.',
'Pre-graded player props with proprietary archetypes. Correlation-aware Parlay Lab. Real-time line movement tracking. Built in Detroit.',
url: 'https://vyndr.app',
siteName: 'VYNDR',
images: [{ url: '/og-image.png', width: 1200, height: 630, alt: 'VYNDR — Sports Prop Intelligence' }],
locale: 'en_US',
type: 'website',
// og:image is supplied by the file-based opengraph-image.tsx convention.
},
twitter: {
card: 'summary_large_image',
title: 'VYNDR',
description: 'The books have every advantage. We built this to give it back.',
images: ['/og-image.png'],
title: "VYNDR — The edge the books don't want you to have",
description: 'Pre-graded player props. Proprietary archetypes. Parlay Lab. Built in Detroit.',
creator: '@getvyndr',
// twitter:image is supplied by the file-based twitter-image.tsx convention.
},
icons: {
icon: [
+56
View File
@@ -0,0 +1,56 @@
import { ImageResponse } from 'next/og';
// Self-hosted standalone build → Node runtime (NOT edge). next/og renders fine
// under Node; edge runtime isn't available outside Vercel's edge platform.
export const alt = "VYNDR — The edge the books don't want you to have";
export const size = { width: 1200, height: 630 };
export const contentType = 'image/png';
export default function Image() {
return new ImageResponse(
(
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
width: '100%',
height: '100%',
background: '#06060B',
color: '#fff',
padding: 64,
}}
>
{/* Top hairline accent */}
<div style={{ display: 'flex', position: 'absolute', top: 0, left: 0, right: 0, height: 6, background: '#00D4A0' }} />
{/* Wordmark */}
<div style={{ display: 'flex', fontSize: 96, fontWeight: 900, letterSpacing: '0.06em' }}>
<span>VYND</span>
<span style={{ color: '#00D4A0' }}>R</span>
</div>
{/* Tagline */}
<div style={{ display: 'flex', fontSize: 30, color: '#B8BCC8', marginTop: 28 }}>
The books have every advantage.
</div>
<div style={{ display: 'flex', fontSize: 30, color: '#00D4A0', marginTop: 6, fontWeight: 700 }}>
We built this to give it back.
</div>
{/* Feature row */}
<div style={{ display: 'flex', gap: 18, marginTop: 48, fontSize: 18, color: '#707080', letterSpacing: '0.08em' }}>
<span>PRE-GRADED PROPS</span>
<span style={{ color: '#3A3A48' }}>·</span>
<span>ARCHETYPES</span>
<span style={{ color: '#3A3A48' }}>·</span>
<span>PARLAY LAB</span>
<span style={{ color: '#3A3A48' }}>·</span>
<span>BUILT IN DETROIT</span>
</div>
</div>
),
{ ...size },
);
}
+2
View File
@@ -0,0 +1,2 @@
// Twitter's summary_large_image reuses the same generated card as Open Graph.
export { default, alt, size, contentType } from './opengraph-image';