Merge pull request #8 from runyanjake/agent/swe-worker/4e283abf1548

PWS-15: Capitalize tags
This commit is contained in:
Jake Runyan 2026-11-12 15:52:58 -08:00 committed by GitHub
commit 50c6fcad36
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 26 additions and 11 deletions

View File

@ -50,6 +50,7 @@ lib/
recipe-content.test.ts # Format tests; also parses every real recipe (npm test)
recipe-card.tsx # Card layout (measured with satori) and PNG rendering
recipe-urls.ts # Client-safe helper that maps ./assets/ paths to served URLs
format.ts # Client-safe display formatting (capitalize categories/tags)
recipes/ # ALL recipe content lives here (markdown + images together)
[category]/
@ -69,5 +70,5 @@ public/
- **Content first**: recipe pages are minimal — no sidebar, just the recipe
- **Server components by default**: only add `'use client'` when interactivity is needed
- **The markdown is the recipe**: tabs, their titles and order, and what the card prints are all written in the `.md` file. The renderer only turns what's written into the page; never add hardcoded tab names, ordering, or fallbacks
- **No taxonomy file**: categories and tags are derived directly from frontmatter across all recipes — no external registry to keep in sync
- **No taxonomy file**: categories and tags are derived directly from frontmatter across all recipes — no external registry to keep in sync. They stay lowercase in frontmatter and URLs; display them through `capitalize` from `lib/format.ts`
- **Content is top level**: markdown and images are colocated in `recipes/`; images are served by a statically prerendered route handler, so there's no copy step. `public/` is only for site branding and data

View File

@ -1,6 +1,7 @@
'use client';
import type { FilterState } from '@/lib/types';
import { capitalize } from '@/lib/format';
interface ActiveFiltersProps {
filters: FilterState;
@ -19,11 +20,11 @@ export default function ActiveFilters({ filters, onFilterChange }: ActiveFilters
? [{ key: 'search', label: `“${filters.search}”`, remove: { ...filters, search: '' } }]
: []),
...(filters.category
? [{ key: 'category', label: filters.category.charAt(0).toUpperCase() + filters.category.slice(1), remove: { ...filters, category: '' } }]
? [{ key: 'category', label: capitalize(filters.category), remove: { ...filters, category: '' } }]
: []),
...filters.selectedTags.map((tag) => ({
key: `tag-${tag}`,
label: tag,
label: capitalize(tag),
remove: { ...filters, selectedTags: filters.selectedTags.filter((t) => t !== tag) },
})),
];

View File

@ -2,6 +2,7 @@ import Link from 'next/link';
import Image from 'next/image';
import type { RecipeMetadata } from '@/lib/recipes';
import { resolveRecipeAssetUrl } from '@/lib/recipe-urls';
import { capitalize } from '@/lib/format';
interface RecipeCardProps {
recipe: RecipeMetadata;
@ -57,7 +58,7 @@ export default function RecipeGridCard({ recipe }: RecipeCardProps) {
role="listitem"
className="px-2 py-1 text-xs bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded"
>
{tag}
{capitalize(tag)}
</span>
))}
{recipe.tags.length > 3 && (

View File

@ -1,5 +1,6 @@
import Link from 'next/link';
import type { Recipe } from '@/lib/recipes';
import { capitalize } from '@/lib/format';
interface RecipePageLayoutProps {
recipe: Recipe;
@ -83,7 +84,7 @@ export default function RecipePageLayout({ recipe, children }: RecipePageLayoutP
role="listitem"
className="px-3 py-1 text-sm bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 rounded-full hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors"
>
{tag}
{capitalize(tag)}
</Link>
))}
</div>

View File

@ -3,6 +3,7 @@
import { useState, useEffect } from 'react';
import FacetGroup from './FacetGroup';
import type { FacetCounts, FilterState } from '@/lib/types';
import { capitalize } from '@/lib/format';
interface RecipesSidebarProps {
categories: string[];
@ -15,10 +16,6 @@ interface RecipesSidebarProps {
const COLLAPSED_CATEGORY_COUNT = 8;
const COLLAPSED_TAG_COUNT = 10;
function capitalize(value: string): string {
return value.charAt(0).toUpperCase() + value.slice(1);
}
function ShowMoreButton({ expanded, hiddenCount, onClick }: { expanded: boolean; hiddenCount: number; onClick: () => void }) {
return (
<button
@ -199,7 +196,7 @@ export default function RecipesSidebar({ categories, tags, facetCounts, filters,
onChange={() => handleTagToggle(tag)}
className="h-4 w-4 rounded border-gray-300 dark:border-gray-600 accent-blue-600"
/>
<span className="flex-1 truncate">{tag}</span>
<span className="flex-1 truncate">{capitalize(tag)}</span>
<span className="text-xs tabular-nums text-gray-500 dark:text-gray-400">{count}</span>
</label>
);

10
lib/format.test.ts Normal file
View File

@ -0,0 +1,10 @@
import assert from 'node:assert/strict';
import { test } from 'node:test';
import { capitalize } from './format';
test('capitalize uppercases the first letter only', () => {
assert.equal(capitalize('italian'), 'Italian');
assert.equal(capitalize('ice cream'), 'Ice cream');
assert.equal(capitalize('BBQ'), 'BBQ');
assert.equal(capitalize(''), '');
});

4
lib/format.ts Normal file
View File

@ -0,0 +1,4 @@
// Display formatting for frontmatter values (categories, tags). Client-safe.
export function capitalize(value: string): string {
return value.charAt(0).toUpperCase() + value.slice(1);
}

View File

@ -7,7 +7,7 @@
"build": "next build",
"start": "next start",
"lint": "next lint",
"test": "node --import tsx --test lib/recipe-content.test.ts"
"test": "node --import tsx --test lib/*.test.ts"
},
"dependencies": {
"@fontsource/inter": "^5.3.0",