@sonordev/agency-site-kit 0.1.2 → 0.2.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/cache-CE_ujDK4.d.cts +15 -0
- package/dist/cache-CE_ujDK4.d.ts +15 -0
- package/dist/{chunk-XM2QD3AK.cjs → chunk-EN6QQMKQ.cjs} +12 -6
- package/dist/chunk-EN6QQMKQ.cjs.map +1 -0
- package/dist/chunk-FCKDJ4MJ.cjs +8 -0
- package/dist/chunk-FCKDJ4MJ.cjs.map +1 -0
- package/dist/{chunk-JTI3F3QY.cjs → chunk-GZXXGWHE.cjs} +2 -2
- package/dist/{chunk-JTI3F3QY.cjs.map → chunk-GZXXGWHE.cjs.map} +1 -1
- package/dist/chunk-N7TVKUYQ.js +6 -0
- package/dist/chunk-N7TVKUYQ.js.map +1 -0
- package/dist/{chunk-OMOF4VR5.js → chunk-OWGVZNCO.js} +11 -5
- package/dist/chunk-OWGVZNCO.js.map +1 -0
- package/dist/{chunk-QBLWP25X.cjs → chunk-RVNASHPZ.cjs} +4 -3
- package/dist/chunk-RVNASHPZ.cjs.map +1 -0
- package/dist/{chunk-RLVW7WEK.js → chunk-VVPJ4UFO.js} +2 -2
- package/dist/{chunk-RLVW7WEK.js.map → chunk-VVPJ4UFO.js.map} +1 -1
- package/dist/{chunk-A4I4IK7V.js → chunk-ZEAOULCH.js} +4 -3
- package/dist/chunk-ZEAOULCH.js.map +1 -0
- package/dist/index.cjs +14 -14
- package/dist/index.d.cts +6 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +3 -3
- package/dist/layout/index.cjs +3 -3
- package/dist/layout/index.d.cts +6 -1
- package/dist/layout/index.d.ts +6 -1
- package/dist/layout/index.js +2 -2
- package/dist/portfolio/index.cjs +3 -3
- package/dist/portfolio/index.js +1 -1
- package/dist/portfolio/server.cjs +13 -5
- package/dist/portfolio/server.cjs.map +1 -1
- package/dist/portfolio/server.d.cts +1 -0
- package/dist/portfolio/server.d.ts +1 -0
- package/dist/portfolio/server.js +10 -5
- package/dist/portfolio/server.js.map +1 -1
- package/dist/revalidate.cjs +98 -0
- package/dist/revalidate.cjs.map +1 -0
- package/dist/revalidate.d.cts +53 -0
- package/dist/revalidate.d.ts +53 -0
- package/dist/revalidate.js +93 -0
- package/dist/revalidate.js.map +1 -0
- package/package.json +7 -1
- package/dist/chunk-A4I4IK7V.js.map +0 -1
- package/dist/chunk-OMOF4VR5.js.map +0 -1
- package/dist/chunk-QBLWP25X.cjs.map +0 -1
- package/dist/chunk-XM2QD3AK.cjs.map +0 -1
package/dist/layout/index.d.ts
CHANGED
|
@@ -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 };
|
package/dist/layout/index.js
CHANGED
package/dist/portfolio/index.cjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
3
|
+
var chunkGZXXGWHE_cjs = require('../chunk-GZXXGWHE.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
|
|
14
|
+
get: function () { return chunkGZXXGWHE_cjs.PORTFOLIO_SECTION_TYPES; }
|
|
15
15
|
});
|
|
16
16
|
Object.defineProperty(exports, "PortfolioPage", {
|
|
17
17
|
enumerable: true,
|
|
18
|
-
get: function () { return
|
|
18
|
+
get: function () { return chunkGZXXGWHE_cjs.PortfolioPage; }
|
|
19
19
|
});
|
|
20
20
|
Object.defineProperty(exports, "getRegisteredSectionTypes", {
|
|
21
21
|
enumerable: true,
|
package/dist/portfolio/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { PORTFOLIO_SECTION_TYPES, PortfolioPage } from '../chunk-
|
|
1
|
+
export { PORTFOLIO_SECTION_TYPES, PortfolioPage } from '../chunk-VVPJ4UFO.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
|
|
3
|
+
var chunkRVNASHPZ_cjs = require('../chunk-RVNASHPZ.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 chunkRVNASHPZ_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
|
|
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
|
|
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
|
|
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
|
|
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"]}
|
package/dist/portfolio/server.js
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { apiFetch } from '../chunk-ZEAOULCH.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
|
|
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
|
|
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
|
|
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
|
|
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":";;;;;AAsDA,SAAS,cAAc,KAAA,EAA0B;AAC/C,EAAA,IAAI,OAAO,KAAA,KAAU,QAAA,EAAU,OAAO,CAAC,KAAK,CAAA;AAC5C,EAAA,IAAI,KAAA,CAAM,OAAA,CAAQ,KAAK,CAAA,EAAG;AACxB,IAAA,OAAO,KAAA,CAAM,OAAO,CAAC,CAAA,KAAmB,OAAO,CAAA,KAAM,QAAA,IAAY,CAAA,CAAE,MAAA,GAAS,CAAC,CAAA;AAAA,EAC/E;AACA,EAAA,OAAO,EAAC;AACV;AAEA,SAAS,YAAA,CAAa,QAAgB,IAAA,EAAyC;AAC7E,EAAA,OAAO,IAAI,QAAA,CAAS,IAAA,CAAK,SAAA,CAAU,IAAI,CAAA,EAAG;AAAA,IACxC,MAAA;AAAA,IACA,OAAA,EAAS,EAAE,cAAA,EAAgB,kBAAA;AAAmB,GAC/C,CAAA;AACH;AAYO,SAAS,uBAAA,CACd,OAAA,GAA0C,EAAC,EACF;AACzC,EAAA,MAAM,YAAY,OAAA,CAAQ,QAAA,IAAY,SAAS,OAAA,CAAQ,MAAA,EAAQ,EAAE,CAAA,IAAK,OAAA;AAEtE,EAAA,OAAO,eAAe,KAAK,OAAA,EAAqC;AAC9D,IAAA,IAAI,OAA8B,EAAC;AACnC,IAAA,IAAI;AACF,MAAA,IAAA,GAAQ,MAAM,QAAQ,IAAA,EAAK;AAAA,IAC7B,CAAA,CAAA,MAAQ;AAAA,IAER;AAGA,IAAA,MAAM,eAAA,GAAkB;AAAA,MACtB,OAAA,CAAQ,MAAA;AAAA,MACR,QAAQ,GAAA,CAAI,aAAA;AAAA,MACZ,QAAQ,GAAA,CAAI,eAAA;AAAA,MACZ,QAAQ,GAAA,CAAI,2BAAA;AAAA,MACZ,QAAQ,GAAA,CAAI;AAAA,MACZ,MAAA,CAAO,CAAC,CAAA,KAAmB,CAAC,CAAC,CAAC,CAAA;AAEhC,IAAA,IAAI,eAAA,CAAgB,WAAW,CAAA,EAAG;AAChC,MAAA,OAAA,CAAQ,KAAA;AAAA,QACN;AAAA,OACF;AACA,MAAA,OAAO,YAAA,CAAa,GAAA,EAAK,EAAE,KAAA,EAAO,wCAAwC,CAAA;AAAA,IAC5E;AAEA,IAAA,MAAM,MAAA,GAAS,OAAA,CAAQ,OAAA,CAAQ,GAAA,CAAI,eAAe,GAAG,OAAA,CAAQ,aAAA,EAAe,EAAE,CAAA,CAAE,IAAA,EAAK;AACrF,IAAA,MAAM,GAAA,GAAM,IAAI,GAAA,CAAI,OAAA,CAAQ,GAAG,CAAA;AAC/B,IAAA,MAAM,QACJ,OAAA,CAAQ,OAAA,CAAQ,IAAI,WAAW,CAAA,EAAG,MAAK,IACvC,MAAA,IACA,IAAI,YAAA,CAAa,GAAA,CAAI,QAAQ,CAAA,KAC5B,OAAO,KAAK,MAAA,KAAW,QAAA,GAAW,KAAK,MAAA,GAAS,MAAA,CAAA;AAEnD,IAAA,IAAI,CAAC,KAAA,IAAS,CAAC,eAAA,CAAgB,QAAA,CAAS,KAAK,CAAA,EAAG;AAC9C,MAAA,OAAO,aAAa,GAAA,EAAK;AAAA,QACvB,KAAA,EAAO;AAAA,OACR,CAAA;AAAA,IACH;AAGA,IAAA,IAAI;AACF,MAAA,MAAM,EAAE,cAAA,EAAgB,aAAA,EAAc,GAAI,MAAM,OAAO,YAAY,CAAA;AACnE,MAAA,MAAM,cAAwB,EAAC;AAI/B,MAAA,MAAM,IAAA,GAAO,CAAC,mBAAG,IAAI,GAAA,CAAI;AAAA,QACvB,GAAG,aAAA,CAAc,IAAA,CAAK,IAAI,CAAA;AAAA,QAC1B,GAAG,aAAA,CAAc,IAAA,CAAK,GAAG;AAAA,OAC1B,CAAC,CAAA;AACF,MAAA,MAAM,aAAA,GAAgB,KAAK,MAAA,GAAS,CAAA,GAAI,OAAQ,OAAA,CAAQ,WAAA,IAAe,CAACA,qCAAmB,CAAA;AAC3F,MAAA,KAAA,MAAW,OAAO,aAAA,EAAe;AAG/B,QAAC,aAAA,CAA0D,KAAK,KAAK,CAAA;AACrE,QAAA,WAAA,CAAY,IAAA,CAAK,CAAA,IAAA,EAAO,GAAG,CAAA,CAAE,CAAA;AAAA,MAC/B;AAEA,MAAA,IAAI,IAAA,CAAK,kBAAkB,IAAA,EAAM;AAC/B,QAAA,cAAA,CAAe,KAAK,QAAQ,CAAA;AAC5B,QAAA,WAAA,CAAY,KAAK,UAAU,CAAA;AAAA,MAC7B;AAGA,MAAA,MAAM,SAAA,GAAY,CAAC,GAAG,aAAA,CAAc,IAAA,CAAK,IAAI,CAAA,EAAG,GAAG,aAAA,CAAc,IAAA,CAAK,KAAK,CAAC,CAAA,CAAE,GAAA;AAAA,QAC5E,CAAC,SAAS,CAAA,EAAG,QAAQ,IAAI,IAAA,CAAK,OAAA,CAAQ,MAAA,EAAQ,EAAE,CAAC,CAAA;AAAA,OACnD;AACA,MAAA,MAAM,KAAA,uBAAY,GAAA,CAAY;AAAA,QAC5B,QAAA;AAAA,QACA,cAAA;AAAA,QACA,GAAI,OAAA,CAAQ,UAAA,IAAc,EAAC;AAAA,QAC3B,GAAG,aAAA,CAAc,IAAA,CAAK,KAAK,CAAA;AAAA,QAC3B,GAAG,aAAA,CAAc,IAAA,CAAK,IAAI,CAAA;AAAA,QAC1B,GAAG;AAAA,OACJ,CAAA;AAED,MAAA,KAAA,MAAW,KAAK,KAAA,EAAO;AACrB,QAAA,MAAM,aAAa,CAAA,CAAE,UAAA,CAAW,GAAG,CAAA,GAAI,CAAA,GAAI,IAAI,CAAC,CAAA,CAAA;AAChD,QAAA,IAAI;AACF,UAAA,cAAA,CAAe,YAAY,MAAM,CAAA;AACjC,UAAA,WAAA,CAAY,KAAK,UAAU,CAAA;AAAA,QAC7B,SAAS,GAAA,EAAK;AACZ,UAAA,OAAA,CAAQ,IAAA,CAAK,CAAA,4DAAA,EAA+D,UAAU,CAAA,CAAA,CAAA,EAAK,GAAG,CAAA;AAAA,QAChG;AAAA,MACF;AAEA,MAAA,OAAO,aAAa,GAAA,EAAK,EAAE,aAAa,IAAA,EAAM,OAAA,EAAS,aAAa,CAAA;AAAA,IACtE,SAAS,GAAA,EAAK;AACZ,MAAA,OAAA,CAAQ,KAAA,CAAM,+DAA+D,GAAG,CAAA;AAChF,MAAA,OAAO,YAAA,CAAa,GAAA,EAAK,EAAE,KAAA,EAAO,uBAAuB,CAAA;AAAA,IAC3D;AAAA,EACF,CAAA;AACF","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 };
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export { P as PORTFOLIO_CACHE_TAG } from './cache-CE_ujDK4.js';
|
|
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 };
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { PORTFOLIO_CACHE_TAG } from './chunk-N7TVKUYQ.js';
|
|
2
|
+
export { PORTFOLIO_CACHE_TAG } from './chunk-N7TVKUYQ.js';
|
|
3
|
+
|
|
4
|
+
// src/revalidate.ts
|
|
5
|
+
function asStringArray(value) {
|
|
6
|
+
if (typeof value === "string") return [value];
|
|
7
|
+
if (Array.isArray(value)) {
|
|
8
|
+
return value.filter((v) => typeof v === "string" && v.length > 0);
|
|
9
|
+
}
|
|
10
|
+
return [];
|
|
11
|
+
}
|
|
12
|
+
function jsonResponse(status, body) {
|
|
13
|
+
return new Response(JSON.stringify(body), {
|
|
14
|
+
status,
|
|
15
|
+
headers: { "Content-Type": "application/json" }
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
function createRevalidateHandler(options = {}) {
|
|
19
|
+
const basePath = (options.basePath ?? "/work").replace(/\/+$/, "") || "/work";
|
|
20
|
+
return async function POST(request) {
|
|
21
|
+
let body = {};
|
|
22
|
+
try {
|
|
23
|
+
body = await request.json();
|
|
24
|
+
} catch {
|
|
25
|
+
}
|
|
26
|
+
const acceptedSecrets = [
|
|
27
|
+
options.secret,
|
|
28
|
+
process.env.SONOR_API_KEY,
|
|
29
|
+
process.env.UPTRADE_API_KEY,
|
|
30
|
+
process.env.NEXT_PUBLIC_UPTRADE_API_KEY,
|
|
31
|
+
process.env.REVALIDATION_SECRET
|
|
32
|
+
].filter((s) => !!s);
|
|
33
|
+
if (acceptedSecrets.length === 0) {
|
|
34
|
+
console.error(
|
|
35
|
+
"[@sonordev/agency-site-kit/revalidate] No secret configured \u2014 set SONOR_API_KEY or pass { secret } to createRevalidateHandler."
|
|
36
|
+
);
|
|
37
|
+
return jsonResponse(401, { error: "Revalidation endpoint not configured" });
|
|
38
|
+
}
|
|
39
|
+
const bearer = request.headers.get("authorization")?.replace(/^Bearer\s+/i, "").trim();
|
|
40
|
+
const url = new URL(request.url);
|
|
41
|
+
const token = request.headers.get("x-api-key")?.trim() || bearer || url.searchParams.get("secret") || (typeof body.secret === "string" ? body.secret : void 0);
|
|
42
|
+
if (!token || !acceptedSecrets.includes(token)) {
|
|
43
|
+
return jsonResponse(401, {
|
|
44
|
+
error: "Unauthorized \u2014 use Authorization: Bearer <SONOR_API_KEY>, x-api-key, or secret"
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
const { revalidatePath, revalidateTag } = await import('next/cache');
|
|
49
|
+
const revalidated = [];
|
|
50
|
+
const tags = [.../* @__PURE__ */ new Set([
|
|
51
|
+
...asStringArray(body.tags),
|
|
52
|
+
...asStringArray(body.tag)
|
|
53
|
+
])];
|
|
54
|
+
const effectiveTags = tags.length > 0 ? tags : options.defaultTags ?? [PORTFOLIO_CACHE_TAG];
|
|
55
|
+
for (const tag of effectiveTags) {
|
|
56
|
+
revalidateTag(tag, "max");
|
|
57
|
+
revalidated.push(`tag:${tag}`);
|
|
58
|
+
}
|
|
59
|
+
if (body.revalidateAll === true) {
|
|
60
|
+
revalidatePath("/", "layout");
|
|
61
|
+
revalidated.push("layout:/");
|
|
62
|
+
}
|
|
63
|
+
const slugPaths = [...asStringArray(body.slug), ...asStringArray(body.slugs)].map(
|
|
64
|
+
(slug) => `${basePath}/${slug.replace(/^\/+/, "")}`
|
|
65
|
+
);
|
|
66
|
+
const paths = /* @__PURE__ */ new Set([
|
|
67
|
+
basePath,
|
|
68
|
+
"/sitemap.xml",
|
|
69
|
+
...options.extraPaths ?? [],
|
|
70
|
+
...asStringArray(body.paths),
|
|
71
|
+
...asStringArray(body.path),
|
|
72
|
+
...slugPaths
|
|
73
|
+
]);
|
|
74
|
+
for (const p of paths) {
|
|
75
|
+
const normalized = p.startsWith("/") ? p : `/${p}`;
|
|
76
|
+
try {
|
|
77
|
+
revalidatePath(normalized, "page");
|
|
78
|
+
revalidated.push(normalized);
|
|
79
|
+
} catch (err) {
|
|
80
|
+
console.warn(`[@sonordev/agency-site-kit/revalidate] Failed to revalidate ${normalized}:`, err);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return jsonResponse(200, { revalidated: true, targets: revalidated });
|
|
84
|
+
} catch (err) {
|
|
85
|
+
console.error("[@sonordev/agency-site-kit/revalidate] Revalidation failed:", err);
|
|
86
|
+
return jsonResponse(500, { error: "Revalidation failed" });
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export { createRevalidateHandler };
|
|
92
|
+
//# sourceMappingURL=revalidate.js.map
|
|
93
|
+
//# sourceMappingURL=revalidate.js.map
|