@seo-console/package 1.0.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.
@@ -0,0 +1,43 @@
1
+ import { Metadata } from 'next';
2
+
3
+ interface GenerateMetadataOptions {
4
+ routePath?: string;
5
+ fallback?: Partial<Metadata>;
6
+ }
7
+ /**
8
+ * Generate Next.js metadata from SEO records
9
+ *
10
+ * @param options - Configuration options
11
+ * @param options.routePath - The route path to look up (defaults to current route)
12
+ * @param options.fallback - Fallback metadata if no SEO record is found
13
+ * @returns Next.js Metadata object
14
+ *
15
+ * @example
16
+ * ```ts
17
+ * export async function generateMetadata(): Promise<Metadata> {
18
+ * return useGenerateMetadata({
19
+ * routePath: "/about",
20
+ * fallback: {
21
+ * title: "About Us",
22
+ * description: "Learn more about our company"
23
+ * }
24
+ * });
25
+ * }
26
+ * ```
27
+ */
28
+ declare function useGenerateMetadata(options?: GenerateMetadataOptions): Promise<Metadata>;
29
+ /**
30
+ * Helper to get route path from Next.js params
31
+ * Useful for dynamic routes
32
+ *
33
+ * @example
34
+ * ```ts
35
+ * export async function generateMetadata({ params }: { params: { slug: string } }): Promise<Metadata> {
36
+ * const routePath = getRoutePathFromParams(params, "/blog/[slug]");
37
+ * return useGenerateMetadata({ routePath });
38
+ * }
39
+ * ```
40
+ */
41
+ declare function getRoutePathFromParams(params: Record<string, string | string[]>, pattern: string): string;
42
+
43
+ export { type GenerateMetadataOptions, getRoutePathFromParams, useGenerateMetadata };
@@ -0,0 +1,43 @@
1
+ import { Metadata } from 'next';
2
+
3
+ interface GenerateMetadataOptions {
4
+ routePath?: string;
5
+ fallback?: Partial<Metadata>;
6
+ }
7
+ /**
8
+ * Generate Next.js metadata from SEO records
9
+ *
10
+ * @param options - Configuration options
11
+ * @param options.routePath - The route path to look up (defaults to current route)
12
+ * @param options.fallback - Fallback metadata if no SEO record is found
13
+ * @returns Next.js Metadata object
14
+ *
15
+ * @example
16
+ * ```ts
17
+ * export async function generateMetadata(): Promise<Metadata> {
18
+ * return useGenerateMetadata({
19
+ * routePath: "/about",
20
+ * fallback: {
21
+ * title: "About Us",
22
+ * description: "Learn more about our company"
23
+ * }
24
+ * });
25
+ * }
26
+ * ```
27
+ */
28
+ declare function useGenerateMetadata(options?: GenerateMetadataOptions): Promise<Metadata>;
29
+ /**
30
+ * Helper to get route path from Next.js params
31
+ * Useful for dynamic routes
32
+ *
33
+ * @example
34
+ * ```ts
35
+ * export async function generateMetadata({ params }: { params: { slug: string } }): Promise<Metadata> {
36
+ * const routePath = getRoutePathFromParams(params, "/blog/[slug]");
37
+ * return useGenerateMetadata({ routePath });
38
+ * }
39
+ * ```
40
+ */
41
+ declare function getRoutePathFromParams(params: Record<string, string | string[]>, pattern: string): string;
42
+
43
+ export { type GenerateMetadataOptions, getRoutePathFromParams, useGenerateMetadata };
@@ -0,0 +1,229 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/hooks/index.ts
21
+ var hooks_exports = {};
22
+ __export(hooks_exports, {
23
+ getRoutePathFromParams: () => getRoutePathFromParams,
24
+ useGenerateMetadata: () => useGenerateMetadata
25
+ });
26
+ module.exports = __toCommonJS(hooks_exports);
27
+
28
+ // src/lib/supabase/server.ts
29
+ var import_ssr = require("@supabase/ssr");
30
+ var import_headers = require("next/headers");
31
+ async function createClient() {
32
+ const cookieStore = await (0, import_headers.cookies)();
33
+ return (0, import_ssr.createServerClient)(
34
+ process.env.NEXT_PUBLIC_SUPABASE_URL,
35
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
36
+ {
37
+ cookies: {
38
+ getAll() {
39
+ return cookieStore.getAll();
40
+ },
41
+ setAll(cookiesToSet) {
42
+ try {
43
+ cookiesToSet.forEach(
44
+ ({ name, value, options }) => cookieStore.set(name, value, options)
45
+ );
46
+ } catch {
47
+ }
48
+ }
49
+ }
50
+ }
51
+ );
52
+ }
53
+
54
+ // src/lib/database/seo-records.ts
55
+ function transformRowToSEORecord(row) {
56
+ return {
57
+ id: row.id,
58
+ userId: row.user_id,
59
+ routePath: row.route_path,
60
+ title: row.title ?? void 0,
61
+ description: row.description ?? void 0,
62
+ keywords: row.keywords ?? void 0,
63
+ ogTitle: row.og_title ?? void 0,
64
+ ogDescription: row.og_description ?? void 0,
65
+ ogImageUrl: row.og_image_url ?? void 0,
66
+ ogImageWidth: row.og_image_width ?? void 0,
67
+ ogImageHeight: row.og_image_height ?? void 0,
68
+ ogType: row.og_type ?? void 0,
69
+ ogUrl: row.og_url ?? void 0,
70
+ ogSiteName: row.og_site_name ?? void 0,
71
+ twitterCard: row.twitter_card ?? void 0,
72
+ twitterTitle: row.twitter_title ?? void 0,
73
+ twitterDescription: row.twitter_description ?? void 0,
74
+ twitterImageUrl: row.twitter_image_url ?? void 0,
75
+ twitterSite: row.twitter_site ?? void 0,
76
+ twitterCreator: row.twitter_creator ?? void 0,
77
+ canonicalUrl: row.canonical_url ?? void 0,
78
+ robots: row.robots ?? void 0,
79
+ author: row.author ?? void 0,
80
+ publishedTime: row.published_time ? new Date(row.published_time) : void 0,
81
+ modifiedTime: row.modified_time ? new Date(row.modified_time) : void 0,
82
+ structuredData: row.structured_data ? row.structured_data : void 0,
83
+ validationStatus: row.validation_status ?? void 0,
84
+ lastValidatedAt: row.last_validated_at ? new Date(row.last_validated_at) : void 0,
85
+ validationErrors: row.validation_errors ? row.validation_errors : void 0,
86
+ createdAt: new Date(row.created_at),
87
+ updatedAt: new Date(row.updated_at)
88
+ };
89
+ }
90
+ async function getSEORecordByRoute(routePath) {
91
+ try {
92
+ const supabase = await createClient();
93
+ const {
94
+ data: { user }
95
+ } = await supabase.auth.getUser();
96
+ if (!user) {
97
+ return {
98
+ success: false,
99
+ error: new Error("User not authenticated")
100
+ };
101
+ }
102
+ const { data, error } = await supabase.from("seo_records").select("*").eq("route_path", routePath).eq("user_id", user.id).maybeSingle();
103
+ if (error) {
104
+ return { success: false, error };
105
+ }
106
+ if (!data) {
107
+ return { success: true, data: null };
108
+ }
109
+ return { success: true, data: transformRowToSEORecord(data) };
110
+ } catch (error) {
111
+ return {
112
+ success: false,
113
+ error: error instanceof Error ? error : new Error("Unknown error")
114
+ };
115
+ }
116
+ }
117
+
118
+ // src/hooks/useGenerateMetadata.ts
119
+ async function useGenerateMetadata(options = {}) {
120
+ const { routePath, fallback = {} } = options;
121
+ if (!routePath) {
122
+ return {
123
+ title: fallback.title,
124
+ description: fallback.description,
125
+ ...fallback
126
+ };
127
+ }
128
+ const result = await getSEORecordByRoute(routePath);
129
+ if (!result.success || !result.data) {
130
+ return {
131
+ title: fallback.title,
132
+ description: fallback.description,
133
+ ...fallback
134
+ };
135
+ }
136
+ const record = result.data;
137
+ const metadata = {};
138
+ if (record.title) {
139
+ metadata.title = record.title;
140
+ }
141
+ if (record.description) {
142
+ metadata.description = record.description;
143
+ }
144
+ if (record.keywords && record.keywords.length > 0) {
145
+ metadata.keywords = record.keywords;
146
+ }
147
+ if (record.author) {
148
+ metadata.authors = [{ name: record.author }];
149
+ }
150
+ if (record.ogTitle || record.ogDescription || record.ogImageUrl || record.ogType) {
151
+ const supportedOGTypes = ["website", "article", "book", "profile"];
152
+ const ogType = record.ogType && supportedOGTypes.includes(record.ogType) ? record.ogType : "website";
153
+ const openGraph = {
154
+ type: ogType,
155
+ title: record.ogTitle || record.title || void 0,
156
+ description: record.ogDescription || record.description || void 0,
157
+ url: record.ogUrl || void 0,
158
+ siteName: record.ogSiteName || void 0
159
+ };
160
+ if (record.ogImageUrl) {
161
+ openGraph.images = [
162
+ {
163
+ url: record.ogImageUrl,
164
+ width: record.ogImageWidth || void 0,
165
+ height: record.ogImageHeight || void 0,
166
+ alt: record.ogTitle || record.title || void 0
167
+ }
168
+ ];
169
+ }
170
+ if (ogType === "article") {
171
+ const articleOpenGraph = {
172
+ ...openGraph,
173
+ ...record.publishedTime && {
174
+ publishedTime: record.publishedTime.toISOString()
175
+ },
176
+ ...record.modifiedTime && {
177
+ modifiedTime: record.modifiedTime.toISOString()
178
+ }
179
+ };
180
+ metadata.openGraph = articleOpenGraph;
181
+ } else {
182
+ metadata.openGraph = openGraph;
183
+ }
184
+ }
185
+ if (record.twitterCard || record.twitterTitle || record.twitterDescription || record.twitterImageUrl) {
186
+ metadata.twitter = {
187
+ card: record.twitterCard || "summary",
188
+ title: record.twitterTitle || record.ogTitle || record.title || void 0,
189
+ description: record.twitterDescription || record.ogDescription || record.description || void 0,
190
+ images: record.twitterImageUrl ? [record.twitterImageUrl] : void 0,
191
+ site: record.twitterSite || void 0,
192
+ creator: record.twitterCreator || void 0
193
+ };
194
+ }
195
+ if (record.canonicalUrl) {
196
+ metadata.alternates = {
197
+ canonical: record.canonicalUrl
198
+ };
199
+ }
200
+ if (record.robots) {
201
+ metadata.robots = record.robots;
202
+ }
203
+ return {
204
+ ...fallback,
205
+ ...metadata,
206
+ // Ensure title and description from record override fallback if present
207
+ title: record.title || fallback.title,
208
+ description: record.description || fallback.description,
209
+ // Merge openGraph if both exist
210
+ openGraph: fallback.openGraph ? { ...metadata.openGraph, ...fallback.openGraph } : metadata.openGraph,
211
+ // Merge twitter if both exist
212
+ twitter: fallback.twitter ? { ...metadata.twitter, ...fallback.twitter } : metadata.twitter
213
+ };
214
+ }
215
+ function getRoutePathFromParams(params, pattern) {
216
+ let routePath = pattern;
217
+ for (const [key, value] of Object.entries(params)) {
218
+ const paramValue = Array.isArray(value) ? value.join("/") : value;
219
+ routePath = routePath.replace(`[${key}]`, paramValue);
220
+ routePath = routePath.replace(`[...${key}]`, paramValue);
221
+ }
222
+ return routePath;
223
+ }
224
+ // Annotate the CommonJS export names for ESM import in node:
225
+ 0 && (module.exports = {
226
+ getRoutePathFromParams,
227
+ useGenerateMetadata
228
+ });
229
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/hooks/index.ts","../../src/lib/supabase/server.ts","../../src/lib/database/seo-records.ts","../../src/hooks/useGenerateMetadata.ts"],"sourcesContent":["export { useGenerateMetadata, getRoutePathFromParams } from \"./useGenerateMetadata\";\r\nexport type { GenerateMetadataOptions } from \"./useGenerateMetadata\";\r\n","import { createServerClient } from \"@supabase/ssr\";\nimport { cookies } from \"next/headers\";\n\nexport async function createClient() {\n const cookieStore = await cookies();\n\n return createServerClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,\n {\n cookies: {\n getAll() {\n return cookieStore.getAll();\n },\n setAll(cookiesToSet: Array<{ name: string; value: string; options?: unknown }>) {\n try {\n cookiesToSet.forEach(({ name, value, options }: { name: string; value: string; options?: unknown }) =>\n cookieStore.set(name, value, options as { [key: string]: unknown })\n );\n } catch {\n // Called from Server Component - ignore\n }\n },\n },\n }\n );\n}\n","import { createClient } from \"../supabase/server\";\r\nimport type { Database } from \"../../types/database.types\";\r\nimport type {\r\n CreateSEORecord,\r\n UpdateSEORecord,\r\n SEORecord,\r\n} from \"../validation/seo-schema\";\r\n\r\ntype SEORecordRow = Database[\"public\"][\"Tables\"][\"seo_records\"][\"Row\"];\r\ntype SEORecordInsert = Database[\"public\"][\"Tables\"][\"seo_records\"][\"Insert\"];\r\ntype SEORecordUpdate = Database[\"public\"][\"Tables\"][\"seo_records\"][\"Update\"];\r\n\r\n// Transform database row to SEORecord type\r\nfunction transformRowToSEORecord(row: SEORecordRow): SEORecord {\r\n return {\r\n id: row.id,\r\n userId: row.user_id,\r\n routePath: row.route_path,\r\n title: row.title ?? undefined,\r\n description: row.description ?? undefined,\r\n keywords: row.keywords ?? undefined,\r\n ogTitle: row.og_title ?? undefined,\r\n ogDescription: row.og_description ?? undefined,\r\n ogImageUrl: row.og_image_url ?? undefined,\r\n ogImageWidth: row.og_image_width ?? undefined,\r\n ogImageHeight: row.og_image_height ?? undefined,\r\n ogType: (row.og_type as SEORecord[\"ogType\"]) ?? undefined,\r\n ogUrl: row.og_url ?? undefined,\r\n ogSiteName: row.og_site_name ?? undefined,\r\n twitterCard: (row.twitter_card as SEORecord[\"twitterCard\"]) ?? undefined,\r\n twitterTitle: row.twitter_title ?? undefined,\r\n twitterDescription: row.twitter_description ?? undefined,\r\n twitterImageUrl: row.twitter_image_url ?? undefined,\r\n twitterSite: row.twitter_site ?? undefined,\r\n twitterCreator: row.twitter_creator ?? undefined,\r\n canonicalUrl: row.canonical_url ?? undefined,\r\n robots: row.robots ?? undefined,\r\n author: row.author ?? undefined,\r\n publishedTime: row.published_time\r\n ? new Date(row.published_time)\r\n : undefined,\r\n modifiedTime: row.modified_time\r\n ? new Date(row.modified_time)\r\n : undefined,\r\n structuredData: row.structured_data\r\n ? (row.structured_data as unknown as Record<string, unknown>)\r\n : undefined,\r\n validationStatus: (row.validation_status as SEORecord[\"validationStatus\"]) ?? undefined,\r\n lastValidatedAt: row.last_validated_at\r\n ? new Date(row.last_validated_at)\r\n : undefined,\r\n validationErrors: row.validation_errors\r\n ? (row.validation_errors as unknown as Record<string, unknown>)\r\n : undefined,\r\n createdAt: new Date(row.created_at),\r\n updatedAt: new Date(row.updated_at),\r\n };\r\n}\r\n\r\n// Transform SEORecord to database insert format\r\nfunction transformToInsert(\r\n record: CreateSEORecord\r\n): Omit<SEORecordInsert, \"id\" | \"created_at\" | \"updated_at\"> {\r\n return {\r\n user_id: record.userId,\r\n route_path: record.routePath,\r\n title: record.title ?? null,\r\n description: record.description ?? null,\r\n keywords: record.keywords ?? null,\r\n og_title: record.ogTitle ?? null,\r\n og_description: record.ogDescription ?? null,\r\n og_image_url: record.ogImageUrl ?? null,\r\n og_image_width: record.ogImageWidth ?? null,\r\n og_image_height: record.ogImageHeight ?? null,\r\n og_type: record.ogType ?? null,\r\n og_url: record.ogUrl ?? null,\r\n og_site_name: record.ogSiteName ?? null,\r\n twitter_card: record.twitterCard ?? null,\r\n twitter_title: record.twitterTitle ?? null,\r\n twitter_description: record.twitterDescription ?? null,\r\n twitter_image_url: record.twitterImageUrl ?? null,\r\n twitter_site: record.twitterSite ?? null,\r\n twitter_creator: record.twitterCreator ?? null,\r\n canonical_url: record.canonicalUrl ?? null,\r\n robots: record.robots ?? null,\r\n author: record.author ?? null,\r\n published_time: record.publishedTime?.toISOString() ?? null,\r\n modified_time: record.modifiedTime?.toISOString() ?? null,\r\n structured_data: (record.structuredData as unknown as Database[\"public\"][\"Tables\"][\"seo_records\"][\"Row\"][\"structured_data\"]) ?? null,\r\n };\r\n}\r\n\r\n// Transform SEORecord to database update format\r\nfunction transformToUpdate(\r\n record: Omit<UpdateSEORecord, \"id\">\r\n): Omit<SEORecordUpdate, \"updated_at\"> {\r\n const update: Partial<SEORecordUpdate> = {};\r\n\r\n if (record.routePath !== undefined) update.route_path = record.routePath;\r\n if (record.title !== undefined) update.title = record.title ?? null;\r\n if (record.description !== undefined)\r\n update.description = record.description ?? null;\r\n if (record.keywords !== undefined) update.keywords = record.keywords ?? null;\r\n if (record.ogTitle !== undefined) update.og_title = record.ogTitle ?? null;\r\n if (record.ogDescription !== undefined)\r\n update.og_description = record.ogDescription ?? null;\r\n if (record.ogImageUrl !== undefined)\r\n update.og_image_url = record.ogImageUrl ?? null;\r\n if (record.ogImageWidth !== undefined)\r\n update.og_image_width = record.ogImageWidth ?? null;\r\n if (record.ogImageHeight !== undefined)\r\n update.og_image_height = record.ogImageHeight ?? null;\r\n if (record.ogType !== undefined) update.og_type = record.ogType ?? null;\r\n if (record.ogUrl !== undefined) update.og_url = record.ogUrl ?? null;\r\n if (record.ogSiteName !== undefined)\r\n update.og_site_name = record.ogSiteName ?? null;\r\n if (record.twitterCard !== undefined)\r\n update.twitter_card = record.twitterCard ?? null;\r\n if (record.twitterTitle !== undefined)\r\n update.twitter_title = record.twitterTitle ?? null;\r\n if (record.twitterDescription !== undefined)\r\n update.twitter_description = record.twitterDescription ?? null;\r\n if (record.twitterImageUrl !== undefined)\r\n update.twitter_image_url = record.twitterImageUrl ?? null;\r\n if (record.twitterSite !== undefined)\r\n update.twitter_site = record.twitterSite ?? null;\r\n if (record.twitterCreator !== undefined)\r\n update.twitter_creator = record.twitterCreator ?? null;\r\n if (record.canonicalUrl !== undefined)\r\n update.canonical_url = record.canonicalUrl ?? null;\r\n if (record.robots !== undefined) update.robots = record.robots ?? null;\r\n if (record.author !== undefined) update.author = record.author ?? null;\r\n if (record.publishedTime !== undefined)\r\n update.published_time = record.publishedTime?.toISOString() ?? null;\r\n if (record.modifiedTime !== undefined)\r\n update.modified_time = record.modifiedTime?.toISOString() ?? null;\r\n if (record.structuredData !== undefined)\r\n update.structured_data = (record.structuredData as unknown as Database[\"public\"][\"Tables\"][\"seo_records\"][\"Row\"][\"structured_data\"]) ?? null;\r\n if (record.validationStatus !== undefined)\r\n update.validation_status = record.validationStatus ?? null;\r\n if (record.lastValidatedAt !== undefined)\r\n update.last_validated_at = record.lastValidatedAt?.toISOString() ?? null;\r\n if (record.validationErrors !== undefined)\r\n update.validation_errors = (record.validationErrors as unknown as Database[\"public\"][\"Tables\"][\"seo_records\"][\"Row\"][\"validation_errors\"]) ?? null;\r\n\r\n return update;\r\n}\r\n\r\n// Result type for operations\r\nexport type Result<T, E = Error> =\r\n | { success: true; data: T }\r\n | { success: false; error: E };\r\n\r\n/**\r\n * Get all SEO records for the current user\r\n */\r\nexport async function getSEORecords(): Promise<Result<SEORecord[]>> {\r\n try {\r\n const supabase = await createClient();\r\n const {\r\n data: { user },\r\n } = await supabase.auth.getUser();\r\n\r\n if (!user) {\r\n return {\r\n success: false,\r\n error: new Error(\"User not authenticated\"),\r\n };\r\n }\r\n\r\n const { data, error } = await supabase\r\n .from(\"seo_records\")\r\n .select(\"*\")\r\n .eq(\"user_id\", user.id)\r\n .order(\"created_at\", { ascending: false });\r\n\r\n if (error) {\r\n return { success: false, error };\r\n }\r\n\r\n const records = (data || []).map(transformRowToSEORecord);\r\n return { success: true, data: records };\r\n } catch (error) {\r\n return {\r\n success: false,\r\n error: error instanceof Error ? error : new Error(\"Unknown error\"),\r\n };\r\n }\r\n}\r\n\r\n/**\r\n * Get a single SEO record by ID\r\n */\r\nexport async function getSEORecordById(\r\n id: string\r\n): Promise<Result<SEORecord>> {\r\n try {\r\n const supabase = await createClient();\r\n const {\r\n data: { user },\r\n } = await supabase.auth.getUser();\r\n\r\n if (!user) {\r\n return {\r\n success: false,\r\n error: new Error(\"User not authenticated\"),\r\n };\r\n }\r\n\r\n const { data, error } = await supabase\r\n .from(\"seo_records\")\r\n .select(\"*\")\r\n .eq(\"id\", id)\r\n .eq(\"user_id\", user.id)\r\n .single();\r\n\r\n if (error) {\r\n return { success: false, error };\r\n }\r\n\r\n if (!data) {\r\n return {\r\n success: false,\r\n error: new Error(\"SEO record not found\"),\r\n };\r\n }\r\n\r\n return { success: true, data: transformRowToSEORecord(data) };\r\n } catch (error) {\r\n return {\r\n success: false,\r\n error: error instanceof Error ? error : new Error(\"Unknown error\"),\r\n };\r\n }\r\n}\r\n\r\n/**\r\n * Get SEO record by route path\r\n */\r\nexport async function getSEORecordByRoute(\r\n routePath: string\r\n): Promise<Result<SEORecord | null>> {\r\n try {\r\n const supabase = await createClient();\r\n const {\r\n data: { user },\r\n } = await supabase.auth.getUser();\r\n\r\n if (!user) {\r\n return {\r\n success: false,\r\n error: new Error(\"User not authenticated\"),\r\n };\r\n }\r\n\r\n const { data, error } = await supabase\r\n .from(\"seo_records\")\r\n .select(\"*\")\r\n .eq(\"route_path\", routePath)\r\n .eq(\"user_id\", user.id)\r\n .maybeSingle();\r\n\r\n if (error) {\r\n return { success: false, error };\r\n }\r\n\r\n if (!data) {\r\n return { success: true, data: null };\r\n }\r\n\r\n return { success: true, data: transformRowToSEORecord(data) };\r\n } catch (error) {\r\n return {\r\n success: false,\r\n error: error instanceof Error ? error : new Error(\"Unknown error\"),\r\n };\r\n }\r\n}\r\n\r\n/**\r\n * Create a new SEO record\r\n */\r\nexport async function createSEORecord(\r\n record: CreateSEORecord\r\n): Promise<Result<SEORecord>> {\r\n try {\r\n const supabase = await createClient();\r\n const {\r\n data: { user },\r\n } = await supabase.auth.getUser();\r\n\r\n if (!user) {\r\n return {\r\n success: false,\r\n error: new Error(\"User not authenticated\"),\r\n };\r\n }\r\n\r\n const insertData = transformToInsert({ ...record, userId: user.id });\r\n\r\n const { data, error } = await supabase\r\n .from(\"seo_records\")\r\n .insert(insertData)\r\n .select()\r\n .single();\r\n\r\n if (error) {\r\n return { success: false, error };\r\n }\r\n\r\n return { success: true, data: transformRowToSEORecord(data) };\r\n } catch (error) {\r\n return {\r\n success: false,\r\n error: error instanceof Error ? error : new Error(\"Unknown error\"),\r\n };\r\n }\r\n}\r\n\r\n/**\r\n * Update an existing SEO record\r\n */\r\nexport async function updateSEORecord(\r\n record: UpdateSEORecord\r\n): Promise<Result<SEORecord>> {\r\n try {\r\n const supabase = await createClient();\r\n const {\r\n data: { user },\r\n } = await supabase.auth.getUser();\r\n\r\n if (!user) {\r\n return {\r\n success: false,\r\n error: new Error(\"User not authenticated\"),\r\n };\r\n }\r\n\r\n const { id, ...updateData } = record;\r\n const transformedUpdate = transformToUpdate(updateData);\r\n\r\n const { data, error } = await supabase\r\n .from(\"seo_records\")\r\n .update(transformedUpdate)\r\n .eq(\"id\", id)\r\n .eq(\"user_id\", user.id)\r\n .select()\r\n .single();\r\n\r\n if (error) {\r\n return { success: false, error };\r\n }\r\n\r\n if (!data) {\r\n return {\r\n success: false,\r\n error: new Error(\"SEO record not found\"),\r\n };\r\n }\r\n\r\n return { success: true, data: transformRowToSEORecord(data) };\r\n } catch (error) {\r\n return {\r\n success: false,\r\n error: error instanceof Error ? error : new Error(\"Unknown error\"),\r\n };\r\n }\r\n}\r\n\r\n/**\r\n * Delete an SEO record\r\n */\r\nexport async function deleteSEORecord(id: string): Promise<Result<void>> {\r\n try {\r\n const supabase = await createClient();\r\n const {\r\n data: { user },\r\n } = await supabase.auth.getUser();\r\n\r\n if (!user) {\r\n return {\r\n success: false,\r\n error: new Error(\"User not authenticated\"),\r\n };\r\n }\r\n\r\n const { error } = await supabase\r\n .from(\"seo_records\")\r\n .delete()\r\n .eq(\"id\", id)\r\n .eq(\"user_id\", user.id);\r\n\r\n if (error) {\r\n return { success: false, error };\r\n }\r\n\r\n return { success: true, data: undefined };\r\n } catch (error) {\r\n return {\r\n success: false,\r\n error: error instanceof Error ? error : new Error(\"Unknown error\"),\r\n };\r\n }\r\n}\r\n","import type { Metadata } from \"next\";\r\nimport { getSEORecordByRoute } from \"../lib/database/seo-records\";\r\n\r\nexport interface GenerateMetadataOptions {\r\n routePath?: string;\r\n fallback?: Partial<Metadata>;\r\n}\r\n\r\n/**\r\n * Generate Next.js metadata from SEO records\r\n * \r\n * @param options - Configuration options\r\n * @param options.routePath - The route path to look up (defaults to current route)\r\n * @param options.fallback - Fallback metadata if no SEO record is found\r\n * @returns Next.js Metadata object\r\n * \r\n * @example\r\n * ```ts\r\n * export async function generateMetadata(): Promise<Metadata> {\r\n * return useGenerateMetadata({\r\n * routePath: \"/about\",\r\n * fallback: {\r\n * title: \"About Us\",\r\n * description: \"Learn more about our company\"\r\n * }\r\n * });\r\n * }\r\n * ```\r\n */\r\nexport async function useGenerateMetadata(\r\n options: GenerateMetadataOptions = {} as GenerateMetadataOptions\r\n): Promise<Metadata> {\r\n const { routePath, fallback = {} as Partial<Metadata> } = options;\r\n\r\n // If no route path provided, return fallback only\r\n if (!routePath) {\r\n return {\r\n title: fallback.title,\r\n description: fallback.description,\r\n ...fallback,\r\n };\r\n }\r\n\r\n // Fetch SEO record from database\r\n const result = await getSEORecordByRoute(routePath);\r\n\r\n if (!result.success || !result.data) {\r\n // Return fallback if record not found or error occurred\r\n return {\r\n title: fallback.title,\r\n description: fallback.description,\r\n ...fallback,\r\n };\r\n }\r\n\r\n const record = result.data;\r\n const metadata: Partial<Metadata> = {};\r\n\r\n // Basic metadata\r\n if (record.title) {\r\n metadata.title = record.title;\r\n }\r\n if (record.description) {\r\n metadata.description = record.description;\r\n }\r\n if (record.keywords && record.keywords.length > 0) {\r\n metadata.keywords = record.keywords;\r\n }\r\n if (record.author) {\r\n metadata.authors = [{ name: record.author }];\r\n }\r\n\r\n // Open Graph metadata\r\n if (\r\n record.ogTitle ||\r\n record.ogDescription ||\r\n record.ogImageUrl ||\r\n record.ogType\r\n ) {\r\n // Next.js only supports specific OG types\r\n const supportedOGTypes = [\"website\", \"article\", \"book\", \"profile\"] as const;\r\n const ogType = record.ogType && supportedOGTypes.includes(record.ogType as typeof supportedOGTypes[number])\r\n ? (record.ogType as typeof supportedOGTypes[number])\r\n : \"website\";\r\n\r\n const openGraph: NonNullable<Metadata[\"openGraph\"]> = {\r\n type: ogType,\r\n title: record.ogTitle || record.title || undefined,\r\n description: record.ogDescription || record.description || undefined,\r\n url: record.ogUrl || undefined,\r\n siteName: record.ogSiteName || undefined,\r\n };\r\n\r\n if (record.ogImageUrl) {\r\n openGraph.images = [\r\n {\r\n url: record.ogImageUrl,\r\n width: record.ogImageWidth || undefined,\r\n height: record.ogImageHeight || undefined,\r\n alt: record.ogTitle || record.title || undefined,\r\n },\r\n ];\r\n }\r\n\r\n // For article type, add published/modified times\r\n if (ogType === \"article\") {\r\n const articleOpenGraph = {\r\n ...openGraph,\r\n ...(record.publishedTime && {\r\n publishedTime: record.publishedTime.toISOString(),\r\n }),\r\n ...(record.modifiedTime && {\r\n modifiedTime: record.modifiedTime.toISOString(),\r\n }),\r\n } as Metadata[\"openGraph\"];\r\n metadata.openGraph = articleOpenGraph;\r\n } else {\r\n metadata.openGraph = openGraph;\r\n }\r\n }\r\n\r\n // Twitter Card metadata\r\n if (\r\n record.twitterCard ||\r\n record.twitterTitle ||\r\n record.twitterDescription ||\r\n record.twitterImageUrl\r\n ) {\r\n metadata.twitter = {\r\n card: record.twitterCard || \"summary\",\r\n title: record.twitterTitle || record.ogTitle || record.title || undefined,\r\n description:\r\n record.twitterDescription ||\r\n record.ogDescription ||\r\n record.description ||\r\n undefined,\r\n images: record.twitterImageUrl\r\n ? [record.twitterImageUrl]\r\n : undefined,\r\n site: record.twitterSite || undefined,\r\n creator: record.twitterCreator || undefined,\r\n };\r\n }\r\n\r\n // Canonical URL\r\n if (record.canonicalUrl) {\r\n metadata.alternates = {\r\n canonical: record.canonicalUrl,\r\n };\r\n }\r\n\r\n // Robots\r\n if (record.robots) {\r\n metadata.robots = record.robots as Metadata[\"robots\"];\r\n }\r\n\r\n // Merge with fallback (fallback takes precedence for missing values)\r\n return {\r\n ...fallback,\r\n ...metadata,\r\n // Ensure title and description from record override fallback if present\r\n title: record.title || fallback.title,\r\n description: record.description || fallback.description,\r\n // Merge openGraph if both exist\r\n openGraph: fallback.openGraph\r\n ? { ...metadata.openGraph, ...fallback.openGraph }\r\n : metadata.openGraph,\r\n // Merge twitter if both exist\r\n twitter: fallback.twitter\r\n ? { ...metadata.twitter, ...fallback.twitter }\r\n : metadata.twitter,\r\n };\r\n}\r\n\r\n/**\r\n * Helper to get route path from Next.js params\r\n * Useful for dynamic routes\r\n * \r\n * @example\r\n * ```ts\r\n * export async function generateMetadata({ params }: { params: { slug: string } }): Promise<Metadata> {\r\n * const routePath = getRoutePathFromParams(params, \"/blog/[slug]\");\r\n * return useGenerateMetadata({ routePath });\r\n * }\r\n * ```\r\n */\r\nexport function getRoutePathFromParams(\r\n params: Record<string, string | string[]>,\r\n pattern: string\r\n): string {\r\n let routePath = pattern;\r\n\r\n // Replace [param] and [...param] patterns with actual values\r\n for (const [key, value] of Object.entries(params)) {\r\n const paramValue = Array.isArray(value) ? value.join(\"/\") : value;\r\n routePath = routePath.replace(`[${key}]`, paramValue);\r\n routePath = routePath.replace(`[...${key}]`, paramValue);\r\n }\r\n\r\n return routePath;\r\n}\r\n"],"mappings":"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","names":[]}
@@ -0,0 +1,201 @@
1
+ // src/lib/supabase/server.ts
2
+ import { createServerClient } from "@supabase/ssr";
3
+ import { cookies } from "next/headers";
4
+ async function createClient() {
5
+ const cookieStore = await cookies();
6
+ return createServerClient(
7
+ process.env.NEXT_PUBLIC_SUPABASE_URL,
8
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
9
+ {
10
+ cookies: {
11
+ getAll() {
12
+ return cookieStore.getAll();
13
+ },
14
+ setAll(cookiesToSet) {
15
+ try {
16
+ cookiesToSet.forEach(
17
+ ({ name, value, options }) => cookieStore.set(name, value, options)
18
+ );
19
+ } catch {
20
+ }
21
+ }
22
+ }
23
+ }
24
+ );
25
+ }
26
+
27
+ // src/lib/database/seo-records.ts
28
+ function transformRowToSEORecord(row) {
29
+ return {
30
+ id: row.id,
31
+ userId: row.user_id,
32
+ routePath: row.route_path,
33
+ title: row.title ?? void 0,
34
+ description: row.description ?? void 0,
35
+ keywords: row.keywords ?? void 0,
36
+ ogTitle: row.og_title ?? void 0,
37
+ ogDescription: row.og_description ?? void 0,
38
+ ogImageUrl: row.og_image_url ?? void 0,
39
+ ogImageWidth: row.og_image_width ?? void 0,
40
+ ogImageHeight: row.og_image_height ?? void 0,
41
+ ogType: row.og_type ?? void 0,
42
+ ogUrl: row.og_url ?? void 0,
43
+ ogSiteName: row.og_site_name ?? void 0,
44
+ twitterCard: row.twitter_card ?? void 0,
45
+ twitterTitle: row.twitter_title ?? void 0,
46
+ twitterDescription: row.twitter_description ?? void 0,
47
+ twitterImageUrl: row.twitter_image_url ?? void 0,
48
+ twitterSite: row.twitter_site ?? void 0,
49
+ twitterCreator: row.twitter_creator ?? void 0,
50
+ canonicalUrl: row.canonical_url ?? void 0,
51
+ robots: row.robots ?? void 0,
52
+ author: row.author ?? void 0,
53
+ publishedTime: row.published_time ? new Date(row.published_time) : void 0,
54
+ modifiedTime: row.modified_time ? new Date(row.modified_time) : void 0,
55
+ structuredData: row.structured_data ? row.structured_data : void 0,
56
+ validationStatus: row.validation_status ?? void 0,
57
+ lastValidatedAt: row.last_validated_at ? new Date(row.last_validated_at) : void 0,
58
+ validationErrors: row.validation_errors ? row.validation_errors : void 0,
59
+ createdAt: new Date(row.created_at),
60
+ updatedAt: new Date(row.updated_at)
61
+ };
62
+ }
63
+ async function getSEORecordByRoute(routePath) {
64
+ try {
65
+ const supabase = await createClient();
66
+ const {
67
+ data: { user }
68
+ } = await supabase.auth.getUser();
69
+ if (!user) {
70
+ return {
71
+ success: false,
72
+ error: new Error("User not authenticated")
73
+ };
74
+ }
75
+ const { data, error } = await supabase.from("seo_records").select("*").eq("route_path", routePath).eq("user_id", user.id).maybeSingle();
76
+ if (error) {
77
+ return { success: false, error };
78
+ }
79
+ if (!data) {
80
+ return { success: true, data: null };
81
+ }
82
+ return { success: true, data: transformRowToSEORecord(data) };
83
+ } catch (error) {
84
+ return {
85
+ success: false,
86
+ error: error instanceof Error ? error : new Error("Unknown error")
87
+ };
88
+ }
89
+ }
90
+
91
+ // src/hooks/useGenerateMetadata.ts
92
+ async function useGenerateMetadata(options = {}) {
93
+ const { routePath, fallback = {} } = options;
94
+ if (!routePath) {
95
+ return {
96
+ title: fallback.title,
97
+ description: fallback.description,
98
+ ...fallback
99
+ };
100
+ }
101
+ const result = await getSEORecordByRoute(routePath);
102
+ if (!result.success || !result.data) {
103
+ return {
104
+ title: fallback.title,
105
+ description: fallback.description,
106
+ ...fallback
107
+ };
108
+ }
109
+ const record = result.data;
110
+ const metadata = {};
111
+ if (record.title) {
112
+ metadata.title = record.title;
113
+ }
114
+ if (record.description) {
115
+ metadata.description = record.description;
116
+ }
117
+ if (record.keywords && record.keywords.length > 0) {
118
+ metadata.keywords = record.keywords;
119
+ }
120
+ if (record.author) {
121
+ metadata.authors = [{ name: record.author }];
122
+ }
123
+ if (record.ogTitle || record.ogDescription || record.ogImageUrl || record.ogType) {
124
+ const supportedOGTypes = ["website", "article", "book", "profile"];
125
+ const ogType = record.ogType && supportedOGTypes.includes(record.ogType) ? record.ogType : "website";
126
+ const openGraph = {
127
+ type: ogType,
128
+ title: record.ogTitle || record.title || void 0,
129
+ description: record.ogDescription || record.description || void 0,
130
+ url: record.ogUrl || void 0,
131
+ siteName: record.ogSiteName || void 0
132
+ };
133
+ if (record.ogImageUrl) {
134
+ openGraph.images = [
135
+ {
136
+ url: record.ogImageUrl,
137
+ width: record.ogImageWidth || void 0,
138
+ height: record.ogImageHeight || void 0,
139
+ alt: record.ogTitle || record.title || void 0
140
+ }
141
+ ];
142
+ }
143
+ if (ogType === "article") {
144
+ const articleOpenGraph = {
145
+ ...openGraph,
146
+ ...record.publishedTime && {
147
+ publishedTime: record.publishedTime.toISOString()
148
+ },
149
+ ...record.modifiedTime && {
150
+ modifiedTime: record.modifiedTime.toISOString()
151
+ }
152
+ };
153
+ metadata.openGraph = articleOpenGraph;
154
+ } else {
155
+ metadata.openGraph = openGraph;
156
+ }
157
+ }
158
+ if (record.twitterCard || record.twitterTitle || record.twitterDescription || record.twitterImageUrl) {
159
+ metadata.twitter = {
160
+ card: record.twitterCard || "summary",
161
+ title: record.twitterTitle || record.ogTitle || record.title || void 0,
162
+ description: record.twitterDescription || record.ogDescription || record.description || void 0,
163
+ images: record.twitterImageUrl ? [record.twitterImageUrl] : void 0,
164
+ site: record.twitterSite || void 0,
165
+ creator: record.twitterCreator || void 0
166
+ };
167
+ }
168
+ if (record.canonicalUrl) {
169
+ metadata.alternates = {
170
+ canonical: record.canonicalUrl
171
+ };
172
+ }
173
+ if (record.robots) {
174
+ metadata.robots = record.robots;
175
+ }
176
+ return {
177
+ ...fallback,
178
+ ...metadata,
179
+ // Ensure title and description from record override fallback if present
180
+ title: record.title || fallback.title,
181
+ description: record.description || fallback.description,
182
+ // Merge openGraph if both exist
183
+ openGraph: fallback.openGraph ? { ...metadata.openGraph, ...fallback.openGraph } : metadata.openGraph,
184
+ // Merge twitter if both exist
185
+ twitter: fallback.twitter ? { ...metadata.twitter, ...fallback.twitter } : metadata.twitter
186
+ };
187
+ }
188
+ function getRoutePathFromParams(params, pattern) {
189
+ let routePath = pattern;
190
+ for (const [key, value] of Object.entries(params)) {
191
+ const paramValue = Array.isArray(value) ? value.join("/") : value;
192
+ routePath = routePath.replace(`[${key}]`, paramValue);
193
+ routePath = routePath.replace(`[...${key}]`, paramValue);
194
+ }
195
+ return routePath;
196
+ }
197
+ export {
198
+ getRoutePathFromParams,
199
+ useGenerateMetadata
200
+ };
201
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/lib/supabase/server.ts","../../src/lib/database/seo-records.ts","../../src/hooks/useGenerateMetadata.ts"],"sourcesContent":["import { createServerClient } from \"@supabase/ssr\";\nimport { cookies } from \"next/headers\";\n\nexport async function createClient() {\n const cookieStore = await cookies();\n\n return createServerClient(\n process.env.NEXT_PUBLIC_SUPABASE_URL!,\n process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,\n {\n cookies: {\n getAll() {\n return cookieStore.getAll();\n },\n setAll(cookiesToSet: Array<{ name: string; value: string; options?: unknown }>) {\n try {\n cookiesToSet.forEach(({ name, value, options }: { name: string; value: string; options?: unknown }) =>\n cookieStore.set(name, value, options as { [key: string]: unknown })\n );\n } catch {\n // Called from Server Component - ignore\n }\n },\n },\n }\n );\n}\n","import { createClient } from \"../supabase/server\";\r\nimport type { Database } from \"../../types/database.types\";\r\nimport type {\r\n CreateSEORecord,\r\n UpdateSEORecord,\r\n SEORecord,\r\n} from \"../validation/seo-schema\";\r\n\r\ntype SEORecordRow = Database[\"public\"][\"Tables\"][\"seo_records\"][\"Row\"];\r\ntype SEORecordInsert = Database[\"public\"][\"Tables\"][\"seo_records\"][\"Insert\"];\r\ntype SEORecordUpdate = Database[\"public\"][\"Tables\"][\"seo_records\"][\"Update\"];\r\n\r\n// Transform database row to SEORecord type\r\nfunction transformRowToSEORecord(row: SEORecordRow): SEORecord {\r\n return {\r\n id: row.id,\r\n userId: row.user_id,\r\n routePath: row.route_path,\r\n title: row.title ?? undefined,\r\n description: row.description ?? undefined,\r\n keywords: row.keywords ?? undefined,\r\n ogTitle: row.og_title ?? undefined,\r\n ogDescription: row.og_description ?? undefined,\r\n ogImageUrl: row.og_image_url ?? undefined,\r\n ogImageWidth: row.og_image_width ?? undefined,\r\n ogImageHeight: row.og_image_height ?? undefined,\r\n ogType: (row.og_type as SEORecord[\"ogType\"]) ?? undefined,\r\n ogUrl: row.og_url ?? undefined,\r\n ogSiteName: row.og_site_name ?? undefined,\r\n twitterCard: (row.twitter_card as SEORecord[\"twitterCard\"]) ?? undefined,\r\n twitterTitle: row.twitter_title ?? undefined,\r\n twitterDescription: row.twitter_description ?? undefined,\r\n twitterImageUrl: row.twitter_image_url ?? undefined,\r\n twitterSite: row.twitter_site ?? undefined,\r\n twitterCreator: row.twitter_creator ?? undefined,\r\n canonicalUrl: row.canonical_url ?? undefined,\r\n robots: row.robots ?? undefined,\r\n author: row.author ?? undefined,\r\n publishedTime: row.published_time\r\n ? new Date(row.published_time)\r\n : undefined,\r\n modifiedTime: row.modified_time\r\n ? new Date(row.modified_time)\r\n : undefined,\r\n structuredData: row.structured_data\r\n ? (row.structured_data as unknown as Record<string, unknown>)\r\n : undefined,\r\n validationStatus: (row.validation_status as SEORecord[\"validationStatus\"]) ?? undefined,\r\n lastValidatedAt: row.last_validated_at\r\n ? new Date(row.last_validated_at)\r\n : undefined,\r\n validationErrors: row.validation_errors\r\n ? (row.validation_errors as unknown as Record<string, unknown>)\r\n : undefined,\r\n createdAt: new Date(row.created_at),\r\n updatedAt: new Date(row.updated_at),\r\n };\r\n}\r\n\r\n// Transform SEORecord to database insert format\r\nfunction transformToInsert(\r\n record: CreateSEORecord\r\n): Omit<SEORecordInsert, \"id\" | \"created_at\" | \"updated_at\"> {\r\n return {\r\n user_id: record.userId,\r\n route_path: record.routePath,\r\n title: record.title ?? null,\r\n description: record.description ?? null,\r\n keywords: record.keywords ?? null,\r\n og_title: record.ogTitle ?? null,\r\n og_description: record.ogDescription ?? null,\r\n og_image_url: record.ogImageUrl ?? null,\r\n og_image_width: record.ogImageWidth ?? null,\r\n og_image_height: record.ogImageHeight ?? null,\r\n og_type: record.ogType ?? null,\r\n og_url: record.ogUrl ?? null,\r\n og_site_name: record.ogSiteName ?? null,\r\n twitter_card: record.twitterCard ?? null,\r\n twitter_title: record.twitterTitle ?? null,\r\n twitter_description: record.twitterDescription ?? null,\r\n twitter_image_url: record.twitterImageUrl ?? null,\r\n twitter_site: record.twitterSite ?? null,\r\n twitter_creator: record.twitterCreator ?? null,\r\n canonical_url: record.canonicalUrl ?? null,\r\n robots: record.robots ?? null,\r\n author: record.author ?? null,\r\n published_time: record.publishedTime?.toISOString() ?? null,\r\n modified_time: record.modifiedTime?.toISOString() ?? null,\r\n structured_data: (record.structuredData as unknown as Database[\"public\"][\"Tables\"][\"seo_records\"][\"Row\"][\"structured_data\"]) ?? null,\r\n };\r\n}\r\n\r\n// Transform SEORecord to database update format\r\nfunction transformToUpdate(\r\n record: Omit<UpdateSEORecord, \"id\">\r\n): Omit<SEORecordUpdate, \"updated_at\"> {\r\n const update: Partial<SEORecordUpdate> = {};\r\n\r\n if (record.routePath !== undefined) update.route_path = record.routePath;\r\n if (record.title !== undefined) update.title = record.title ?? null;\r\n if (record.description !== undefined)\r\n update.description = record.description ?? null;\r\n if (record.keywords !== undefined) update.keywords = record.keywords ?? null;\r\n if (record.ogTitle !== undefined) update.og_title = record.ogTitle ?? null;\r\n if (record.ogDescription !== undefined)\r\n update.og_description = record.ogDescription ?? null;\r\n if (record.ogImageUrl !== undefined)\r\n update.og_image_url = record.ogImageUrl ?? null;\r\n if (record.ogImageWidth !== undefined)\r\n update.og_image_width = record.ogImageWidth ?? null;\r\n if (record.ogImageHeight !== undefined)\r\n update.og_image_height = record.ogImageHeight ?? null;\r\n if (record.ogType !== undefined) update.og_type = record.ogType ?? null;\r\n if (record.ogUrl !== undefined) update.og_url = record.ogUrl ?? null;\r\n if (record.ogSiteName !== undefined)\r\n update.og_site_name = record.ogSiteName ?? null;\r\n if (record.twitterCard !== undefined)\r\n update.twitter_card = record.twitterCard ?? null;\r\n if (record.twitterTitle !== undefined)\r\n update.twitter_title = record.twitterTitle ?? null;\r\n if (record.twitterDescription !== undefined)\r\n update.twitter_description = record.twitterDescription ?? null;\r\n if (record.twitterImageUrl !== undefined)\r\n update.twitter_image_url = record.twitterImageUrl ?? null;\r\n if (record.twitterSite !== undefined)\r\n update.twitter_site = record.twitterSite ?? null;\r\n if (record.twitterCreator !== undefined)\r\n update.twitter_creator = record.twitterCreator ?? null;\r\n if (record.canonicalUrl !== undefined)\r\n update.canonical_url = record.canonicalUrl ?? null;\r\n if (record.robots !== undefined) update.robots = record.robots ?? null;\r\n if (record.author !== undefined) update.author = record.author ?? null;\r\n if (record.publishedTime !== undefined)\r\n update.published_time = record.publishedTime?.toISOString() ?? null;\r\n if (record.modifiedTime !== undefined)\r\n update.modified_time = record.modifiedTime?.toISOString() ?? null;\r\n if (record.structuredData !== undefined)\r\n update.structured_data = (record.structuredData as unknown as Database[\"public\"][\"Tables\"][\"seo_records\"][\"Row\"][\"structured_data\"]) ?? null;\r\n if (record.validationStatus !== undefined)\r\n update.validation_status = record.validationStatus ?? null;\r\n if (record.lastValidatedAt !== undefined)\r\n update.last_validated_at = record.lastValidatedAt?.toISOString() ?? null;\r\n if (record.validationErrors !== undefined)\r\n update.validation_errors = (record.validationErrors as unknown as Database[\"public\"][\"Tables\"][\"seo_records\"][\"Row\"][\"validation_errors\"]) ?? null;\r\n\r\n return update;\r\n}\r\n\r\n// Result type for operations\r\nexport type Result<T, E = Error> =\r\n | { success: true; data: T }\r\n | { success: false; error: E };\r\n\r\n/**\r\n * Get all SEO records for the current user\r\n */\r\nexport async function getSEORecords(): Promise<Result<SEORecord[]>> {\r\n try {\r\n const supabase = await createClient();\r\n const {\r\n data: { user },\r\n } = await supabase.auth.getUser();\r\n\r\n if (!user) {\r\n return {\r\n success: false,\r\n error: new Error(\"User not authenticated\"),\r\n };\r\n }\r\n\r\n const { data, error } = await supabase\r\n .from(\"seo_records\")\r\n .select(\"*\")\r\n .eq(\"user_id\", user.id)\r\n .order(\"created_at\", { ascending: false });\r\n\r\n if (error) {\r\n return { success: false, error };\r\n }\r\n\r\n const records = (data || []).map(transformRowToSEORecord);\r\n return { success: true, data: records };\r\n } catch (error) {\r\n return {\r\n success: false,\r\n error: error instanceof Error ? error : new Error(\"Unknown error\"),\r\n };\r\n }\r\n}\r\n\r\n/**\r\n * Get a single SEO record by ID\r\n */\r\nexport async function getSEORecordById(\r\n id: string\r\n): Promise<Result<SEORecord>> {\r\n try {\r\n const supabase = await createClient();\r\n const {\r\n data: { user },\r\n } = await supabase.auth.getUser();\r\n\r\n if (!user) {\r\n return {\r\n success: false,\r\n error: new Error(\"User not authenticated\"),\r\n };\r\n }\r\n\r\n const { data, error } = await supabase\r\n .from(\"seo_records\")\r\n .select(\"*\")\r\n .eq(\"id\", id)\r\n .eq(\"user_id\", user.id)\r\n .single();\r\n\r\n if (error) {\r\n return { success: false, error };\r\n }\r\n\r\n if (!data) {\r\n return {\r\n success: false,\r\n error: new Error(\"SEO record not found\"),\r\n };\r\n }\r\n\r\n return { success: true, data: transformRowToSEORecord(data) };\r\n } catch (error) {\r\n return {\r\n success: false,\r\n error: error instanceof Error ? error : new Error(\"Unknown error\"),\r\n };\r\n }\r\n}\r\n\r\n/**\r\n * Get SEO record by route path\r\n */\r\nexport async function getSEORecordByRoute(\r\n routePath: string\r\n): Promise<Result<SEORecord | null>> {\r\n try {\r\n const supabase = await createClient();\r\n const {\r\n data: { user },\r\n } = await supabase.auth.getUser();\r\n\r\n if (!user) {\r\n return {\r\n success: false,\r\n error: new Error(\"User not authenticated\"),\r\n };\r\n }\r\n\r\n const { data, error } = await supabase\r\n .from(\"seo_records\")\r\n .select(\"*\")\r\n .eq(\"route_path\", routePath)\r\n .eq(\"user_id\", user.id)\r\n .maybeSingle();\r\n\r\n if (error) {\r\n return { success: false, error };\r\n }\r\n\r\n if (!data) {\r\n return { success: true, data: null };\r\n }\r\n\r\n return { success: true, data: transformRowToSEORecord(data) };\r\n } catch (error) {\r\n return {\r\n success: false,\r\n error: error instanceof Error ? error : new Error(\"Unknown error\"),\r\n };\r\n }\r\n}\r\n\r\n/**\r\n * Create a new SEO record\r\n */\r\nexport async function createSEORecord(\r\n record: CreateSEORecord\r\n): Promise<Result<SEORecord>> {\r\n try {\r\n const supabase = await createClient();\r\n const {\r\n data: { user },\r\n } = await supabase.auth.getUser();\r\n\r\n if (!user) {\r\n return {\r\n success: false,\r\n error: new Error(\"User not authenticated\"),\r\n };\r\n }\r\n\r\n const insertData = transformToInsert({ ...record, userId: user.id });\r\n\r\n const { data, error } = await supabase\r\n .from(\"seo_records\")\r\n .insert(insertData)\r\n .select()\r\n .single();\r\n\r\n if (error) {\r\n return { success: false, error };\r\n }\r\n\r\n return { success: true, data: transformRowToSEORecord(data) };\r\n } catch (error) {\r\n return {\r\n success: false,\r\n error: error instanceof Error ? error : new Error(\"Unknown error\"),\r\n };\r\n }\r\n}\r\n\r\n/**\r\n * Update an existing SEO record\r\n */\r\nexport async function updateSEORecord(\r\n record: UpdateSEORecord\r\n): Promise<Result<SEORecord>> {\r\n try {\r\n const supabase = await createClient();\r\n const {\r\n data: { user },\r\n } = await supabase.auth.getUser();\r\n\r\n if (!user) {\r\n return {\r\n success: false,\r\n error: new Error(\"User not authenticated\"),\r\n };\r\n }\r\n\r\n const { id, ...updateData } = record;\r\n const transformedUpdate = transformToUpdate(updateData);\r\n\r\n const { data, error } = await supabase\r\n .from(\"seo_records\")\r\n .update(transformedUpdate)\r\n .eq(\"id\", id)\r\n .eq(\"user_id\", user.id)\r\n .select()\r\n .single();\r\n\r\n if (error) {\r\n return { success: false, error };\r\n }\r\n\r\n if (!data) {\r\n return {\r\n success: false,\r\n error: new Error(\"SEO record not found\"),\r\n };\r\n }\r\n\r\n return { success: true, data: transformRowToSEORecord(data) };\r\n } catch (error) {\r\n return {\r\n success: false,\r\n error: error instanceof Error ? error : new Error(\"Unknown error\"),\r\n };\r\n }\r\n}\r\n\r\n/**\r\n * Delete an SEO record\r\n */\r\nexport async function deleteSEORecord(id: string): Promise<Result<void>> {\r\n try {\r\n const supabase = await createClient();\r\n const {\r\n data: { user },\r\n } = await supabase.auth.getUser();\r\n\r\n if (!user) {\r\n return {\r\n success: false,\r\n error: new Error(\"User not authenticated\"),\r\n };\r\n }\r\n\r\n const { error } = await supabase\r\n .from(\"seo_records\")\r\n .delete()\r\n .eq(\"id\", id)\r\n .eq(\"user_id\", user.id);\r\n\r\n if (error) {\r\n return { success: false, error };\r\n }\r\n\r\n return { success: true, data: undefined };\r\n } catch (error) {\r\n return {\r\n success: false,\r\n error: error instanceof Error ? error : new Error(\"Unknown error\"),\r\n };\r\n }\r\n}\r\n","import type { Metadata } from \"next\";\r\nimport { getSEORecordByRoute } from \"../lib/database/seo-records\";\r\n\r\nexport interface GenerateMetadataOptions {\r\n routePath?: string;\r\n fallback?: Partial<Metadata>;\r\n}\r\n\r\n/**\r\n * Generate Next.js metadata from SEO records\r\n * \r\n * @param options - Configuration options\r\n * @param options.routePath - The route path to look up (defaults to current route)\r\n * @param options.fallback - Fallback metadata if no SEO record is found\r\n * @returns Next.js Metadata object\r\n * \r\n * @example\r\n * ```ts\r\n * export async function generateMetadata(): Promise<Metadata> {\r\n * return useGenerateMetadata({\r\n * routePath: \"/about\",\r\n * fallback: {\r\n * title: \"About Us\",\r\n * description: \"Learn more about our company\"\r\n * }\r\n * });\r\n * }\r\n * ```\r\n */\r\nexport async function useGenerateMetadata(\r\n options: GenerateMetadataOptions = {} as GenerateMetadataOptions\r\n): Promise<Metadata> {\r\n const { routePath, fallback = {} as Partial<Metadata> } = options;\r\n\r\n // If no route path provided, return fallback only\r\n if (!routePath) {\r\n return {\r\n title: fallback.title,\r\n description: fallback.description,\r\n ...fallback,\r\n };\r\n }\r\n\r\n // Fetch SEO record from database\r\n const result = await getSEORecordByRoute(routePath);\r\n\r\n if (!result.success || !result.data) {\r\n // Return fallback if record not found or error occurred\r\n return {\r\n title: fallback.title,\r\n description: fallback.description,\r\n ...fallback,\r\n };\r\n }\r\n\r\n const record = result.data;\r\n const metadata: Partial<Metadata> = {};\r\n\r\n // Basic metadata\r\n if (record.title) {\r\n metadata.title = record.title;\r\n }\r\n if (record.description) {\r\n metadata.description = record.description;\r\n }\r\n if (record.keywords && record.keywords.length > 0) {\r\n metadata.keywords = record.keywords;\r\n }\r\n if (record.author) {\r\n metadata.authors = [{ name: record.author }];\r\n }\r\n\r\n // Open Graph metadata\r\n if (\r\n record.ogTitle ||\r\n record.ogDescription ||\r\n record.ogImageUrl ||\r\n record.ogType\r\n ) {\r\n // Next.js only supports specific OG types\r\n const supportedOGTypes = [\"website\", \"article\", \"book\", \"profile\"] as const;\r\n const ogType = record.ogType && supportedOGTypes.includes(record.ogType as typeof supportedOGTypes[number])\r\n ? (record.ogType as typeof supportedOGTypes[number])\r\n : \"website\";\r\n\r\n const openGraph: NonNullable<Metadata[\"openGraph\"]> = {\r\n type: ogType,\r\n title: record.ogTitle || record.title || undefined,\r\n description: record.ogDescription || record.description || undefined,\r\n url: record.ogUrl || undefined,\r\n siteName: record.ogSiteName || undefined,\r\n };\r\n\r\n if (record.ogImageUrl) {\r\n openGraph.images = [\r\n {\r\n url: record.ogImageUrl,\r\n width: record.ogImageWidth || undefined,\r\n height: record.ogImageHeight || undefined,\r\n alt: record.ogTitle || record.title || undefined,\r\n },\r\n ];\r\n }\r\n\r\n // For article type, add published/modified times\r\n if (ogType === \"article\") {\r\n const articleOpenGraph = {\r\n ...openGraph,\r\n ...(record.publishedTime && {\r\n publishedTime: record.publishedTime.toISOString(),\r\n }),\r\n ...(record.modifiedTime && {\r\n modifiedTime: record.modifiedTime.toISOString(),\r\n }),\r\n } as Metadata[\"openGraph\"];\r\n metadata.openGraph = articleOpenGraph;\r\n } else {\r\n metadata.openGraph = openGraph;\r\n }\r\n }\r\n\r\n // Twitter Card metadata\r\n if (\r\n record.twitterCard ||\r\n record.twitterTitle ||\r\n record.twitterDescription ||\r\n record.twitterImageUrl\r\n ) {\r\n metadata.twitter = {\r\n card: record.twitterCard || \"summary\",\r\n title: record.twitterTitle || record.ogTitle || record.title || undefined,\r\n description:\r\n record.twitterDescription ||\r\n record.ogDescription ||\r\n record.description ||\r\n undefined,\r\n images: record.twitterImageUrl\r\n ? [record.twitterImageUrl]\r\n : undefined,\r\n site: record.twitterSite || undefined,\r\n creator: record.twitterCreator || undefined,\r\n };\r\n }\r\n\r\n // Canonical URL\r\n if (record.canonicalUrl) {\r\n metadata.alternates = {\r\n canonical: record.canonicalUrl,\r\n };\r\n }\r\n\r\n // Robots\r\n if (record.robots) {\r\n metadata.robots = record.robots as Metadata[\"robots\"];\r\n }\r\n\r\n // Merge with fallback (fallback takes precedence for missing values)\r\n return {\r\n ...fallback,\r\n ...metadata,\r\n // Ensure title and description from record override fallback if present\r\n title: record.title || fallback.title,\r\n description: record.description || fallback.description,\r\n // Merge openGraph if both exist\r\n openGraph: fallback.openGraph\r\n ? { ...metadata.openGraph, ...fallback.openGraph }\r\n : metadata.openGraph,\r\n // Merge twitter if both exist\r\n twitter: fallback.twitter\r\n ? { ...metadata.twitter, ...fallback.twitter }\r\n : metadata.twitter,\r\n };\r\n}\r\n\r\n/**\r\n * Helper to get route path from Next.js params\r\n * Useful for dynamic routes\r\n * \r\n * @example\r\n * ```ts\r\n * export async function generateMetadata({ params }: { params: { slug: string } }): Promise<Metadata> {\r\n * const routePath = getRoutePathFromParams(params, \"/blog/[slug]\");\r\n * return useGenerateMetadata({ routePath });\r\n * }\r\n * ```\r\n */\r\nexport function getRoutePathFromParams(\r\n params: Record<string, string | string[]>,\r\n pattern: string\r\n): string {\r\n let routePath = pattern;\r\n\r\n // Replace [param] and [...param] patterns with actual values\r\n for (const [key, value] of Object.entries(params)) {\r\n const paramValue = Array.isArray(value) ? value.join(\"/\") : value;\r\n routePath = routePath.replace(`[${key}]`, paramValue);\r\n routePath = routePath.replace(`[...${key}]`, paramValue);\r\n }\r\n\r\n return routePath;\r\n}\r\n"],"mappings":";AAAA,SAAS,0BAA0B;AACnC,SAAS,eAAe;AAExB,eAAsB,eAAe;AACnC,QAAM,cAAc,MAAM,QAAQ;AAElC,SAAO;AAAA,IACL,QAAQ,IAAI;AAAA,IACZ,QAAQ,IAAI;AAAA,IACZ;AAAA,MACE,SAAS;AAAA,QACP,SAAS;AACP,iBAAO,YAAY,OAAO;AAAA,QAC5B;AAAA,QACA,OAAO,cAAyE;AAC9E,cAAI;AACF,yBAAa;AAAA,cAAQ,CAAC,EAAE,MAAM,OAAO,QAAQ,MAC3C,YAAY,IAAI,MAAM,OAAO,OAAqC;AAAA,YACpE;AAAA,UACF,QAAQ;AAAA,UAER;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;;;ACbA,SAAS,wBAAwB,KAA8B;AAC7D,SAAO;AAAA,IACL,IAAI,IAAI;AAAA,IACR,QAAQ,IAAI;AAAA,IACZ,WAAW,IAAI;AAAA,IACf,OAAO,IAAI,SAAS;AAAA,IACpB,aAAa,IAAI,eAAe;AAAA,IAChC,UAAU,IAAI,YAAY;AAAA,IAC1B,SAAS,IAAI,YAAY;AAAA,IACzB,eAAe,IAAI,kBAAkB;AAAA,IACrC,YAAY,IAAI,gBAAgB;AAAA,IAChC,cAAc,IAAI,kBAAkB;AAAA,IACpC,eAAe,IAAI,mBAAmB;AAAA,IACtC,QAAS,IAAI,WAAmC;AAAA,IAChD,OAAO,IAAI,UAAU;AAAA,IACrB,YAAY,IAAI,gBAAgB;AAAA,IAChC,aAAc,IAAI,gBAA6C;AAAA,IAC/D,cAAc,IAAI,iBAAiB;AAAA,IACnC,oBAAoB,IAAI,uBAAuB;AAAA,IAC/C,iBAAiB,IAAI,qBAAqB;AAAA,IAC1C,aAAa,IAAI,gBAAgB;AAAA,IACjC,gBAAgB,IAAI,mBAAmB;AAAA,IACvC,cAAc,IAAI,iBAAiB;AAAA,IACnC,QAAQ,IAAI,UAAU;AAAA,IACtB,QAAQ,IAAI,UAAU;AAAA,IACtB,eAAe,IAAI,iBACf,IAAI,KAAK,IAAI,cAAc,IAC3B;AAAA,IACJ,cAAc,IAAI,gBACd,IAAI,KAAK,IAAI,aAAa,IAC1B;AAAA,IACJ,gBAAgB,IAAI,kBACf,IAAI,kBACL;AAAA,IACJ,kBAAmB,IAAI,qBAAuD;AAAA,IAC9E,iBAAiB,IAAI,oBACjB,IAAI,KAAK,IAAI,iBAAiB,IAC9B;AAAA,IACJ,kBAAkB,IAAI,oBACjB,IAAI,oBACL;AAAA,IACJ,WAAW,IAAI,KAAK,IAAI,UAAU;AAAA,IAClC,WAAW,IAAI,KAAK,IAAI,UAAU;AAAA,EACpC;AACF;AAsLA,eAAsB,oBACpB,WACmC;AACnC,MAAI;AACF,UAAM,WAAW,MAAM,aAAa;AACpC,UAAM;AAAA,MACJ,MAAM,EAAE,KAAK;AAAA,IACf,IAAI,MAAM,SAAS,KAAK,QAAQ;AAEhC,QAAI,CAAC,MAAM;AACT,aAAO;AAAA,QACL,SAAS;AAAA,QACT,OAAO,IAAI,MAAM,wBAAwB;AAAA,MAC3C;AAAA,IACF;AAEA,UAAM,EAAE,MAAM,MAAM,IAAI,MAAM,SAC3B,KAAK,aAAa,EAClB,OAAO,GAAG,EACV,GAAG,cAAc,SAAS,EAC1B,GAAG,WAAW,KAAK,EAAE,EACrB,YAAY;AAEf,QAAI,OAAO;AACT,aAAO,EAAE,SAAS,OAAO,MAAM;AAAA,IACjC;AAEA,QAAI,CAAC,MAAM;AACT,aAAO,EAAE,SAAS,MAAM,MAAM,KAAK;AAAA,IACrC;AAEA,WAAO,EAAE,SAAS,MAAM,MAAM,wBAAwB,IAAI,EAAE;AAAA,EAC9D,SAAS,OAAO;AACd,WAAO;AAAA,MACL,SAAS;AAAA,MACT,OAAO,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,eAAe;AAAA,IACnE;AAAA,EACF;AACF;;;ACxPA,eAAsB,oBACpB,UAAmC,CAAC,GACjB;AACnB,QAAM,EAAE,WAAW,WAAW,CAAC,EAAuB,IAAI;AAG1D,MAAI,CAAC,WAAW;AACd,WAAO;AAAA,MACL,OAAO,SAAS;AAAA,MAChB,aAAa,SAAS;AAAA,MACtB,GAAG;AAAA,IACL;AAAA,EACF;AAGA,QAAM,SAAS,MAAM,oBAAoB,SAAS;AAElD,MAAI,CAAC,OAAO,WAAW,CAAC,OAAO,MAAM;AAEnC,WAAO;AAAA,MACL,OAAO,SAAS;AAAA,MAChB,aAAa,SAAS;AAAA,MACtB,GAAG;AAAA,IACL;AAAA,EACF;AAEA,QAAM,SAAS,OAAO;AACtB,QAAM,WAA8B,CAAC;AAGrC,MAAI,OAAO,OAAO;AAChB,aAAS,QAAQ,OAAO;AAAA,EAC1B;AACA,MAAI,OAAO,aAAa;AACtB,aAAS,cAAc,OAAO;AAAA,EAChC;AACA,MAAI,OAAO,YAAY,OAAO,SAAS,SAAS,GAAG;AACjD,aAAS,WAAW,OAAO;AAAA,EAC7B;AACA,MAAI,OAAO,QAAQ;AACjB,aAAS,UAAU,CAAC,EAAE,MAAM,OAAO,OAAO,CAAC;AAAA,EAC7C;AAGA,MACE,OAAO,WACP,OAAO,iBACP,OAAO,cACP,OAAO,QACP;AAEA,UAAM,mBAAmB,CAAC,WAAW,WAAW,QAAQ,SAAS;AACjE,UAAM,SAAS,OAAO,UAAU,iBAAiB,SAAS,OAAO,MAAyC,IACrG,OAAO,SACR;AAEJ,UAAM,YAAgD;AAAA,MACpD,MAAM;AAAA,MACN,OAAO,OAAO,WAAW,OAAO,SAAS;AAAA,MACzC,aAAa,OAAO,iBAAiB,OAAO,eAAe;AAAA,MAC3D,KAAK,OAAO,SAAS;AAAA,MACrB,UAAU,OAAO,cAAc;AAAA,IACjC;AAEA,QAAI,OAAO,YAAY;AACrB,gBAAU,SAAS;AAAA,QACjB;AAAA,UACE,KAAK,OAAO;AAAA,UACZ,OAAO,OAAO,gBAAgB;AAAA,UAC9B,QAAQ,OAAO,iBAAiB;AAAA,UAChC,KAAK,OAAO,WAAW,OAAO,SAAS;AAAA,QACzC;AAAA,MACF;AAAA,IACF;AAGA,QAAI,WAAW,WAAW;AACxB,YAAM,mBAAmB;AAAA,QACvB,GAAG;AAAA,QACH,GAAI,OAAO,iBAAiB;AAAA,UAC1B,eAAe,OAAO,cAAc,YAAY;AAAA,QAClD;AAAA,QACA,GAAI,OAAO,gBAAgB;AAAA,UACzB,cAAc,OAAO,aAAa,YAAY;AAAA,QAChD;AAAA,MACF;AACA,eAAS,YAAY;AAAA,IACvB,OAAO;AACL,eAAS,YAAY;AAAA,IACvB;AAAA,EACF;AAGA,MACE,OAAO,eACP,OAAO,gBACP,OAAO,sBACP,OAAO,iBACP;AACA,aAAS,UAAU;AAAA,MACjB,MAAM,OAAO,eAAe;AAAA,MAC5B,OAAO,OAAO,gBAAgB,OAAO,WAAW,OAAO,SAAS;AAAA,MAChE,aACE,OAAO,sBACP,OAAO,iBACP,OAAO,eACP;AAAA,MACF,QAAQ,OAAO,kBACX,CAAC,OAAO,eAAe,IACvB;AAAA,MACJ,MAAM,OAAO,eAAe;AAAA,MAC5B,SAAS,OAAO,kBAAkB;AAAA,IACpC;AAAA,EACF;AAGA,MAAI,OAAO,cAAc;AACvB,aAAS,aAAa;AAAA,MACpB,WAAW,OAAO;AAAA,IACpB;AAAA,EACF;AAGA,MAAI,OAAO,QAAQ;AACjB,aAAS,SAAS,OAAO;AAAA,EAC3B;AAGA,SAAO;AAAA,IACL,GAAG;AAAA,IACH,GAAG;AAAA;AAAA,IAEH,OAAO,OAAO,SAAS,SAAS;AAAA,IAChC,aAAa,OAAO,eAAe,SAAS;AAAA;AAAA,IAE5C,WAAW,SAAS,YAChB,EAAE,GAAG,SAAS,WAAW,GAAG,SAAS,UAAU,IAC/C,SAAS;AAAA;AAAA,IAEb,SAAS,SAAS,UACd,EAAE,GAAG,SAAS,SAAS,GAAG,SAAS,QAAQ,IAC3C,SAAS;AAAA,EACf;AACF;AAcO,SAAS,uBACd,QACA,SACQ;AACR,MAAI,YAAY;AAGhB,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,MAAM,GAAG;AACjD,UAAM,aAAa,MAAM,QAAQ,KAAK,IAAI,MAAM,KAAK,GAAG,IAAI;AAC5D,gBAAY,UAAU,QAAQ,IAAI,GAAG,KAAK,UAAU;AACpD,gBAAY,UAAU,QAAQ,OAAO,GAAG,KAAK,UAAU;AAAA,EACzD;AAEA,SAAO;AACT;","names":[]}