@seo-console/package 1.0.4 → 1.1.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 CHANGED
@@ -12,7 +12,7 @@ npm install @seo-console/package
12
12
 
13
13
  ### Step 1: Configure Storage (Optional)
14
14
 
15
- The package uses **file storage by default** (no database required). SEO records are stored in `seo-records.json`.
15
+ The package uses **file storage** (no database required). SEO records are stored in `seo-records.json` in your project root.
16
16
 
17
17
  To customize the storage location, add to your `.env.local`:
18
18
 
@@ -20,14 +20,7 @@ To customize the storage location, add to your `.env.local`:
20
20
  SEO_CONSOLE_STORAGE_PATH=./data/seo-records.json
21
21
  ```
22
22
 
23
- **Optional:** If you prefer Supabase, set these environment variables:
24
-
25
- ```env
26
- NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
27
- NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
28
- ```
29
-
30
- The package will automatically detect and use Supabase if these are set.
23
+ That's it! No database setup needed.
31
24
 
32
25
  ### Step 2: Create API Routes (REQUIRED - This is why you're getting 404 errors!)
33
26
 
@@ -43,7 +36,7 @@ import { createSEORecordSchema } from "@seo-console/package/server";
43
36
  // GET - Fetch all SEO records
44
37
  export async function GET() {
45
38
  try {
46
- // Auto-detect storage type (file or Supabase)
39
+ // Get file storage adapter
47
40
  const config = detectStorageConfig();
48
41
  const storage = createStorageAdapter(config);
49
42
 
@@ -186,9 +179,7 @@ export async function DELETE(
186
179
  }
187
180
  ```
188
181
 
189
- > **Important:** These API routes use the storage adapter system, which automatically works with:
190
- > - **File storage** (default) - if no Supabase credentials are set
191
- > - **Supabase** - if `NEXT_PUBLIC_SUPABASE_URL` and `NEXT_PUBLIC_SUPABASE_ANON_KEY` are set
182
+ > **Important:** These API routes use file storage. SEO records are stored in `seo-records.json` in your project root (or the path specified in `SEO_CONSOLE_STORAGE_PATH`).
192
183
 
193
184
  ### Step 3: Add Admin Pages
194
185
 
@@ -344,15 +335,28 @@ const metadata = await useGenerateMetadata({
344
335
  });
345
336
  ```
346
337
 
347
- ### Server-Side Functions
338
+ ### Storage Functions
339
+
340
+ ```typescript
341
+ import {
342
+ detectStorageConfig,
343
+ createStorageAdapter
344
+ } from "@seo-console/package";
345
+
346
+ // Get storage adapter (uses file storage)
347
+ const config = detectStorageConfig();
348
+ const storage = createStorageAdapter(config);
349
+
350
+ // Use storage methods
351
+ const records = await storage.getRecords();
352
+ const record = await storage.getRecordByRoute("/about");
353
+ await storage.createRecord({ routePath: "/contact", title: "Contact" });
354
+ ```
355
+
356
+ ### Other Server-Side Functions
348
357
 
349
358
  ```typescript
350
359
  import {
351
- getSEORecords,
352
- getSEORecordByRoute,
353
- createSEORecord,
354
- updateSEORecord,
355
- deleteSEORecord,
356
360
  generateSitemapFromRecords,
357
361
  generateRobotsTxt,
358
362
  discoverNextJSRoutes,
@@ -360,21 +364,14 @@ import {
360
364
  } from "@seo-console/package/server";
361
365
  ```
362
366
 
363
- ## Storage Backends
364
-
365
- ### File Storage (Default)
366
-
367
- - **No database required** - stores data in a JSON file
368
- - **Perfect for small to medium sites** - simple and fast
369
- - **File location**: `seo-records.json` (configurable via `SEO_CONSOLE_STORAGE_PATH`)
370
- - **Automatic**: Works out of the box with no configuration
367
+ ## Storage
371
368
 
372
- ### Supabase Storage (Optional)
369
+ The package uses **file storage** - SEO records are stored in a JSON file (`seo-records.json` by default).
373
370
 
374
- - **Database-backed** - uses Supabase PostgreSQL
375
- - **Better for larger sites** - scalable and supports concurrent access
376
- - **Requires**: Supabase project and migrations
377
- - **Auto-detected**: If `NEXT_PUBLIC_SUPABASE_URL` and `NEXT_PUBLIC_SUPABASE_ANON_KEY` are set
371
+ - **No database required** - works out of the box
372
+ - **Simple and fast** - perfect for most sites
373
+ - **Version controlled** - the JSON file can be committed to git
374
+ - **Configurable** - set `SEO_CONSOLE_STORAGE_PATH` to customize the file location
378
375
 
379
376
  ## License
380
377
 
@@ -25,311 +25,6 @@ __export(hooks_exports, {
25
25
  });
26
26
  module.exports = __toCommonJS(hooks_exports);
27
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
- function transformToInsert(record) {
91
- return {
92
- user_id: record.userId,
93
- route_path: record.routePath,
94
- title: record.title ?? null,
95
- description: record.description ?? null,
96
- keywords: record.keywords ?? null,
97
- og_title: record.ogTitle ?? null,
98
- og_description: record.ogDescription ?? null,
99
- og_image_url: record.ogImageUrl ?? null,
100
- og_image_width: record.ogImageWidth ?? null,
101
- og_image_height: record.ogImageHeight ?? null,
102
- og_type: record.ogType ?? null,
103
- og_url: record.ogUrl ?? null,
104
- og_site_name: record.ogSiteName ?? null,
105
- twitter_card: record.twitterCard ?? null,
106
- twitter_title: record.twitterTitle ?? null,
107
- twitter_description: record.twitterDescription ?? null,
108
- twitter_image_url: record.twitterImageUrl ?? null,
109
- twitter_site: record.twitterSite ?? null,
110
- twitter_creator: record.twitterCreator ?? null,
111
- canonical_url: record.canonicalUrl ?? null,
112
- robots: record.robots ?? null,
113
- author: record.author ?? null,
114
- published_time: record.publishedTime?.toISOString() ?? null,
115
- modified_time: record.modifiedTime?.toISOString() ?? null,
116
- structured_data: record.structuredData ?? null
117
- };
118
- }
119
- function transformToUpdate(record) {
120
- const update = {};
121
- if (record.routePath !== void 0) update.route_path = record.routePath;
122
- if (record.title !== void 0) update.title = record.title ?? null;
123
- if (record.description !== void 0)
124
- update.description = record.description ?? null;
125
- if (record.keywords !== void 0) update.keywords = record.keywords ?? null;
126
- if (record.ogTitle !== void 0) update.og_title = record.ogTitle ?? null;
127
- if (record.ogDescription !== void 0)
128
- update.og_description = record.ogDescription ?? null;
129
- if (record.ogImageUrl !== void 0)
130
- update.og_image_url = record.ogImageUrl ?? null;
131
- if (record.ogImageWidth !== void 0)
132
- update.og_image_width = record.ogImageWidth ?? null;
133
- if (record.ogImageHeight !== void 0)
134
- update.og_image_height = record.ogImageHeight ?? null;
135
- if (record.ogType !== void 0) update.og_type = record.ogType ?? null;
136
- if (record.ogUrl !== void 0) update.og_url = record.ogUrl ?? null;
137
- if (record.ogSiteName !== void 0)
138
- update.og_site_name = record.ogSiteName ?? null;
139
- if (record.twitterCard !== void 0)
140
- update.twitter_card = record.twitterCard ?? null;
141
- if (record.twitterTitle !== void 0)
142
- update.twitter_title = record.twitterTitle ?? null;
143
- if (record.twitterDescription !== void 0)
144
- update.twitter_description = record.twitterDescription ?? null;
145
- if (record.twitterImageUrl !== void 0)
146
- update.twitter_image_url = record.twitterImageUrl ?? null;
147
- if (record.twitterSite !== void 0)
148
- update.twitter_site = record.twitterSite ?? null;
149
- if (record.twitterCreator !== void 0)
150
- update.twitter_creator = record.twitterCreator ?? null;
151
- if (record.canonicalUrl !== void 0)
152
- update.canonical_url = record.canonicalUrl ?? null;
153
- if (record.robots !== void 0) update.robots = record.robots ?? null;
154
- if (record.author !== void 0) update.author = record.author ?? null;
155
- if (record.publishedTime !== void 0)
156
- update.published_time = record.publishedTime?.toISOString() ?? null;
157
- if (record.modifiedTime !== void 0)
158
- update.modified_time = record.modifiedTime?.toISOString() ?? null;
159
- if (record.structuredData !== void 0)
160
- update.structured_data = record.structuredData ?? null;
161
- if (record.validationStatus !== void 0)
162
- update.validation_status = record.validationStatus ?? null;
163
- if (record.lastValidatedAt !== void 0)
164
- update.last_validated_at = record.lastValidatedAt?.toISOString() ?? null;
165
- if (record.validationErrors !== void 0)
166
- update.validation_errors = record.validationErrors ?? null;
167
- return update;
168
- }
169
- async function getSEORecords() {
170
- try {
171
- const supabase = await createClient();
172
- const {
173
- data: { user }
174
- } = await supabase.auth.getUser();
175
- if (!user) {
176
- return {
177
- success: false,
178
- error: new Error("User not authenticated")
179
- };
180
- }
181
- const { data, error } = await supabase.from("seo_records").select("*").eq("user_id", user.id).order("created_at", { ascending: false });
182
- if (error) {
183
- return { success: false, error };
184
- }
185
- const records = (data || []).map(transformRowToSEORecord);
186
- return { success: true, data: records };
187
- } catch (error) {
188
- return {
189
- success: false,
190
- error: error instanceof Error ? error : new Error("Unknown error")
191
- };
192
- }
193
- }
194
- async function getSEORecordById(id) {
195
- try {
196
- const supabase = await createClient();
197
- const {
198
- data: { user }
199
- } = await supabase.auth.getUser();
200
- if (!user) {
201
- return {
202
- success: false,
203
- error: new Error("User not authenticated")
204
- };
205
- }
206
- const { data, error } = await supabase.from("seo_records").select("*").eq("id", id).eq("user_id", user.id).single();
207
- if (error) {
208
- return { success: false, error };
209
- }
210
- if (!data) {
211
- return {
212
- success: false,
213
- error: new Error("SEO record not found")
214
- };
215
- }
216
- return { success: true, data: transformRowToSEORecord(data) };
217
- } catch (error) {
218
- return {
219
- success: false,
220
- error: error instanceof Error ? error : new Error("Unknown error")
221
- };
222
- }
223
- }
224
- async function getSEORecordByRoute(routePath) {
225
- try {
226
- const supabase = await createClient();
227
- const {
228
- data: { user }
229
- } = await supabase.auth.getUser();
230
- if (!user) {
231
- return {
232
- success: false,
233
- error: new Error("User not authenticated")
234
- };
235
- }
236
- const { data, error } = await supabase.from("seo_records").select("*").eq("route_path", routePath).eq("user_id", user.id).maybeSingle();
237
- if (error) {
238
- return { success: false, error };
239
- }
240
- if (!data) {
241
- return { success: true, data: null };
242
- }
243
- return { success: true, data: transformRowToSEORecord(data) };
244
- } catch (error) {
245
- return {
246
- success: false,
247
- error: error instanceof Error ? error : new Error("Unknown error")
248
- };
249
- }
250
- }
251
- async function createSEORecord(record) {
252
- try {
253
- const supabase = await createClient();
254
- const {
255
- data: { user }
256
- } = await supabase.auth.getUser();
257
- if (!user) {
258
- return {
259
- success: false,
260
- error: new Error("User not authenticated")
261
- };
262
- }
263
- const insertData = transformToInsert({ ...record, userId: user.id });
264
- const { data, error } = await supabase.from("seo_records").insert(insertData).select().single();
265
- if (error) {
266
- return { success: false, error };
267
- }
268
- return { success: true, data: transformRowToSEORecord(data) };
269
- } catch (error) {
270
- return {
271
- success: false,
272
- error: error instanceof Error ? error : new Error("Unknown error")
273
- };
274
- }
275
- }
276
- async function updateSEORecord(record) {
277
- try {
278
- const supabase = await createClient();
279
- const {
280
- data: { user }
281
- } = await supabase.auth.getUser();
282
- if (!user) {
283
- return {
284
- success: false,
285
- error: new Error("User not authenticated")
286
- };
287
- }
288
- const { id, ...updateData } = record;
289
- const transformedUpdate = transformToUpdate(updateData);
290
- const { data, error } = await supabase.from("seo_records").update(transformedUpdate).eq("id", id).eq("user_id", user.id).select().single();
291
- if (error) {
292
- return { success: false, error };
293
- }
294
- if (!data) {
295
- return {
296
- success: false,
297
- error: new Error("SEO record not found")
298
- };
299
- }
300
- return { success: true, data: transformRowToSEORecord(data) };
301
- } catch (error) {
302
- return {
303
- success: false,
304
- error: error instanceof Error ? error : new Error("Unknown error")
305
- };
306
- }
307
- }
308
- async function deleteSEORecord(id) {
309
- try {
310
- const supabase = await createClient();
311
- const {
312
- data: { user }
313
- } = await supabase.auth.getUser();
314
- if (!user) {
315
- return {
316
- success: false,
317
- error: new Error("User not authenticated")
318
- };
319
- }
320
- const { error } = await supabase.from("seo_records").delete().eq("id", id).eq("user_id", user.id);
321
- if (error) {
322
- return { success: false, error };
323
- }
324
- return { success: true, data: void 0 };
325
- } catch (error) {
326
- return {
327
- success: false,
328
- error: error instanceof Error ? error : new Error("Unknown error")
329
- };
330
- }
331
- }
332
-
333
28
  // src/lib/storage/file-storage.ts
334
29
  var import_fs = require("fs");
335
30
  var FileStorage = class {
@@ -440,106 +135,15 @@ var FileStorage = class {
440
135
  }
441
136
  };
442
137
 
443
- // src/lib/storage/supabase-storage.ts
444
- var SupabaseStorage = class {
445
- constructor(supabaseUrl, supabaseKey) {
446
- this.supabaseUrl = supabaseUrl;
447
- this.supabaseKey = supabaseKey;
448
- if (typeof process !== "undefined") {
449
- process.env.NEXT_PUBLIC_SUPABASE_URL = supabaseUrl;
450
- process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY = supabaseKey;
451
- }
452
- }
453
- async isAvailable() {
454
- try {
455
- const result = await getSEORecords();
456
- return result.success;
457
- } catch {
458
- return false;
459
- }
460
- }
461
- async getRecords() {
462
- const result = await getSEORecords();
463
- if (!result.success) {
464
- throw new Error(result.error?.message || "Failed to get records");
465
- }
466
- return result.data;
467
- }
468
- async getRecordById(id) {
469
- const result = await getSEORecordById(id);
470
- if (!result.success) {
471
- if (result.error?.message?.includes("not found")) {
472
- return null;
473
- }
474
- throw new Error(result.error?.message || "Failed to get record");
475
- }
476
- return result.data || null;
477
- }
478
- async getRecordByRoute(routePath) {
479
- const result = await getSEORecordByRoute(routePath);
480
- if (!result.success) {
481
- if (result.error?.message?.includes("not found")) {
482
- return null;
483
- }
484
- throw new Error(result.error?.message || "Failed to get record");
485
- }
486
- return result.data || null;
487
- }
488
- async createRecord(record) {
489
- const result = await createSEORecord(record);
490
- if (!result.success) {
491
- throw new Error(result.error?.message || "Failed to create record");
492
- }
493
- return result.data;
494
- }
495
- async updateRecord(record) {
496
- const result = await updateSEORecord(record);
497
- if (!result.success) {
498
- throw new Error(result.error?.message || "Failed to update record");
499
- }
500
- return result.data;
501
- }
502
- async deleteRecord(id) {
503
- const result = await deleteSEORecord(id);
504
- if (!result.success) {
505
- throw new Error(result.error?.message || "Failed to delete record");
506
- }
507
- }
508
- };
509
-
510
138
  // src/lib/storage/storage-factory.ts
511
139
  function createStorageAdapter(config) {
512
- switch (config.type) {
513
- case "file":
514
- return new FileStorage(config.filePath || "seo-records.json");
515
- case "supabase":
516
- if (!config.supabaseUrl || !config.supabaseKey) {
517
- throw new Error("Supabase URL and key are required for Supabase storage");
518
- }
519
- return new SupabaseStorage(config.supabaseUrl, config.supabaseKey);
520
- case "memory":
521
- return new FileStorage(":memory:");
522
- default:
523
- throw new Error(`Unsupported storage type: ${config.type}`);
524
- }
140
+ const filePath = config?.filePath || process.env.SEO_CONSOLE_STORAGE_PATH || "seo-records.json";
141
+ return new FileStorage(filePath);
525
142
  }
526
143
  function detectStorageConfig() {
527
- if (process.env.NEXT_PUBLIC_SUPABASE_URL && process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY) {
528
- return {
529
- type: "supabase",
530
- supabaseUrl: process.env.NEXT_PUBLIC_SUPABASE_URL,
531
- supabaseKey: process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
532
- };
533
- }
534
- if (process.env.SEO_CONSOLE_STORAGE_PATH) {
535
- return {
536
- type: "file",
537
- filePath: process.env.SEO_CONSOLE_STORAGE_PATH
538
- };
539
- }
540
144
  return {
541
145
  type: "file",
542
- filePath: "seo-records.json"
146
+ filePath: process.env.SEO_CONSOLE_STORAGE_PATH || "seo-records.json"
543
147
  };
544
148
  }
545
149
 
@@ -556,16 +160,10 @@ async function useGenerateMetadata(options = {}) {
556
160
  let record = null;
557
161
  try {
558
162
  const storageConfig = detectStorageConfig();
559
- if (storageConfig.type === "file" || storageConfig.type === "memory") {
560
- const storage = createStorageAdapter(storageConfig);
561
- record = await storage.getRecordByRoute(routePath);
562
- } else {
563
- const result = await getSEORecordByRoute(routePath);
564
- record = result.success ? result.data || null : null;
565
- }
163
+ const storage = createStorageAdapter(storageConfig);
164
+ record = await storage.getRecordByRoute(routePath);
566
165
  } catch (error) {
567
- const result = await getSEORecordByRoute(routePath);
568
- record = result.success ? result.data || null : null;
166
+ console.warn("Failed to load SEO record from storage:", error);
569
167
  }
570
168
  if (!record) {
571
169
  return {