@sonordev/agency-site-kit 0.1.3 → 0.3.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.
Files changed (55) hide show
  1. package/dist/{SiteArchitectureSection-EE6VQSXM.cjs → SiteArchitectureSection-J7BPBERH.cjs} +2 -2
  2. package/dist/SiteArchitectureSection-J7BPBERH.cjs.map +1 -0
  3. package/dist/{SiteArchitectureSection-PBBRTARV.js → SiteArchitectureSection-TE22OAWV.js} +2 -2
  4. package/dist/SiteArchitectureSection-TE22OAWV.js.map +1 -0
  5. package/dist/cache-CE_ujDK4.d.cts +15 -0
  6. package/dist/cache-CE_ujDK4.d.ts +15 -0
  7. package/dist/{chunk-RLVW7WEK.js → chunk-2UNKXWQM.js} +22 -4
  8. package/dist/chunk-2UNKXWQM.js.map +1 -0
  9. package/dist/chunk-EE75WRCM.cjs +56 -0
  10. package/dist/chunk-EE75WRCM.cjs.map +1 -0
  11. package/dist/chunk-FCKDJ4MJ.cjs +8 -0
  12. package/dist/chunk-FCKDJ4MJ.cjs.map +1 -0
  13. package/dist/{chunk-JTI3F3QY.cjs → chunk-FDFVLYAM.cjs} +22 -4
  14. package/dist/chunk-FDFVLYAM.cjs.map +1 -0
  15. package/dist/chunk-N7TVKUYQ.js +6 -0
  16. package/dist/chunk-N7TVKUYQ.js.map +1 -0
  17. package/dist/{chunk-OMOF4VR5.js → chunk-RXBK2EOE.js} +13 -13
  18. package/dist/chunk-RXBK2EOE.js.map +1 -0
  19. package/dist/chunk-TVFXN6W6.js +52 -0
  20. package/dist/chunk-TVFXN6W6.js.map +1 -0
  21. package/dist/{chunk-XM2QD3AK.cjs → chunk-WNR4KNG4.cjs} +14 -14
  22. package/dist/chunk-WNR4KNG4.cjs.map +1 -0
  23. package/dist/index.cjs +14 -14
  24. package/dist/index.d.cts +34 -13
  25. package/dist/index.d.ts +34 -13
  26. package/dist/index.js +3 -3
  27. package/dist/layout/index.cjs +3 -3
  28. package/dist/layout/index.d.cts +6 -1
  29. package/dist/layout/index.d.ts +6 -1
  30. package/dist/layout/index.js +2 -2
  31. package/dist/portfolio/index.cjs +3 -3
  32. package/dist/portfolio/index.js +1 -1
  33. package/dist/portfolio/server.cjs +13 -5
  34. package/dist/portfolio/server.cjs.map +1 -1
  35. package/dist/portfolio/server.d.cts +1 -0
  36. package/dist/portfolio/server.d.ts +1 -0
  37. package/dist/portfolio/server.js +10 -5
  38. package/dist/portfolio/server.js.map +1 -1
  39. package/dist/revalidate.cjs +98 -0
  40. package/dist/revalidate.cjs.map +1 -0
  41. package/dist/revalidate.d.cts +53 -0
  42. package/dist/revalidate.d.ts +53 -0
  43. package/dist/revalidate.js +93 -0
  44. package/dist/revalidate.js.map +1 -0
  45. package/package.json +9 -1
  46. package/dist/SiteArchitectureSection-EE6VQSXM.cjs.map +0 -1
  47. package/dist/SiteArchitectureSection-PBBRTARV.js.map +0 -1
  48. package/dist/chunk-A4I4IK7V.js +0 -69
  49. package/dist/chunk-A4I4IK7V.js.map +0 -1
  50. package/dist/chunk-JTI3F3QY.cjs.map +0 -1
  51. package/dist/chunk-OMOF4VR5.js.map +0 -1
  52. package/dist/chunk-QBLWP25X.cjs +0 -73
  53. package/dist/chunk-QBLWP25X.cjs.map +0 -1
  54. package/dist/chunk-RLVW7WEK.js.map +0 -1
  55. package/dist/chunk-XM2QD3AK.cjs.map +0 -1
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { B as BrandConfig } from './types-DL4t_Cfa.js';
2
2
  export { M as MetricSource, e as MetricsDelta, f as MetricsSnapshot, g as PORTFOLIO_SECTION_TYPES, h as PortfolioAnnotation, i as PortfolioBeforeAfterData, j as PortfolioCTAData, k as PortfolioChallengeItem, l as PortfolioChallengesData, m as PortfolioConfigResponse, n as PortfolioConversionFunnelData, o as PortfolioDetailsData, p as PortfolioFeatureSpotlightData, q as PortfolioFunnelStage, r as PortfolioGalleryData, s as PortfolioGalleryImage, t as PortfolioHeroData, u as PortfolioItem, P as PortfolioItemFull, v as PortfolioKPI, a as PortfolioListResponse, w as PortfolioMetricsDataPoint, x as PortfolioMetricsTimelineData, y as PortfolioResultItem, z as PortfolioResultsData, A as PortfolioSection, d as PortfolioSectionData, c as PortfolioSectionDataMap, b as PortfolioSectionType, C as PortfolioSeoData, D as PortfolioServiceItem, E as PortfolioServicesData, F as PortfolioStrategyData, G as PortfolioStrategyPhase, H as PortfolioTeamData, I as PortfolioTeamMember, J as PortfolioTechItem, K as PortfolioTechStackData, L as PortfolioTestimonialData, N as PortfolioVideoData, S as SanityImageRef } from './types-DL4t_Cfa.js';
3
+ import { ServerApiConfig } from '@sonordev/site-kit/server';
3
4
  import React from 'react';
4
5
  export { AgencySiteKitLayout } from './layout/index.js';
5
6
  export { PortfolioPage } from './portfolio/index.js';
@@ -41,22 +42,37 @@ declare function mergeBrandConfig(partial: Partial<BrandConfig>): BrandConfig;
41
42
  /**
42
43
  * @sonordev/agency-site-kit — API fetch utilities
43
44
  *
44
- * Server-side only. Reads SONOR_API_KEY and SONOR_API_URL from process.env.
45
- * Uses Next.js ISR caching via `next: { revalidate }` on every fetch.
45
+ * Server-side only. Thin adapters over core `@sonordev/site-kit`'s shared server
46
+ * fetcher (`@sonordev/site-kit/server`) the single source of truth for the
47
+ * Sonor server data plane. This package no longer ships its own `apiFetch` copy;
48
+ * it reuses core's so the two can never drift (monorepo CLAUDE.md,
49
+ * "Consolidate Duplicated Logic").
50
+ *
51
+ * Config resolution (SONOR_API_KEY → `x-api-key`, SONOR_API_URL, the legacy
52
+ * UPTRADE_API_KEY fallback) and the `https://api.sonor.io` base all live in core.
53
+ *
54
+ * NOTE: importing this module pulls core's `server-only` guard, so it (and any
55
+ * barrel that re-exports it, including the package root and AgencySiteKitLayout)
56
+ * is server-only. Client components must import from
57
+ * `@sonordev/agency-site-kit/portfolio/client`.
46
58
  */
47
- interface ApiConfig {
48
- apiKey: string;
49
- apiUrl: string;
50
- }
59
+
60
+ type ApiConfig = ServerApiConfig;
51
61
  /**
52
- * Reads API configuration from environment variables.
53
- * Falls back to UPTRADE_API_KEY for legacy compatibility.
54
- * Returns null if no API key is found.
62
+ * Reads API configuration from overrides → environment (delegates to core's
63
+ * `resolveServerApiConfig`). Returns null and warns when no API key is found, so
64
+ * portfolio data degrades gracefully instead of throwing.
55
65
  */
56
66
  declare function getApiConfig(): ApiConfig | null;
57
67
  interface ApiFetchOptions extends Omit<RequestInit, 'headers'> {
58
68
  /** ISR revalidation interval in seconds. Defaults to 3600. Set to 0 for no caching. */
59
69
  revalidate?: number;
70
+ /**
71
+ * Next.js cache tags for on-demand invalidation. Portfolio fetches use
72
+ * PORTFOLIO_CACHE_TAG so the Portal can bust list + detail + sitemap data
73
+ * with a single revalidateTag call (see ./revalidate).
74
+ */
75
+ tags?: string[];
60
76
  /** Additional headers to merge with defaults */
61
77
  headers?: Record<string, string>;
62
78
  /** Override API key (e.g. passed from layout props) */
@@ -65,12 +81,17 @@ interface ApiFetchOptions extends Omit<RequestInit, 'headers'> {
65
81
  apiUrl?: string;
66
82
  }
67
83
  /**
68
- * Fetch wrapper for the public portfolio API.
84
+ * Fetch wrapper for the public portfolio API — delegates to core's
85
+ * `serverApiFetch`.
69
86
  *
70
- * - Injects `x-api-key` header automatically
87
+ * - Injects `x-api-key` automatically
71
88
  * - Parses JSON responses
72
- * - Uses Next.js ISR caching via `next.revalidate`
73
- * - Returns null on failure instead of throwing
89
+ * - Uses Next.js ISR caching via `next.revalidate` (+ optional cache `tags`)
90
+ * - Returns null on failure (or a missing key) instead of throwing
91
+ *
92
+ * Single-attempt by design — portfolio pages degrade gracefully rather than
93
+ * hard-failing. (Core's fetcher supports retry/backoff; this preserves
94
+ * agency-site-kit's original behavior.)
74
95
  */
75
96
  declare function apiFetch<T>(path: string, options?: ApiFetchOptions): Promise<T | null>;
76
97
  /**
package/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
- export { PORTFOLIO_SECTION_TYPES, PortfolioPage } from './chunk-RLVW7WEK.js';
1
+ export { PORTFOLIO_SECTION_TYPES, PortfolioPage } from './chunk-2UNKXWQM.js';
2
2
  import './chunk-2Y4O3LWM.js';
3
3
  export { getRegisteredSectionTypes, getSectionRenderer, registerSectionRenderer } from './chunk-XCKXHK44.js';
4
4
  export { PortfolioCard, PortfolioGrid } from './chunk-APG2QSMB.js';
5
- export { AgencySiteKitLayout, DEFAULT_BRAND_CONFIG, DEFAULT_DARK_MODE, generateBrandCSS, hexToRgb, mergeBrandConfig } from './chunk-OMOF4VR5.js';
6
- export { apiFetch, apiGet, getApiConfig } from './chunk-A4I4IK7V.js';
5
+ export { AgencySiteKitLayout, DEFAULT_BRAND_CONFIG, DEFAULT_DARK_MODE, generateBrandCSS, hexToRgb, mergeBrandConfig } from './chunk-RXBK2EOE.js';
6
+ export { apiFetch, apiGet, getApiConfig } from './chunk-TVFXN6W6.js';
7
7
  import './chunk-YB4B3OMC.js';
8
8
  import './chunk-7CFFAKDM.js';
9
9
  import React from 'react';
@@ -1,13 +1,13 @@
1
1
  'use strict';
2
2
 
3
- var chunkXM2QD3AK_cjs = require('../chunk-XM2QD3AK.cjs');
4
- require('../chunk-QBLWP25X.cjs');
3
+ var chunkWNR4KNG4_cjs = require('../chunk-WNR4KNG4.cjs');
4
+ require('../chunk-EE75WRCM.cjs');
5
5
 
6
6
 
7
7
 
8
8
  Object.defineProperty(exports, "AgencySiteKitLayout", {
9
9
  enumerable: true,
10
- get: function () { return chunkXM2QD3AK_cjs.AgencySiteKitLayout; }
10
+ get: function () { return chunkWNR4KNG4_cjs.AgencySiteKitLayout; }
11
11
  });
12
12
  //# sourceMappingURL=index.cjs.map
13
13
  //# sourceMappingURL=index.cjs.map
@@ -39,6 +39,11 @@ interface AgencySiteKitLayoutProps {
39
39
  theme?: 'light' | 'dark';
40
40
  /** Additional class name for the wrapper div */
41
41
  className?: string;
42
+ /**
43
+ * When true, screen-reader-only link to `/llms.txt` after children. Default false.
44
+ * Prefer `buildAiDiscoveryHeaders` from `@sonordev/site-kit/llms` in root layout.
45
+ */
46
+ showLlmsTxtFooterLink?: boolean;
42
47
  }
43
48
  /**
44
49
  * AgencySiteKitLayout — Server component wrapper
@@ -49,6 +54,6 @@ interface AgencySiteKitLayoutProps {
49
54
  * This is an async server component (RSC). It makes one API call at render
50
55
  * time, cached via Next.js ISR (default 1 hour revalidation).
51
56
  */
52
- declare function AgencySiteKitLayout({ children, apiKey, apiUrl, brandConfig: brandConfigOverride, theme, className, }: AgencySiteKitLayoutProps): Promise<React.ReactElement>;
57
+ declare function AgencySiteKitLayout({ children, apiKey, apiUrl, brandConfig: brandConfigOverride, theme, className, showLlmsTxtFooterLink, }: AgencySiteKitLayoutProps): Promise<React.ReactElement>;
53
58
 
54
59
  export { AgencySiteKitLayout, type AgencySiteKitLayoutProps };
@@ -39,6 +39,11 @@ interface AgencySiteKitLayoutProps {
39
39
  theme?: 'light' | 'dark';
40
40
  /** Additional class name for the wrapper div */
41
41
  className?: string;
42
+ /**
43
+ * When true, screen-reader-only link to `/llms.txt` after children. Default false.
44
+ * Prefer `buildAiDiscoveryHeaders` from `@sonordev/site-kit/llms` in root layout.
45
+ */
46
+ showLlmsTxtFooterLink?: boolean;
42
47
  }
43
48
  /**
44
49
  * AgencySiteKitLayout — Server component wrapper
@@ -49,6 +54,6 @@ interface AgencySiteKitLayoutProps {
49
54
  * This is an async server component (RSC). It makes one API call at render
50
55
  * time, cached via Next.js ISR (default 1 hour revalidation).
51
56
  */
52
- declare function AgencySiteKitLayout({ children, apiKey, apiUrl, brandConfig: brandConfigOverride, theme, className, }: AgencySiteKitLayoutProps): Promise<React.ReactElement>;
57
+ declare function AgencySiteKitLayout({ children, apiKey, apiUrl, brandConfig: brandConfigOverride, theme, className, showLlmsTxtFooterLink, }: AgencySiteKitLayoutProps): Promise<React.ReactElement>;
53
58
 
54
59
  export { AgencySiteKitLayout, type AgencySiteKitLayoutProps };
@@ -1,4 +1,4 @@
1
- export { AgencySiteKitLayout } from '../chunk-OMOF4VR5.js';
2
- import '../chunk-A4I4IK7V.js';
1
+ export { AgencySiteKitLayout } from '../chunk-RXBK2EOE.js';
2
+ import '../chunk-TVFXN6W6.js';
3
3
  //# sourceMappingURL=index.js.map
4
4
  //# sourceMappingURL=index.js.map
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var chunkJTI3F3QY_cjs = require('../chunk-JTI3F3QY.cjs');
3
+ var chunkFDFVLYAM_cjs = require('../chunk-FDFVLYAM.cjs');
4
4
  require('../chunk-XONXEFJY.cjs');
5
5
  var chunkXQNJED46_cjs = require('../chunk-XQNJED46.cjs');
6
6
  var chunkOA5ZM4OA_cjs = require('../chunk-OA5ZM4OA.cjs');
@@ -11,11 +11,11 @@ require('../chunk-IKBK7HYX.cjs');
11
11
 
12
12
  Object.defineProperty(exports, "PORTFOLIO_SECTION_TYPES", {
13
13
  enumerable: true,
14
- get: function () { return chunkJTI3F3QY_cjs.PORTFOLIO_SECTION_TYPES; }
14
+ get: function () { return chunkFDFVLYAM_cjs.PORTFOLIO_SECTION_TYPES; }
15
15
  });
16
16
  Object.defineProperty(exports, "PortfolioPage", {
17
17
  enumerable: true,
18
- get: function () { return chunkJTI3F3QY_cjs.PortfolioPage; }
18
+ get: function () { return chunkFDFVLYAM_cjs.PortfolioPage; }
19
19
  });
20
20
  Object.defineProperty(exports, "getRegisteredSectionTypes", {
21
21
  enumerable: true,
@@ -1,4 +1,4 @@
1
- export { PORTFOLIO_SECTION_TYPES, PortfolioPage } from '../chunk-RLVW7WEK.js';
1
+ export { PORTFOLIO_SECTION_TYPES, PortfolioPage } from '../chunk-2UNKXWQM.js';
2
2
  import '../chunk-2Y4O3LWM.js';
3
3
  export { getRegisteredSectionTypes, getSectionRenderer, registerSectionRenderer } from '../chunk-XCKXHK44.js';
4
4
  export { PortfolioCard, PortfolioGrid } from '../chunk-APG2QSMB.js';
@@ -1,9 +1,13 @@
1
1
  'use strict';
2
2
 
3
- var chunkQBLWP25X_cjs = require('../chunk-QBLWP25X.cjs');
3
+ var chunkEE75WRCM_cjs = require('../chunk-EE75WRCM.cjs');
4
+ var chunkFCKDJ4MJ_cjs = require('../chunk-FCKDJ4MJ.cjs');
4
5
 
5
6
  // src/portfolio/server.ts
6
7
  var LOG_PREFIX = "[@sonordev/agency-site-kit/portfolio]";
8
+ function portfolioGet(path) {
9
+ return chunkEE75WRCM_cjs.apiFetch(path, { method: "GET", tags: [chunkFCKDJ4MJ_cjs.PORTFOLIO_CACHE_TAG] });
10
+ }
7
11
  function buildQuery(params) {
8
12
  const entries = Object.entries(params).filter(
9
13
  (entry) => entry[1] != null
@@ -22,7 +26,7 @@ async function getPortfolioItems(options) {
22
26
  limit: options?.limit,
23
27
  offset: options?.offset
24
28
  });
25
- const data = await chunkQBLWP25X_cjs.apiGet(
29
+ const data = await portfolioGet(
26
30
  `/api/public/portfolio/items${query}`
27
31
  );
28
32
  if (!data) {
@@ -36,7 +40,7 @@ async function getPortfolioItem(slug) {
36
40
  console.warn(`${LOG_PREFIX} getPortfolioItem called with empty slug.`);
37
41
  return null;
38
42
  }
39
- const data = await chunkQBLWP25X_cjs.apiGet(
43
+ const data = await portfolioGet(
40
44
  `/api/public/portfolio/items/${encodeURIComponent(slug)}`
41
45
  );
42
46
  if (!data || !data.found || !data.item) {
@@ -71,7 +75,7 @@ async function getPortfolioItem(slug) {
71
75
  };
72
76
  }
73
77
  async function getPortfolioCategories() {
74
- const data = await chunkQBLWP25X_cjs.apiGet("/api/public/portfolio/categories");
78
+ const data = await portfolioGet("/api/public/portfolio/categories");
75
79
  if (!data) {
76
80
  console.warn(`${LOG_PREFIX} Failed to fetch portfolio categories.`);
77
81
  return [];
@@ -93,7 +97,7 @@ async function getPortfolioBrandConfig() {
93
97
  fontHeading: "Inter, system-ui, sans-serif",
94
98
  fontBody: "Inter, system-ui, sans-serif"
95
99
  };
96
- const data = await chunkQBLWP25X_cjs.apiGet(
100
+ const data = await portfolioGet(
97
101
  "/api/public/portfolio/config"
98
102
  );
99
103
  if (!data || !data.brand) {
@@ -132,6 +136,10 @@ async function generatePortfolioStaticParams() {
132
136
  return data.items.map((item) => ({ slug: item.slug }));
133
137
  }
134
138
 
139
+ Object.defineProperty(exports, "PORTFOLIO_CACHE_TAG", {
140
+ enumerable: true,
141
+ get: function () { return chunkFCKDJ4MJ_cjs.PORTFOLIO_CACHE_TAG; }
142
+ });
135
143
  exports.generatePortfolioMetadata = generatePortfolioMetadata;
136
144
  exports.generatePortfolioStaticParams = generatePortfolioStaticParams;
137
145
  exports.getPortfolioBrandConfig = getPortfolioBrandConfig;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/portfolio/server.ts"],"names":["apiGet"],"mappings":"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","file":"server.cjs","sourcesContent":["/**\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 { apiGet } from '../shared/api';\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// ---------------------------------------------------------------------------\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 */\nexport async function getPortfolioItems(options?: {\n category?: string;\n featured?: boolean;\n limit?: number;\n offset?: number;\n}): Promise<PortfolioListResponse> {\n const fallback: PortfolioListResponse = { items: [], total: 0, limit: 0, offset: 0 };\n\n const query = buildQuery({\n category: options?.category,\n featured: options?.featured,\n limit: options?.limit,\n offset: options?.offset,\n });\n\n const data = await apiGet<PortfolioListResponse>(\n `/api/public/portfolio/items${query}`,\n );\n\n if (!data) {\n console.warn(`${LOG_PREFIX} Failed to fetch portfolio items.`);\n return fallback;\n }\n\n return data;\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 };\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 when the item is not found or the API call fails.\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 const data = await apiGet<PortfolioItemApiResponse>(\n `/api/public/portfolio/items/${encodeURIComponent(slug)}`,\n );\n\n if (!data || !data.found || !data.item) {\n console.warn(`${LOG_PREFIX} Portfolio item not found for slug: \"${slug}\".`);\n return null;\n }\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 sections: item.sections ?? [],\n metricsDelta: item.metricsDelta ?? [],\n };\n}\n\n// ---------------------------------------------------------------------------\n// Public API — Categories\n// ---------------------------------------------------------------------------\n\n/**\n * Fetches available portfolio categories.\n * Returns an empty array on failure.\n */\nexport async function getPortfolioCategories(): Promise<string[]> {\n const data = await apiGet<string[]>('/api/public/portfolio/categories');\n\n if (!data) {\n console.warn(`${LOG_PREFIX} Failed to fetch portfolio categories.`);\n return [];\n }\n\n return data;\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.\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 const data = await apiGet<PortfolioConfigResponse>(\n '/api/public/portfolio/config',\n );\n\n if (!data || !data.brand) {\n console.warn(`${LOG_PREFIX} Failed to fetch brand config, using defaults.`);\n return fallback;\n }\n\n return data.brand;\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 cannot be found, which is safe\n * to spread into the metadata return value.\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 */\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"]}
1
+ {"version":3,"sources":["../../src/portfolio/server.ts"],"names":["apiFetch","PORTFOLIO_CACHE_TAG"],"mappings":"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","file":"server.cjs","sourcesContent":["/**\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 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 * 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 */\nfunction portfolioGet<T>(path: string): Promise<T | null> {\n return apiFetch<T>(path, { method: 'GET', tags: [PORTFOLIO_CACHE_TAG] });\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 */\nexport async function getPortfolioItems(options?: {\n category?: string;\n featured?: boolean;\n limit?: number;\n offset?: number;\n}): Promise<PortfolioListResponse> {\n const fallback: PortfolioListResponse = { items: [], total: 0, limit: 0, offset: 0 };\n\n const query = buildQuery({\n category: options?.category,\n featured: options?.featured,\n limit: options?.limit,\n offset: options?.offset,\n });\n\n const data = await portfolioGet<PortfolioListResponse>(\n `/api/public/portfolio/items${query}`,\n );\n\n if (!data) {\n console.warn(`${LOG_PREFIX} Failed to fetch portfolio items.`);\n return fallback;\n }\n\n return data;\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 };\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 when the item is not found or the API call fails.\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 const data = await portfolioGet<PortfolioItemApiResponse>(\n `/api/public/portfolio/items/${encodeURIComponent(slug)}`,\n );\n\n if (!data || !data.found || !data.item) {\n console.warn(`${LOG_PREFIX} Portfolio item not found for slug: \"${slug}\".`);\n return null;\n }\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 sections: item.sections ?? [],\n metricsDelta: item.metricsDelta ?? [],\n };\n}\n\n// ---------------------------------------------------------------------------\n// Public API — Categories\n// ---------------------------------------------------------------------------\n\n/**\n * Fetches available portfolio categories.\n * Returns an empty array on failure.\n */\nexport async function getPortfolioCategories(): Promise<string[]> {\n const data = await portfolioGet<string[]>('/api/public/portfolio/categories');\n\n if (!data) {\n console.warn(`${LOG_PREFIX} Failed to fetch portfolio categories.`);\n return [];\n }\n\n return data;\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.\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 const data = await portfolioGet<PortfolioConfigResponse>(\n '/api/public/portfolio/config',\n );\n\n if (!data || !data.brand) {\n console.warn(`${LOG_PREFIX} Failed to fetch brand config, using defaults.`);\n return fallback;\n }\n\n return data.brand;\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 cannot be found, which is safe\n * to spread into the metadata return value.\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 */\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"]}
@@ -1,4 +1,5 @@
1
1
  import { B as BrandConfig, P as PortfolioItemFull, a as PortfolioListResponse } from '../types-DL4t_Cfa.cjs';
2
+ export { P as PORTFOLIO_CACHE_TAG } from '../cache-CE_ujDK4.cjs';
2
3
 
3
4
  /**
4
5
  * @sonordev/agency-site-kit — Portfolio server-side data fetching
@@ -1,4 +1,5 @@
1
1
  import { B as BrandConfig, P as PortfolioItemFull, a as PortfolioListResponse } from '../types-DL4t_Cfa.js';
2
+ export { P as PORTFOLIO_CACHE_TAG } from '../cache-CE_ujDK4.js';
2
3
 
3
4
  /**
4
5
  * @sonordev/agency-site-kit — Portfolio server-side data fetching
@@ -1,7 +1,12 @@
1
- import { apiGet } from '../chunk-A4I4IK7V.js';
1
+ import { apiFetch } from '../chunk-TVFXN6W6.js';
2
+ import { PORTFOLIO_CACHE_TAG } from '../chunk-N7TVKUYQ.js';
3
+ export { PORTFOLIO_CACHE_TAG } from '../chunk-N7TVKUYQ.js';
2
4
 
3
5
  // src/portfolio/server.ts
4
6
  var LOG_PREFIX = "[@sonordev/agency-site-kit/portfolio]";
7
+ function portfolioGet(path) {
8
+ return apiFetch(path, { method: "GET", tags: [PORTFOLIO_CACHE_TAG] });
9
+ }
5
10
  function buildQuery(params) {
6
11
  const entries = Object.entries(params).filter(
7
12
  (entry) => entry[1] != null
@@ -20,7 +25,7 @@ async function getPortfolioItems(options) {
20
25
  limit: options?.limit,
21
26
  offset: options?.offset
22
27
  });
23
- const data = await apiGet(
28
+ const data = await portfolioGet(
24
29
  `/api/public/portfolio/items${query}`
25
30
  );
26
31
  if (!data) {
@@ -34,7 +39,7 @@ async function getPortfolioItem(slug) {
34
39
  console.warn(`${LOG_PREFIX} getPortfolioItem called with empty slug.`);
35
40
  return null;
36
41
  }
37
- const data = await apiGet(
42
+ const data = await portfolioGet(
38
43
  `/api/public/portfolio/items/${encodeURIComponent(slug)}`
39
44
  );
40
45
  if (!data || !data.found || !data.item) {
@@ -69,7 +74,7 @@ async function getPortfolioItem(slug) {
69
74
  };
70
75
  }
71
76
  async function getPortfolioCategories() {
72
- const data = await apiGet("/api/public/portfolio/categories");
77
+ const data = await portfolioGet("/api/public/portfolio/categories");
73
78
  if (!data) {
74
79
  console.warn(`${LOG_PREFIX} Failed to fetch portfolio categories.`);
75
80
  return [];
@@ -91,7 +96,7 @@ async function getPortfolioBrandConfig() {
91
96
  fontHeading: "Inter, system-ui, sans-serif",
92
97
  fontBody: "Inter, system-ui, sans-serif"
93
98
  };
94
- const data = await apiGet(
99
+ const data = await portfolioGet(
95
100
  "/api/public/portfolio/config"
96
101
  );
97
102
  if (!data || !data.brand) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/portfolio/server.ts"],"names":[],"mappings":"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","file":"server.js","sourcesContent":["/**\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 { apiGet } from '../shared/api';\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// ---------------------------------------------------------------------------\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 */\nexport async function getPortfolioItems(options?: {\n category?: string;\n featured?: boolean;\n limit?: number;\n offset?: number;\n}): Promise<PortfolioListResponse> {\n const fallback: PortfolioListResponse = { items: [], total: 0, limit: 0, offset: 0 };\n\n const query = buildQuery({\n category: options?.category,\n featured: options?.featured,\n limit: options?.limit,\n offset: options?.offset,\n });\n\n const data = await apiGet<PortfolioListResponse>(\n `/api/public/portfolio/items${query}`,\n );\n\n if (!data) {\n console.warn(`${LOG_PREFIX} Failed to fetch portfolio items.`);\n return fallback;\n }\n\n return data;\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 };\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 when the item is not found or the API call fails.\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 const data = await apiGet<PortfolioItemApiResponse>(\n `/api/public/portfolio/items/${encodeURIComponent(slug)}`,\n );\n\n if (!data || !data.found || !data.item) {\n console.warn(`${LOG_PREFIX} Portfolio item not found for slug: \"${slug}\".`);\n return null;\n }\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 sections: item.sections ?? [],\n metricsDelta: item.metricsDelta ?? [],\n };\n}\n\n// ---------------------------------------------------------------------------\n// Public API — Categories\n// ---------------------------------------------------------------------------\n\n/**\n * Fetches available portfolio categories.\n * Returns an empty array on failure.\n */\nexport async function getPortfolioCategories(): Promise<string[]> {\n const data = await apiGet<string[]>('/api/public/portfolio/categories');\n\n if (!data) {\n console.warn(`${LOG_PREFIX} Failed to fetch portfolio categories.`);\n return [];\n }\n\n return data;\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.\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 const data = await apiGet<PortfolioConfigResponse>(\n '/api/public/portfolio/config',\n );\n\n if (!data || !data.brand) {\n console.warn(`${LOG_PREFIX} Failed to fetch brand config, using defaults.`);\n return fallback;\n }\n\n return data.brand;\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 cannot be found, which is safe\n * to spread into the metadata return value.\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 */\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"]}
1
+ {"version":3,"sources":["../../src/portfolio/server.ts"],"names":[],"mappings":"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","file":"server.js","sourcesContent":["/**\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 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 * 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 */\nfunction portfolioGet<T>(path: string): Promise<T | null> {\n return apiFetch<T>(path, { method: 'GET', tags: [PORTFOLIO_CACHE_TAG] });\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 */\nexport async function getPortfolioItems(options?: {\n category?: string;\n featured?: boolean;\n limit?: number;\n offset?: number;\n}): Promise<PortfolioListResponse> {\n const fallback: PortfolioListResponse = { items: [], total: 0, limit: 0, offset: 0 };\n\n const query = buildQuery({\n category: options?.category,\n featured: options?.featured,\n limit: options?.limit,\n offset: options?.offset,\n });\n\n const data = await portfolioGet<PortfolioListResponse>(\n `/api/public/portfolio/items${query}`,\n );\n\n if (!data) {\n console.warn(`${LOG_PREFIX} Failed to fetch portfolio items.`);\n return fallback;\n }\n\n return data;\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 };\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 when the item is not found or the API call fails.\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 const data = await portfolioGet<PortfolioItemApiResponse>(\n `/api/public/portfolio/items/${encodeURIComponent(slug)}`,\n );\n\n if (!data || !data.found || !data.item) {\n console.warn(`${LOG_PREFIX} Portfolio item not found for slug: \"${slug}\".`);\n return null;\n }\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 sections: item.sections ?? [],\n metricsDelta: item.metricsDelta ?? [],\n };\n}\n\n// ---------------------------------------------------------------------------\n// Public API — Categories\n// ---------------------------------------------------------------------------\n\n/**\n * Fetches available portfolio categories.\n * Returns an empty array on failure.\n */\nexport async function getPortfolioCategories(): Promise<string[]> {\n const data = await portfolioGet<string[]>('/api/public/portfolio/categories');\n\n if (!data) {\n console.warn(`${LOG_PREFIX} Failed to fetch portfolio categories.`);\n return [];\n }\n\n return data;\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.\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 const data = await portfolioGet<PortfolioConfigResponse>(\n '/api/public/portfolio/config',\n );\n\n if (!data || !data.brand) {\n console.warn(`${LOG_PREFIX} Failed to fetch brand config, using defaults.`);\n return fallback;\n }\n\n return data.brand;\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 cannot be found, which is safe\n * to spread into the metadata return value.\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 */\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"]}
@@ -0,0 +1,98 @@
1
+ 'use strict';
2
+
3
+ var chunkFCKDJ4MJ_cjs = require('./chunk-FCKDJ4MJ.cjs');
4
+
5
+ // src/revalidate.ts
6
+ function asStringArray(value) {
7
+ if (typeof value === "string") return [value];
8
+ if (Array.isArray(value)) {
9
+ return value.filter((v) => typeof v === "string" && v.length > 0);
10
+ }
11
+ return [];
12
+ }
13
+ function jsonResponse(status, body) {
14
+ return new Response(JSON.stringify(body), {
15
+ status,
16
+ headers: { "Content-Type": "application/json" }
17
+ });
18
+ }
19
+ function createRevalidateHandler(options = {}) {
20
+ const basePath = (options.basePath ?? "/work").replace(/\/+$/, "") || "/work";
21
+ return async function POST(request) {
22
+ let body = {};
23
+ try {
24
+ body = await request.json();
25
+ } catch {
26
+ }
27
+ const acceptedSecrets = [
28
+ options.secret,
29
+ process.env.SONOR_API_KEY,
30
+ process.env.UPTRADE_API_KEY,
31
+ process.env.NEXT_PUBLIC_UPTRADE_API_KEY,
32
+ process.env.REVALIDATION_SECRET
33
+ ].filter((s) => !!s);
34
+ if (acceptedSecrets.length === 0) {
35
+ console.error(
36
+ "[@sonordev/agency-site-kit/revalidate] No secret configured \u2014 set SONOR_API_KEY or pass { secret } to createRevalidateHandler."
37
+ );
38
+ return jsonResponse(401, { error: "Revalidation endpoint not configured" });
39
+ }
40
+ const bearer = request.headers.get("authorization")?.replace(/^Bearer\s+/i, "").trim();
41
+ const url = new URL(request.url);
42
+ const token = request.headers.get("x-api-key")?.trim() || bearer || url.searchParams.get("secret") || (typeof body.secret === "string" ? body.secret : void 0);
43
+ if (!token || !acceptedSecrets.includes(token)) {
44
+ return jsonResponse(401, {
45
+ error: "Unauthorized \u2014 use Authorization: Bearer <SONOR_API_KEY>, x-api-key, or secret"
46
+ });
47
+ }
48
+ try {
49
+ const { revalidatePath, revalidateTag } = await import('next/cache');
50
+ const revalidated = [];
51
+ const tags = [.../* @__PURE__ */ new Set([
52
+ ...asStringArray(body.tags),
53
+ ...asStringArray(body.tag)
54
+ ])];
55
+ const effectiveTags = tags.length > 0 ? tags : options.defaultTags ?? [chunkFCKDJ4MJ_cjs.PORTFOLIO_CACHE_TAG];
56
+ for (const tag of effectiveTags) {
57
+ revalidateTag(tag, "max");
58
+ revalidated.push(`tag:${tag}`);
59
+ }
60
+ if (body.revalidateAll === true) {
61
+ revalidatePath("/", "layout");
62
+ revalidated.push("layout:/");
63
+ }
64
+ const slugPaths = [...asStringArray(body.slug), ...asStringArray(body.slugs)].map(
65
+ (slug) => `${basePath}/${slug.replace(/^\/+/, "")}`
66
+ );
67
+ const paths = /* @__PURE__ */ new Set([
68
+ basePath,
69
+ "/sitemap.xml",
70
+ ...options.extraPaths ?? [],
71
+ ...asStringArray(body.paths),
72
+ ...asStringArray(body.path),
73
+ ...slugPaths
74
+ ]);
75
+ for (const p of paths) {
76
+ const normalized = p.startsWith("/") ? p : `/${p}`;
77
+ try {
78
+ revalidatePath(normalized, "page");
79
+ revalidated.push(normalized);
80
+ } catch (err) {
81
+ console.warn(`[@sonordev/agency-site-kit/revalidate] Failed to revalidate ${normalized}:`, err);
82
+ }
83
+ }
84
+ return jsonResponse(200, { revalidated: true, targets: revalidated });
85
+ } catch (err) {
86
+ console.error("[@sonordev/agency-site-kit/revalidate] Revalidation failed:", err);
87
+ return jsonResponse(500, { error: "Revalidation failed" });
88
+ }
89
+ };
90
+ }
91
+
92
+ Object.defineProperty(exports, "PORTFOLIO_CACHE_TAG", {
93
+ enumerable: true,
94
+ get: function () { return chunkFCKDJ4MJ_cjs.PORTFOLIO_CACHE_TAG; }
95
+ });
96
+ exports.createRevalidateHandler = createRevalidateHandler;
97
+ //# sourceMappingURL=revalidate.cjs.map
98
+ //# sourceMappingURL=revalidate.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/revalidate.ts"],"names":["PORTFOLIO_CACHE_TAG"],"mappings":"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","file":"revalidate.cjs","sourcesContent":["/**\n * @sonordev/agency-site-kit/revalidate — On-demand cache revalidation\n *\n * Route handler factory the Sonor Portal calls when portfolio content changes\n * (publish / update / unpublish / delete), so agency sites reflect Portal\n * publishes near-instantly instead of waiting out the ISR window.\n *\n * @example\n * ```ts\n * // app/api/revalidate/route.ts\n * import { createRevalidateHandler } from '@sonordev/agency-site-kit/revalidate'\n *\n * export const POST = createRevalidateHandler()\n * ```\n *\n * Auth accepts the project API key — the same `SONOR_API_KEY` the rest of\n * site-kit uses, sent by the Portal as `Authorization: Bearer` or `x-api-key` —\n * or a legacy `REVALIDATION_SECRET` via body/query. No extra env var needed.\n *\n * The request body matches what the Portal's SiteRevalidationService sends\n * (and is a superset of the legacy /api/seo-revalidate contract):\n * `{ paths?, path?, slug?, slugs?, tags?, tag?, revalidateAll? }`.\n */\n\nimport { PORTFOLIO_CACHE_TAG } from './shared/cache';\n\nexport { PORTFOLIO_CACHE_TAG };\n\nexport interface CreateRevalidateHandlerOptions {\n /**\n * Shared secret the caller must present. Defaults to the site's\n * SONOR_API_KEY / UPTRADE_API_KEY / REVALIDATION_SECRET env vars\n * (any of them is accepted).\n */\n secret?: string;\n /** Route prefix for portfolio detail pages. Defaults to '/work'. */\n basePath?: string;\n /** Tags revalidated when the request names none. Defaults to ['portfolio']. */\n defaultTags?: string[];\n /** Extra literal paths revalidated on every call (e.g. '/'). */\n extraPaths?: string[];\n}\n\ninterface RevalidateRequestBody {\n paths?: unknown;\n path?: unknown;\n slug?: unknown;\n slugs?: unknown;\n tags?: unknown;\n tag?: unknown;\n revalidateAll?: unknown;\n secret?: unknown;\n}\n\nfunction asStringArray(value: unknown): string[] {\n if (typeof value === 'string') return [value];\n if (Array.isArray(value)) {\n return value.filter((v): v is string => typeof v === 'string' && v.length > 0);\n }\n return [];\n}\n\nfunction jsonResponse(status: number, body: Record<string, unknown>): Response {\n return new Response(JSON.stringify(body), {\n status,\n headers: { 'Content-Type': 'application/json' },\n });\n}\n\n/**\n * Creates the POST handler for the site's revalidation endpoint.\n *\n * On each authorized call it:\n * 1. Revalidates the requested cache tags (default: PORTFOLIO_CACHE_TAG),\n * which busts every tagged portfolio fetch — list, detail, categories,\n * brand config, and sitemap data — in one shot.\n * 2. Revalidates the requested paths plus `basePath` and `/sitemap.xml`,\n * so the statically rendered routes re-render on the next visit.\n */\nexport function createRevalidateHandler(\n options: CreateRevalidateHandlerOptions = {},\n): (request: Request) => Promise<Response> {\n const basePath = (options.basePath ?? '/work').replace(/\\/+$/, '') || '/work';\n\n return async function POST(request: Request): Promise<Response> {\n let body: RevalidateRequestBody = {};\n try {\n body = (await request.json()) as RevalidateRequestBody;\n } catch {\n // Empty or non-JSON body — fall through to defaults.\n }\n\n // ── Auth ────────────────────────────────────────────────────────────────\n const acceptedSecrets = [\n options.secret,\n process.env.SONOR_API_KEY,\n process.env.UPTRADE_API_KEY,\n process.env.NEXT_PUBLIC_UPTRADE_API_KEY,\n process.env.REVALIDATION_SECRET,\n ].filter((s): s is string => !!s);\n\n if (acceptedSecrets.length === 0) {\n console.error(\n '[@sonordev/agency-site-kit/revalidate] No secret configured — set SONOR_API_KEY or pass { secret } to createRevalidateHandler.',\n );\n return jsonResponse(401, { error: 'Revalidation endpoint not configured' });\n }\n\n const bearer = request.headers.get('authorization')?.replace(/^Bearer\\s+/i, '').trim();\n const url = new URL(request.url);\n const token =\n request.headers.get('x-api-key')?.trim() ||\n bearer ||\n url.searchParams.get('secret') ||\n (typeof body.secret === 'string' ? body.secret : undefined);\n\n if (!token || !acceptedSecrets.includes(token)) {\n return jsonResponse(401, {\n error: 'Unauthorized — use Authorization: Bearer <SONOR_API_KEY>, x-api-key, or secret',\n });\n }\n\n // ── Revalidate ──────────────────────────────────────────────────────────\n try {\n const { revalidatePath, revalidateTag } = await import('next/cache');\n const revalidated: string[] = [];\n\n // Tags first: one revalidateTag busts every tagged portfolio fetch.\n // Deduped — the Portal sends both `tags` and a legacy single `tag`.\n const tags = [...new Set([\n ...asStringArray(body.tags),\n ...asStringArray(body.tag),\n ])];\n const effectiveTags = tags.length > 0 ? tags : (options.defaultTags ?? [PORTFOLIO_CACHE_TAG]);\n for (const tag of effectiveTags) {\n // Next 16 takes a cache profile ('max' = revalidate everywhere);\n // earlier versions ignore the extra argument.\n (revalidateTag as (tag: string, profile?: string) => void)(tag, 'max');\n revalidated.push(`tag:${tag}`);\n }\n\n if (body.revalidateAll === true) {\n revalidatePath('/', 'layout');\n revalidated.push('layout:/');\n }\n\n // Paths: explicit ones, slug shorthands, plus the hub + sitemap always.\n const slugPaths = [...asStringArray(body.slug), ...asStringArray(body.slugs)].map(\n (slug) => `${basePath}/${slug.replace(/^\\/+/, '')}`,\n );\n const paths = new Set<string>([\n basePath,\n '/sitemap.xml',\n ...(options.extraPaths ?? []),\n ...asStringArray(body.paths),\n ...asStringArray(body.path),\n ...slugPaths,\n ]);\n\n for (const p of paths) {\n const normalized = p.startsWith('/') ? p : `/${p}`;\n try {\n revalidatePath(normalized, 'page');\n revalidated.push(normalized);\n } catch (err) {\n console.warn(`[@sonordev/agency-site-kit/revalidate] Failed to revalidate ${normalized}:`, err);\n }\n }\n\n return jsonResponse(200, { revalidated: true, targets: revalidated });\n } catch (err) {\n console.error('[@sonordev/agency-site-kit/revalidate] Revalidation failed:', err);\n return jsonResponse(500, { error: 'Revalidation failed' });\n }\n };\n}\n"]}
@@ -0,0 +1,53 @@
1
+ export { P as PORTFOLIO_CACHE_TAG } from './cache-CE_ujDK4.cjs';
2
+
3
+ /**
4
+ * @sonordev/agency-site-kit/revalidate — On-demand cache revalidation
5
+ *
6
+ * Route handler factory the Sonor Portal calls when portfolio content changes
7
+ * (publish / update / unpublish / delete), so agency sites reflect Portal
8
+ * publishes near-instantly instead of waiting out the ISR window.
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * // app/api/revalidate/route.ts
13
+ * import { createRevalidateHandler } from '@sonordev/agency-site-kit/revalidate'
14
+ *
15
+ * export const POST = createRevalidateHandler()
16
+ * ```
17
+ *
18
+ * Auth accepts the project API key — the same `SONOR_API_KEY` the rest of
19
+ * site-kit uses, sent by the Portal as `Authorization: Bearer` or `x-api-key` —
20
+ * or a legacy `REVALIDATION_SECRET` via body/query. No extra env var needed.
21
+ *
22
+ * The request body matches what the Portal's SiteRevalidationService sends
23
+ * (and is a superset of the legacy /api/seo-revalidate contract):
24
+ * `{ paths?, path?, slug?, slugs?, tags?, tag?, revalidateAll? }`.
25
+ */
26
+
27
+ interface CreateRevalidateHandlerOptions {
28
+ /**
29
+ * Shared secret the caller must present. Defaults to the site's
30
+ * SONOR_API_KEY / UPTRADE_API_KEY / REVALIDATION_SECRET env vars
31
+ * (any of them is accepted).
32
+ */
33
+ secret?: string;
34
+ /** Route prefix for portfolio detail pages. Defaults to '/work'. */
35
+ basePath?: string;
36
+ /** Tags revalidated when the request names none. Defaults to ['portfolio']. */
37
+ defaultTags?: string[];
38
+ /** Extra literal paths revalidated on every call (e.g. '/'). */
39
+ extraPaths?: string[];
40
+ }
41
+ /**
42
+ * Creates the POST handler for the site's revalidation endpoint.
43
+ *
44
+ * On each authorized call it:
45
+ * 1. Revalidates the requested cache tags (default: PORTFOLIO_CACHE_TAG),
46
+ * which busts every tagged portfolio fetch — list, detail, categories,
47
+ * brand config, and sitemap data — in one shot.
48
+ * 2. Revalidates the requested paths plus `basePath` and `/sitemap.xml`,
49
+ * so the statically rendered routes re-render on the next visit.
50
+ */
51
+ declare function createRevalidateHandler(options?: CreateRevalidateHandlerOptions): (request: Request) => Promise<Response>;
52
+
53
+ export { type CreateRevalidateHandlerOptions, createRevalidateHandler };