@seo-console/package 1.1.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +103 -1
- package/dist/index.d.mts +2 -79
- package/dist/index.d.ts +2 -79
- package/dist/index.js +0 -230
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +0 -224
- package/dist/index.mjs.map +1 -1
- package/dist/server.d.mts +80 -3
- package/dist/server.d.ts +80 -3
- package/dist/server.js +12858 -0
- package/dist/server.js.map +1 -1
- package/dist/server.mjs +12869 -0
- package/dist/server.mjs.map +1 -1
- package/dist/{robots-generator-CYA9Ofu_.d.ts → storage-factory-CdCI1VHl.d.mts} +54 -23
- package/dist/{robots-generator-9d9aTULk.d.mts → storage-factory-L2YGjVID.d.ts} +54 -23
- package/package.json +3 -2
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 SEO Console Package
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -305,13 +305,115 @@ Add a link to the SEO admin section in your main admin navigation:
|
|
|
305
305
|
</nav>
|
|
306
306
|
```
|
|
307
307
|
|
|
308
|
+
### Step 5: Discover and Import Existing Pages (Optional but Recommended)
|
|
309
|
+
|
|
310
|
+
**⚠️ IMPORTANT:** The package doesn't automatically detect your existing pages or SEO settings. You need to discover and import them.
|
|
311
|
+
|
|
312
|
+
**Create `app/api/discover-routes/route.ts` to find all your pages:**
|
|
313
|
+
|
|
314
|
+
```typescript
|
|
315
|
+
import { NextResponse } from "next/server";
|
|
316
|
+
import { discoverNextJSRoutes } from "@seo-console/package/server";
|
|
317
|
+
|
|
318
|
+
export async function POST() {
|
|
319
|
+
try {
|
|
320
|
+
const appDir = process.env.NEXT_PUBLIC_APP_DIR || "app";
|
|
321
|
+
const routes = await discoverNextJSRoutes(appDir);
|
|
322
|
+
return NextResponse.json({ routes });
|
|
323
|
+
} catch (error) {
|
|
324
|
+
return NextResponse.json(
|
|
325
|
+
{ error: error instanceof Error ? error.message : "Failed to discover routes" },
|
|
326
|
+
{ status: 500 }
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
**Create `app/api/import-from-site/route.ts` to import existing SEO metadata:**
|
|
333
|
+
|
|
334
|
+
```typescript
|
|
335
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
336
|
+
import { detectStorageConfig, createStorageAdapter } from "@seo-console/package";
|
|
337
|
+
import { extractMetadataFromURL, metadataToSEORecord } from "@seo-console/package/server";
|
|
338
|
+
|
|
339
|
+
export async function POST(request: NextRequest) {
|
|
340
|
+
try {
|
|
341
|
+
const { baseUrl, routes } = await request.json();
|
|
342
|
+
|
|
343
|
+
if (!baseUrl) {
|
|
344
|
+
return NextResponse.json({ error: "Base URL is required" }, { status: 400 });
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const routesToImport = Array.isArray(routes) && routes.length > 0 ? routes : ["/"];
|
|
348
|
+
const results: Array<{ route: string; success: boolean; error?: string }> = [];
|
|
349
|
+
|
|
350
|
+
const config = detectStorageConfig();
|
|
351
|
+
const storage = createStorageAdapter(config);
|
|
352
|
+
|
|
353
|
+
for (const route of routesToImport) {
|
|
354
|
+
try {
|
|
355
|
+
const url = new URL(route, baseUrl).toString();
|
|
356
|
+
const metadata = await extractMetadataFromURL(url);
|
|
357
|
+
|
|
358
|
+
if (Object.keys(metadata).length === 0) {
|
|
359
|
+
results.push({ route, success: false, error: "No metadata found" });
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const recordData = metadataToSEORecord(metadata, route, "file-user");
|
|
364
|
+
await storage.createRecord(recordData as { userId: string; routePath: string; [key: string]: unknown });
|
|
365
|
+
results.push({ route, success: true });
|
|
366
|
+
|
|
367
|
+
// Rate limiting - wait 200ms between requests
|
|
368
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
369
|
+
} catch (err) {
|
|
370
|
+
results.push({
|
|
371
|
+
route,
|
|
372
|
+
success: false,
|
|
373
|
+
error: err instanceof Error ? err.message : "Unknown error",
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return NextResponse.json({ results });
|
|
379
|
+
} catch (error) {
|
|
380
|
+
return NextResponse.json(
|
|
381
|
+
{ error: error instanceof Error ? error.message : "Failed to import from site" },
|
|
382
|
+
{ status: 500 }
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
**How to use these routes:**
|
|
389
|
+
|
|
390
|
+
1. **Discover routes:** Call `POST /api/discover-routes` to get a list of all your pages
|
|
391
|
+
2. **Import SEO:** Call `POST /api/import-from-site` with your site's base URL and the routes you want to import
|
|
392
|
+
|
|
393
|
+
Example:
|
|
394
|
+
```typescript
|
|
395
|
+
// In your admin UI or a script
|
|
396
|
+
const discoverResponse = await fetch("/api/discover-routes", { method: "POST" });
|
|
397
|
+
const { routes } = await discoverResponse.json();
|
|
398
|
+
|
|
399
|
+
const importResponse = await fetch("/api/import-from-site", {
|
|
400
|
+
method: "POST",
|
|
401
|
+
headers: { "Content-Type": "application/json" },
|
|
402
|
+
body: JSON.stringify({
|
|
403
|
+
baseUrl: "https://yoursite.com",
|
|
404
|
+
routes: routes.map((r: any) => r.routePath)
|
|
405
|
+
})
|
|
406
|
+
});
|
|
407
|
+
```
|
|
408
|
+
|
|
308
409
|
## Quick Start Summary
|
|
309
410
|
|
|
310
411
|
1. **Install:** `npm install @seo-console/package`
|
|
311
412
|
2. **Create API route:** `app/api/seo-records/route.ts` (see Step 2)
|
|
312
413
|
3. **Create admin pages:** `app/admin/seo/` directory with pages (see Step 3)
|
|
313
414
|
4. **Add to navigation:** Link to `/admin/seo` in your admin menu
|
|
314
|
-
5. **
|
|
415
|
+
5. **Discover & import pages:** Create `app/api/discover-routes/route.ts` and `app/api/import-from-site/route.ts` (see Step 5)
|
|
416
|
+
6. **Use in pages:** Add `generateMetadata` to your pages (see Step 4)
|
|
315
417
|
|
|
316
418
|
That's it! The SEO admin interface will be accessible at `/admin/seo` as a new tab in your admin area.
|
|
317
419
|
|
package/dist/index.d.mts
CHANGED
|
@@ -1,83 +1,6 @@
|
|
|
1
1
|
export { Button, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, Input, OGImagePreview, SEORecordForm, SEORecordList, Spinner, ValidationDashboard } from './components/index.mjs';
|
|
2
|
-
|
|
3
|
-
export { c as
|
|
4
|
-
export { D as DiscoveredRoute, E as ExtractedMetadata, I as ImageValidationResult, R as RobotsTxtOptions, S as SitemapEntry, n as SitemapOptions, c as ValidationIssue, V as ValidationResult, h as crawlSiteForSEO, i as discoverNextJSRoutes, k as extractMetadataFromHTML, f as extractMetadataFromURL, o as extractSitemapFromRobotsTxt, j as generateExamplePaths, e as generateRobotsTxt, d as generateSitemapFromRecords, g as generateSitemapXML, m as metadataToSEORecord, s as seoRecordsToSitemapEntries, u as updateRobotsTxtWithSitemap, l as validateSitemapEntry } from './robots-generator-9d9aTULk.mjs';
|
|
2
|
+
export { C as CreateSEORecord, S as SEORecord, U as UpdateSEORecord, c as createSEORecordSchema, u as updateSEORecordSchema } from './seo-schema-D8EwzllB.mjs';
|
|
3
|
+
export { E as ExtractedMetadata, I as ImageValidationResult, R as RobotsTxtOptions, S as SitemapEntry, c as SitemapOptions, h as StorageAdapter, j as StorageConfig, i as StorageType, a as ValidationIssue, V as ValidationResult, k as detectStorageConfig, e as extractMetadataFromHTML, f as extractSitemapFromRobotsTxt, d as generateRobotsTxt, b as generateSitemapFromRecords, g as generateSitemapXML, m as metadataToSEORecord, s as seoRecordsToSitemapEntries, u as updateRobotsTxtWithSitemap, v as validateSitemapEntry } from './storage-factory-CdCI1VHl.mjs';
|
|
5
4
|
import 'react/jsx-runtime';
|
|
6
5
|
import 'react';
|
|
7
6
|
import 'zod';
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Storage Adapter Interface
|
|
11
|
-
* Allows SEO Console to work with different storage backends
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
interface StorageAdapter {
|
|
15
|
-
/**
|
|
16
|
-
* Get all SEO records
|
|
17
|
-
*/
|
|
18
|
-
getRecords(): Promise<SEORecord[]>;
|
|
19
|
-
/**
|
|
20
|
-
* Get a single SEO record by ID
|
|
21
|
-
*/
|
|
22
|
-
getRecordById(id: string): Promise<SEORecord | null>;
|
|
23
|
-
/**
|
|
24
|
-
* Get SEO record by route path
|
|
25
|
-
*/
|
|
26
|
-
getRecordByRoute(routePath: string): Promise<SEORecord | null>;
|
|
27
|
-
/**
|
|
28
|
-
* Create a new SEO record
|
|
29
|
-
*/
|
|
30
|
-
createRecord(record: CreateSEORecord): Promise<SEORecord>;
|
|
31
|
-
/**
|
|
32
|
-
* Update an existing SEO record
|
|
33
|
-
*/
|
|
34
|
-
updateRecord(record: UpdateSEORecord): Promise<SEORecord>;
|
|
35
|
-
/**
|
|
36
|
-
* Delete an SEO record
|
|
37
|
-
*/
|
|
38
|
-
deleteRecord(id: string): Promise<void>;
|
|
39
|
-
/**
|
|
40
|
-
* Check if storage is available/configured
|
|
41
|
-
*/
|
|
42
|
-
isAvailable(): Promise<boolean>;
|
|
43
|
-
}
|
|
44
|
-
type StorageType = "file" | "memory";
|
|
45
|
-
interface StorageConfig {
|
|
46
|
-
type?: StorageType;
|
|
47
|
-
filePath?: string;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Storage Factory
|
|
52
|
-
* Creates file-based storage adapter
|
|
53
|
-
*/
|
|
54
|
-
|
|
55
|
-
declare function createStorageAdapter(config?: StorageConfig): StorageAdapter;
|
|
56
|
-
/**
|
|
57
|
-
* Auto-detect storage config from environment
|
|
58
|
-
*/
|
|
59
|
-
declare function detectStorageConfig(): StorageConfig;
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* File-based storage adapter
|
|
63
|
-
* Stores SEO records in a JSON file
|
|
64
|
-
* No database required!
|
|
65
|
-
*/
|
|
66
|
-
|
|
67
|
-
declare class FileStorage implements StorageAdapter {
|
|
68
|
-
private filePath;
|
|
69
|
-
private records;
|
|
70
|
-
private initialized;
|
|
71
|
-
constructor(filePath?: string);
|
|
72
|
-
private ensureInitialized;
|
|
73
|
-
private save;
|
|
74
|
-
isAvailable(): Promise<boolean>;
|
|
75
|
-
getRecords(): Promise<SEORecord[]>;
|
|
76
|
-
getRecordById(id: string): Promise<SEORecord | null>;
|
|
77
|
-
getRecordByRoute(routePath: string): Promise<SEORecord | null>;
|
|
78
|
-
createRecord(record: CreateSEORecord): Promise<SEORecord>;
|
|
79
|
-
updateRecord(record: UpdateSEORecord): Promise<SEORecord>;
|
|
80
|
-
deleteRecord(id: string): Promise<void>;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export { CreateSEORecord, FileStorage, SEORecord, type StorageAdapter, type StorageConfig, type StorageType, UpdateSEORecord, createStorageAdapter, detectStorageConfig };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,83 +1,6 @@
|
|
|
1
1
|
export { Button, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, Input, OGImagePreview, SEORecordForm, SEORecordList, Spinner, ValidationDashboard } from './components/index.js';
|
|
2
|
-
|
|
3
|
-
export { c as
|
|
4
|
-
export { D as DiscoveredRoute, E as ExtractedMetadata, I as ImageValidationResult, R as RobotsTxtOptions, S as SitemapEntry, n as SitemapOptions, c as ValidationIssue, V as ValidationResult, h as crawlSiteForSEO, i as discoverNextJSRoutes, k as extractMetadataFromHTML, f as extractMetadataFromURL, o as extractSitemapFromRobotsTxt, j as generateExamplePaths, e as generateRobotsTxt, d as generateSitemapFromRecords, g as generateSitemapXML, m as metadataToSEORecord, s as seoRecordsToSitemapEntries, u as updateRobotsTxtWithSitemap, l as validateSitemapEntry } from './robots-generator-CYA9Ofu_.js';
|
|
2
|
+
export { C as CreateSEORecord, S as SEORecord, U as UpdateSEORecord, c as createSEORecordSchema, u as updateSEORecordSchema } from './seo-schema-D8EwzllB.js';
|
|
3
|
+
export { E as ExtractedMetadata, I as ImageValidationResult, R as RobotsTxtOptions, S as SitemapEntry, c as SitemapOptions, h as StorageAdapter, j as StorageConfig, i as StorageType, a as ValidationIssue, V as ValidationResult, k as detectStorageConfig, e as extractMetadataFromHTML, f as extractSitemapFromRobotsTxt, d as generateRobotsTxt, b as generateSitemapFromRecords, g as generateSitemapXML, m as metadataToSEORecord, s as seoRecordsToSitemapEntries, u as updateRobotsTxtWithSitemap, v as validateSitemapEntry } from './storage-factory-L2YGjVID.js';
|
|
5
4
|
import 'react/jsx-runtime';
|
|
6
5
|
import 'react';
|
|
7
6
|
import 'zod';
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Storage Adapter Interface
|
|
11
|
-
* Allows SEO Console to work with different storage backends
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
interface StorageAdapter {
|
|
15
|
-
/**
|
|
16
|
-
* Get all SEO records
|
|
17
|
-
*/
|
|
18
|
-
getRecords(): Promise<SEORecord[]>;
|
|
19
|
-
/**
|
|
20
|
-
* Get a single SEO record by ID
|
|
21
|
-
*/
|
|
22
|
-
getRecordById(id: string): Promise<SEORecord | null>;
|
|
23
|
-
/**
|
|
24
|
-
* Get SEO record by route path
|
|
25
|
-
*/
|
|
26
|
-
getRecordByRoute(routePath: string): Promise<SEORecord | null>;
|
|
27
|
-
/**
|
|
28
|
-
* Create a new SEO record
|
|
29
|
-
*/
|
|
30
|
-
createRecord(record: CreateSEORecord): Promise<SEORecord>;
|
|
31
|
-
/**
|
|
32
|
-
* Update an existing SEO record
|
|
33
|
-
*/
|
|
34
|
-
updateRecord(record: UpdateSEORecord): Promise<SEORecord>;
|
|
35
|
-
/**
|
|
36
|
-
* Delete an SEO record
|
|
37
|
-
*/
|
|
38
|
-
deleteRecord(id: string): Promise<void>;
|
|
39
|
-
/**
|
|
40
|
-
* Check if storage is available/configured
|
|
41
|
-
*/
|
|
42
|
-
isAvailable(): Promise<boolean>;
|
|
43
|
-
}
|
|
44
|
-
type StorageType = "file" | "memory";
|
|
45
|
-
interface StorageConfig {
|
|
46
|
-
type?: StorageType;
|
|
47
|
-
filePath?: string;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Storage Factory
|
|
52
|
-
* Creates file-based storage adapter
|
|
53
|
-
*/
|
|
54
|
-
|
|
55
|
-
declare function createStorageAdapter(config?: StorageConfig): StorageAdapter;
|
|
56
|
-
/**
|
|
57
|
-
* Auto-detect storage config from environment
|
|
58
|
-
*/
|
|
59
|
-
declare function detectStorageConfig(): StorageConfig;
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* File-based storage adapter
|
|
63
|
-
* Stores SEO records in a JSON file
|
|
64
|
-
* No database required!
|
|
65
|
-
*/
|
|
66
|
-
|
|
67
|
-
declare class FileStorage implements StorageAdapter {
|
|
68
|
-
private filePath;
|
|
69
|
-
private records;
|
|
70
|
-
private initialized;
|
|
71
|
-
constructor(filePath?: string);
|
|
72
|
-
private ensureInitialized;
|
|
73
|
-
private save;
|
|
74
|
-
isAvailable(): Promise<boolean>;
|
|
75
|
-
getRecords(): Promise<SEORecord[]>;
|
|
76
|
-
getRecordById(id: string): Promise<SEORecord | null>;
|
|
77
|
-
getRecordByRoute(routePath: string): Promise<SEORecord | null>;
|
|
78
|
-
createRecord(record: CreateSEORecord): Promise<SEORecord>;
|
|
79
|
-
updateRecord(record: UpdateSEORecord): Promise<SEORecord>;
|
|
80
|
-
deleteRecord(id: string): Promise<void>;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export { CreateSEORecord, FileStorage, SEORecord, type StorageAdapter, type StorageConfig, type StorageType, UpdateSEORecord, createStorageAdapter, detectStorageConfig };
|
package/dist/index.js
CHANGED
|
@@ -37,22 +37,16 @@ __export(index_exports, {
|
|
|
37
37
|
CardFooter: () => CardFooter,
|
|
38
38
|
CardHeader: () => CardHeader,
|
|
39
39
|
CardTitle: () => CardTitle,
|
|
40
|
-
FileStorage: () => FileStorage,
|
|
41
40
|
Input: () => Input,
|
|
42
41
|
OGImagePreview: () => OGImagePreview,
|
|
43
42
|
SEORecordForm: () => SEORecordForm,
|
|
44
43
|
SEORecordList: () => SEORecordList,
|
|
45
44
|
Spinner: () => Spinner,
|
|
46
45
|
ValidationDashboard: () => ValidationDashboard,
|
|
47
|
-
crawlSiteForSEO: () => crawlSiteForSEO,
|
|
48
46
|
createSEORecordSchema: () => createSEORecordSchema,
|
|
49
|
-
createStorageAdapter: () => createStorageAdapter,
|
|
50
47
|
detectStorageConfig: () => detectStorageConfig,
|
|
51
|
-
discoverNextJSRoutes: () => discoverNextJSRoutes,
|
|
52
48
|
extractMetadataFromHTML: () => extractMetadataFromHTML,
|
|
53
|
-
extractMetadataFromURL: () => extractMetadataFromURL,
|
|
54
49
|
extractSitemapFromRobotsTxt: () => extractSitemapFromRobotsTxt,
|
|
55
|
-
generateExamplePaths: () => generateExamplePaths,
|
|
56
50
|
generateRobotsTxt: () => generateRobotsTxt,
|
|
57
51
|
generateSitemapFromRecords: () => generateSitemapFromRecords,
|
|
58
52
|
generateSitemapXML: () => generateSitemapXML,
|
|
@@ -3644,82 +3638,6 @@ function ValidationDashboard() {
|
|
|
3644
3638
|
] });
|
|
3645
3639
|
}
|
|
3646
3640
|
|
|
3647
|
-
// src/lib/route-discovery.ts
|
|
3648
|
-
var import_path = require("path");
|
|
3649
|
-
var import_glob = require("glob");
|
|
3650
|
-
async function discoverNextJSRoutes(appDir = "app", rootDir = process.cwd()) {
|
|
3651
|
-
const routes = [];
|
|
3652
|
-
const appPath = (0, import_path.join)(rootDir, appDir);
|
|
3653
|
-
try {
|
|
3654
|
-
const pageFiles = await (0, import_glob.glob)("**/page.tsx", {
|
|
3655
|
-
cwd: appPath,
|
|
3656
|
-
absolute: false,
|
|
3657
|
-
ignore: ["**/node_modules/**", "**/.next/**"]
|
|
3658
|
-
});
|
|
3659
|
-
for (const file of pageFiles) {
|
|
3660
|
-
const route = fileToRoute(file, appDir);
|
|
3661
|
-
if (route) {
|
|
3662
|
-
routes.push(route);
|
|
3663
|
-
}
|
|
3664
|
-
}
|
|
3665
|
-
} catch (error) {
|
|
3666
|
-
console.error("Error discovering routes:", error);
|
|
3667
|
-
}
|
|
3668
|
-
return routes;
|
|
3669
|
-
}
|
|
3670
|
-
function fileToRoute(filePath, appDir) {
|
|
3671
|
-
let routePath = filePath.replace(/^app\//, "").replace(/\/page\.tsx$/, "").replace(/\/page$/, "");
|
|
3672
|
-
if (routePath === "page" || routePath === "") {
|
|
3673
|
-
routePath = "/";
|
|
3674
|
-
} else {
|
|
3675
|
-
routePath = "/" + routePath;
|
|
3676
|
-
}
|
|
3677
|
-
const segments = routePath.split("/").filter(Boolean);
|
|
3678
|
-
const params = [];
|
|
3679
|
-
let isDynamic = false;
|
|
3680
|
-
let isCatchAll = false;
|
|
3681
|
-
for (const segment of segments) {
|
|
3682
|
-
if (segment.startsWith("[...") && segment.endsWith("]")) {
|
|
3683
|
-
const param = segment.slice(4, -1);
|
|
3684
|
-
params.push(param);
|
|
3685
|
-
isDynamic = true;
|
|
3686
|
-
isCatchAll = true;
|
|
3687
|
-
} else if (segment.startsWith("[") && segment.endsWith("]")) {
|
|
3688
|
-
const param = segment.slice(1, -1);
|
|
3689
|
-
params.push(param);
|
|
3690
|
-
isDynamic = true;
|
|
3691
|
-
}
|
|
3692
|
-
}
|
|
3693
|
-
return {
|
|
3694
|
-
routePath,
|
|
3695
|
-
filePath: (0, import_path.join)(appDir, filePath),
|
|
3696
|
-
isDynamic,
|
|
3697
|
-
isCatchAll,
|
|
3698
|
-
params
|
|
3699
|
-
};
|
|
3700
|
-
}
|
|
3701
|
-
function generateExamplePaths(route, count = 3) {
|
|
3702
|
-
if (!route.isDynamic) {
|
|
3703
|
-
return [route.routePath];
|
|
3704
|
-
}
|
|
3705
|
-
const examples = [];
|
|
3706
|
-
const segments = route.routePath.split("/").filter(Boolean);
|
|
3707
|
-
for (let i = 0; i < count; i++) {
|
|
3708
|
-
let examplePath = "";
|
|
3709
|
-
for (const segment of segments) {
|
|
3710
|
-
if (segment.startsWith("[...")) {
|
|
3711
|
-
examplePath += `/example-${i}-part-1/example-${i}-part-2`;
|
|
3712
|
-
} else if (segment.startsWith("[")) {
|
|
3713
|
-
examplePath += `/example-${i}`;
|
|
3714
|
-
} else {
|
|
3715
|
-
examplePath += `/${segment}`;
|
|
3716
|
-
}
|
|
3717
|
-
}
|
|
3718
|
-
examples.push(examplePath || "/");
|
|
3719
|
-
}
|
|
3720
|
-
return examples;
|
|
3721
|
-
}
|
|
3722
|
-
|
|
3723
3641
|
// src/lib/metadata-extractor.ts
|
|
3724
3642
|
var cheerio = __toESM(require("cheerio"));
|
|
3725
3643
|
function extractMetadataFromHTML(html, baseUrl) {
|
|
@@ -3748,23 +3666,6 @@ function extractMetadataFromHTML(html, baseUrl) {
|
|
|
3748
3666
|
}
|
|
3749
3667
|
return metadata;
|
|
3750
3668
|
}
|
|
3751
|
-
async function extractMetadataFromURL(url) {
|
|
3752
|
-
try {
|
|
3753
|
-
const response = await fetch(url, {
|
|
3754
|
-
headers: {
|
|
3755
|
-
"User-Agent": "SEO-Console/1.0"
|
|
3756
|
-
}
|
|
3757
|
-
});
|
|
3758
|
-
if (!response.ok) {
|
|
3759
|
-
throw new Error(`Failed to fetch ${url}: ${response.statusText}`);
|
|
3760
|
-
}
|
|
3761
|
-
const html = await response.text();
|
|
3762
|
-
return extractMetadataFromHTML(html, url);
|
|
3763
|
-
} catch (error) {
|
|
3764
|
-
console.error(`Error extracting metadata from ${url}:`, error);
|
|
3765
|
-
return {};
|
|
3766
|
-
}
|
|
3767
|
-
}
|
|
3768
3669
|
function metadataToSEORecord(metadata, routePath, userId = "extracted") {
|
|
3769
3670
|
return {
|
|
3770
3671
|
userId,
|
|
@@ -3782,20 +3683,6 @@ function metadataToSEORecord(metadata, routePath, userId = "extracted") {
|
|
|
3782
3683
|
validationStatus: "pending"
|
|
3783
3684
|
};
|
|
3784
3685
|
}
|
|
3785
|
-
async function crawlSiteForSEO(baseUrl, routes) {
|
|
3786
|
-
const results = /* @__PURE__ */ new Map();
|
|
3787
|
-
for (const route of routes) {
|
|
3788
|
-
const url = new URL(route, baseUrl).toString();
|
|
3789
|
-
try {
|
|
3790
|
-
const metadata = await extractMetadataFromURL(url);
|
|
3791
|
-
results.set(route, metadata);
|
|
3792
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
3793
|
-
} catch (error) {
|
|
3794
|
-
console.error(`Failed to crawl ${url}:`, error);
|
|
3795
|
-
}
|
|
3796
|
-
}
|
|
3797
|
-
return results;
|
|
3798
|
-
}
|
|
3799
3686
|
|
|
3800
3687
|
// src/lib/sitemap-generator.ts
|
|
3801
3688
|
function generateSitemapXML(options) {
|
|
@@ -3937,119 +3824,8 @@ function extractSitemapFromRobotsTxt(content) {
|
|
|
3937
3824
|
|
|
3938
3825
|
// src/lib/storage/file-storage.ts
|
|
3939
3826
|
var import_fs = require("fs");
|
|
3940
|
-
var FileStorage = class {
|
|
3941
|
-
constructor(filePath = "seo-records.json") {
|
|
3942
|
-
this.records = [];
|
|
3943
|
-
this.initialized = false;
|
|
3944
|
-
this.filePath = filePath;
|
|
3945
|
-
}
|
|
3946
|
-
async ensureInitialized() {
|
|
3947
|
-
if (this.initialized) return;
|
|
3948
|
-
try {
|
|
3949
|
-
const data = await import_fs.promises.readFile(this.filePath, "utf-8");
|
|
3950
|
-
this.records = JSON.parse(data);
|
|
3951
|
-
} catch (error) {
|
|
3952
|
-
if (error.code === "ENOENT") {
|
|
3953
|
-
this.records = [];
|
|
3954
|
-
await this.save();
|
|
3955
|
-
} else {
|
|
3956
|
-
throw error;
|
|
3957
|
-
}
|
|
3958
|
-
}
|
|
3959
|
-
this.initialized = true;
|
|
3960
|
-
}
|
|
3961
|
-
async save() {
|
|
3962
|
-
await import_fs.promises.writeFile(this.filePath, JSON.stringify(this.records, null, 2), "utf-8");
|
|
3963
|
-
}
|
|
3964
|
-
async isAvailable() {
|
|
3965
|
-
try {
|
|
3966
|
-
const dir = this.filePath.includes("/") ? this.filePath.substring(0, this.filePath.lastIndexOf("/")) : ".";
|
|
3967
|
-
await import_fs.promises.access(dir);
|
|
3968
|
-
return true;
|
|
3969
|
-
} catch {
|
|
3970
|
-
return false;
|
|
3971
|
-
}
|
|
3972
|
-
}
|
|
3973
|
-
async getRecords() {
|
|
3974
|
-
await this.ensureInitialized();
|
|
3975
|
-
return [...this.records];
|
|
3976
|
-
}
|
|
3977
|
-
async getRecordById(id) {
|
|
3978
|
-
await this.ensureInitialized();
|
|
3979
|
-
return this.records.find((r) => r.id === id) || null;
|
|
3980
|
-
}
|
|
3981
|
-
async getRecordByRoute(routePath) {
|
|
3982
|
-
await this.ensureInitialized();
|
|
3983
|
-
return this.records.find((r) => r.routePath === routePath) || null;
|
|
3984
|
-
}
|
|
3985
|
-
async createRecord(record) {
|
|
3986
|
-
await this.ensureInitialized();
|
|
3987
|
-
const newRecord = {
|
|
3988
|
-
id: typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
|
|
3989
|
-
userId: "file-user",
|
|
3990
|
-
// File storage doesn't need user IDs
|
|
3991
|
-
routePath: record.routePath,
|
|
3992
|
-
title: record.title,
|
|
3993
|
-
description: record.description,
|
|
3994
|
-
keywords: record.keywords,
|
|
3995
|
-
ogTitle: record.ogTitle,
|
|
3996
|
-
ogDescription: record.ogDescription,
|
|
3997
|
-
ogImageUrl: record.ogImageUrl,
|
|
3998
|
-
ogImageWidth: record.ogImageWidth,
|
|
3999
|
-
ogImageHeight: record.ogImageHeight,
|
|
4000
|
-
ogType: record.ogType,
|
|
4001
|
-
ogUrl: record.ogUrl,
|
|
4002
|
-
ogSiteName: record.ogSiteName,
|
|
4003
|
-
twitterCard: record.twitterCard,
|
|
4004
|
-
twitterTitle: record.twitterTitle,
|
|
4005
|
-
twitterDescription: record.twitterDescription,
|
|
4006
|
-
twitterImageUrl: record.twitterImageUrl,
|
|
4007
|
-
twitterSite: record.twitterSite,
|
|
4008
|
-
twitterCreator: record.twitterCreator,
|
|
4009
|
-
canonicalUrl: record.canonicalUrl,
|
|
4010
|
-
robots: record.robots,
|
|
4011
|
-
author: record.author,
|
|
4012
|
-
publishedTime: record.publishedTime,
|
|
4013
|
-
modifiedTime: record.modifiedTime,
|
|
4014
|
-
structuredData: record.structuredData,
|
|
4015
|
-
validationStatus: "pending",
|
|
4016
|
-
lastValidatedAt: void 0,
|
|
4017
|
-
validationErrors: void 0
|
|
4018
|
-
};
|
|
4019
|
-
this.records.push(newRecord);
|
|
4020
|
-
await this.save();
|
|
4021
|
-
return newRecord;
|
|
4022
|
-
}
|
|
4023
|
-
async updateRecord(record) {
|
|
4024
|
-
await this.ensureInitialized();
|
|
4025
|
-
const index = this.records.findIndex((r) => r.id === record.id);
|
|
4026
|
-
if (index === -1) {
|
|
4027
|
-
throw new Error(`SEO record with id ${record.id} not found`);
|
|
4028
|
-
}
|
|
4029
|
-
const updated = {
|
|
4030
|
-
...this.records[index],
|
|
4031
|
-
...record
|
|
4032
|
-
};
|
|
4033
|
-
this.records[index] = updated;
|
|
4034
|
-
await this.save();
|
|
4035
|
-
return updated;
|
|
4036
|
-
}
|
|
4037
|
-
async deleteRecord(id) {
|
|
4038
|
-
await this.ensureInitialized();
|
|
4039
|
-
const index = this.records.findIndex((r) => r.id === id);
|
|
4040
|
-
if (index === -1) {
|
|
4041
|
-
throw new Error(`SEO record with id ${id} not found`);
|
|
4042
|
-
}
|
|
4043
|
-
this.records.splice(index, 1);
|
|
4044
|
-
await this.save();
|
|
4045
|
-
}
|
|
4046
|
-
};
|
|
4047
3827
|
|
|
4048
3828
|
// src/lib/storage/storage-factory.ts
|
|
4049
|
-
function createStorageAdapter(config) {
|
|
4050
|
-
const filePath = config?.filePath || process.env.SEO_CONSOLE_STORAGE_PATH || "seo-records.json";
|
|
4051
|
-
return new FileStorage(filePath);
|
|
4052
|
-
}
|
|
4053
3829
|
function detectStorageConfig() {
|
|
4054
3830
|
return {
|
|
4055
3831
|
type: "file",
|
|
@@ -4065,22 +3841,16 @@ function detectStorageConfig() {
|
|
|
4065
3841
|
CardFooter,
|
|
4066
3842
|
CardHeader,
|
|
4067
3843
|
CardTitle,
|
|
4068
|
-
FileStorage,
|
|
4069
3844
|
Input,
|
|
4070
3845
|
OGImagePreview,
|
|
4071
3846
|
SEORecordForm,
|
|
4072
3847
|
SEORecordList,
|
|
4073
3848
|
Spinner,
|
|
4074
3849
|
ValidationDashboard,
|
|
4075
|
-
crawlSiteForSEO,
|
|
4076
3850
|
createSEORecordSchema,
|
|
4077
|
-
createStorageAdapter,
|
|
4078
3851
|
detectStorageConfig,
|
|
4079
|
-
discoverNextJSRoutes,
|
|
4080
3852
|
extractMetadataFromHTML,
|
|
4081
|
-
extractMetadataFromURL,
|
|
4082
3853
|
extractSitemapFromRobotsTxt,
|
|
4083
|
-
generateExamplePaths,
|
|
4084
3854
|
generateRobotsTxt,
|
|
4085
3855
|
generateSitemapFromRecords,
|
|
4086
3856
|
generateSitemapXML,
|