@sonordev/agency-site-kit 0.4.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-4IES23GV.js → chunk-2RW4C42Y.js} +16 -4
- package/dist/chunk-2RW4C42Y.js.map +1 -0
- package/dist/{chunk-QGSW43TM.cjs → chunk-2Y2RJB3A.cjs} +12 -12
- package/dist/{chunk-QGSW43TM.cjs.map → chunk-2Y2RJB3A.cjs.map} +1 -1
- package/dist/{chunk-UXUU7FVT.cjs → chunk-6OWFFV6N.cjs} +4 -65
- package/dist/chunk-6OWFFV6N.cjs.map +1 -0
- package/dist/{chunk-NLGUPJZA.js → chunk-7FFZX7C5.js} +4 -4
- package/dist/{chunk-NLGUPJZA.js.map → chunk-7FFZX7C5.js.map} +1 -1
- package/dist/{chunk-7ZHRMWJU.cjs → chunk-A5WCMHAE.cjs} +2 -2
- package/dist/{chunk-7ZHRMWJU.cjs.map → chunk-A5WCMHAE.cjs.map} +1 -1
- package/dist/chunk-FM25LB67.js +50 -0
- package/dist/chunk-FM25LB67.js.map +1 -0
- package/dist/{chunk-KXLROAO5.js → chunk-FT2SNEYP.js} +2 -2
- package/dist/{chunk-KXLROAO5.js.map → chunk-FT2SNEYP.js.map} +1 -1
- package/dist/{chunk-5MEZU3OG.cjs → chunk-OG5RBKQJ.cjs} +15 -3
- package/dist/chunk-OG5RBKQJ.cjs.map +1 -0
- package/dist/chunk-X76U7VLF.cjs +57 -0
- package/dist/chunk-X76U7VLF.cjs.map +1 -0
- package/dist/{chunk-RWQ675N6.js → chunk-X7UCJ45A.js} +4 -61
- package/dist/chunk-X7UCJ45A.js.map +1 -0
- package/dist/format-C-axf13G.d.cts +68 -0
- package/dist/format-poUpLtQ2.d.ts +68 -0
- package/dist/index.cjs +25 -24
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -3
- package/dist/index.d.ts +5 -3
- package/dist/index.js +5 -4
- package/dist/index.js.map +1 -1
- package/dist/layout/index.cjs +2 -2
- package/dist/layout/index.d.cts +12 -2
- package/dist/layout/index.d.ts +12 -2
- package/dist/layout/index.js +1 -1
- package/dist/portfolio/client.cjs +2 -17
- package/dist/portfolio/client.cjs.map +1 -1
- package/dist/portfolio/client.js +1 -16
- package/dist/portfolio/client.js.map +1 -1
- package/dist/portfolio/index.cjs +18 -17
- package/dist/portfolio/index.d.cts +5 -74
- package/dist/portfolio/index.d.ts +5 -74
- package/dist/portfolio/index.js +4 -3
- package/dist/portfolio/sections/ChallengesSection.d.cts +1 -1
- package/dist/portfolio/sections/ChallengesSection.d.ts +1 -1
- package/dist/portfolio/sections/DetailsSection.d.cts +1 -1
- package/dist/portfolio/sections/DetailsSection.d.ts +1 -1
- package/dist/portfolio/sections/HeroSection.d.cts +1 -1
- package/dist/portfolio/sections/HeroSection.d.ts +1 -1
- package/dist/portfolio/sections/ResultsSection.cjs +4 -3
- package/dist/portfolio/sections/ResultsSection.cjs.map +1 -1
- package/dist/portfolio/sections/ResultsSection.d.cts +1 -1
- package/dist/portfolio/sections/ResultsSection.d.ts +1 -1
- package/dist/portfolio/sections/ResultsSection.js +2 -1
- package/dist/portfolio/sections/ResultsSection.js.map +1 -1
- package/dist/portfolio/sections/ServicesSection.d.cts +1 -1
- package/dist/portfolio/sections/ServicesSection.d.ts +1 -1
- package/dist/portfolio/sections/TechStackSection.d.cts +1 -1
- package/dist/portfolio/sections/TechStackSection.d.ts +1 -1
- package/dist/portfolio/sections/TestimonialSection.d.cts +1 -1
- package/dist/portfolio/sections/TestimonialSection.d.ts +1 -1
- package/dist/portfolio/server.cjs +40 -14
- package/dist/portfolio/server.d.cts +3 -1
- package/dist/portfolio/server.d.ts +3 -1
- package/dist/portfolio/server.js +3 -1
- package/dist/{types-0tVPy4pU.d.ts → types-He5A9okM.d.cts} +1 -1
- package/dist/{types-0tVPy4pU.d.cts → types-He5A9okM.d.ts} +1 -1
- package/package.json +3 -3
- package/dist/chunk-4IES23GV.js.map +0 -1
- package/dist/chunk-5MEZU3OG.cjs.map +0 -1
- package/dist/chunk-RWQ675N6.js.map +0 -1
- package/dist/chunk-UXUU7FVT.cjs.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/portfolio/sanitize.ts","../src/portfolio/server.ts"],"names":[],"mappings":"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","file":"chunk-KXLROAO5.js","sourcesContent":["/**\n * Portfolio section sanitization — single source of truth.\n *\n * Site-architecture entries arrive from generation with raw CLIENT-site paths\n * (often title === path, e.g. \"/wheels/amp-tires\"). Those strings must never\n * cross a server→client props boundary: Googlebot extracts URL-like strings\n * from the RSC flight payload and crawls them against the AGENCY site's\n * origin, flooding Search Console with 404s (seen on upforge.io, June 2026 —\n * ~100 404'd client paths, all traced to /work/* case-study payloads).\n *\n * The enforcement point is `getPortfolioItem` (portfolio/server.ts) — the\n * last hop that is guaranteed server-side no matter how the build behaves.\n * PortfolioPage is a genuine RSC since 0.4.0 (and `pnpm verify` enforces\n * that), but rendered section data still crosses the client boundary as\n * props, and any future bundling regression would leak the whole item again.\n * Sanitize at the fetch, always. The PortfolioPage call is defense-in-depth.\n */\n\ninterface ArchPageEntry {\n path?: string;\n title?: string;\n}\n\n/** Convert a raw path/title entry into a display-only { title } label. */\nexport function humanizeArchPageEntry(page: ArchPageEntry): { title: string } {\n const title = (page?.title || '').trim();\n if (title && !title.startsWith('/')) return { title };\n const source = title || (page?.path || '').trim();\n const lastSegment = source.split(/[?#]/)[0].split('/').filter(Boolean).pop() || '';\n const label = lastSegment\n .replace(/\\.(html?|php|aspx?)$/i, '')\n .replace(/[-_]+/g, ' ')\n .trim()\n .replace(/\\b\\w/g, (c) => c.toUpperCase());\n return { title: label || 'Home' };\n}\n\n/** Strip raw page paths from a portfolioSiteArchitecture data payload. */\nexport function sanitizeSiteArchitectureData(data: any): any {\n if (!data || !Array.isArray(data.groups)) return data;\n return {\n ...data,\n groups: data.groups.map((group: any) => ({\n ...group,\n pages: Array.isArray(group.pages) ? group.pages.map(humanizeArchPageEntry) : [],\n })),\n };\n}\n\n/** Sanitize every section that can carry raw client-site paths. */\nexport function sanitizePortfolioSections<T extends { sectionType?: string; data?: any }>(\n sections: T[],\n): T[] {\n return sections.map((section) =>\n section?.sectionType === 'portfolioSiteArchitecture'\n ? { ...section, data: sanitizeSiteArchitectureData(section.data) }\n : section,\n );\n}\n","/**\n * @sonordev/agency-site-kit — Portfolio server-side data fetching\n *\n * RSC (React Server Component) data layer for portfolio pages.\n * All functions are async and designed for use in Next.js App Router\n * server components, generateMetadata(), and generateStaticParams().\n *\n * This file is server-only — no 'use client' directive.\n */\n\nimport { apiFetch } from '../shared/api';\nimport { PORTFOLIO_CACHE_TAG } from '../shared/cache';\nimport { PORTFOLIO_PREVIEW_COOKIE } from '../shared/preview';\nimport { sanitizePortfolioSections } from './sanitize';\nimport type {\n BrandConfig,\n MetricsDelta,\n PortfolioConfigResponse,\n PortfolioItemFull,\n PortfolioListResponse,\n PortfolioSeoData,\n PortfolioSection,\n} from '../types';\n\nconst LOG_PREFIX = '[@sonordev/agency-site-kit/portfolio]';\n\n// Re-export so consumers and route handlers can reference the same tag.\nexport { PORTFOLIO_CACHE_TAG } from '../shared/cache';\n\n/**\n * Error contract (single source of truth for portfolio content fetching):\n *\n * - \"Not found\" is a DATA answer: the API returns 200 with `found: false`,\n * and `getPortfolioItem` returns null. Consumers map null → notFound().\n * - Infrastructure failure (network error, 5xx, rate-limit exhaustion,\n * missing SONOR_API_KEY) THROWS PortfolioApiError. It must never be\n * conflated with \"not found\": treating an outage as a 404 turns indexed\n * /work URLs into real 404s during ISR regeneration (the GSC 404-flood\n * class of incident). A thrown error makes Next.js keep serving the last\n * good page during background revalidation, and fails builds loudly\n * instead of silently generating an empty portfolio.\n *\n * The one exception is getPortfolioBrandConfig, which degrades to default\n * branding on failure — cosmetic fallback is safe; content fallback is not.\n */\nexport class PortfolioApiError extends Error {\n readonly path: string;\n\n constructor(path: string, message?: string) {\n super(\n message ??\n `${LOG_PREFIX} Portfolio API request failed: ${path}. ` +\n 'This is an infrastructure/config failure (network, 5xx, or missing ' +\n 'SONOR_API_KEY), not a missing item — failing loudly so it cannot be ' +\n 'mistaken for a 404 or an empty portfolio.',\n );\n this.name = 'PortfolioApiError';\n this.path = path;\n }\n}\n\n/**\n * A 200 response whose body violates the expected shape. Thrown before any\n * mapping so drift between the API and this package surfaces as a loud,\n * named error instead of `undefined` text on live pages.\n */\nexport class PortfolioContractError extends PortfolioApiError {\n readonly field: string;\n\n constructor(path: string, field: string, detail: string) {\n super(\n path,\n `${LOG_PREFIX} Portfolio API contract violation at \"${field}\" (${path}): ${detail}. ` +\n 'Refusing to render partial or corrupt data.',\n );\n this.name = 'PortfolioContractError';\n this.field = field;\n }\n}\n\n/**\n * Minimal runtime shape check for the single-item response. Deliberately\n * lenient about unknown extra fields (the API may grow) and strict about\n * the fields this package renders from.\n */\nexport function assertPortfolioItemShape(\n item: unknown,\n path: string,\n): asserts item is PortfolioItemApiResponse['item'] {\n const fail = (field: string, detail: string): never => {\n throw new PortfolioContractError(path, field, detail);\n };\n\n if (typeof item !== 'object' || item === null) fail('item', 'not an object');\n const record = item as Record<string, unknown>;\n\n for (const field of ['id', 'slug', 'title'] as const) {\n const v = record[field];\n if (typeof v !== 'string' || v.length === 0) {\n fail(field, `expected a non-empty string, got ${JSON.stringify(v)}`);\n }\n }\n\n for (const field of ['sections', 'services', 'kpis', 'metricsDelta'] as const) {\n const v = record[field];\n if (v != null && !Array.isArray(v)) {\n fail(field, `expected an array when present, got ${typeof v}`);\n }\n }\n\n const sections = (record.sections ?? []) as unknown[];\n sections.forEach((section, i) => {\n if (typeof section !== 'object' || section === null) {\n fail(`sections[${i}]`, 'not an object');\n }\n const s = section as Record<string, unknown>;\n if (typeof s.sectionType !== 'string' || s.sectionType.length === 0) {\n fail(`sections[${i}].sectionType`, 'expected a non-empty string');\n }\n if (typeof s.data !== 'object' || s.data === null) {\n fail(`sections[${i}].data`, 'expected an object');\n }\n });\n}\n\n/**\n * GET against the public portfolio API with the shared portfolio cache tag,\n * so the Portal's revalidation webhook can invalidate every portfolio fetch\n * (list, detail, categories, config, sitemap data) via one revalidateTag.\n *\n * Retries transient failures (429/502/503 + network) up to 3 attempts, then\n * throws PortfolioApiError per the error contract above. All portfolio\n * endpoints answer \"unknown slug\" with 200 + `found: false`, so a null from\n * the fetcher can only mean infrastructure failure.\n */\nasync function portfolioGet<T>(\n path: string,\n opts: { revalidate?: number } = {},\n): Promise<T> {\n const data = await apiFetch<T>(path, {\n method: 'GET',\n tags: [PORTFOLIO_CACHE_TAG],\n retry: true,\n maxAttempts: 3,\n requireKey: true,\n ...(opts.revalidate !== undefined ? { revalidate: opts.revalidate } : {}),\n });\n\n if (data === null) throw new PortfolioApiError(path);\n return data;\n}\n\n/**\n * Draft preview: when Next draft mode is active AND the preview cookie is\n * set (both done by createPortfolioPreviewHandler), returns the raw token\n * so getPortfolioItem can fetch the unpublished item uncached. Fails soft —\n * outside a request scope (build, tests) this is simply not a draft.\n */\nasync function getDraftPreviewToken(): Promise<string | null> {\n try {\n const { draftMode, cookies } = await import('next/headers');\n const dm = await draftMode();\n if (!dm.isEnabled) return null;\n const jar = await cookies();\n return jar.get(PORTFOLIO_PREVIEW_COOKIE)?.value ?? null;\n } catch {\n return null;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Builds a query string from an object, omitting undefined/null values.\n */\nfunction buildQuery(params: Record<string, string | number | boolean | undefined | null>): string {\n const entries = Object.entries(params).filter(\n (entry): entry is [string, string | number | boolean] => entry[1] != null,\n );\n if (entries.length === 0) return '';\n const qs = new URLSearchParams(\n entries.map(([k, v]) => [k, String(v)]),\n ).toString();\n return `?${qs}`;\n}\n\n// ---------------------------------------------------------------------------\n// Public API — List items\n// ---------------------------------------------------------------------------\n\n/**\n * Fetches published portfolio items from the public API.\n * Cached via Next.js ISR (revalidate: 3600).\n *\n * An empty portfolio is `{ items: [] }` from a healthy API. Infrastructure\n * failure throws PortfolioApiError (see the error contract above) — it never\n * silently renders an empty portfolio or an empty sitemap.\n */\nexport async function getPortfolioItems(options?: {\n category?: string;\n featured?: boolean;\n limit?: number;\n offset?: number;\n}): Promise<PortfolioListResponse> {\n const query = buildQuery({\n category: options?.category,\n featured: options?.featured,\n limit: options?.limit,\n offset: options?.offset,\n });\n\n return portfolioGet<PortfolioListResponse>(\n `/api/public/portfolio/items${query}`,\n );\n}\n\n// ---------------------------------------------------------------------------\n// Public API — Single item\n// ---------------------------------------------------------------------------\n\n/** Shape returned by the single-item API endpoint. */\ninterface PortfolioItemApiResponse {\n found: boolean;\n item: {\n id: string;\n slug: string;\n title: string;\n subtitle: string;\n category: string;\n services: string[];\n description: string;\n hero_image: string;\n hero_image_alt: string;\n live_url: string | null;\n project_id: string | null;\n kpis: PortfolioItemFull['kpis'];\n details: PortfolioItemFull['details'];\n seo: PortfolioItemFull['seo'];\n featured: boolean;\n order?: number;\n published_at: string | null;\n hero_screenshots: PortfolioItemFull['hero_screenshots'];\n baseline_metrics: PortfolioItemFull['baseline_metrics'];\n current_metrics: PortfolioItemFull['current_metrics'];\n metrics_last_refreshed_at: string | null;\n sections: PortfolioSection[];\n metricsDelta: MetricsDelta[];\n sectionsOrdered?: boolean;\n };\n}\n\n/**\n * Fetches a single portfolio item by slug, including full Sanity sections\n * and live metrics delta. This is the main data function for portfolio detail pages.\n *\n * Returns null ONLY when the item genuinely does not exist (API answered\n * `found: false`) — safe to map to notFound(). Infrastructure failures throw\n * PortfolioApiError instead, so an API outage during ISR regeneration keeps\n * serving the last good page rather than minting a real 404.\n */\nexport async function getPortfolioItem(slug: string): Promise<PortfolioItemFull | null> {\n if (!slug) {\n console.warn(`${LOG_PREFIX} getPortfolioItem called with empty slug.`);\n return null;\n }\n\n // Draft preview (dashboard Preview button): fetch with the token, uncached.\n const previewToken = await getDraftPreviewToken();\n const path = previewToken\n ? `/api/public/portfolio/items/${encodeURIComponent(slug)}?preview=${encodeURIComponent(previewToken)}`\n : `/api/public/portfolio/items/${encodeURIComponent(slug)}`;\n\n const data = await portfolioGet<PortfolioItemApiResponse>(\n path,\n previewToken ? { revalidate: 0 } : {},\n );\n\n if (!data.found || !data.item) {\n console.warn(`${LOG_PREFIX} Portfolio item not found for slug: \"${slug}\".`);\n return null;\n }\n\n // Contract guard — a malformed 200 must not reach the renderer.\n assertPortfolioItemShape(data.item, `/api/public/portfolio/items/${slug}`);\n\n // Map the API response to the PortfolioItemFull type\n const { item } = data;\n return {\n id: item.id,\n slug: item.slug,\n title: item.title,\n subtitle: item.subtitle,\n category: item.category,\n services: item.services,\n description: item.description,\n hero_image: item.hero_image,\n hero_image_alt: item.hero_image_alt,\n live_url: item.live_url,\n project_id: item.project_id ?? null,\n kpis: item.kpis,\n details: item.details,\n seo: item.seo,\n featured: item.featured,\n order: item.order,\n published_at: item.published_at,\n hero_screenshots: item.hero_screenshots,\n baseline_metrics: item.baseline_metrics,\n current_metrics: item.current_metrics,\n metrics_last_refreshed_at: item.metrics_last_refreshed_at,\n // Sanitized HERE — the enforcing layer. Even with PortfolioPage as a\n // genuine RSC, each rendered section's data still crosses the client\n // boundary into the flight payload, and any future bundling regression\n // would leak the whole item again (the 0.3.1 GSC 404-flood vector).\n // Sanitize at the last server-side hop, always.\n sections: sanitizePortfolioSections(item.sections ?? []),\n metricsDelta: item.metricsDelta ?? [],\n sectionsOrdered: item.sectionsOrdered ?? false,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Public API — Categories\n// ---------------------------------------------------------------------------\n\n/**\n * Fetches available portfolio categories.\n * Infrastructure failure throws PortfolioApiError (see error contract).\n */\nexport async function getPortfolioCategories(): Promise<string[]> {\n return portfolioGet<string[]>('/api/public/portfolio/categories');\n}\n\n// ---------------------------------------------------------------------------\n// Public API — Brand config\n// ---------------------------------------------------------------------------\n\n/**\n * Fetches brand configuration for the agency (colors, fonts, logo).\n * Returns sensible defaults when the API call fails — the ONE deliberate\n * exception to the throw-on-failure contract: brand config is cosmetic, and\n * an API blip must not take down the whole layout that wraps every page.\n */\nexport async function getPortfolioBrandConfig(): Promise<BrandConfig> {\n const fallback: BrandConfig = {\n primary: '#6366f1',\n secondary: '#8b5cf6',\n background: '#0a0a0f',\n backgroundElevated: '#12121a',\n surface: '#1a1a2e',\n surfaceHover: '#22223a',\n surfaceBorder: '#2a2a3e',\n textPrimary: '#f0f0f5',\n textSecondary: '#a0a0b5',\n textTertiary: '#6b6b80',\n fontHeading: 'Inter, system-ui, sans-serif',\n fontBody: 'Inter, system-ui, sans-serif',\n };\n\n try {\n const data = await portfolioGet<PortfolioConfigResponse>(\n '/api/public/portfolio/config',\n );\n if (!data.brand) {\n console.warn(`${LOG_PREFIX} Brand config response empty, using defaults.`);\n return fallback;\n }\n return data.brand;\n } catch {\n console.warn(`${LOG_PREFIX} Failed to fetch brand config, using defaults.`);\n return fallback;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Next.js Metadata helper\n// ---------------------------------------------------------------------------\n\n/**\n * Generates Next.js Metadata for a portfolio item page.\n * For use in generateMetadata() in the App Router.\n *\n * Returns an empty object when the item does not exist (safe to spread into\n * the metadata return value); infrastructure failures throw so a build never\n * silently ships pages with missing metadata.\n */\nexport async function generatePortfolioMetadata(slug: string): Promise<{\n title?: string;\n description?: string;\n keywords?: string[];\n openGraph?: {\n title?: string;\n description?: string;\n images?: string[];\n type?: string;\n };\n}> {\n const item = await getPortfolioItem(slug);\n\n if (!item) {\n return {};\n }\n\n // Prefer dedicated SEO data if available, fall back to item fields\n const seo: PortfolioSeoData | null = item.seo;\n const title = seo?.metaTitle || item.title;\n const description = seo?.metaDescription || item.description;\n const keywords = seo?.keywords ?? [];\n\n // Collect OG images: prefer SEO og image, then hero image\n const ogImages: string[] = [];\n if (item.hero_image) {\n ogImages.push(item.hero_image);\n }\n\n return {\n title,\n description,\n keywords: keywords.length > 0 ? keywords : undefined,\n openGraph: {\n title,\n description,\n images: ogImages.length > 0 ? ogImages : undefined,\n type: 'article',\n },\n };\n}\n\n// ---------------------------------------------------------------------------\n// Next.js Static Params helper\n// ---------------------------------------------------------------------------\n\n/**\n * Generates static params for portfolio pages.\n * For use in generateStaticParams() in the App Router.\n *\n * Fetches all published portfolio items and returns their slugs.\n * Infrastructure failure throws, failing the build loudly instead of\n * silently building a site with zero portfolio pages.\n */\nexport async function generatePortfolioStaticParams(): Promise<Array<{ slug: string }>> {\n // Fetch a large batch to cover all published items\n const data = await getPortfolioItems({ limit: 500, offset: 0 });\n\n return data.items.map((item) => ({ slug: item.slug }));\n}\n\n// ---------------------------------------------------------------------------\n// Category helpers (route-based, crawlable filtering)\n// ---------------------------------------------------------------------------\n\n/**\n * URL slug for a category display value (\"Web Development\" → \"web-development\").\n * Single source of truth for category URLs — PortfolioIndex resolves slugs\n * back to the original values by matching against getPortfolioCategories().\n */\nexport function slugifyCategory(category: string): string {\n return category\n .toLowerCase()\n .trim()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '');\n}\n\n/**\n * Static params for the category filter routes.\n * For use in app/work/category/[category]/page.tsx:\n *\n * ```ts\n * export async function generateStaticParams() {\n * return generatePortfolioCategoryStaticParams();\n * }\n * ```\n */\nexport async function generatePortfolioCategoryStaticParams(): Promise<\n Array<{ category: string }>\n> {\n const categories = await getPortfolioCategories();\n return categories.map((c) => ({ category: slugifyCategory(c) }));\n}\n\n/**\n * Metadata for index + category + paginated portfolio routes: a stable\n * title/canonical so filtered and paginated variants don't compete in\n * search. Spread into generateMetadata()'s return value.\n */\nexport async function generatePortfolioIndexMetadata(options: {\n /** Site origin (e.g. \"https://youragency.com\"). */\n baseUrl: string;\n /** Route prefix. Defaults to \"/work\". */\n basePath?: string;\n /** Active category SLUG (from the route param), if any. */\n category?: string | null;\n /** 1-based page number. */\n page?: number;\n /** Title base, e.g. \"Our Work\". Category/page qualifiers are appended. */\n titleBase?: string;\n}): Promise<{ title: string; alternates: { canonical: string } }> {\n const {\n baseUrl,\n basePath = '/work',\n category = null,\n page = 1,\n titleBase = 'Our Work',\n } = options;\n\n const origin = baseUrl.replace(/\\/+$/, '');\n const prefix = basePath.startsWith('/') ? basePath : `/${basePath}`;\n\n let title = titleBase;\n let path = prefix;\n\n if (category) {\n const categories = await getPortfolioCategories();\n const match = categories.find((c) => slugifyCategory(c) === category);\n if (match) title = `${match} | ${titleBase}`;\n path = `${prefix}/category/${category}`;\n }\n if (page > 1) {\n title = `${title} — Page ${page}`;\n path = `${path}?page=${page}`;\n }\n\n return { title, alternates: { canonical: `${origin}${path}` } };\n}\n\n// ---------------------------------------------------------------------------\n// Next.js Sitemap helper\n// ---------------------------------------------------------------------------\n\n/** One entry in the shape Next.js expects from app/sitemap.(js|ts). */\nexport interface PortfolioSitemapEntry {\n url: string;\n lastModified?: string;\n changeFrequency?: 'always' | 'hourly' | 'daily' | 'weekly' | 'monthly' | 'yearly' | 'never';\n priority?: number;\n}\n\n/**\n * Builds sitemap entries for every published portfolio item, in the shape\n * Next.js `app/sitemap.(js|ts)` expects — spread the result into the array\n * you return:\n *\n * ```ts\n * const portfolio = await generatePortfolioSitemapEntries({\n * baseUrl: 'https://youragency.com',\n * });\n * return [...staticEntries, ...portfolio];\n * ```\n *\n * `lastModified` prefers the metrics refresh timestamp (case studies change\n * when their live metrics update) and falls back to the publish date.\n * Infrastructure failure throws — a sitemap silently missing every case\n * study is how portfolio URLs fall out of the index.\n */\nexport async function generatePortfolioSitemapEntries(options: {\n /** Site origin, no trailing slash needed (e.g. \"https://youragency.com\"). */\n baseUrl: string;\n /** Route prefix for case studies. Defaults to \"/work\". */\n basePath?: string;\n changeFrequency?: PortfolioSitemapEntry['changeFrequency'];\n priority?: number;\n}): Promise<PortfolioSitemapEntry[]> {\n const {\n baseUrl,\n basePath = '/work',\n changeFrequency = 'monthly',\n priority = 0.7,\n } = options;\n\n const origin = baseUrl.replace(/\\/+$/, '');\n const prefix = basePath.startsWith('/') ? basePath : `/${basePath}`;\n\n const data = await getPortfolioItems({ limit: 500, offset: 0 });\n\n return data.items.map((item) => {\n const lastModified =\n item.metrics_last_refreshed_at || item.published_at || undefined;\n return {\n url: `${origin}${prefix}/${item.slug}`,\n ...(lastModified ? { lastModified } : {}),\n changeFrequency,\n priority,\n };\n });\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/portfolio/sanitize.ts","../src/portfolio/server.ts"],"names":[],"mappings":"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","file":"chunk-FT2SNEYP.js","sourcesContent":["/**\n * Portfolio section sanitization — single source of truth.\n *\n * Site-architecture entries arrive from generation with raw CLIENT-site paths\n * (often title === path, e.g. \"/wheels/amp-tires\"). Those strings must never\n * cross a server→client props boundary: Googlebot extracts URL-like strings\n * from the RSC flight payload and crawls them against the AGENCY site's\n * origin, flooding Search Console with 404s (seen on upforge.io, June 2026 —\n * ~100 404'd client paths, all traced to /work/* case-study payloads).\n *\n * The enforcement point is `getPortfolioItem` (portfolio/server.ts) — the\n * last hop that is guaranteed server-side no matter how the build behaves.\n * PortfolioPage is a genuine RSC since 0.4.0 (and `pnpm verify` enforces\n * that), but rendered section data still crosses the client boundary as\n * props, and any future bundling regression would leak the whole item again.\n * Sanitize at the fetch, always. The PortfolioPage call is defense-in-depth.\n */\n\ninterface ArchPageEntry {\n path?: string;\n title?: string;\n}\n\n/** Convert a raw path/title entry into a display-only { title } label. */\nexport function humanizeArchPageEntry(page: ArchPageEntry): { title: string } {\n const title = (page?.title || '').trim();\n if (title && !title.startsWith('/')) return { title };\n const source = title || (page?.path || '').trim();\n const lastSegment = source.split(/[?#]/)[0].split('/').filter(Boolean).pop() || '';\n const label = lastSegment\n .replace(/\\.(html?|php|aspx?)$/i, '')\n .replace(/[-_]+/g, ' ')\n .trim()\n .replace(/\\b\\w/g, (c) => c.toUpperCase());\n return { title: label || 'Home' };\n}\n\n/** Strip raw page paths from a portfolioSiteArchitecture data payload. */\nexport function sanitizeSiteArchitectureData(data: any): any {\n if (!data || !Array.isArray(data.groups)) return data;\n return {\n ...data,\n groups: data.groups.map((group: any) => ({\n ...group,\n pages: Array.isArray(group.pages) ? group.pages.map(humanizeArchPageEntry) : [],\n })),\n };\n}\n\n/** Sanitize every section that can carry raw client-site paths. */\nexport function sanitizePortfolioSections<T extends { sectionType?: string; data?: any }>(\n sections: T[],\n): T[] {\n return sections.map((section) =>\n section?.sectionType === 'portfolioSiteArchitecture'\n ? { ...section, data: sanitizeSiteArchitectureData(section.data) }\n : section,\n );\n}\n","/**\n * @sonordev/agency-site-kit — Portfolio server-side data fetching\n *\n * RSC (React Server Component) data layer for portfolio pages.\n * All functions are async and designed for use in Next.js App Router\n * server components, generateMetadata(), and generateStaticParams().\n *\n * This file is server-only — no 'use client' directive.\n */\n\nimport { apiFetch } from '../shared/api';\nimport { PORTFOLIO_CACHE_TAG } from '../shared/cache';\nimport { PORTFOLIO_PREVIEW_COOKIE } from '../shared/preview';\nimport { sanitizePortfolioSections } from './sanitize';\nimport type {\n BrandConfig,\n MetricsDelta,\n PortfolioConfigResponse,\n PortfolioItemFull,\n PortfolioListResponse,\n PortfolioSeoData,\n PortfolioSection,\n} from '../types';\n\nconst LOG_PREFIX = '[@sonordev/agency-site-kit/portfolio]';\n\n// Re-export so consumers and route handlers can reference the same tag.\nexport { PORTFOLIO_CACHE_TAG } from '../shared/cache';\n\n/**\n * Error contract (single source of truth for portfolio content fetching):\n *\n * - \"Not found\" is a DATA answer: the API returns 200 with `found: false`,\n * and `getPortfolioItem` returns null. Consumers map null → notFound().\n * - Infrastructure failure (network error, 5xx, rate-limit exhaustion,\n * missing SONOR_API_KEY) THROWS PortfolioApiError. It must never be\n * conflated with \"not found\": treating an outage as a 404 turns indexed\n * /work URLs into real 404s during ISR regeneration (the GSC 404-flood\n * class of incident). A thrown error makes Next.js keep serving the last\n * good page during background revalidation, and fails builds loudly\n * instead of silently generating an empty portfolio.\n *\n * The one exception is getPortfolioBrandConfig, which degrades to default\n * branding on failure — cosmetic fallback is safe; content fallback is not.\n */\nexport class PortfolioApiError extends Error {\n readonly path: string;\n\n constructor(path: string, message?: string) {\n super(\n message ??\n `${LOG_PREFIX} Portfolio API request failed: ${path}. ` +\n 'This is an infrastructure/config failure (network, 5xx, or missing ' +\n 'SONOR_API_KEY), not a missing item — failing loudly so it cannot be ' +\n 'mistaken for a 404 or an empty portfolio.',\n );\n this.name = 'PortfolioApiError';\n this.path = path;\n }\n}\n\n/**\n * A 200 response whose body violates the expected shape. Thrown before any\n * mapping so drift between the API and this package surfaces as a loud,\n * named error instead of `undefined` text on live pages.\n */\nexport class PortfolioContractError extends PortfolioApiError {\n readonly field: string;\n\n constructor(path: string, field: string, detail: string) {\n super(\n path,\n `${LOG_PREFIX} Portfolio API contract violation at \"${field}\" (${path}): ${detail}. ` +\n 'Refusing to render partial or corrupt data.',\n );\n this.name = 'PortfolioContractError';\n this.field = field;\n }\n}\n\n/**\n * Minimal runtime shape check for the single-item response. Deliberately\n * lenient about unknown extra fields (the API may grow) and strict about\n * the fields this package renders from.\n */\nexport function assertPortfolioItemShape(\n item: unknown,\n path: string,\n): asserts item is PortfolioItemApiResponse['item'] {\n const fail = (field: string, detail: string): never => {\n throw new PortfolioContractError(path, field, detail);\n };\n\n if (typeof item !== 'object' || item === null) fail('item', 'not an object');\n const record = item as Record<string, unknown>;\n\n for (const field of ['id', 'slug', 'title'] as const) {\n const v = record[field];\n if (typeof v !== 'string' || v.length === 0) {\n fail(field, `expected a non-empty string, got ${JSON.stringify(v)}`);\n }\n }\n\n for (const field of ['sections', 'services', 'kpis', 'metricsDelta'] as const) {\n const v = record[field];\n if (v != null && !Array.isArray(v)) {\n fail(field, `expected an array when present, got ${typeof v}`);\n }\n }\n\n const sections = (record.sections ?? []) as unknown[];\n sections.forEach((section, i) => {\n if (typeof section !== 'object' || section === null) {\n fail(`sections[${i}]`, 'not an object');\n }\n const s = section as Record<string, unknown>;\n if (typeof s.sectionType !== 'string' || s.sectionType.length === 0) {\n fail(`sections[${i}].sectionType`, 'expected a non-empty string');\n }\n if (typeof s.data !== 'object' || s.data === null) {\n fail(`sections[${i}].data`, 'expected an object');\n }\n });\n}\n\n/**\n * GET against the public portfolio API with the shared portfolio cache tag,\n * so the Portal's revalidation webhook can invalidate every portfolio fetch\n * (list, detail, categories, config, sitemap data) via one revalidateTag.\n *\n * Retries transient failures (429/502/503 + network) up to 3 attempts, then\n * throws PortfolioApiError per the error contract above. All portfolio\n * endpoints answer \"unknown slug\" with 200 + `found: false`, so a null from\n * the fetcher can only mean infrastructure failure.\n */\nasync function portfolioGet<T>(\n path: string,\n opts: { revalidate?: number } = {},\n): Promise<T> {\n const data = await apiFetch<T>(path, {\n method: 'GET',\n tags: [PORTFOLIO_CACHE_TAG],\n retry: true,\n maxAttempts: 3,\n requireKey: true,\n ...(opts.revalidate !== undefined ? { revalidate: opts.revalidate } : {}),\n });\n\n if (data === null) throw new PortfolioApiError(path);\n return data;\n}\n\n/**\n * Draft preview: when Next draft mode is active AND the preview cookie is\n * set (both done by createPortfolioPreviewHandler), returns the raw token\n * so getPortfolioItem can fetch the unpublished item uncached. Fails soft —\n * outside a request scope (build, tests) this is simply not a draft.\n */\nasync function getDraftPreviewToken(): Promise<string | null> {\n try {\n const { draftMode, cookies } = await import('next/headers');\n const dm = await draftMode();\n if (!dm.isEnabled) return null;\n const jar = await cookies();\n return jar.get(PORTFOLIO_PREVIEW_COOKIE)?.value ?? null;\n } catch {\n return null;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Builds a query string from an object, omitting undefined/null values.\n */\nfunction buildQuery(params: Record<string, string | number | boolean | undefined | null>): string {\n const entries = Object.entries(params).filter(\n (entry): entry is [string, string | number | boolean] => entry[1] != null,\n );\n if (entries.length === 0) return '';\n const qs = new URLSearchParams(\n entries.map(([k, v]) => [k, String(v)]),\n ).toString();\n return `?${qs}`;\n}\n\n// ---------------------------------------------------------------------------\n// Public API — List items\n// ---------------------------------------------------------------------------\n\n/**\n * Fetches published portfolio items from the public API.\n * Cached via Next.js ISR (revalidate: 3600).\n *\n * An empty portfolio is `{ items: [] }` from a healthy API. Infrastructure\n * failure throws PortfolioApiError (see the error contract above) — it never\n * silently renders an empty portfolio or an empty sitemap.\n */\nexport async function getPortfolioItems(options?: {\n category?: string;\n featured?: boolean;\n limit?: number;\n offset?: number;\n}): Promise<PortfolioListResponse> {\n const query = buildQuery({\n category: options?.category,\n featured: options?.featured,\n limit: options?.limit,\n offset: options?.offset,\n });\n\n return portfolioGet<PortfolioListResponse>(\n `/api/public/portfolio/items${query}`,\n );\n}\n\n// ---------------------------------------------------------------------------\n// Public API — Single item\n// ---------------------------------------------------------------------------\n\n/** Shape returned by the single-item API endpoint. */\ninterface PortfolioItemApiResponse {\n found: boolean;\n item: {\n id: string;\n slug: string;\n title: string;\n subtitle: string;\n category: string;\n services: string[];\n description: string;\n hero_image: string;\n hero_image_alt: string;\n live_url: string | null;\n project_id: string | null;\n kpis: PortfolioItemFull['kpis'];\n details: PortfolioItemFull['details'];\n seo: PortfolioItemFull['seo'];\n featured: boolean;\n order?: number;\n published_at: string | null;\n hero_screenshots: PortfolioItemFull['hero_screenshots'];\n baseline_metrics: PortfolioItemFull['baseline_metrics'];\n current_metrics: PortfolioItemFull['current_metrics'];\n metrics_last_refreshed_at: string | null;\n sections: PortfolioSection[];\n metricsDelta: MetricsDelta[];\n sectionsOrdered?: boolean;\n };\n}\n\n/**\n * Fetches a single portfolio item by slug, including full Sanity sections\n * and live metrics delta. This is the main data function for portfolio detail pages.\n *\n * Returns null ONLY when the item genuinely does not exist (API answered\n * `found: false`) — safe to map to notFound(). Infrastructure failures throw\n * PortfolioApiError instead, so an API outage during ISR regeneration keeps\n * serving the last good page rather than minting a real 404.\n */\nexport async function getPortfolioItem(slug: string): Promise<PortfolioItemFull | null> {\n if (!slug) {\n console.warn(`${LOG_PREFIX} getPortfolioItem called with empty slug.`);\n return null;\n }\n\n // Draft preview (dashboard Preview button): fetch with the token, uncached.\n const previewToken = await getDraftPreviewToken();\n const path = previewToken\n ? `/api/public/portfolio/items/${encodeURIComponent(slug)}?preview=${encodeURIComponent(previewToken)}`\n : `/api/public/portfolio/items/${encodeURIComponent(slug)}`;\n\n const data = await portfolioGet<PortfolioItemApiResponse>(\n path,\n previewToken ? { revalidate: 0 } : {},\n );\n\n if (!data.found || !data.item) {\n console.warn(`${LOG_PREFIX} Portfolio item not found for slug: \"${slug}\".`);\n return null;\n }\n\n // Contract guard — a malformed 200 must not reach the renderer.\n assertPortfolioItemShape(data.item, `/api/public/portfolio/items/${slug}`);\n\n // Map the API response to the PortfolioItemFull type\n const { item } = data;\n return {\n id: item.id,\n slug: item.slug,\n title: item.title,\n subtitle: item.subtitle,\n category: item.category,\n services: item.services,\n description: item.description,\n hero_image: item.hero_image,\n hero_image_alt: item.hero_image_alt,\n live_url: item.live_url,\n project_id: item.project_id ?? null,\n kpis: item.kpis,\n details: item.details,\n seo: item.seo,\n featured: item.featured,\n order: item.order,\n published_at: item.published_at,\n hero_screenshots: item.hero_screenshots,\n baseline_metrics: item.baseline_metrics,\n current_metrics: item.current_metrics,\n metrics_last_refreshed_at: item.metrics_last_refreshed_at,\n // Sanitized HERE — the enforcing layer. Even with PortfolioPage as a\n // genuine RSC, each rendered section's data still crosses the client\n // boundary into the flight payload, and any future bundling regression\n // would leak the whole item again (the 0.3.1 GSC 404-flood vector).\n // Sanitize at the last server-side hop, always.\n sections: sanitizePortfolioSections(item.sections ?? []),\n metricsDelta: item.metricsDelta ?? [],\n sectionsOrdered: item.sectionsOrdered ?? false,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Public API — Categories\n// ---------------------------------------------------------------------------\n\n/**\n * Fetches available portfolio categories.\n * Infrastructure failure throws PortfolioApiError (see error contract).\n */\nexport async function getPortfolioCategories(): Promise<string[]> {\n return portfolioGet<string[]>('/api/public/portfolio/categories');\n}\n\n// ---------------------------------------------------------------------------\n// Public API — Brand config\n// ---------------------------------------------------------------------------\n\n/**\n * Fetches brand configuration for the agency (colors, fonts, logo).\n * Returns sensible defaults when the API call fails — the ONE deliberate\n * exception to the throw-on-failure contract: brand config is cosmetic, and\n * an API blip must not take down the whole layout that wraps every page.\n */\nexport async function getPortfolioBrandConfig(): Promise<BrandConfig> {\n const fallback: BrandConfig = {\n primary: '#6366f1',\n secondary: '#8b5cf6',\n background: '#0a0a0f',\n backgroundElevated: '#12121a',\n surface: '#1a1a2e',\n surfaceHover: '#22223a',\n surfaceBorder: '#2a2a3e',\n textPrimary: '#f0f0f5',\n textSecondary: '#a0a0b5',\n textTertiary: '#6b6b80',\n fontHeading: 'Inter, system-ui, sans-serif',\n fontBody: 'Inter, system-ui, sans-serif',\n };\n\n try {\n const data = await portfolioGet<PortfolioConfigResponse>(\n '/api/public/portfolio/config',\n );\n if (!data.brand) {\n console.warn(`${LOG_PREFIX} Brand config response empty, using defaults.`);\n return fallback;\n }\n return data.brand;\n } catch {\n console.warn(`${LOG_PREFIX} Failed to fetch brand config, using defaults.`);\n return fallback;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Next.js Metadata helper\n// ---------------------------------------------------------------------------\n\n/**\n * Generates Next.js Metadata for a portfolio item page.\n * For use in generateMetadata() in the App Router.\n *\n * Returns an empty object when the item does not exist (safe to spread into\n * the metadata return value); infrastructure failures throw so a build never\n * silently ships pages with missing metadata.\n */\nexport async function generatePortfolioMetadata(slug: string): Promise<{\n title?: string;\n description?: string;\n keywords?: string[];\n openGraph?: {\n title?: string;\n description?: string;\n images?: string[];\n type?: string;\n };\n}> {\n const item = await getPortfolioItem(slug);\n\n if (!item) {\n return {};\n }\n\n // Prefer dedicated SEO data if available, fall back to item fields\n const seo: PortfolioSeoData | null = item.seo;\n const title = seo?.metaTitle || item.title;\n const description = seo?.metaDescription || item.description;\n const keywords = seo?.keywords ?? [];\n\n // Collect OG images: prefer SEO og image, then hero image\n const ogImages: string[] = [];\n if (item.hero_image) {\n ogImages.push(item.hero_image);\n }\n\n return {\n title,\n description,\n keywords: keywords.length > 0 ? keywords : undefined,\n openGraph: {\n title,\n description,\n images: ogImages.length > 0 ? ogImages : undefined,\n type: 'article',\n },\n };\n}\n\n// ---------------------------------------------------------------------------\n// Next.js Static Params helper\n// ---------------------------------------------------------------------------\n\n/**\n * Generates static params for portfolio pages.\n * For use in generateStaticParams() in the App Router.\n *\n * Fetches all published portfolio items and returns their slugs.\n * Infrastructure failure throws, failing the build loudly instead of\n * silently building a site with zero portfolio pages.\n */\nexport async function generatePortfolioStaticParams(): Promise<Array<{ slug: string }>> {\n // Fetch a large batch to cover all published items\n const data = await getPortfolioItems({ limit: 500, offset: 0 });\n\n return data.items.map((item) => ({ slug: item.slug }));\n}\n\n// ---------------------------------------------------------------------------\n// Category helpers (route-based, crawlable filtering)\n// ---------------------------------------------------------------------------\n\n/**\n * URL slug for a category display value (\"Web Development\" → \"web-development\").\n * Single source of truth for category URLs — PortfolioIndex resolves slugs\n * back to the original values by matching against getPortfolioCategories().\n */\nexport function slugifyCategory(category: string): string {\n return category\n .toLowerCase()\n .trim()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '');\n}\n\n/**\n * Static params for the category filter routes.\n * For use in app/work/category/[category]/page.tsx:\n *\n * ```ts\n * export async function generateStaticParams() {\n * return generatePortfolioCategoryStaticParams();\n * }\n * ```\n */\nexport async function generatePortfolioCategoryStaticParams(): Promise<\n Array<{ category: string }>\n> {\n const categories = await getPortfolioCategories();\n return categories.map((c) => ({ category: slugifyCategory(c) }));\n}\n\n/**\n * Metadata for index + category + paginated portfolio routes: a stable\n * title/canonical so filtered and paginated variants don't compete in\n * search. Spread into generateMetadata()'s return value.\n */\nexport async function generatePortfolioIndexMetadata(options: {\n /** Site origin (e.g. \"https://youragency.com\"). */\n baseUrl: string;\n /** Route prefix. Defaults to \"/work\". */\n basePath?: string;\n /** Active category SLUG (from the route param), if any. */\n category?: string | null;\n /** 1-based page number. */\n page?: number;\n /** Title base, e.g. \"Our Work\". Category/page qualifiers are appended. */\n titleBase?: string;\n}): Promise<{ title: string; alternates: { canonical: string } }> {\n const {\n baseUrl,\n basePath = '/work',\n category = null,\n page = 1,\n titleBase = 'Our Work',\n } = options;\n\n const origin = baseUrl.replace(/\\/+$/, '');\n const prefix = basePath.startsWith('/') ? basePath : `/${basePath}`;\n\n let title = titleBase;\n let path = prefix;\n\n if (category) {\n const categories = await getPortfolioCategories();\n const match = categories.find((c) => slugifyCategory(c) === category);\n if (match) title = `${match} | ${titleBase}`;\n path = `${prefix}/category/${category}`;\n }\n if (page > 1) {\n title = `${title} — Page ${page}`;\n path = `${path}?page=${page}`;\n }\n\n return { title, alternates: { canonical: `${origin}${path}` } };\n}\n\n// ---------------------------------------------------------------------------\n// Next.js Sitemap helper\n// ---------------------------------------------------------------------------\n\n/** One entry in the shape Next.js expects from app/sitemap.(js|ts). */\nexport interface PortfolioSitemapEntry {\n url: string;\n lastModified?: string;\n changeFrequency?: 'always' | 'hourly' | 'daily' | 'weekly' | 'monthly' | 'yearly' | 'never';\n priority?: number;\n}\n\n/**\n * Builds sitemap entries for every published portfolio item, in the shape\n * Next.js `app/sitemap.(js|ts)` expects — spread the result into the array\n * you return:\n *\n * ```ts\n * const portfolio = await generatePortfolioSitemapEntries({\n * baseUrl: 'https://youragency.com',\n * });\n * return [...staticEntries, ...portfolio];\n * ```\n *\n * `lastModified` prefers the metrics refresh timestamp (case studies change\n * when their live metrics update) and falls back to the publish date.\n * Infrastructure failure throws — a sitemap silently missing every case\n * study is how portfolio URLs fall out of the index.\n */\nexport async function generatePortfolioSitemapEntries(options: {\n /** Site origin, no trailing slash needed (e.g. \"https://youragency.com\"). */\n baseUrl: string;\n /** Route prefix for case studies. Defaults to \"/work\". */\n basePath?: string;\n changeFrequency?: PortfolioSitemapEntry['changeFrequency'];\n priority?: number;\n}): Promise<PortfolioSitemapEntry[]> {\n const {\n baseUrl,\n basePath = '/work',\n changeFrequency = 'monthly',\n priority = 0.7,\n } = options;\n\n const origin = baseUrl.replace(/\\/+$/, '');\n const prefix = basePath.startsWith('/') ? basePath : `/${basePath}`;\n\n const data = await getPortfolioItems({ limit: 500, offset: 0 });\n\n return data.items.map((item) => {\n const lastModified =\n item.metrics_last_refreshed_at || item.published_at || undefined;\n return {\n url: `${origin}${prefix}/${item.slug}`,\n ...(lastModified ? { lastModified } : {}),\n changeFrequency,\n priority,\n };\n });\n}\n\n/* ──────────────────────────────────────────────────────────────────────────\n Display policy + label formatters (pure, server-safe).\n\n Also exported from the /portfolio barrel, but importing them from THERE in\n a server component registers every portfolio client component (the barrel\n re-exports /portfolio/client) into the importing page's chunk graph —\n ~40KB gz of sections/primitives on pages that only wanted a formatter\n (found on upforge's home page, 2026-07-14). Server components should\n import these from '@sonordev/agency-site-kit/portfolio/server'.\n ────────────────────────────────────────────────────────────────────────── */\nexport {\n curatePortfolioProof,\n curateHeroKpis,\n curateResultItems,\n gateMetricsDeltas,\n} from './curate-proof';\nexport { formatCategoryLabel, formatServiceTag } from './format';\n"]}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
var chunkXOWSXZYW_cjs = require('./chunk-XOWSXZYW.cjs');
|
|
4
4
|
var React = require('react');
|
|
5
5
|
var server = require('@sonordev/site-kit/server');
|
|
6
|
+
var client = require('@sonordev/site-kit/client');
|
|
6
7
|
|
|
7
8
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
8
9
|
|
|
@@ -167,12 +168,15 @@ async function AgencySiteKitLayout({
|
|
|
167
168
|
brandConfig: brandConfigOverride,
|
|
168
169
|
theme,
|
|
169
170
|
className,
|
|
170
|
-
showLlmsTxtFooterLink = false
|
|
171
|
+
showLlmsTxtFooterLink = false,
|
|
172
|
+
credential = true
|
|
171
173
|
}) {
|
|
172
174
|
const brandConfig = brandConfigOverride ? mergeBrandConfig(brandConfigOverride) : await fetchBrandConfig(apiKey, apiUrl);
|
|
173
175
|
const cssString = generateBrandCSS(brandConfig);
|
|
174
176
|
const resolvedApiUrl = apiUrl ?? process.env.SONOR_API_URL ?? process.env.UPTRADE_API_URL ?? "https://api.sonor.io";
|
|
175
177
|
const preconnectHref = server.resolveApiOrigin(resolvedApiUrl);
|
|
178
|
+
const rawKey = credential ? server.resolveServerApiConfig({ apiKey, apiUrl })?.apiKey ?? "" : "";
|
|
179
|
+
const clientCredential = rawKey ? await server.resolveClientCredential(rawKey, resolvedApiUrl) : "";
|
|
176
180
|
return React__default.default.createElement(
|
|
177
181
|
React__default.default.Fragment,
|
|
178
182
|
null,
|
|
@@ -189,6 +193,14 @@ async function AgencySiteKitLayout({
|
|
|
189
193
|
dangerouslySetInnerHTML: { __html: cssString },
|
|
190
194
|
"data-agency-site-kit": "brand"
|
|
191
195
|
}),
|
|
196
|
+
// Publish the minted token + __SITE_KIT_* globals. A childless sibling
|
|
197
|
+
// rendering null — it never encloses page content, so it can't drag the
|
|
198
|
+
// tree into a client boundary (CLAUDE.md's cardinal SSR rule).
|
|
199
|
+
clientCredential ? React__default.default.createElement(client.SiteKitCredential, {
|
|
200
|
+
key: "sk-credential",
|
|
201
|
+
apiKey: clientCredential,
|
|
202
|
+
apiUrl: resolvedApiUrl
|
|
203
|
+
}) : null,
|
|
192
204
|
React__default.default.createElement(
|
|
193
205
|
"div",
|
|
194
206
|
{
|
|
@@ -217,5 +229,5 @@ exports.DEFAULT_DARK_MODE = DEFAULT_DARK_MODE;
|
|
|
217
229
|
exports.generateBrandCSS = generateBrandCSS;
|
|
218
230
|
exports.hexToRgb = hexToRgb;
|
|
219
231
|
exports.mergeBrandConfig = mergeBrandConfig;
|
|
220
|
-
//# sourceMappingURL=chunk-
|
|
221
|
-
//# sourceMappingURL=chunk-
|
|
232
|
+
//# sourceMappingURL=chunk-OG5RBKQJ.cjs.map
|
|
233
|
+
//# sourceMappingURL=chunk-OG5RBKQJ.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/shared/brand.ts","../src/layout/AgencySiteKitLayout.tsx"],"names":["apiGet","resolveApiOrigin","resolveServerApiConfig","resolveClientCredential","React","SiteKitCredential"],"mappings":"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","file":"chunk-OG5RBKQJ.cjs","sourcesContent":["/**\n * @sonordev/agency-site-kit — Brand customization system\n *\n * Generates CSS custom properties from a BrandConfig object.\n * Supports light/dark mode with automatic fallbacks.\n */\n\nimport type { BrandConfig } from '../types';\n\n// ---------------------------------------------------------------------------\n// Hex to RGB helper\n// ---------------------------------------------------------------------------\n\n/**\n * Converts a hex color string to an \"R, G, B\" string for use in rgb()/rgba().\n * Supports 3-digit (#abc) and 6-digit (#aabbcc) hex values.\n * Returns null for invalid inputs.\n */\nexport function hexToRgb(hex: string): string | null {\n if (!hex || typeof hex !== 'string') return null;\n\n const cleaned = hex.replace(/^#/, '');\n\n let r: number;\n let g: number;\n let b: number;\n\n if (cleaned.length === 3) {\n r = parseInt(cleaned[0] + cleaned[0], 16);\n g = parseInt(cleaned[1] + cleaned[1], 16);\n b = parseInt(cleaned[2] + cleaned[2], 16);\n } else if (cleaned.length === 6) {\n r = parseInt(cleaned.substring(0, 2), 16);\n g = parseInt(cleaned.substring(2, 4), 16);\n b = parseInt(cleaned.substring(4, 6), 16);\n } else {\n return null;\n }\n\n if (isNaN(r) || isNaN(g) || isNaN(b)) return null;\n\n return `${r}, ${g}, ${b}`;\n}\n\n// ---------------------------------------------------------------------------\n// Default theme values\n// ---------------------------------------------------------------------------\n\nexport const DEFAULT_BRAND_CONFIG: BrandConfig = {\n primary: '#6366f1',\n secondary: '#8b5cf6',\n background: '#ffffff',\n backgroundElevated: '#f8fafc',\n surface: '#f1f5f9',\n surfaceHover: '#e2e8f0',\n surfaceBorder: '#e2e8f0',\n textPrimary: '#0f172a',\n textSecondary: '#475569',\n textTertiary: '#94a3b8',\n fontHeading: \"'Inter', system-ui, -apple-system, sans-serif\",\n fontBody: \"'Inter', system-ui, -apple-system, sans-serif\",\n radius: {\n sm: '0.375rem',\n md: '0.5rem',\n lg: '0.75rem',\n },\n};\n\nexport const DEFAULT_DARK_MODE: Partial<BrandConfig> = {\n background: '#1a1a1f',\n backgroundElevated: '#222228',\n surface: '#2a2a30',\n surfaceHover: '#333338',\n surfaceBorder: '#334155',\n textPrimary: '#f8fafc',\n textSecondary: '#cbd5e1',\n textTertiary: '#64748b',\n};\n\n// ---------------------------------------------------------------------------\n// CSS variable generation\n// ---------------------------------------------------------------------------\n\nfunction buildVariables(config: BrandConfig): string {\n const primaryRgb = hexToRgb(config.primary) || '99, 102, 241';\n const secondaryRgb = hexToRgb(config.secondary) || '139, 92, 246';\n\n const radius = config.radius || DEFAULT_BRAND_CONFIG.radius!;\n\n return [\n // Primary tokens (--sk-*)\n `--sk-primary: ${config.primary};`,\n `--sk-primary-rgb: ${primaryRgb};`,\n `--sk-secondary: ${config.secondary};`,\n `--sk-secondary-rgb: ${secondaryRgb};`,\n `--sk-bg: ${config.background};`,\n `--sk-bg-elevated: ${config.backgroundElevated};`,\n `--sk-surface: ${config.surface};`,\n `--sk-surface-hover: ${config.surfaceHover};`,\n `--sk-border: ${config.surfaceBorder};`,\n `--sk-text-primary: ${config.textPrimary};`,\n `--sk-text-secondary: ${config.textSecondary};`,\n `--sk-text-tertiary: ${config.textTertiary};`,\n `--sk-radius-sm: ${radius.sm};`,\n `--sk-radius: ${radius.md};`,\n `--sk-radius-lg: ${radius.lg};`,\n `--sk-font-heading: ${config.fontHeading};`,\n `--sk-font: ${config.fontBody};`,\n ].join('\\n ');\n}\n\nfunction buildDarkVariables(base: BrandConfig): string {\n const dark = {\n ...DEFAULT_DARK_MODE,\n ...base.darkMode,\n // Primary/secondary carry over unless explicitly overridden\n primary: base.darkMode?.primary || base.primary,\n secondary: base.darkMode?.secondary || base.secondary,\n };\n\n const primaryRgb = hexToRgb(dark.primary!) || hexToRgb(base.primary) || '99, 102, 241';\n const secondaryRgb = hexToRgb(dark.secondary!) || hexToRgb(base.secondary) || '139, 92, 246';\n\n return [\n // Primary tokens (--sk-*)\n `--sk-primary: ${dark.primary};`,\n `--sk-primary-rgb: ${primaryRgb};`,\n `--sk-secondary: ${dark.secondary};`,\n `--sk-secondary-rgb: ${secondaryRgb};`,\n `--sk-bg: ${dark.background || base.background || DEFAULT_DARK_MODE.background};`,\n `--sk-bg-elevated: ${dark.backgroundElevated || base.backgroundElevated || DEFAULT_DARK_MODE.backgroundElevated};`,\n `--sk-surface: ${dark.surface || base.surface || DEFAULT_DARK_MODE.surface};`,\n `--sk-surface-hover: ${dark.surfaceHover || base.surfaceHover || DEFAULT_DARK_MODE.surfaceHover};`,\n `--sk-border: ${dark.surfaceBorder || base.surfaceBorder || DEFAULT_DARK_MODE.surfaceBorder};`,\n `--sk-text-primary: ${dark.textPrimary || base.textPrimary || DEFAULT_DARK_MODE.textPrimary};`,\n `--sk-text-secondary: ${dark.textSecondary || base.textSecondary || DEFAULT_DARK_MODE.textSecondary};`,\n `--sk-text-tertiary: ${dark.textTertiary || base.textTertiary || DEFAULT_DARK_MODE.textTertiary};`,\n ].join('\\n ');\n}\n\n/**\n * Generates a complete CSS string with all --sk-* custom properties.\n *\n * Produces:\n * - `:root` / `[data-theme=\"light\"]` block with light mode values\n * - `[data-theme=\"dark\"]` block with dark mode values\n * - `@media (prefers-color-scheme: dark)` fallback for auto detection\n *\n * Usage: inject the returned string into a `<style>` tag.\n */\nexport function generateBrandCSS(config: BrandConfig): string {\n const merged: BrandConfig = {\n ...DEFAULT_BRAND_CONFIG,\n ...config,\n radius: {\n ...DEFAULT_BRAND_CONFIG.radius!,\n ...config.radius,\n },\n };\n\n const lightVars = buildVariables(merged);\n const darkVars = buildDarkVariables(merged);\n\n return `\n:root, [data-theme=\"light\"] {\n ${lightVars}\n}\n\n[data-theme=\"dark\"] {\n ${darkVars}\n}\n\n@media (prefers-color-scheme: dark) {\n :root:not([data-theme=\"light\"]) {\n ${darkVars}\n }\n}\n`.trim();\n}\n\n/**\n * Merges a partial brand config with defaults, returning a complete BrandConfig.\n */\nexport function mergeBrandConfig(partial: Partial<BrandConfig>): BrandConfig {\n return {\n ...DEFAULT_BRAND_CONFIG,\n ...partial,\n radius: {\n ...DEFAULT_BRAND_CONFIG.radius!,\n ...partial.radius,\n },\n };\n}\n","/**\n * @sonordev/agency-site-kit — AgencySiteKitLayout\n *\n * Server component that wraps children with brand CSS variables.\n * Fetches the brand configuration from the Portal API and injects\n * a <style> tag with all --sk-* custom properties (plus --agency-* aliases for backwards compatibility).\n *\n * Usage in app/layout.tsx:\n *\n * ```tsx\n * import { AgencySiteKitLayout } from '@sonordev/agency-site-kit';\n *\n * export default function RootLayout({ children }) {\n * return (\n * <html lang=\"en\">\n * <body>\n * <AgencySiteKitLayout>\n * {children}\n * </AgencySiteKitLayout>\n * </body>\n * </html>\n * );\n * }\n * ```\n */\n\nimport React from 'react';\nimport {\n resolveApiOrigin,\n resolveClientCredential,\n resolveServerApiConfig,\n} from '@sonordev/site-kit/server';\nimport { SiteKitCredential } from '@sonordev/site-kit/client';\nimport { apiGet } from '../shared/api';\nimport { generateBrandCSS, mergeBrandConfig } from '../shared/brand';\nimport type { BrandConfig, PortfolioConfigResponse } from '../types';\n\nexport interface AgencySiteKitLayoutProps {\n children: React.ReactNode;\n /** Override the API key (defaults to process.env.SONOR_API_KEY) */\n apiKey?: string;\n /** Override the API URL (defaults to process.env.SONOR_API_URL or https://api.sonor.io) */\n apiUrl?: string;\n /** Override brand config instead of fetching from API */\n brandConfig?: Partial<BrandConfig>;\n /** Force a specific theme: \"light\" | \"dark\". Omit for system preference. */\n theme?: 'light' | 'dark';\n /** Additional class name for the wrapper div */\n className?: string;\n /**\n * When true, screen-reader-only link to `/llms.txt` after children. Default false.\n * Prefer `buildAiDiscoveryHeaders` from `@sonordev/site-kit/llms` in root layout.\n */\n showLlmsTxtFooterLink?: boolean;\n /**\n * Mint a short-lived client token from `SONOR_API_KEY` and publish it for\n * client-side site-kit modules (Analytics, Forms, Engage, BookingWidget).\n * Default true.\n *\n * Set false ONLY when the app already mounts core `SiteKitLayout`, which\n * publishes its own credential — two publishers would race. Turning it off\n * without another publisher leaves client modules unauthenticated.\n */\n credential?: boolean;\n}\n\n/**\n * Fetches brand configuration from the Portal API.\n * Returns a merged BrandConfig with defaults applied.\n * Falls back to defaults if the API call fails.\n */\nasync function fetchBrandConfig(\n apiKey?: string,\n apiUrl?: string,\n): Promise<BrandConfig> {\n const response = await apiGet<PortfolioConfigResponse>(\n '/api/public/portfolio/config',\n 3600,\n apiKey,\n apiUrl,\n );\n\n if (response?.brand) {\n return mergeBrandConfig(response.brand);\n }\n\n return mergeBrandConfig({});\n}\n\n/**\n * AgencySiteKitLayout — Server component wrapper\n *\n * Fetches the agency's brand configuration and injects CSS custom properties\n * so all child components can use --sk-* variables for theming.\n *\n * This is an async server component (RSC). It makes one API call at render\n * time, cached via Next.js ISR (default 1 hour revalidation).\n */\nexport async function AgencySiteKitLayout({\n children,\n apiKey,\n apiUrl,\n brandConfig: brandConfigOverride,\n theme,\n className,\n showLlmsTxtFooterLink = false,\n credential = true,\n}: AgencySiteKitLayoutProps): Promise<React.ReactElement> {\n // Use override brand config if provided, otherwise fetch from API\n const brandConfig = brandConfigOverride\n ? mergeBrandConfig(brandConfigOverride)\n : await fetchBrandConfig(apiKey, apiUrl);\n\n const cssString = generateBrandCSS(brandConfig);\n\n const resolvedApiUrl =\n apiUrl ??\n process.env.SONOR_API_URL ??\n process.env.UPTRADE_API_URL ??\n 'https://api.sonor.io';\n // Origin-resolution shared with core SiteKitLayout (one source of truth).\n // The preconnect itself stays — upforge relies on AgencySiteKitLayout to emit\n // it (it doesn't mount core's SiteKitLayout).\n const preconnectHref = resolveApiOrigin(resolvedApiUrl);\n\n /**\n * Client credential. This layout does NOT mount core analytics, so agency\n * sites compose client modules (AnalyticsProvider, BookingWidget, forms)\n * themselves — and before this they had no minted token to hand them. Every\n * one of them hand-rolled the same workaround: read a `NEXT_PUBLIC_*` copy of\n * the project key from a client component, which inlines the long-lived\n * `sonor_` secret straight into the browser bundle.\n *\n * So mint here exactly as core `SiteKitLayout` does. `resolveClientCredential`\n * is server-only (the raw key never crosses the client boundary) and falls\n * back to the raw key only if minting is genuinely unreachable.\n */\n // resolveServerApiConfig is the single source of truth for override → env\n // key resolution; don't re-implement the fallback chain here.\n const rawKey = credential\n ? (resolveServerApiConfig({ apiKey, apiUrl })?.apiKey ?? '')\n : '';\n const clientCredential = rawKey\n ? await resolveClientCredential(rawKey, resolvedApiUrl)\n : '';\n\n return React.createElement(\n React.Fragment,\n null,\n React.createElement('link', {\n rel: 'preconnect',\n href: preconnectHref,\n crossOrigin: 'anonymous',\n }),\n React.createElement('link', {\n rel: 'dns-prefetch',\n href: preconnectHref,\n }),\n React.createElement('style', {\n dangerouslySetInnerHTML: { __html: cssString },\n 'data-agency-site-kit': 'brand',\n }),\n // Publish the minted token + __SITE_KIT_* globals. A childless sibling\n // rendering null — it never encloses page content, so it can't drag the\n // tree into a client boundary (CLAUDE.md's cardinal SSR rule).\n clientCredential\n ? React.createElement(SiteKitCredential, {\n key: 'sk-credential',\n apiKey: clientCredential,\n apiUrl: resolvedApiUrl,\n })\n : null,\n React.createElement(\n 'div',\n {\n 'data-theme': theme || undefined,\n 'data-agency-site-kit': 'root',\n className,\n style: {\n fontFamily: 'var(--sk-font)',\n color: 'var(--sk-text-primary)',\n backgroundColor: 'var(--sk-bg)',\n },\n },\n children,\n showLlmsTxtFooterLink\n ? React.createElement(\n 'footer',\n { className: 'sr-only', 'aria-hidden': true },\n React.createElement('a', { href: '/llms.txt' }, 'llms.txt'),\n )\n : null,\n ),\n );\n}\n"]}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var contract = require('@sonordev/site-kit/portfolio/contract');
|
|
4
|
+
|
|
5
|
+
// src/portfolio/curate-proof.ts
|
|
6
|
+
var LIGHTHOUSE_RE = /lighthouse/i;
|
|
7
|
+
function isLighthouseRestatement(item) {
|
|
8
|
+
if (!/score/i.test(item.title)) return false;
|
|
9
|
+
if (LIGHTHOUSE_RE.test(`${item.title} ${item.description}`)) return true;
|
|
10
|
+
return item.metric?.suffix === "/100";
|
|
11
|
+
}
|
|
12
|
+
function curateResultItems(items, hasPerformanceSection) {
|
|
13
|
+
if (!hasPerformanceSection) return items;
|
|
14
|
+
const curated = items.filter((item) => !isLighthouseRestatement(item));
|
|
15
|
+
return curated.length > 0 ? curated : items.slice(0, 1);
|
|
16
|
+
}
|
|
17
|
+
function gateMetricsDeltas(deltas) {
|
|
18
|
+
if (!deltas?.length) return [];
|
|
19
|
+
return deltas.filter((d) => {
|
|
20
|
+
if (d.direction !== "up") return false;
|
|
21
|
+
if (Math.abs(d.deltaPercent) < 10) return false;
|
|
22
|
+
if (/bounce/i.test(d.metric) && d.current > 70) return false;
|
|
23
|
+
return true;
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
function curatePortfolioProof(item) {
|
|
27
|
+
const hasPerformanceSection = item.sections.some(
|
|
28
|
+
(s) => s.sectionType === "portfolioPerformance"
|
|
29
|
+
);
|
|
30
|
+
return {
|
|
31
|
+
...item,
|
|
32
|
+
sections: item.sections.map((section) => {
|
|
33
|
+
if (section.sectionType === "portfolioHero") {
|
|
34
|
+
const data = section.data;
|
|
35
|
+
return { ...section, data: { ...data, kpis: contract.curateHeroKpis(data.kpis ?? []) } };
|
|
36
|
+
}
|
|
37
|
+
if (section.sectionType === "portfolioResults") {
|
|
38
|
+
const data = section.data;
|
|
39
|
+
return {
|
|
40
|
+
...section,
|
|
41
|
+
data: { ...data, items: curateResultItems(data.items ?? [], hasPerformanceSection) }
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
return section;
|
|
45
|
+
})
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
Object.defineProperty(exports, "curateHeroKpis", {
|
|
50
|
+
enumerable: true,
|
|
51
|
+
get: function () { return contract.curateHeroKpis; }
|
|
52
|
+
});
|
|
53
|
+
exports.curatePortfolioProof = curatePortfolioProof;
|
|
54
|
+
exports.curateResultItems = curateResultItems;
|
|
55
|
+
exports.gateMetricsDeltas = gateMetricsDeltas;
|
|
56
|
+
//# sourceMappingURL=chunk-X76U7VLF.cjs.map
|
|
57
|
+
//# sourceMappingURL=chunk-X76U7VLF.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/portfolio/curate-proof.ts"],"names":["curateHeroKpis"],"mappings":";;;;;AAwCA,IAAM,aAAA,GAAgB,aAAA;AASf,SAAS,wBAAwB,IAAA,EAAoC;AAC1E,EAAA,IAAI,CAAC,QAAA,CAAS,IAAA,CAAK,IAAA,CAAK,KAAK,GAAG,OAAO,KAAA;AACvC,EAAA,IAAI,aAAA,CAAc,IAAA,CAAK,CAAA,EAAG,IAAA,CAAK,KAAK,IAAI,IAAA,CAAK,WAAW,CAAA,CAAE,CAAA,EAAG,OAAO,IAAA;AACpE,EAAA,OAAO,IAAA,CAAK,QAAQ,MAAA,KAAW,MAAA;AACjC;AAMO,SAAS,iBAAA,CACd,OACA,qBAAA,EACuB;AACvB,EAAA,IAAI,CAAC,uBAAuB,OAAO,KAAA;AACnC,EAAA,MAAM,OAAA,GAAU,MAAM,MAAA,CAAO,CAAC,SAAS,CAAC,uBAAA,CAAwB,IAAI,CAAC,CAAA;AAGrE,EAAA,OAAO,QAAQ,MAAA,GAAS,CAAA,GAAI,UAAU,KAAA,CAAM,KAAA,CAAM,GAAG,CAAC,CAAA;AACxD;AAYO,SAAS,kBAAkB,MAAA,EAA2D;AAC3F,EAAA,IAAI,CAAC,MAAA,EAAQ,MAAA,EAAQ,OAAO,EAAC;AAC7B,EAAA,OAAO,MAAA,CAAO,MAAA,CAAO,CAAC,CAAA,KAAM;AAC1B,IAAA,IAAI,CAAA,CAAE,SAAA,KAAc,IAAA,EAAM,OAAO,KAAA;AACjC,IAAA,IAAI,KAAK,GAAA,CAAI,CAAA,CAAE,YAAY,CAAA,GAAI,IAAI,OAAO,KAAA;AAC1C,IAAA,IAAI,SAAA,CAAU,KAAK,CAAA,CAAE,MAAM,KAAK,CAAA,CAAE,OAAA,GAAU,IAAI,OAAO,KAAA;AACvD,IAAA,OAAO,IAAA;AAAA,EACT,CAAC,CAAA;AACH;AAMO,SAAS,qBAAqB,IAAA,EAA4C;AAC/E,EAAA,MAAM,qBAAA,GAAwB,KAAK,QAAA,CAAS,IAAA;AAAA,IAC1C,CAAC,CAAA,KAAM,CAAA,CAAE,WAAA,KAAgB;AAAA,GAC3B;AAEA,EAAA,OAAO;AAAA,IACL,GAAG,IAAA;AAAA,IACH,QAAA,EAAU,IAAA,CAAK,QAAA,CAAS,GAAA,CAAI,CAAC,OAAA,KAAY;AACvC,MAAA,IAAI,OAAA,CAAQ,gBAAgB,eAAA,EAAiB;AAC3C,QAAA,MAAM,OAAO,OAAA,CAAQ,IAAA;AACrB,QAAA,OAAO,EAAE,GAAG,OAAA,EAAS,IAAA,EAAM,EAAE,GAAG,IAAA,EAAM,IAAA,EAAMA,uBAAA,CAAe,IAAA,CAAK,IAAA,IAAQ,EAAE,GAAE,EAAE;AAAA,MAChF;AACA,MAAA,IAAI,OAAA,CAAQ,gBAAgB,kBAAA,EAAoB;AAC9C,QAAA,MAAM,OAAO,OAAA,CAAQ,IAAA;AACrB,QAAA,OAAO;AAAA,UACL,GAAG,OAAA;AAAA,UACH,IAAA,EAAM,EAAE,GAAG,IAAA,EAAM,KAAA,EAAO,iBAAA,CAAkB,IAAA,CAAK,KAAA,IAAS,EAAC,EAAG,qBAAqB,CAAA;AAAE,SACrF;AAAA,MACF;AACA,MAAA,OAAO,OAAA;AAAA,IACT,CAAC;AAAA,GACH;AACF","file":"chunk-X76U7VLF.cjs","sourcesContent":["/**\n * curate-proof — single source of truth for which proof points a public case\n * study is allowed to display, and in what shape.\n *\n * Two classes of problem this guards against (both shipped to production on\n * upforge.io before this existed):\n *\n * 1. Repetition: generated items arrive with four \"100/100 Lighthouse X\"\n * KPIs in the hero, the same four restated as Results cards, AND a full\n * Performance section with score gauges. One differentiator repeated\n * twelve times per page stops differentiating. The Performance section is\n * the one home for the Lighthouse breakdown; hero/results/cards collapse\n * to at most one summary tile.\n *\n * 2. Self-harming live metrics: MetricsDelta rows are computed from raw\n * analytics baselines with no editorial judgment — a bounce rate of\n * \"100 → 99.69\" rendered as a green win. Marketing surfaces only show a\n * delta when it's a genuinely flattering, meaningful story.\n *\n * Used by PortfolioPage (hero KPIs + results items), PortfolioCard (KPI\n * preview), and MetricsProvenance (delta gate). Change display policy here,\n * nowhere else.\n */\n\nimport type {\n MetricsDelta,\n PortfolioHeroData,\n PortfolioItemFull,\n PortfolioResultItem,\n PortfolioResultsData,\n} from '../types';\n// The Lighthouse-KPI collapse policy is the shared cross-repo contract\n// (@sonordev/site-kit/portfolio/contract) — the same source signal-api mirrors\n// for content-time collapse. Imported (not re-implemented) so the display side\n// and the content side can never drift; re-exported so this module stays the\n// portfolio's single display-policy entry point.\nimport { curateHeroKpis, isLighthouseKpi } from '@sonordev/site-kit/portfolio/contract';\n\nexport { curateHeroKpis, isLighthouseKpi };\n\nconst LIGHTHOUSE_RE = /lighthouse/i;\n\n/**\n * True when a Results card merely restates a Lighthouse category score\n * (e.g. \"Perfect performance score … measured Lighthouse score of 100/100\").\n * Business outcomes (\"Complete 14-page rebuild\", \"682% session growth\")\n * never match: the title must be about a score AND the card must reference\n * Lighthouse or carry a x/100 metric.\n */\nexport function isLighthouseRestatement(item: PortfolioResultItem): boolean {\n if (!/score/i.test(item.title)) return false;\n if (LIGHTHOUSE_RE.test(`${item.title} ${item.description}`)) return true;\n return item.metric?.suffix === '/100';\n}\n\n/**\n * Drop Results cards that restate Lighthouse scores when the case study also\n * renders a Performance section (the score breakdown's single home).\n */\nexport function curateResultItems(\n items: PortfolioResultItem[],\n hasPerformanceSection: boolean,\n): PortfolioResultItem[] {\n if (!hasPerformanceSection) return items;\n const curated = items.filter((item) => !isLighthouseRestatement(item));\n // Never curate a Results section into emptiness — if every card was a\n // Lighthouse restatement, keep the strongest single card instead.\n return curated.length > 0 ? curated : items.slice(0, 1);\n}\n\n/**\n * Editorial gate for live metric deltas (\"Verified from live analytics\").\n * `direction` comes from the platform with 'up' meaning IMPROVED (lower-is-\n * better metrics are inverted server-side). A public case study only shows a\n * delta when it's a win worth telling:\n * - improved ('up'), never a regression or flat noise\n * - moved by a double-digit relative margin (small drift isn't a story)\n * - bounce-rate-style metrics must also be respectable in absolute terms —\n * \"100 → 99.69\" is technically an improvement and still self-harm\n */\nexport function gateMetricsDeltas(deltas: MetricsDelta[] | undefined | null): MetricsDelta[] {\n if (!deltas?.length) return [];\n return deltas.filter((d) => {\n if (d.direction !== 'up') return false;\n if (Math.abs(d.deltaPercent) < 10) return false;\n if (/bounce/i.test(d.metric) && d.current > 70) return false;\n return true;\n });\n}\n\n/**\n * Apply the full display policy to a portfolio item before rendering.\n * Pure — returns a new item; the fetched payload is never mutated.\n */\nexport function curatePortfolioProof(item: PortfolioItemFull): PortfolioItemFull {\n const hasPerformanceSection = item.sections.some(\n (s) => s.sectionType === 'portfolioPerformance',\n );\n\n return {\n ...item,\n sections: item.sections.map((section) => {\n if (section.sectionType === 'portfolioHero') {\n const data = section.data as PortfolioHeroData;\n return { ...section, data: { ...data, kpis: curateHeroKpis(data.kpis ?? []) } };\n }\n if (section.sectionType === 'portfolioResults') {\n const data = section.data as PortfolioResultsData;\n return {\n ...section,\n data: { ...data, items: curateResultItems(data.items ?? [], hasPerformanceSection) },\n };\n }\n return section;\n }),\n };\n}\n"]}
|
|
@@ -1,63 +1,6 @@
|
|
|
1
|
+
import { gateMetricsDeltas } from './chunk-FM25LB67.js';
|
|
1
2
|
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
2
3
|
|
|
3
|
-
// src/portfolio/curate-proof.ts
|
|
4
|
-
var LIGHTHOUSE_RE = /lighthouse/i;
|
|
5
|
-
function isLighthouseKpi(kpi) {
|
|
6
|
-
return LIGHTHOUSE_RE.test(kpi.label);
|
|
7
|
-
}
|
|
8
|
-
function isLighthouseRestatement(item) {
|
|
9
|
-
if (!/score/i.test(item.title)) return false;
|
|
10
|
-
if (LIGHTHOUSE_RE.test(`${item.title} ${item.description}`)) return true;
|
|
11
|
-
return item.metric?.suffix === "/100";
|
|
12
|
-
}
|
|
13
|
-
function curateHeroKpis(kpis) {
|
|
14
|
-
const business = kpis.filter((k) => !isLighthouseKpi(k));
|
|
15
|
-
const lighthouse = kpis.filter(isLighthouseKpi);
|
|
16
|
-
if (lighthouse.length <= 1) return kpis;
|
|
17
|
-
const allPerfect = lighthouse.length >= 4 && lighthouse.every((k) => k.value === 100);
|
|
18
|
-
const summary = allPerfect ? {
|
|
19
|
-
...lighthouse[0],
|
|
20
|
-
label: "Lighthouse, all 4 categories",
|
|
21
|
-
description: "Perfect scores across performance, SEO, accessibility, and best practices."
|
|
22
|
-
} : lighthouse.find((k) => /performance/i.test(k.label)) ?? lighthouse[0];
|
|
23
|
-
return [...business, summary];
|
|
24
|
-
}
|
|
25
|
-
function curateResultItems(items, hasPerformanceSection) {
|
|
26
|
-
if (!hasPerformanceSection) return items;
|
|
27
|
-
const curated = items.filter((item) => !isLighthouseRestatement(item));
|
|
28
|
-
return curated.length > 0 ? curated : items.slice(0, 1);
|
|
29
|
-
}
|
|
30
|
-
function gateMetricsDeltas(deltas) {
|
|
31
|
-
if (!deltas?.length) return [];
|
|
32
|
-
return deltas.filter((d) => {
|
|
33
|
-
if (d.direction !== "up") return false;
|
|
34
|
-
if (Math.abs(d.deltaPercent) < 10) return false;
|
|
35
|
-
if (/bounce/i.test(d.metric) && d.current > 70) return false;
|
|
36
|
-
return true;
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
function curatePortfolioProof(item) {
|
|
40
|
-
const hasPerformanceSection = item.sections.some(
|
|
41
|
-
(s) => s.sectionType === "portfolioPerformance"
|
|
42
|
-
);
|
|
43
|
-
return {
|
|
44
|
-
...item,
|
|
45
|
-
sections: item.sections.map((section) => {
|
|
46
|
-
if (section.sectionType === "portfolioHero") {
|
|
47
|
-
const data = section.data;
|
|
48
|
-
return { ...section, data: { ...data, kpis: curateHeroKpis(data.kpis ?? []) } };
|
|
49
|
-
}
|
|
50
|
-
if (section.sectionType === "portfolioResults") {
|
|
51
|
-
const data = section.data;
|
|
52
|
-
return {
|
|
53
|
-
...section,
|
|
54
|
-
data: { ...data, items: curateResultItems(data.items ?? [], hasPerformanceSection) }
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
return section;
|
|
58
|
-
})
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
4
|
var DIRECTION_COLOR = {
|
|
62
5
|
up: "#10b981",
|
|
63
6
|
down: "#ef4444",
|
|
@@ -160,6 +103,6 @@ function MetricsProvenance({ deltas, refreshedAt }) {
|
|
|
160
103
|
);
|
|
161
104
|
}
|
|
162
105
|
|
|
163
|
-
export { MetricsProvenance
|
|
164
|
-
//# sourceMappingURL=chunk-
|
|
165
|
-
//# sourceMappingURL=chunk-
|
|
106
|
+
export { MetricsProvenance };
|
|
107
|
+
//# sourceMappingURL=chunk-X7UCJ45A.js.map
|
|
108
|
+
//# sourceMappingURL=chunk-X7UCJ45A.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/portfolio/components/MetricsProvenance.tsx"],"names":[],"mappings":"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","file":"chunk-X7UCJ45A.js","sourcesContent":["/**\n * MetricsProvenance — \"these numbers are real\" strip for case studies.\n *\n * Server component, zero JS. Renders the live metric deltas the platform\n * computes from GSC/analytics baselines (MetricsDelta[]) plus the refresh\n * timestamp — the trust signal a static agency portfolio can't fake.\n *\n * `direction` semantics come from the API: 'up' ALWAYS means \"improved\"\n * (lower-is-better metrics like Avg Position/LCP are inverted server-side\n * in metrics-baseline.service), so 'up' renders green. The ARROW glyph\n * follows the raw numbers instead — an improved bounce rate shows a green ▼,\n * never a green ▲ next to a value that visibly went down.\n *\n * Deltas pass through gateMetricsDeltas (curate-proof.ts) before display:\n * raw platform deltas carry no editorial judgment, and this strip only\n * exists to flatter — see that module for the policy.\n */\n\nimport React from 'react';\nimport type { MetricsDelta } from '../../types';\nimport { gateMetricsDeltas } from '../curate-proof';\n\ninterface MetricsProvenanceProps {\n deltas: MetricsDelta[];\n /** ISO timestamp of the last live-metrics refresh. */\n refreshedAt?: string | null;\n}\n\nconst DIRECTION_COLOR: Record<MetricsDelta['direction'], string> = {\n up: '#10b981',\n down: '#ef4444',\n flat: 'var(--sk-text-tertiary, #71717a)',\n};\n\nfunction formatValue(v: number): string {\n return Number.isInteger(v)\n ? v.toLocaleString('en-US')\n : v.toLocaleString('en-US', { maximumFractionDigits: 2 });\n}\n\nfunction formatDate(iso: string): string {\n try {\n return new Intl.DateTimeFormat('en-US', {\n year: 'numeric',\n month: 'long',\n day: 'numeric',\n }).format(new Date(iso));\n } catch {\n return iso;\n }\n}\n\nexport default function MetricsProvenance({ deltas, refreshedAt }: MetricsProvenanceProps) {\n const shown = gateMetricsDeltas(deltas);\n if (!shown.length) return null;\n\n return (\n <div\n className=\"mt-10 rounded-2xl overflow-hidden\"\n style={{\n background: 'var(--sk-surface, rgba(255,255,255,0.05))',\n border: '1px solid var(--sk-border, rgba(255,255,255,0.1))',\n }}\n >\n <div className=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3\">\n {shown.map((d, i) => {\n const color = DIRECTION_COLOR[d.direction] ?? DIRECTION_COLOR.flat;\n // Arrow tracks the raw numbers; color tracks improved/worsened.\n const arrow = d.current === d.baseline ? '—' : d.current > d.baseline ? '▲' : '▼';\n return (\n <div\n key={`${d.metric}-${i}`}\n className=\"flex flex-col gap-1 p-5\"\n style={{ borderBottom: '1px solid var(--sk-border, rgba(255,255,255,0.06))' }}\n >\n <span\n className=\"text-xs font-semibold uppercase tracking-wider\"\n style={{ color: 'var(--sk-text-tertiary, #71717a)' }}\n >\n {d.metric}\n </span>\n <span className=\"flex items-baseline gap-2\">\n <span\n className=\"text-lg font-bold\"\n style={{ color: 'var(--sk-text-primary, #ffffff)' }}\n >\n {formatValue(d.baseline)} → {formatValue(d.current)}\n </span>\n <span className=\"text-sm font-semibold\" style={{ color }}>\n {arrow} {Math.abs(d.deltaPercent)}%\n </span>\n </span>\n <span className=\"text-xs\" style={{ color: 'var(--sk-text-tertiary, #71717a)' }}>\n {d.timespan}\n </span>\n </div>\n );\n })}\n </div>\n\n <div\n className=\"flex items-center gap-2 px-5 py-3\"\n style={{\n background: 'color-mix(in srgb, #10b981 6%, transparent)',\n color: 'var(--sk-text-secondary, #a1a1aa)',\n }}\n >\n <span\n className=\"inline-block w-2 h-2 rounded-full shrink-0\"\n style={{ background: '#10b981' }}\n />\n <span className=\"text-xs font-medium\">\n Verified from live analytics\n {refreshedAt ? ` · Refreshed ${formatDate(refreshedAt)}` : ''}\n </span>\n </div>\n </div>\n );\n}\n"]}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { P as PortfolioItemFull, j as PortfolioResultItem, M as MetricsDelta } from './types-He5A9okM.cjs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* curate-proof — single source of truth for which proof points a public case
|
|
5
|
+
* study is allowed to display, and in what shape.
|
|
6
|
+
*
|
|
7
|
+
* Two classes of problem this guards against (both shipped to production on
|
|
8
|
+
* upforge.io before this existed):
|
|
9
|
+
*
|
|
10
|
+
* 1. Repetition: generated items arrive with four "100/100 Lighthouse X"
|
|
11
|
+
* KPIs in the hero, the same four restated as Results cards, AND a full
|
|
12
|
+
* Performance section with score gauges. One differentiator repeated
|
|
13
|
+
* twelve times per page stops differentiating. The Performance section is
|
|
14
|
+
* the one home for the Lighthouse breakdown; hero/results/cards collapse
|
|
15
|
+
* to at most one summary tile.
|
|
16
|
+
*
|
|
17
|
+
* 2. Self-harming live metrics: MetricsDelta rows are computed from raw
|
|
18
|
+
* analytics baselines with no editorial judgment — a bounce rate of
|
|
19
|
+
* "100 → 99.69" rendered as a green win. Marketing surfaces only show a
|
|
20
|
+
* delta when it's a genuinely flattering, meaningful story.
|
|
21
|
+
*
|
|
22
|
+
* Used by PortfolioPage (hero KPIs + results items), PortfolioCard (KPI
|
|
23
|
+
* preview), and MetricsProvenance (delta gate). Change display policy here,
|
|
24
|
+
* nowhere else.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Drop Results cards that restate Lighthouse scores when the case study also
|
|
29
|
+
* renders a Performance section (the score breakdown's single home).
|
|
30
|
+
*/
|
|
31
|
+
declare function curateResultItems(items: PortfolioResultItem[], hasPerformanceSection: boolean): PortfolioResultItem[];
|
|
32
|
+
/**
|
|
33
|
+
* Editorial gate for live metric deltas ("Verified from live analytics").
|
|
34
|
+
* `direction` comes from the platform with 'up' meaning IMPROVED (lower-is-
|
|
35
|
+
* better metrics are inverted server-side). A public case study only shows a
|
|
36
|
+
* delta when it's a win worth telling:
|
|
37
|
+
* - improved ('up'), never a regression or flat noise
|
|
38
|
+
* - moved by a double-digit relative margin (small drift isn't a story)
|
|
39
|
+
* - bounce-rate-style metrics must also be respectable in absolute terms —
|
|
40
|
+
* "100 → 99.69" is technically an improvement and still self-harm
|
|
41
|
+
*/
|
|
42
|
+
declare function gateMetricsDeltas(deltas: MetricsDelta[] | undefined | null): MetricsDelta[];
|
|
43
|
+
/**
|
|
44
|
+
* Apply the full display policy to a portfolio item before rendering.
|
|
45
|
+
* Pure — returns a new item; the fetched payload is never mutated.
|
|
46
|
+
*/
|
|
47
|
+
declare function curatePortfolioProof(item: PortfolioItemFull): PortfolioItemFull;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Presentation formatters shared by portfolio components and consuming apps.
|
|
51
|
+
*/
|
|
52
|
+
/**
|
|
53
|
+
* Human label for a category slug. Single source of truth — HeroSection,
|
|
54
|
+
* PortfolioCard, and app-side filter chips all render categories through
|
|
55
|
+
* this. Underscores become spaces; hyphens are PRESERVED so compound slugs
|
|
56
|
+
* keep their canonical form ("e-commerce" → "E-Commerce", never "E Commerce"),
|
|
57
|
+
* and every word start (including after a hyphen) is capitalized.
|
|
58
|
+
*/
|
|
59
|
+
declare function formatCategoryLabel(slug: string | undefined | null): string;
|
|
60
|
+
/**
|
|
61
|
+
* Normalize a service tag's casing for display: the first character of each
|
|
62
|
+
* word is uppercased, the rest left alone (so "Next.js development" becomes
|
|
63
|
+
* "Next.js Development" and "Technical SEO implementation" keeps "SEO").
|
|
64
|
+
* Fixes per-item casing drift in stored tags without touching the data.
|
|
65
|
+
*/
|
|
66
|
+
declare function formatServiceTag(tag: string | undefined | null): string;
|
|
67
|
+
|
|
68
|
+
export { curateResultItems as a, formatServiceTag as b, curatePortfolioProof as c, formatCategoryLabel as f, gateMetricsDeltas as g };
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { P as PortfolioItemFull, j as PortfolioResultItem, M as MetricsDelta } from './types-He5A9okM.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* curate-proof — single source of truth for which proof points a public case
|
|
5
|
+
* study is allowed to display, and in what shape.
|
|
6
|
+
*
|
|
7
|
+
* Two classes of problem this guards against (both shipped to production on
|
|
8
|
+
* upforge.io before this existed):
|
|
9
|
+
*
|
|
10
|
+
* 1. Repetition: generated items arrive with four "100/100 Lighthouse X"
|
|
11
|
+
* KPIs in the hero, the same four restated as Results cards, AND a full
|
|
12
|
+
* Performance section with score gauges. One differentiator repeated
|
|
13
|
+
* twelve times per page stops differentiating. The Performance section is
|
|
14
|
+
* the one home for the Lighthouse breakdown; hero/results/cards collapse
|
|
15
|
+
* to at most one summary tile.
|
|
16
|
+
*
|
|
17
|
+
* 2. Self-harming live metrics: MetricsDelta rows are computed from raw
|
|
18
|
+
* analytics baselines with no editorial judgment — a bounce rate of
|
|
19
|
+
* "100 → 99.69" rendered as a green win. Marketing surfaces only show a
|
|
20
|
+
* delta when it's a genuinely flattering, meaningful story.
|
|
21
|
+
*
|
|
22
|
+
* Used by PortfolioPage (hero KPIs + results items), PortfolioCard (KPI
|
|
23
|
+
* preview), and MetricsProvenance (delta gate). Change display policy here,
|
|
24
|
+
* nowhere else.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Drop Results cards that restate Lighthouse scores when the case study also
|
|
29
|
+
* renders a Performance section (the score breakdown's single home).
|
|
30
|
+
*/
|
|
31
|
+
declare function curateResultItems(items: PortfolioResultItem[], hasPerformanceSection: boolean): PortfolioResultItem[];
|
|
32
|
+
/**
|
|
33
|
+
* Editorial gate for live metric deltas ("Verified from live analytics").
|
|
34
|
+
* `direction` comes from the platform with 'up' meaning IMPROVED (lower-is-
|
|
35
|
+
* better metrics are inverted server-side). A public case study only shows a
|
|
36
|
+
* delta when it's a win worth telling:
|
|
37
|
+
* - improved ('up'), never a regression or flat noise
|
|
38
|
+
* - moved by a double-digit relative margin (small drift isn't a story)
|
|
39
|
+
* - bounce-rate-style metrics must also be respectable in absolute terms —
|
|
40
|
+
* "100 → 99.69" is technically an improvement and still self-harm
|
|
41
|
+
*/
|
|
42
|
+
declare function gateMetricsDeltas(deltas: MetricsDelta[] | undefined | null): MetricsDelta[];
|
|
43
|
+
/**
|
|
44
|
+
* Apply the full display policy to a portfolio item before rendering.
|
|
45
|
+
* Pure — returns a new item; the fetched payload is never mutated.
|
|
46
|
+
*/
|
|
47
|
+
declare function curatePortfolioProof(item: PortfolioItemFull): PortfolioItemFull;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Presentation formatters shared by portfolio components and consuming apps.
|
|
51
|
+
*/
|
|
52
|
+
/**
|
|
53
|
+
* Human label for a category slug. Single source of truth — HeroSection,
|
|
54
|
+
* PortfolioCard, and app-side filter chips all render categories through
|
|
55
|
+
* this. Underscores become spaces; hyphens are PRESERVED so compound slugs
|
|
56
|
+
* keep their canonical form ("e-commerce" → "E-Commerce", never "E Commerce"),
|
|
57
|
+
* and every word start (including after a hyphen) is capitalized.
|
|
58
|
+
*/
|
|
59
|
+
declare function formatCategoryLabel(slug: string | undefined | null): string;
|
|
60
|
+
/**
|
|
61
|
+
* Normalize a service tag's casing for display: the first character of each
|
|
62
|
+
* word is uppercased, the rest left alone (so "Next.js development" becomes
|
|
63
|
+
* "Next.js Development" and "Technical SEO implementation" keeps "SEO").
|
|
64
|
+
* Fixes per-item casing drift in stored tags without touching the data.
|
|
65
|
+
*/
|
|
66
|
+
declare function formatServiceTag(tag: string | undefined | null): string;
|
|
67
|
+
|
|
68
|
+
export { curateResultItems as a, formatServiceTag as b, curatePortfolioProof as c, formatCategoryLabel as f, gateMetricsDeltas as g };
|