Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions .github/scripts/update-showcase-stats.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// Refreshes stars/forks in showcase project frontmatter from the GitHub API.
// Surgical line edits keep diffs minimal (no frontmatter reserialization).
// Run via: node .github/scripts/update-showcase-stats.js

/* eslint-disable @typescript-eslint/no-require-imports */
const fs = require('fs');
const path = require('path');

const SHOWCASE_DIR = path.join(process.cwd(), 'src/content/showcase');
const TOKEN = process.env.GITHUB_TOKEN;

async function fetchRepo(repo) {
const headers = { Accept: 'application/vnd.github+json', 'User-Agent': 'codestz-showcase-stats' };
if (TOKEN) headers.Authorization = `Bearer ${TOKEN}`;

const res = await fetch(`https://api.github.com/repos/${repo}`, { headers });
if (!res.ok) {
throw new Error(`GitHub API ${res.status} for ${repo}: ${await res.text()}`);
}
return res.json();
}

// Split a file into its YAML frontmatter block and the rest.
function splitFrontmatter(text) {
const match = text.match(/^---\n([\s\S]*?)\n---/);
if (!match) return null;
return { block: match[1], start: match.index, end: match.index + match[0].length };
}

function readField(block, key) {
const m = block.match(new RegExp(`^${key}:\\s*['"]?([^'"\\n]+)['"]?\\s*$`, 'm'));
return m ? m[1].trim() : null;
}

// Set `key: value` inside the frontmatter block. Replaces the line if present,
// otherwise inserts it right after the `repo:` line.
function setField(block, key, value) {
const line = `${key}: ${value}`;
const re = new RegExp(`^${key}:.*$`, 'm');
if (re.test(block)) return block.replace(re, line);
return block.replace(/^(repo:.*)$/m, `$1\n${line}`);
}

async function main() {
if (!fs.existsSync(SHOWCASE_DIR)) {
console.log('No showcase directory, nothing to do.');
return;
}

const files = fs.readdirSync(SHOWCASE_DIR).filter((f) => f.endsWith('.mdx'));
let changed = 0;

for (const file of files) {
const filePath = path.join(SHOWCASE_DIR, file);
const text = fs.readFileSync(filePath, 'utf8');
const fm = splitFrontmatter(text);

if (!fm) {
console.warn(`! ${file}: no frontmatter, skipping`);
continue;
}

const repo = readField(fm.block, 'repo');
if (!repo) {
console.warn(`! ${file}: no repo field, skipping`);
continue;
}

try {
const data = await fetchRepo(repo);
const stars = data.stargazers_count ?? 0;
const forks = data.forks_count ?? 0;

let block = setField(fm.block, 'stars', stars);
block = setField(block, 'forks', forks);

if (block !== fm.block) {
const updated = `---\n${block}\n---` + text.slice(fm.end);
fs.writeFileSync(filePath, updated);
changed++;
console.log(`✓ ${file}: ${repo} → ★${stars} ⑂${forks}`);
} else {
console.log(`= ${file}: ${repo} unchanged (★${stars} ⑂${forks})`);
}
} catch (err) {
console.error(`! ${file}: ${err.message}`);
}
}

console.log(`Done. ${changed} file(s) updated.`);
}

main().catch((err) => {
console.error(err);
process.exit(1);
});
34 changes: 34 additions & 0 deletions .github/workflows/update-showcase-stats.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: Update showcase stars/forks

on:
schedule:
- cron: '0 */6 * * *' # every 6 hours
push:
paths:
- 'src/content/showcase/**'
workflow_dispatch:

permissions:
contents: write

jobs:
update-stats:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: '20'

- name: Refresh stars/forks from GitHub API
run: node .github/scripts/update-showcase-stats.js
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Commit updated frontmatter
run: |
git config --local user.email "github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
git add src/content/showcase/
git diff --cached --quiet || (git commit -m "chore: refresh showcase stars/forks" && git push)
Binary file added public/images/blog/claude-hindsight.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/images/blog/hireloom.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/images/blog/mcpx.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13 changes: 12 additions & 1 deletion src/app/api/search-content/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ import { contentService } from '@/lib/services';

export async function GET() {
try {
const [postsResult, projectsResult] = await Promise.all([
const [postsResult, projectsResult, showcaseResult] = await Promise.all([
contentService.getAllPosts(),
contentService.getAllProjects(),
contentService.getAllShowcase(),
]);

const posts = postsResult.success ? Array.from(postsResult.data) : [];
const projects = projectsResult.success ? Array.from(projectsResult.data) : [];
const showcase = showcaseResult.success ? Array.from(showcaseResult.data) : [];

const searchContent = [
...posts.map((post) => ({
Expand All @@ -30,6 +32,15 @@ export async function GET() {
tags: 'tags' in project ? project.tags : [],
url: `/experience/${project.slug}`,
})),
...showcase.map((project) => ({
type: 'showcase' as const,
slug: project.slug,
title: project.title,
description: project.description,
category: 'project',
tags: project.technologies,
url: `/projects/${project.slug}`,
})),
];

return NextResponse.json(searchContent);
Expand Down
87 changes: 87 additions & 0 deletions src/app/projects/[slug]/opengraph-image.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { ImageResponse } from 'next/og';
import { contentService } from '@/lib/services';
import { APP_CONFIG } from '@/lib/constants';

export const alt = 'Project on codestz.dev';
export const size = { width: 1200, height: 630 };
export const contentType = 'image/png';

const PURPLE = '#7c3aed';
const BG = '#0a0a0a';
const FG = '#fafafa';

export default async function OgImage({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
const result = await contentService.getShowcaseBySlug(slug);
const project = result.success ? result.data : null;

const title = project?.title ?? 'codestz.dev';
const tags = project?.technologies?.slice(0, 4) ?? [];
const repo = project?.repo ?? '';
const stars = typeof project?.stars === 'number' ? `★ ${project.stars}` : '';

return new ImageResponse(
<div
style={{
width: '100%',
height: '100%',
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
background: BG,
color: FG,
padding: '64px',
border: `16px solid ${PURPLE}`,
fontFamily: 'sans-serif',
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
<div style={{ width: '20px', height: '20px', background: PURPLE }} />
<div style={{ fontSize: 30, fontWeight: 700, letterSpacing: '2px' }}>CODESTZ.DEV</div>
</div>

<div
style={{
display: 'flex',
fontSize: title.length > 50 ? 64 : 78,
fontWeight: 800,
lineHeight: 1.05,
letterSpacing: '-1px',
}}
>
{title}
</div>

<div style={{ display: 'flex', flexDirection: 'column', gap: '20px' }}>
<div style={{ display: 'flex', gap: '12px', flexWrap: 'wrap' }}>
{tags.map((tag) => (
<div
key={tag}
style={{
display: 'flex',
fontSize: 24,
color: PURPLE,
border: `2px solid ${PURPLE}`,
padding: '4px 16px',
}}
>
{tag}
</div>
))}
</div>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
fontSize: 28,
color: '#a1a1aa',
}}
>
<span>{[repo, stars].filter(Boolean).join(' · ')}</span>
<span>{APP_CONFIG.author.name}</span>
</div>
</div>
</div>,
{ ...size }
);
}
Loading