@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.
- package/README.md +114 -0
- package/dist/components/index.d.mts +4 -0
- package/dist/components/index.d.ts +4 -0
- package/dist/components/index.js +3642 -0
- package/dist/components/index.js.map +1 -0
- package/dist/components/index.mjs +3593 -0
- package/dist/components/index.mjs.map +1 -0
- package/dist/hooks/index.d.mts +43 -0
- package/dist/hooks/index.d.ts +43 -0
- package/dist/hooks/index.js +229 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/hooks/index.mjs +201 -0
- package/dist/hooks/index.mjs.map +1 -0
- package/dist/index-6lAOwFXQ.d.mts +329 -0
- package/dist/index-6lAOwFXQ.d.ts +329 -0
- package/dist/index.d.mts +81 -0
- package/dist/index.d.ts +81 -0
- package/dist/index.js +4377 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +4316 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +68 -0
|
@@ -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":"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","names":[]}
|