Item 8 — wire the 5 real articles into /blog, retire the backdated orphan
The live /blog showed "How to Read Line Movement Like a Sharp" dated 2026-03-22 — an orphaned, uncommitted file that predates the product. Meanwhile 5 genuinely-real articles sat in content/articles/, unwired. - blog.ts now reads content/articles (not the untracked content/blog). Maps `excerpt` → description, skips `status: draft`, reads explicit `slug`. - Added honest dates (2026-07-17, the real publish day) + flipped the 5 articles to `status: published`. Real content: how-vyndr-grades-a-prop, why-our-misses-are-public, what-clv-is, how-streaks-lie, the-vyndr-originals. - Retired the orphan: /blog/line-movement-guide 301s to /blog (next.config). - Added a minimal, dependency-free, XSS-safe markdown renderer so headers/bold render as HTML instead of literal "##". Each article keeps title/date/ read-time + the existing OG/JSON-LD metadata (affiliate-review ready). Richer media (images/charts) is the separate design train. Web build exit 0 (SSGs all 5 slugs); backend suite unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,9 +1,10 @@
|
|||||||
---
|
---
|
||||||
title: How Streaks Lie
|
title: How Streaks Lie
|
||||||
slug: how-streaks-lie
|
slug: how-streaks-lie
|
||||||
|
date: '2026-07-17'
|
||||||
excerpt: Most hot streaks are soft schedules wearing a costume. How to read a streak against what it was built on, the way the VYNDR lens does.
|
excerpt: Most hot streaks are soft schedules wearing a costume. How to read a streak against what it was built on, the way the VYNDR lens does.
|
||||||
tags: [streaks, the-read-room]
|
tags: [streaks, the-read-room]
|
||||||
status: draft
|
status: published
|
||||||
---
|
---
|
||||||
|
|
||||||
Most hot streaks are soft schedules wearing a costume.
|
Most hot streaks are soft schedules wearing a costume.
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
---
|
---
|
||||||
title: How VYNDR Grades a Prop
|
title: How VYNDR Grades a Prop
|
||||||
slug: how-vyndr-grades-a-prop
|
slug: how-vyndr-grades-a-prop
|
||||||
|
date: '2026-07-17'
|
||||||
excerpt: From the line feed to the letter. What actually happens inside the pipeline, including the part where the model refuses.
|
excerpt: From the line feed to the letter. What actually happens inside the pipeline, including the part where the model refuses.
|
||||||
tags: [methodology, the-read-room]
|
tags: [methodology, the-read-room]
|
||||||
status: draft
|
status: published
|
||||||
---
|
---
|
||||||
|
|
||||||
Most grading products are a vibe wearing a letter. Here is what ours actually does.
|
Most grading products are a vibe wearing a letter. Here is what ours actually does.
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
---
|
---
|
||||||
title: "The VYNDR Originals: 41 Archetypes and Why They Exist"
|
title: "The VYNDR Originals: 41 Archetypes and Why They Exist"
|
||||||
slug: the-vyndr-originals
|
slug: the-vyndr-originals
|
||||||
|
date: '2026-07-17'
|
||||||
excerpt: TORCH, BOMBER, GHOST, CONDUCTOR. What the archetype system is, how a player earns one, and why it changes which props you should trust.
|
excerpt: TORCH, BOMBER, GHOST, CONDUCTOR. What the archetype system is, how a player earns one, and why it changes which props you should trust.
|
||||||
tags: [archetypes, the-read-room]
|
tags: [archetypes, the-read-room]
|
||||||
status: draft
|
status: published
|
||||||
---
|
---
|
||||||
|
|
||||||
Every player profile on VYNDR carries a word like TORCH or BOMBER. This is not decoration. It is the compressed answer to a question the box score never asks: how does this player produce, and which of his props can you actually trust.
|
Every player profile on VYNDR carries a word like TORCH or BOMBER. This is not decoration. It is the compressed answer to a question the box score never asks: how does this player produce, and which of his props can you actually trust.
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
---
|
---
|
||||||
title: "CLV: The Only Number That Proves an Edge"
|
title: "CLV: The Only Number That Proves an Edge"
|
||||||
slug: what-clv-is
|
slug: what-clv-is
|
||||||
|
date: '2026-07-17'
|
||||||
excerpt: Closing line value in plain language. Why beating the close matters more than winning the bet, and how VYNDR captures it on every grade.
|
excerpt: Closing line value in plain language. Why beating the close matters more than winning the bet, and how VYNDR captures it on every grade.
|
||||||
tags: [clv, methodology, the-read-room]
|
tags: [clv, methodology, the-read-room]
|
||||||
status: draft
|
status: published
|
||||||
---
|
---
|
||||||
|
|
||||||
You can win a bet with a bad read and lose one with a great read. That is variance, and it is why the win-loss column — the number everyone stares at — is the slowest, noisiest possible way to judge a model. There is a faster, cleaner number. The industry calls it CLV: closing line value.
|
You can win a bet with a bad read and lose one with a great read. That is variance, and it is why the win-loss column — the number everyone stares at — is the slowest, noisiest possible way to judge a model. There is a faster, cleaner number. The industry calls it CLV: closing line value.
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
---
|
---
|
||||||
title: Why Our Misses Are Public
|
title: Why Our Misses Are Public
|
||||||
slug: why-our-misses-are-public
|
slug: why-our-misses-are-public
|
||||||
|
date: '2026-07-17'
|
||||||
excerpt: Every miss stays on the Ledger, by name, in the same format as the wins, permanently. The two reasons, and why nobody else in the industry can copy this.
|
excerpt: Every miss stays on the Ledger, by name, in the same format as the wins, permanently. The two reasons, and why nobody else in the industry can copy this.
|
||||||
tags: [the-ledger, methodology]
|
tags: [the-ledger, methodology]
|
||||||
status: draft
|
status: published
|
||||||
---
|
---
|
||||||
|
|
||||||
Open the VYNDR Ledger on any morning and you will find last night's misses listed by name, in the same format, the same placement, and the same energy as the wins. Wilson over 23.5 settled at 19. It is on the record. It stays on the record.
|
Open the VYNDR Ledger on any morning and you will find last night's misses listed by name, in the same format, the same placement, and the same energy as the wins. Wilson over 23.5 settled at 19. It is on the record. It stays on the record.
|
||||||
|
|||||||
@@ -45,6 +45,14 @@ const nextConfig: NextConfig = {
|
|||||||
},
|
},
|
||||||
poweredByHeader: false,
|
poweredByHeader: false,
|
||||||
reactStrictMode: true,
|
reactStrictMode: true,
|
||||||
|
async redirects() {
|
||||||
|
return [
|
||||||
|
// Truth-Everywhere Part 2 (item 8) — the backdated 2026-03-22
|
||||||
|
// "line-movement-guide" orphan is retired. Any inbound link 301s to the
|
||||||
|
// real blog index (the 5 committed articles).
|
||||||
|
{ source: '/blog/line-movement-guide', destination: '/blog', permanent: true },
|
||||||
|
];
|
||||||
|
},
|
||||||
async headers() {
|
async headers() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { getAllPosts, getPostBySlug } from '@/lib/blog';
|
import { getAllPosts, getPostBySlug, renderMarkdown } from '@/lib/blog';
|
||||||
import { notFound } from 'next/navigation';
|
import { notFound } from 'next/navigation';
|
||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
|
|
||||||
@@ -48,9 +48,10 @@ export default async function BlogPost({ params }: { params: Promise<{ slug: str
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="prose prose-invert max-w-none text-[var(--text)] leading-relaxed whitespace-pre-wrap">
|
<div
|
||||||
{post.content}
|
className="prose prose-invert max-w-none text-[var(--text)] leading-relaxed"
|
||||||
</div>
|
dangerouslySetInnerHTML={{ __html: renderMarkdown(post.content) }}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script
|
<script
|
||||||
|
|||||||
+55
-18
@@ -2,7 +2,11 @@ import fs from 'fs';
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
import matter from 'gray-matter';
|
import matter from 'gray-matter';
|
||||||
|
|
||||||
const BLOG_DIR = path.join(process.cwd(), 'content', 'blog');
|
// Truth-Everywhere Part 2 (item 8): the blog reads the REAL committed articles
|
||||||
|
// in content/articles (5 genuine pieces with honest dates), NOT the untracked
|
||||||
|
// content/blog that held the backdated 2026-03-22 orphan. Drafts are skipped so
|
||||||
|
// unfinished pieces never leak to the public index.
|
||||||
|
const BLOG_DIR = path.join(process.cwd(), 'content', 'articles');
|
||||||
|
|
||||||
export interface BlogPost {
|
export interface BlogPost {
|
||||||
slug: string;
|
slug: string;
|
||||||
@@ -19,28 +23,61 @@ export function getAllPosts(): BlogPost[] {
|
|||||||
|
|
||||||
const files = fs.readdirSync(BLOG_DIR).filter((f) => f.endsWith('.mdx') || f.endsWith('.md'));
|
const files = fs.readdirSync(BLOG_DIR).filter((f) => f.endsWith('.mdx') || f.endsWith('.md'));
|
||||||
|
|
||||||
const posts = files.map((file) => {
|
const posts = files
|
||||||
const raw = fs.readFileSync(path.join(BLOG_DIR, file), 'utf-8');
|
.map((file) => {
|
||||||
const { data, content } = matter(raw);
|
const raw = fs.readFileSync(path.join(BLOG_DIR, file), 'utf-8');
|
||||||
const slug = file.replace(/\.mdx?$/, '');
|
const { data, content } = matter(raw);
|
||||||
const wordCount = content.split(/\s+/).length;
|
const slug = data.slug || file.replace(/\.mdx?$/, '');
|
||||||
const readingTime = Math.max(1, Math.ceil(wordCount / 200));
|
const wordCount = content.split(/\s+/).length;
|
||||||
|
const readingTime = Math.max(1, Math.ceil(wordCount / 200));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
slug,
|
slug,
|
||||||
title: data.title || slug,
|
title: data.title || slug,
|
||||||
date: data.date || '',
|
date: data.date || '',
|
||||||
description: data.description || '',
|
// articles carry `excerpt`; older posts used `description` — accept either
|
||||||
tags: data.tags || [],
|
description: data.description || data.excerpt || '',
|
||||||
content,
|
tags: data.tags || [],
|
||||||
readingTime,
|
content,
|
||||||
};
|
readingTime,
|
||||||
});
|
status: data.status || 'published',
|
||||||
|
};
|
||||||
|
})
|
||||||
|
// Only published pieces reach the public index — drafts never leak.
|
||||||
|
.filter((p) => p.status !== 'draft');
|
||||||
|
|
||||||
return posts.sort((a, b) => (a.date > b.date ? -1 : 1));
|
return posts
|
||||||
|
.map(({ status, ...p }) => p)
|
||||||
|
.sort((a, b) => (a.date > b.date ? -1 : 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getPostBySlug(slug: string): BlogPost | null {
|
export function getPostBySlug(slug: string): BlogPost | null {
|
||||||
const posts = getAllPosts();
|
const posts = getAllPosts();
|
||||||
return posts.find((p) => p.slug === slug) || null;
|
return posts.find((p) => p.slug === slug) || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Minimal, dependency-free markdown → HTML for our OWN committed articles
|
||||||
|
* (headers, bold, italic, paragraphs). Content is trusted (files we author),
|
||||||
|
* and we HTML-escape before transforming, so it's XSS-safe. The richer media
|
||||||
|
* train (images/charts) is separate — this just stops "## Header" rendering as
|
||||||
|
* literal hashes on the public blog.
|
||||||
|
*/
|
||||||
|
export function renderMarkdown(md: string): string {
|
||||||
|
const esc = (s: string) => s
|
||||||
|
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||||
|
const inline = (s: string) => esc(s)
|
||||||
|
.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
|
||||||
|
.replace(/(^|[^*])\*([^*]+)\*(?!\*)/g, '$1<em>$2</em>');
|
||||||
|
return md
|
||||||
|
.trim()
|
||||||
|
.split(/\n{2,}/)
|
||||||
|
.map((block) => {
|
||||||
|
const b = block.trim();
|
||||||
|
if (b.startsWith('### ')) return `<h3>${inline(b.slice(4))}</h3>`;
|
||||||
|
if (b.startsWith('## ')) return `<h2>${inline(b.slice(3))}</h2>`;
|
||||||
|
if (b.startsWith('# ')) return `<h1>${inline(b.slice(2))}</h1>`;
|
||||||
|
return `<p>${inline(b).replace(/\n/g, '<br/>')}</p>`;
|
||||||
|
})
|
||||||
|
.join('\n');
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user