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:
+21
-3
@@ -1,11 +1,29 @@
|
|||||||
# VYNDR — Build State
|
# VYNDR — Build State
|
||||||
|
|
||||||
## Last Updated
|
## Last Updated
|
||||||
2026-06-18
|
2026-06-19
|
||||||
|
|
||||||
## Current Phase
|
## Current Phase
|
||||||
SHIP BUILD v52.0 — Push-to-Book "Coming Soon" teaser + infrastructure
|
SHIP BUILD v53.0 — Social preview fix: clean OG/Twitter meta + dynamically
|
||||||
verification (snapshot status probe, scheduler logs, delta pipeline confirmed).
|
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
|
## Session 52 (2026-06-19) — SHIPPED ✅ TEASER + INFRA VERIFICATION
|
||||||
|
|
||||||
|
|||||||
@@ -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 ·
|
- **Push-to-Book is a TEASER only** — "BOOK IT ⟶" (StatStrip) + "PUSH-TO-BOOK ·
|
||||||
COMING SOON" (GradeResultCard) advertise an unbuilt feature. No backend.
|
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
|
## Active Skills
|
||||||
- vyndr-voice (all user-facing output)
|
- vyndr-voice (all user-facing output)
|
||||||
- prop-analysis (grading methodology)
|
- prop-analysis (grading methodology)
|
||||||
|
|||||||
@@ -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
File diff suppressed because one or more lines are too long
@@ -46,7 +46,7 @@ export const metadata: Metadata = {
|
|||||||
template: '%s · VYNDR',
|
template: '%s · VYNDR',
|
||||||
},
|
},
|
||||||
description:
|
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',
|
applicationName: 'VYNDR',
|
||||||
authors: [{ name: 'VYNDR', url: 'https://vyndr.app' }],
|
authors: [{ name: 'VYNDR', url: 'https://vyndr.app' }],
|
||||||
manifest: '/manifest.json',
|
manifest: '/manifest.json',
|
||||||
@@ -62,20 +62,21 @@ export const metadata: Metadata = {
|
|||||||
'prop betting tools',
|
'prop betting tools',
|
||||||
],
|
],
|
||||||
openGraph: {
|
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:
|
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',
|
url: 'https://vyndr.app',
|
||||||
siteName: 'VYNDR',
|
siteName: 'VYNDR',
|
||||||
images: [{ url: '/og-image.png', width: 1200, height: 630, alt: 'VYNDR — Sports Prop Intelligence' }],
|
locale: 'en_US',
|
||||||
type: 'website',
|
type: 'website',
|
||||||
|
// og:image is supplied by the file-based opengraph-image.tsx convention.
|
||||||
},
|
},
|
||||||
twitter: {
|
twitter: {
|
||||||
card: 'summary_large_image',
|
card: 'summary_large_image',
|
||||||
title: 'VYNDR',
|
title: "VYNDR — The edge the books don't want you to have",
|
||||||
description: 'The books have every advantage. We built this to give it back.',
|
description: 'Pre-graded player props. Proprietary archetypes. Parlay Lab. Built in Detroit.',
|
||||||
images: ['/og-image.png'],
|
|
||||||
creator: '@getvyndr',
|
creator: '@getvyndr',
|
||||||
|
// twitter:image is supplied by the file-based twitter-image.tsx convention.
|
||||||
},
|
},
|
||||||
icons: {
|
icons: {
|
||||||
icon: [
|
icon: [
|
||||||
|
|||||||
@@ -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 },
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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';
|
||||||
Reference in New Issue
Block a user