Skip to content

Commit e75d236

Browse files
authored
Merge pull request #7 from Codestz/feat/seo-rss-og-improvements
feat: SEO, RSS, and dynamic OG improvements
2 parents 5cd534d + 23e4ec4 commit e75d236

27 files changed

Lines changed: 474 additions & 58 deletions

.github/scripts/generate-stats.js

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ const USERNAME = 'Codestz';
99
const PURPLE = '#7c3aed';
1010
const TOKEN = process.env.GITHUB_TOKEN;
1111

12+
// Featured repos rendered as standalone cards (stars/forks auto-refreshed)
13+
const FEATURED_REPOS = ['claude-hindsight'];
14+
1215
if (!TOKEN) {
1316
console.error('GITHUB_TOKEN is required');
1417
process.exit(1);
@@ -69,6 +72,19 @@ async function fetchStats() {
6972
return data.user;
7073
}
7174

75+
async function fetchRepo(name) {
76+
const { data } = await githubAPI(`{
77+
repository(owner: "${USERNAME}", name: "${name}") {
78+
name
79+
description
80+
stargazerCount
81+
forkCount
82+
primaryLanguage { name color }
83+
}
84+
}`);
85+
return data.repository;
86+
}
87+
7288
function calculateStreak(weeks) {
7389
const days = weeks
7490
.flatMap((w) => w.contributionDays)
@@ -340,6 +356,73 @@ function generateContributionGraphSVG(weeks, theme) {
340356
</svg>`;
341357
}
342358

359+
function escapeXml(str) {
360+
return String(str || '')
361+
.replace(/&/g, '&amp;')
362+
.replace(/</g, '&lt;')
363+
.replace(/>/g, '&gt;');
364+
}
365+
366+
function wrapText(text, maxChars, maxLines) {
367+
const words = String(text || '')
368+
.split(/\s+/)
369+
.filter(Boolean);
370+
const all = [];
371+
let line = '';
372+
for (const word of words) {
373+
const candidate = line ? `${line} ${word}` : word;
374+
if (candidate.length > maxChars && line) {
375+
all.push(line);
376+
line = word;
377+
} else {
378+
line = candidate;
379+
}
380+
}
381+
if (line) all.push(line);
382+
if (all.length <= maxLines) return all;
383+
384+
const kept = all.slice(0, maxLines);
385+
const last = kept[maxLines - 1];
386+
kept[maxLines - 1] = (last.length > maxChars - 1 ? last.slice(0, maxChars - 1) : last) + '…';
387+
return kept;
388+
}
389+
390+
function generateRepoCardSVG(repo, theme) {
391+
const isDark = theme === 'dark';
392+
const textColor = isDark ? '#c9d1d9' : '#333333';
393+
const subColor = isDark ? '#888888' : '#666666';
394+
const lang = repo.primaryLanguage;
395+
const descLines = wrapText(repo.description, 52, 2);
396+
397+
const descSvg = descLines
398+
.map(
399+
(l, i) =>
400+
`<text x="25" y="${66 + i * 20}" fill="${subColor}" font-size="13" font-family="'Segoe UI', Ubuntu, 'Helvetica Neue', sans-serif">${escapeXml(l)}</text>`
401+
)
402+
.join('\n ');
403+
404+
const footerY = 66 + descLines.length * 20 + 18;
405+
406+
const langSvg = lang
407+
? `<circle cx="29" cy="${footerY - 4}" r="6" fill="${lang.color || PURPLE}"/>
408+
<text x="42" y="${footerY}" fill="${textColor}" font-size="13" font-family="'Segoe UI', Ubuntu, 'Helvetica Neue', sans-serif">${escapeXml(lang.name)}</text>`
409+
: '';
410+
411+
const height = footerY + 20;
412+
413+
return `<svg xmlns="http://www.w3.org/2000/svg" width="400" height="${height}" viewBox="0 0 400 ${height}">
414+
<rect x="2" y="2" width="396" height="${height - 4}" fill="none" stroke="${PURPLE}" stroke-width="3"/>
415+
<svg x="21" y="20" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="${PURPLE}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
416+
<path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22"/>
417+
</svg>
418+
<text x="48" y="34" fill="${PURPLE}" font-size="17" font-weight="bold" font-family="'Segoe UI', Ubuntu, 'Helvetica Neue', sans-serif">${escapeXml(repo.name)}</text>
419+
${descSvg}
420+
${langSvg}
421+
<text x="300" y="${footerY}" fill="${textColor}" font-size="13" font-family="'Segoe UI', Ubuntu, 'Helvetica Neue', sans-serif">★ ${repo.stargazerCount.toLocaleString()}</text>
422+
<text x="350" y="${footerY}" fill="${textColor}" font-size="13" font-family="'Segoe UI', Ubuntu, 'Helvetica Neue', sans-serif">⑂ ${repo.forkCount.toLocaleString()}</text>
423+
</svg>`;
424+
}
425+
343426
async function main() {
344427
console.log('Fetching GitHub stats...');
345428
const stats = await fetchStats();
@@ -374,6 +457,25 @@ async function main() {
374457
console.log(`Generated ${theme} theme SVGs`);
375458
}
376459

460+
for (const name of FEATURED_REPOS) {
461+
try {
462+
const repo = await fetchRepo(name);
463+
if (!repo) {
464+
console.log(`Repo ${name} not found, skipping card`);
465+
continue;
466+
}
467+
for (const theme of ['dark', 'light']) {
468+
fs.writeFileSync(
469+
path.join(outDir, `repo-${name}-${theme}.svg`),
470+
generateRepoCardSVG(repo, theme)
471+
);
472+
}
473+
console.log(`Generated repo card for ${name}`);
474+
} catch (err) {
475+
console.log(`Failed to generate card for ${name}: ${err.message}`);
476+
}
477+
}
478+
377479
console.log('Done! SVGs written to public/stats/');
378480
}
379481

.github/workflows/update-readme.yml

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,17 +40,32 @@ jobs:
4040
4141
try {
4242
const rss = await fetchRSS('https://codestz.dev/feed');
43-
const items = [...rss.matchAll(/<item>[\s\S]*?<title>(.*?)<\/title>[\s\S]*?<link>(.*?)<\/link>[\s\S]*?<\/item>/g)];
43+
const items = [...rss.matchAll(/<item>([\s\S]*?)<\/item>/g)];
4444
4545
if (items.length === 0) {
4646
console.log('No items found in RSS feed, skipping update');
4747
return;
4848
}
4949
50-
const posts = items.slice(0, 7).map(match => {
51-
const title = match[1].replace(/&amp;/g, '&').replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&quot;/g, '"').replace(/&apos;/g, "'");
52-
const link = match[2];
53-
return `- [${title}](${link})`;
50+
const decode = (s) => s.replace(/&amp;/g, '&').replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&quot;/g, '"').replace(/&apos;/g, "'");
51+
const MONTHS = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
52+
53+
const posts = items.slice(0, 7).map(([, block]) => {
54+
const grab = (tag) => (block.match(new RegExp(`<${tag}>([\\s\\S]*?)<\\/${tag}>`)) || [])[1] || '';
55+
const title = decode(grab('title').trim());
56+
const link = grab('link').trim();
57+
const pubDate = grab('pubDate').trim();
58+
const readingTime = decode(grab('blog:readingTime').trim());
59+
60+
const meta = [];
61+
if (pubDate) {
62+
const d = new Date(pubDate);
63+
if (!isNaN(d)) meta.push(`${MONTHS[d.getUTCMonth()]} ${d.getUTCFullYear()}`);
64+
}
65+
if (readingTime) meta.push(readingTime.replace(/ read$/, ''));
66+
67+
const suffix = meta.length ? ` — ${meta.join(' · ')}` : '';
68+
return `- [${title}](${link})${suffix}`;
5469
}).join('\n');
5570
5671
const readme = fs.readFileSync('README.md', 'utf8');

README.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<div align="center">
22

3-
[![Typing SVG](https://readme-typing-svg.demolab.com?font=JetBrains+Mono&weight=700&size=32&duration=3000&pause=1000&color=7C3AED&center=true&vCenter=true&random=false&width=700&height=70&lines=Hey%2C+I'm+Esteban+Estrada;Software+Engineer+II+%40+Recurly;Building+tools+for+AI+agents;Rust+%7C+TypeScript+%7C+Go)](https://codestz.dev)
3+
[![Typing SVG](https://readme-typing-svg.demolab.com?font=JetBrains+Mono&weight=700&size=32&duration=3000&pause=1000&color=7C3AED&center=true&vCenter=true&random=false&width=700&height=70&lines=Hey%2C+I'm+Esteban+Estrada;Senior+Software+Engineer+%40+Recurly;Building+tools+for+AI+agents;Rust+%7C+TypeScript+%7C+Go)](https://codestz.dev)
44

55
I build developer tools that make AI agents smarter.
66
<br/>Rust, TypeScript, Go — CLIs, MCP servers, LSP integrations.
@@ -86,6 +86,18 @@ I build developer tools that make AI agents smarter.
8686

8787
---
8888

89+
### Featured Project
90+
91+
<a href="https://github.com/Codestz/claude-hindsight">
92+
<picture>
93+
<source media="(prefers-color-scheme: dark)" srcset="./public/stats/repo-claude-hindsight-dark.svg"/>
94+
<source media="(prefers-color-scheme: light)" srcset="./public/stats/repo-claude-hindsight-light.svg"/>
95+
<img src="./public/stats/repo-claude-hindsight-dark.svg" alt="claude-hindsight"/>
96+
</picture>
97+
</a>
98+
99+
---
100+
89101
### Projects
90102

91103
[**krait**](https://github.com/Codestz/krait) -- Code intelligence CLI for AI agents. LSP-backed symbol search, semantic editing, and diagnostics in a single Rust binary. `rust`
@@ -107,6 +119,7 @@ I build developer tools that make AI agents smarter.
107119
> I write about AI-driven development, MCP integrations, and developer tooling at **[codestz.dev](https://codestz.dev)**
108120
109121
<!-- BLOG-POST-LIST:START -->
122+
110123
- [Stop Vibe Coding. Start Vibe Engineering.](https://codestz.dev/experiments/stop-vibe-coding-start-vibe-engineering)
111124
- [Stop Loading 100K Tokens Just to Call a Tool: Why I Built MCPX](https://codestz.dev/experiments/mcpx-mcp-gateway)
112125
- [The AI Convergence: Why Every Coding Tool Lands on the Same Two Defaults](https://codestz.dev/experiments/why-ai-defaults-to-typescript)

public/robots.txt

Lines changed: 0 additions & 10 deletions
This file was deleted.

public/stats/contributions-dark.svg

Lines changed: 2 additions & 2 deletions
Loading

public/stats/contributions-light.svg

Lines changed: 2 additions & 2 deletions
Loading
Lines changed: 13 additions & 0 deletions
Loading
Lines changed: 13 additions & 0 deletions
Loading

public/stats/stats-dark.svg

Lines changed: 1 addition & 1 deletion
Loading

public/stats/stats-light.svg

Lines changed: 1 addition & 1 deletion
Loading

0 commit comments

Comments
 (0)