diff --git a/app/[locale]/(website)/work/[slug]/page.tsx b/app/[locale]/(website)/work/[slug]/page.tsx index 5e99afb7..951338ef 100644 --- a/app/[locale]/(website)/work/[slug]/page.tsx +++ b/app/[locale]/(website)/work/[slug]/page.tsx @@ -23,13 +23,16 @@ export async function generateMetadata({ params: Promise<{ locale: string; slug: string }>; }): Promise { const { locale, slug } = await params; + const isHe = locale === 'he'; const caseStudy = getCaseStudyBySlug(slug, locale); if (!caseStudy) { return genMeta( { - title: 'Case Study Not Found', - description: 'The requested case study could not be found.', + title: isHe ? 'הפרויקט לא נמצא' : 'Case Study Not Found', + description: isHe + ? 'הפרויקט המבוקש לא נמצא.' + : 'The requested case study could not be found.', url: `/work/${slug}`, noindex: true, }, @@ -40,14 +43,14 @@ export async function generateMetadata({ const keywords = [ caseStudy.platform.toLowerCase(), caseStudy.industry.toLowerCase(), - 'case study', - 'e-commerce success', - `${caseStudy.platform} development`, + isHe ? 'מקרה בוחן' : 'case study', + isHe ? 'פיתוח מסחר אלקטרוני' : 'e-commerce development', + `${caseStudy.platform} ${isHe ? 'פיתוח' : 'development'}`, ]; return genMeta( { - title: `${caseStudy.title} | Case Study`, + title: `${caseStudy.title} | ${isHe ? 'מקרה בוחן' : 'Case Study'}`, description: caseStudy.summary, url: `/work/${slug}`, type: 'article', @@ -73,10 +76,11 @@ export default async function CaseStudyPage({ const siteUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://cart-shift.com'; + const isHe = locale === 'he'; const breadcrumbSchema = generateBreadcrumbSchema( [ - { name: 'Home', url: '/' }, - { name: 'Work', url: '/work' }, + { name: isHe ? 'בית' : 'Home', url: '/' }, + { name: isHe ? 'פרויקטים' : 'Work', url: '/work' }, { name: caseStudy.title, url: `/work/${slug}` }, ], locale as 'en' | 'he' @@ -85,7 +89,6 @@ export default async function CaseStudyPage({ const articleSchema = generateArticleSchema({ title: caseStudy.title, description: caseStudy.summary, - date: new Date().toISOString(), url: `${siteUrl}/${locale}/work/${slug}`, locale: locale as 'en' | 'he', category: caseStudy.industry, diff --git a/app/[locale]/(website)/work/page.tsx b/app/[locale]/(website)/work/page.tsx index 1fe83a1a..f2a6a905 100644 --- a/app/[locale]/(website)/work/page.tsx +++ b/app/[locale]/(website)/work/page.tsx @@ -15,24 +15,37 @@ export async function generateMetadata({ params: Promise<{ locale: string }>; }): Promise { const { locale } = await params; + const isHe = locale === 'he'; const featured = getAllCaseStudies(locale)[0]; + return genMeta( { - title: 'Shopify, WordPress, and Web App Case Studies | CartShift Studio Work', - description: - 'Explore recent CartShift projects across Shopify, WordPress, custom web apps, migrations, and performance optimization. See how we turn technical work into sharper digital products and clearer growth paths.', + title: isHe + ? 'פרויקטים ב-Shopify, WordPress ו-Web Apps | CartShift Studio' + : 'Shopify, WordPress, and Web App Case Studies | CartShift Studio Work', + description: isHe + ? 'פרויקטים נבחרים של CartShift ב-Shopify, WordPress, אפליקציות ווב, מיגרציות ואופטימיזציה, עם מסכים אמיתיים, החלטות פיתוח ותוצאות מדודות כשיש נתונים.' + : 'Explore CartShift projects across Shopify, WordPress, custom web apps, migrations, and performance optimization, with real screens, implementation decisions, and measured results where available.', url: '/work', image: featured?.hero.image, - keywords: [ - 'shopify case studies', - 'wordpress case studies', - 'web app case studies', - 'next.js case studies', - 'ecommerce migration case study', - 'shopify performance optimization', - 'client work', - 'ecommerce project examples', - ], + keywords: isHe + ? [ + 'פרויקטים בשופיפיי', + 'פרויקטים בוורדפרס', + 'פיתוח אפליקציות ווב', + 'מקרי בוחן מסחר אלקטרוני', + 'מיגרציה לשופיפיי', + 'אופטימיזציית ביצועים', + ] + : [ + 'shopify case studies', + 'wordpress case studies', + 'web app case studies', + 'next.js case studies', + 'ecommerce migration case study', + 'shopify performance optimization', + 'ecommerce project examples', + ], }, locale as 'en' | 'he' ); @@ -43,18 +56,22 @@ export default async function WorkPage({ params }: { params: Promise<{ locale: s setRequestLocale(locale as 'en' | 'he'); const caseStudies = getAllCaseStudies(locale); + const isHe = locale === 'he'; const breadcrumbSchema = generateBreadcrumbSchema( [ - { name: 'Home', url: '/' }, - { name: 'Work', url: '/work' }, + { name: isHe ? 'בית' : 'Home', url: '/' }, + { name: isHe ? 'פרויקטים' : 'Work', url: '/work' }, ], locale as 'en' | 'he' ); const collectionSchema = generateCollectionPageSchema({ - name: 'Our Work | Shopify, WordPress & Web App Projects', - description: - 'Recent Shopify, WordPress, web app, migration, and optimization projects from CartShift Studio.', + name: isHe + ? 'הפרויקטים שלנו | Shopify, WordPress ואפליקציות ווב' + : 'Our Work | Shopify, WordPress & Web App Projects', + description: isHe + ? 'פרויקטים נבחרים של CartShift ב-Shopify, WordPress, אפליקציות ווב, מיגרציות ואופטימיזציה.' + : 'Recent Shopify, WordPress, web app, migration, and optimization projects from CartShift Studio.', url: '/work', locale: locale as 'en' | 'he', items: caseStudies.map(cs => ({ diff --git a/app/sitemap.ts b/app/sitemap.ts index 719c7027..ff602220 100644 --- a/app/sitemap.ts +++ b/app/sitemap.ts @@ -22,13 +22,13 @@ function generateAlternates(baseUrl: string, path: string): { languages: Record< function createLocalizedUrls( baseUrl: string, path: string, - lastModified: Date, + lastModified: Date | undefined, changeFrequency: 'always' | 'hourly' | 'daily' | 'weekly' | 'monthly' | 'yearly' | 'never', priority: number ) { return locales.map(locale => ({ url: `${baseUrl}/${locale}${path}`, - lastModified, + ...(lastModified && { lastModified }), changeFrequency, priority, alternates: generateAlternates(baseUrl, path), @@ -47,7 +47,7 @@ export default async function sitemap(): Promise { ); const caseStudyUrls = caseStudies.flatMap(study => - createLocalizedUrls(baseUrl, `/work/${study.slug}`, now, 'monthly', 0.7) + createLocalizedUrls(baseUrl, `/work/${study.slug}`, undefined, 'monthly', 0.7) ); const portfolioProjectUrls = portfolioShowcaseSlugs.flatMap(slug => diff --git a/components/sections/CaseStudyDetailContent.tsx b/components/sections/CaseStudyDetailContent.tsx index 170bb1de..c425aa42 100644 --- a/components/sections/CaseStudyDetailContent.tsx +++ b/components/sections/CaseStudyDetailContent.tsx @@ -39,11 +39,17 @@ type WorkDetailTranslations = { getConsultation: string; viewMore: string; projectOverview: string; + overviewEyebrow: string; whatWeShipped: string; + deliveryEyebrow: string; selectedScreens: string; evidenceTitle: string; + evidenceEyebrow: string; + evidenceResultsTitle: string; + whatChangedTitle: string; clientQuote: string; projectNotes: string; + notesEyebrow: string; visitSite: string; galleryHint: string; openImage: string; @@ -83,6 +89,10 @@ export const CaseStudyDetailContent: React.FC = ({ const heroStoryRotate = useTransform(heroProgress, [0, 1], [-6, -2]); const workT = t.raw('work' as never) as WorkDetailTranslations; + const hasMeasuredResults = caseStudy.evidence.some(item => item.tone === 'quantitative'); + const evidenceHeading = hasMeasuredResults + ? workT.detail.evidenceResultsTitle + : workT.detail.whatChangedTitle; const breadcrumbItems = [ { label: t('navigation.home'), href: '/' }, @@ -281,7 +291,7 @@ export const CaseStudyDetailContent: React.FC = ({ >

- {workT.detail.projectOverview} + {workT.detail.overviewEyebrow}

{caseStudy.overview.title} @@ -289,6 +299,11 @@ export const CaseStudyDetailContent: React.FC = ({

{caseStudy.overview.summary}

+ {caseStudy.attribution && ( +

+ {caseStudy.attribution} +

+ )}

= ({

{caseStudy.duration}

-

- {caseStudy.hero.supportingCopy || caseStudy.summary} -

)} @@ -379,7 +391,7 @@ export const CaseStudyDetailContent: React.FC = ({

- {workT.detail.whatWeShipped} + {workT.detail.deliveryEyebrow}

{workT.detail.whatWeShipped} @@ -441,10 +453,10 @@ export const CaseStudyDetailContent: React.FC = ({

- {workT.detail.evidenceTitle} + {workT.detail.evidenceEyebrow}

- {workT.detail.evidenceTitle} + {evidenceHeading}

@@ -465,6 +477,11 @@ export const CaseStudyDetailContent: React.FC = ({

{item.description}

+ {item.context && ( +

+ {item.context} +

+ )} {(item.before || item.after) && (
{item.before && ( @@ -549,7 +566,7 @@ export const CaseStudyDetailContent: React.FC = ({

- {workT.detail.projectNotes} + {workT.detail.notesEyebrow}

{workT.detail.projectNotes} @@ -581,10 +598,10 @@ export const CaseStudyDetailContent: React.FC = ({ {workT.cta.title}

- {workT.detail.wantResults} + {caseStudy.cta?.title || workT.detail.wantResults}

- {workT.detail.wantResultsDesc} + {caseStudy.cta?.description || workT.detail.wantResultsDesc}

diff --git a/components/sections/WorkPageContent.tsx b/components/sections/WorkPageContent.tsx index 24a5ee7a..edd281c7 100644 --- a/components/sections/WorkPageContent.tsx +++ b/components/sections/WorkPageContent.tsx @@ -366,9 +366,7 @@ function PortfolioWorkTile({ platformLabel: string; }) { const themeStyle = getCaseStudyThemeStyle(caseStudy.brand); - const metaLine = caseStudy.duration - ? `${caseStudy.client} / ${caseStudy.duration}` - : caseStudy.client; + const metaLine = caseStudy.duration || platformLabel; return ( ; overview?: Partial; results?: CaseStudyResult[]; @@ -106,7 +117,10 @@ interface CaseStudyFrontmatter { platform?: string; duration?: string; featured?: boolean; + portfolioOrder?: number; siteUrl?: string; + attribution?: string; + cta?: CaseStudyCTA; brand?: Partial; hero?: Partial; overview?: Partial; @@ -129,17 +143,22 @@ function normalizeGalleryWithHero( ): CaseStudyGalleryItem[] { const dedupedGallery = gallery.filter(item => item.image && item.image !== hero.image); - if (!hero.image) { + // When dedicated gallery screens exist, keep the hero out of the gallery. + // The hero is already the dominant visual at the top of the case study. + if (dedupedGallery.length > 0) { return dedupedGallery; } + if (!hero.image) { + return []; + } + return [ { image: hero.image, alt: hero.alt, caption: hero.supportingCopy || fallbackCaption || hero.alt, }, - ...dedupedGallery, ]; } @@ -205,6 +224,8 @@ export function normalizeCaseStudyRecord( const services = (isHebrew && heTranslations.services) || data.services || []; const results = (isHebrew && heTranslations.results) || data.results || []; const testimonial = (isHebrew && heTranslations.testimonial) || data.testimonial; + const attribution = (isHebrew && heTranslations.attribution) || data.attribution || ''; + const cta = (isHebrew && heTranslations.cta) || data.cta; const hero = mergeLocalizedObject( data.hero as CaseStudyHero | undefined, isHebrew ? heTranslations.hero : undefined @@ -243,7 +264,11 @@ export function normalizeCaseStudyRecord( platform: data.platform || '', duration, featured: data.featured || false, + portfolioOrder: + typeof data.portfolioOrder === 'number' ? data.portfolioOrder : undefined, siteUrl: data.siteUrl || '', + attribution, + cta, brand, hero: resolvedHero, overview: resolvedOverview, @@ -307,7 +332,11 @@ export function getAllCaseStudies(locale: string = 'en'): CaseStudyMeta[] { .filter((study): study is CaseStudyMeta => study !== null); return caseStudies.sort( - (a, b) => Number(b.featured) - Number(a.featured) || a.title.localeCompare(b.title, locale) + (a, b) => + (a.portfolioOrder ?? Number.MAX_SAFE_INTEGER) - + (b.portfolioOrder ?? Number.MAX_SAFE_INTEGER) || + Number(b.featured) - Number(a.featured) || + a.title.localeCompare(b.title, locale) ); } diff --git a/lib/seo.ts b/lib/seo.ts index aeccfa6e..d8b01e20 100644 --- a/lib/seo.ts +++ b/lib/seo.ts @@ -355,7 +355,7 @@ export function generateSoftwareApplicationSchema(config: SoftwareApplicationSch export function generateArticleSchema(post: { title: string; description: string; - date: string; + date?: string; url: string; locale?: 'en' | 'he'; author?: string; @@ -376,8 +376,10 @@ export function generateArticleSchema(post: { '@id': `${articleUrl}/#article`, headline: post.title, description: post.description, - datePublished: post.date, - dateModified: post.modifiedDate || post.date, + ...(post.date && { datePublished: post.date }), + ...((post.modifiedDate || post.date) && { + dateModified: post.modifiedDate || post.date, + }), author: { '@type': 'Organization', '@id': `${siteUrl}/#organization`, @@ -405,7 +407,7 @@ export function generateArticleSchema(post: { '@type': 'Organization', name: 'CartShift Studio', }, - copyrightYear: new Date(post.date).getFullYear(), + ...(post.date && { copyrightYear: new Date(post.date).getFullYear() }), ...(post.category && { articleSection: post.category }), ...(post.wordCount && { wordCount: post.wordCount }), ...(post.readingTime && { timeRequired: `PT${post.readingTime}M` }), diff --git a/messages/en.json b/messages/en.json index 61b75847..76fb6b6c 100644 --- a/messages/en.json +++ b/messages/en.json @@ -4651,7 +4651,7 @@ "hero": { "title": "Our Work", "subtitle": "Editorial case studies shaped around the brands behind the build.", - "description": "A curated look at the stores and websites we've designed, rebuilt, localized, and organized, using real visuals and verified project outcomes only.", + "description": "A curated look at the stores, platforms, and websites we designed or rebuilt, using real product screens, documented implementation decisions, and measured results where available.", "badge": "Selected Work", "featuredLabel": "Featured Project" }, @@ -4700,7 +4700,7 @@ "projectOverview": "Project Overview", "whatWeShipped": "What We Shipped", "selectedScreens": "Selected Screens", - "evidenceTitle": "Evidence & Outcomes", + "evidenceTitle": "Evidence & Change", "clientQuote": "Client Quote", "projectNotes": "Project Notes", "visitSite": "Visit Live Site", @@ -4714,7 +4714,13 @@ "getConsultation": "Get a Free Consultation", "viewMore": "View More Projects", "noProjects": "No Projects Found", - "noProjectsDesc": "Try changing the filter to see more projects." + "noProjectsDesc": "Try changing the filter to see more projects.", + "overviewEyebrow": "The Brief", + "deliveryEyebrow": "Delivery", + "evidenceEyebrow": "Project Evidence", + "evidenceResultsTitle": "Evidence & Results", + "whatChangedTitle": "What Changed", + "notesEyebrow": "Deeper Context" } }, "testimonials": { @@ -5802,4 +5808,4 @@ "secondaryButton": "Contact Us" } } -} \ No newline at end of file +} diff --git a/messages/he.json b/messages/he.json index f786c252..42014140 100644 --- a/messages/he.json +++ b/messages/he.json @@ -4651,7 +4651,7 @@ "hero": { "title": "העבודות שלנו", "subtitle": "עמודי פרויקט ויזואליים שמותאמים למותג שמאחורי כל עבודה.", - "description": "מבט אוצרתי על האתרים והחנויות שבנינו, שדרגנו, לוקליזצנו וארגנו מחדש, עם ויזואל אמיתי ותוצאות מבוססות בלבד.", + "description": "מבט ממוקד על החנויות, הפלטפורמות והאתרים שתכננו או בנינו מחדש, עם מסכים אמיתיים, החלטות פיתוח מתועדות ותוצאות מדודות כשיש נתונים.", "badge": "עבודות נבחרות", "featuredLabel": "פרויקט נבחר" }, @@ -4700,7 +4700,7 @@ "projectOverview": "סקירת הפרויקט", "whatWeShipped": "מה בנינו", "selectedScreens": "מסכים נבחרים", - "evidenceTitle": "הוכחות ותוצרים", + "evidenceTitle": "הוכחות ושינוי", "clientQuote": "מה הלקוח אומר", "projectNotes": "הערות פרויקט", "visitSite": "לצפייה באתר", @@ -4714,7 +4714,13 @@ "getConsultation": "קבלו ייעוץ חינם", "viewMore": "עוד פרויקטים", "noProjects": "אין פרויקטים", - "noProjectsDesc": "נסו לשנות את הסינון כדי לראות יותר פרויקטים." + "noProjectsDesc": "נסו לשנות את הסינון כדי לראות יותר פרויקטים.", + "overviewEyebrow": "הבריף", + "deliveryEyebrow": "ביצוע", + "evidenceEyebrow": "הוכחות מהפרויקט", + "evidenceResultsTitle": "הוכחות ותוצאות", + "whatChangedTitle": "מה השתנה", + "notesEyebrow": "הקשר נוסף" } }, "testimonials": { @@ -5802,4 +5808,4 @@ "secondaryButton": "צרו קשר" } } -} \ No newline at end of file +} diff --git a/messages/src/en/website.json b/messages/src/en/website.json index 56086050..cddcd19d 100644 --- a/messages/src/en/website.json +++ b/messages/src/en/website.json @@ -1255,7 +1255,7 @@ "hero": { "title": "Our Work", "subtitle": "Editorial case studies shaped around the brands behind the build.", - "description": "A curated look at the stores and websites we've designed, rebuilt, localized, and organized, using real visuals and verified project outcomes only.", + "description": "A curated look at the stores, platforms, and websites we designed or rebuilt, using real product screens, documented implementation decisions, and measured results where available.", "badge": "Selected Work", "featuredLabel": "Featured Project" }, @@ -1304,7 +1304,7 @@ "projectOverview": "Project Overview", "whatWeShipped": "What We Shipped", "selectedScreens": "Selected Screens", - "evidenceTitle": "Evidence & Outcomes", + "evidenceTitle": "Evidence & Change", "clientQuote": "Client Quote", "projectNotes": "Project Notes", "visitSite": "Visit Live Site", @@ -1318,7 +1318,13 @@ "getConsultation": "Get a Free Consultation", "viewMore": "View More Projects", "noProjects": "No Projects Found", - "noProjectsDesc": "Try changing the filter to see more projects." + "noProjectsDesc": "Try changing the filter to see more projects.", + "overviewEyebrow": "The Brief", + "deliveryEyebrow": "Delivery", + "evidenceEyebrow": "Project Evidence", + "evidenceResultsTitle": "Evidence & Results", + "whatChangedTitle": "What Changed", + "notesEyebrow": "Deeper Context" } }, "testimonials": { @@ -1398,7 +1404,12 @@ "wordpress": { "title": "Custom Web Design", "description": "A website as unique as your brand. Flexible, easy to manage, and built to stand out.", - "features": ["Custom Design", "Easy to Edit", "Online Store Ready", "SEO Optimized"] + "features": [ + "Custom Design", + "Easy to Edit", + "Online Store Ready", + "SEO Optimized" + ] }, "seo": { "title": "SEO & Organic Growth", @@ -1600,7 +1611,9 @@ "description": "Before spending more on ads, check the cart and checkout path for friction." } }, - "intentPicker": { "label": "Choose an optional analysis focus" }, + "intentPicker": { + "label": "Choose an optional analysis focus" + }, "relatedArticles": "Related store improvement guides", "form": { "title": "Analyze Your Store", diff --git a/messages/src/he/website.json b/messages/src/he/website.json index 75ba728f..fe646324 100644 --- a/messages/src/he/website.json +++ b/messages/src/he/website.json @@ -1207,7 +1207,12 @@ }, "performance": { "title": "ביצועים", - "items": ["ניטור מהירות", "אופטימיזציית ביצועים", "מעקב Core Web Vitals", "דוחות חודשיים"] + "items": [ + "ניטור מהירות", + "אופטימיזציית ביצועים", + "מעקב Core Web Vitals", + "דוחות חודשיים" + ] } }, "terms": { @@ -1250,7 +1255,7 @@ "hero": { "title": "העבודות שלנו", "subtitle": "עמודי פרויקט ויזואליים שמותאמים למותג שמאחורי כל עבודה.", - "description": "מבט אוצרתי על האתרים והחנויות שבנינו, שדרגנו, לוקליזצנו וארגנו מחדש, עם ויזואל אמיתי ותוצאות מבוססות בלבד.", + "description": "מבט ממוקד על החנויות, הפלטפורמות והאתרים שתכננו או בנינו מחדש, עם מסכים אמיתיים, החלטות פיתוח מתועדות ותוצאות מדודות כשיש נתונים.", "badge": "עבודות נבחרות", "featuredLabel": "פרויקט נבחר" }, @@ -1299,7 +1304,7 @@ "projectOverview": "סקירת הפרויקט", "whatWeShipped": "מה בנינו", "selectedScreens": "מסכים נבחרים", - "evidenceTitle": "הוכחות ותוצרים", + "evidenceTitle": "הוכחות ושינוי", "clientQuote": "מה הלקוח אומר", "projectNotes": "הערות פרויקט", "visitSite": "לצפייה באתר", @@ -1313,7 +1318,13 @@ "getConsultation": "קבלו ייעוץ חינם", "viewMore": "עוד פרויקטים", "noProjects": "אין פרויקטים", - "noProjectsDesc": "נסו לשנות את הסינון כדי לראות יותר פרויקטים." + "noProjectsDesc": "נסו לשנות את הסינון כדי לראות יותר פרויקטים.", + "overviewEyebrow": "הבריף", + "deliveryEyebrow": "ביצוע", + "evidenceEyebrow": "הוכחות מהפרויקט", + "evidenceResultsTitle": "הוכחות ותוצאות", + "whatChangedTitle": "מה השתנה", + "notesEyebrow": "הקשר נוסף" } }, "testimonials": { @@ -1383,12 +1394,22 @@ "shopify": { "title": "צמיחה באונליין", "description": "כל מה שצריך כדי למכור אונליין. אנחנו דואגים לעיצוב, להקמה ולביצועים כדי שתוכלו להתמקד במוצרים.", - "features": ["עיצוב והקמת חנויות", "פיתוחים מותאמים", "אופטימיזציית מהירות", "ליווי לצמיחה"] + "features": [ + "עיצוב והקמת חנויות", + "פיתוחים מותאמים", + "אופטימיזציית מהירות", + "ליווי לצמיחה" + ] }, "wordpress": { "title": "עיצוב אתרים מותאם", "description": "אתר ייחודי כמו המותג שלכם. גמיש, קל לניהול ובנוי לבלוט.", - "features": ["עיצוב ייחודי", "קל לעריכה", "מוכן לחנות", "מוכן ל-SEO"] + "features": [ + "עיצוב ייחודי", + "קל לעריכה", + "מוכן לחנות", + "מוכן ל-SEO" + ] }, "seo": { "title": "SEO וצמיחה אורגנית", @@ -1590,7 +1611,9 @@ "description": "לפני שמוציאים עוד על פרסום, בדקו חיכוך בעגלה ובדרך לתשלום." } }, - "intentPicker": { "label": "בחרו מוקד אופציונלי לבדיקה" }, + "intentPicker": { + "label": "בחרו מוקד אופציונלי לבדיקה" + }, "relatedArticles": "מדריכים קשורים לשיפור החנות", "form": { "title": "נתחו את החנות שלכם", diff --git a/tests/utils/case-studies.test.ts b/tests/utils/case-studies.test.ts index 651a6520..0158c7cc 100644 --- a/tests/utils/case-studies.test.ts +++ b/tests/utils/case-studies.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { getCaseStudyBySlug, normalizeCaseStudyRecord } from '@/lib/case-studies'; +import { getAllCaseStudies, getCaseStudyBySlug, normalizeCaseStudyRecord } from '@/lib/case-studies'; describe('case studies normalization', () => { it('loads localized structured fields from markdown frontmatter', () => { @@ -8,8 +8,8 @@ describe('case studies normalization', () => { expect(study).not.toBeNull(); expect(study?.overview.title).toContain('לרענן פורטל'); expect(study?.hero.alt).toContain('עלונדון'); - expect(study?.gallery[0]?.image).toBe(study?.hero.image); - expect(study?.gallery[1]?.caption).toContain('עמודי כתבה'); + expect(study?.gallery[0]?.image).not.toBe(study?.hero.image); + expect(study?.gallery[0]?.caption).toContain('עמודי כתבה'); expect(study?.siteUrl).toBe('https://alondon.net/'); }); @@ -22,12 +22,26 @@ describe('case studies normalization', () => { expect.arrayContaining([ expect.objectContaining({ value: '99.9% uptime', + context: expect.stringContaining('operational dashboards'), }), expect.objectContaining({ value: '85% fewer failures', + context: expect.stringContaining('legacy Shopify–HubSpot sync path'), }), ]) ); + expect(study?.attribution).toContain('Full Stack Developer & R&D Lead'); + expect(study?.cta?.title).toContain('complex Shopify'); + }); + + it('uses explicit portfolio order for the Work narrative', () => { + const studies = getAllCaseStudies('en'); + + expect(studies.slice(0, 3).map(study => study.slug)).toEqual([ + 'curalife-metabolic-wellness-platform', + 'i-love-my-honeypot-eco-ecommerce', + 'stiletto-piercing-disposable-jewelry-supply', + ]); }); it('supports case studies that omit duration metadata', () => { @@ -45,7 +59,7 @@ describe('case studies normalization', () => { expect(study?.siteUrl).toBe('https://atlasirwin.com/'); expect(study?.platform).toBe('Next.js + Vercel'); expect(study?.thumbnail).toBe('/images/case-studies/atlas-irwin-music-identity/hero.jpg'); - expect(study?.gallery[0]?.image).toBe(study?.hero.image); + expect(study?.gallery[0]?.image).not.toBe(study?.hero.image); expect(study?.evidence).toEqual( expect.arrayContaining([ expect.objectContaining({ @@ -63,7 +77,7 @@ describe('case studies normalization', () => { expect(englishStudy).not.toBeNull(); expect(englishStudy?.siteUrl).toBe('https://handsandvision.com/'); expect(englishStudy?.platform).toBe('WordPress + WooCommerce'); - expect(englishStudy?.gallery).toHaveLength(4); + expect(englishStudy?.gallery).toHaveLength(3); expect(englishStudy?.evidence).toEqual( expect.arrayContaining([ expect.objectContaining({ value: '5 Service Verticals', tone: 'qualitative' }), @@ -73,10 +87,10 @@ describe('case studies normalization', () => { expect(hebrewStudy?.title).toContain('Hands & Vision'); expect(hebrewStudy?.hero.supportingCopy).toContain('דו-לשונית'); - expect(hebrewStudy?.content).toContain('## על Hands & Vision'); + expect(hebrewStudy?.content).toContain('## האתגר'); }); - it('keeps the homepage hero image as the first gallery item and default thumbnail', () => { + it('keeps the hero as the thumbnail without repeating it when dedicated gallery screens exist', () => { const normalized = normalizeCaseStudyRecord( { title: 'Homepage First', @@ -110,11 +124,11 @@ describe('case studies normalization', () => { expect(normalized.thumbnail).toBe('/images/homepage-first/hero.jpg'); expect(normalized.gallery[0]).toMatchObject({ - image: '/images/homepage-first/hero.jpg', - alt: 'Homepage screenshot', - caption: 'Homepage first copy', + image: '/images/homepage-first/gallery-02.jpg', + alt: 'Secondary screen', + caption: 'Secondary screen caption', }); - expect(normalized.gallery).toHaveLength(2); + expect(normalized.gallery).toHaveLength(1); }); it('normalizes legacy result rows into evidence entries when evidence is missing', () => { diff --git a/tests/utils/test-utils.tsx b/tests/utils/test-utils.tsx index 92140a0f..4424ff33 100644 --- a/tests/utils/test-utils.tsx +++ b/tests/utils/test-utils.tsx @@ -48,11 +48,17 @@ const messages = { duration: 'Duration', services: 'Services', projectOverview: 'Project Overview', + overviewEyebrow: 'The Brief', whatWeShipped: 'What We Shipped', + deliveryEyebrow: 'Delivery', selectedScreens: 'Selected Screens', - evidenceTitle: 'Evidence & Outcomes', + evidenceTitle: 'Evidence & Change', + evidenceEyebrow: 'Project Evidence', + evidenceResultsTitle: 'Evidence & Results', + whatChangedTitle: 'What Changed', clientQuote: 'Client Quote', projectNotes: 'Project Notes', + notesEyebrow: 'Deeper Context', visitSite: 'Visit Live Site', galleryHint: 'Real homepage and supporting screens captured from the live site.', openImage: 'Expand Screenshot', diff --git a/tests/work/case-study-detail.test.tsx b/tests/work/case-study-detail.test.tsx index e16e7636..91f0ae94 100644 --- a/tests/work/case-study-detail.test.tsx +++ b/tests/work/case-study-detail.test.tsx @@ -12,6 +12,11 @@ const baseCaseStudy: CaseStudy = { duration: '8 weeks', featured: true, siteUrl: 'https://example.com', + attribution: 'Work completed in an in-house product role.', + cta: { + title: 'Need a clearer Shopify buying journey?', + description: 'We can help connect UX, content, and implementation.', + }, brand: { primary: '#8db43f', accent: '#f6df87', @@ -56,6 +61,7 @@ const baseCaseStudy: CaseStudy = { title: 'Localization completed', value: 'RTL Complete', description: 'The storefront now supports Hebrew and RTL layouts end to end.', + context: 'Observed in the delivered storefront.', before: 'Partial', after: 'Complete', tone: 'qualitative', @@ -77,8 +83,12 @@ describe('CaseStudyDetailContent', () => { expect(screen.getAllByText('Project Overview').length).toBeGreaterThan(0); expect(screen.getAllByText('What We Shipped').length).toBeGreaterThan(0); expect(screen.getAllByText('Selected Screens').length).toBeGreaterThan(0); - expect(screen.getAllByText('Evidence & Outcomes').length).toBeGreaterThan(0); + expect(screen.getByRole('heading', { name: 'What Changed' })).toBeInTheDocument(); expect(screen.getByText('RTL Complete')).toBeInTheDocument(); + expect(screen.getByText('Observed in the delivered storefront.')).toBeInTheDocument(); + expect( + screen.getByRole('heading', { name: 'Need a clearer Shopify buying journey?' }) + ).toBeInTheDocument(); expect(screen.getByRole('link', { name: /Visit Live Site/i })).toHaveAttribute( 'href', 'https://example.com' @@ -98,7 +108,28 @@ describe('CaseStudyDetailContent', () => { expect(screen.getByRole('heading', { name: 'Test Case Study' })).toBeInTheDocument(); expect(screen.queryByText('Client Quote')).not.toBeInTheDocument(); - expect(screen.queryByText('Evidence & Outcomes')).not.toBeInTheDocument(); + expect(screen.queryByText('What Changed')).not.toBeInTheDocument(); + expect(screen.queryByText('Project Evidence')).not.toBeInTheDocument(); + }); + + it('labels quantitative evidence as results', () => { + const measuredCaseStudy: CaseStudy = { + ...baseCaseStudy, + evidence: [ + { + title: 'Integration stability', + value: '85% fewer failures', + description: 'The replacement integration path reduced sync failures.', + context: 'Relative comparison with the legacy sync path.', + tone: 'quantitative', + }, + ], + }; + + render(); + + expect(screen.getByRole('heading', { name: 'Evidence & Results' })).toBeInTheDocument(); + expect(screen.getByText('85% fewer failures')).toBeInTheDocument(); }); it('renders in RTL mode for Hebrew locale', () => {