From c58e09db1dfcbb02f4f01bbec8e1b1e484c534d7 Mon Sep 17 00:00:00 2001 From: "Olomana (multica agent)" Date: Thu, 12 Nov 2026 15:48:30 -0800 Subject: [PATCH] PWS-15: Capitalize tags for display Tags render with their first letter uppercased on the recipe page, grid cards, sidebar facet, and active-filter chips, matching categories. The shared capitalize helper moves to lib/format.ts. Co-Authored-By: Claude Opus 5.5 Co-authored-by: multica-agent --- .claude/CLAUDE.md | 3 ++- components/ActiveFilters.tsx | 5 +++-- components/RecipeGridCard.tsx | 3 ++- components/RecipePageLayout.tsx | 3 ++- components/RecipesSidebar.tsx | 7 ++----- lib/format.test.ts | 10 ++++++++++ lib/format.ts | 4 ++++ package.json | 2 +- 8 files changed, 26 insertions(+), 11 deletions(-) create mode 100644 lib/format.test.ts create mode 100644 lib/format.ts diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index b13944a..badd075 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -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 diff --git a/components/ActiveFilters.tsx b/components/ActiveFilters.tsx index 53b0a26..82d7b4e 100644 --- a/components/ActiveFilters.tsx +++ b/components/ActiveFilters.tsx @@ -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) }, })), ]; diff --git a/components/RecipeGridCard.tsx b/components/RecipeGridCard.tsx index 69c8d2f..b10a285 100644 --- a/components/RecipeGridCard.tsx +++ b/components/RecipeGridCard.tsx @@ -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)} ))} {recipe.tags.length > 3 && ( diff --git a/components/RecipePageLayout.tsx b/components/RecipePageLayout.tsx index d5ad302..737a8ab 100644 --- a/components/RecipePageLayout.tsx +++ b/components/RecipePageLayout.tsx @@ -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)} ))} diff --git a/components/RecipesSidebar.tsx b/components/RecipesSidebar.tsx index 03dc175..01b4c53 100644 --- a/components/RecipesSidebar.tsx +++ b/components/RecipesSidebar.tsx @@ -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 (