@orion-studios/payload-seo-audit 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 +127 -0
- package/bin/init.js +267 -0
- package/dist/api/backlinks-import.d.ts +4 -0
- package/dist/api/backlinks-import.d.ts.map +1 -0
- package/dist/api/backlinks-import.js +182 -0
- package/dist/api/cron.d.ts +4 -0
- package/dist/api/cron.d.ts.map +1 -0
- package/dist/api/cron.js +89 -0
- package/dist/api/index.d.ts +10 -0
- package/dist/api/index.d.ts.map +1 -0
- package/dist/api/index.js +21 -0
- package/dist/api/page-result.d.ts +4 -0
- package/dist/api/page-result.d.ts.map +1 -0
- package/dist/api/page-result.js +93 -0
- package/dist/api/page-results.d.ts +4 -0
- package/dist/api/page-results.d.ts.map +1 -0
- package/dist/api/page-results.js +83 -0
- package/dist/api/run-stream.d.ts +4 -0
- package/dist/api/run-stream.d.ts.map +1 -0
- package/dist/api/run-stream.js +273 -0
- package/dist/api/run.d.ts +4 -0
- package/dist/api/run.d.ts.map +1 -0
- package/dist/api/run.js +102 -0
- package/dist/api/snapshot-report.d.ts +4 -0
- package/dist/api/snapshot-report.d.ts.map +1 -0
- package/dist/api/snapshot-report.js +130 -0
- package/dist/api/snapshots.d.ts +4 -0
- package/dist/api/snapshots.d.ts.map +1 -0
- package/dist/api/snapshots.js +138 -0
- package/dist/api/trend.d.ts +4 -0
- package/dist/api/trend.d.ts.map +1 -0
- package/dist/api/trend.js +71 -0
- package/dist/collections/SeoAuthoritySnapshots.d.ts +3 -0
- package/dist/collections/SeoAuthoritySnapshots.d.ts.map +1 -0
- package/dist/collections/SeoAuthoritySnapshots.js +83 -0
- package/dist/collections/SeoKeywordVisibility.d.ts +3 -0
- package/dist/collections/SeoKeywordVisibility.d.ts.map +1 -0
- package/dist/collections/SeoKeywordVisibility.js +65 -0
- package/dist/collections/SeoPageResults.d.ts +3 -0
- package/dist/collections/SeoPageResults.d.ts.map +1 -0
- package/dist/collections/SeoPageResults.js +170 -0
- package/dist/collections/SeoSnapshots.d.ts +3 -0
- package/dist/collections/SeoSnapshots.d.ts.map +1 -0
- package/dist/collections/SeoSnapshots.js +131 -0
- package/dist/components/hooks/useSeoApi.d.ts +7 -0
- package/dist/components/hooks/useSeoApi.d.ts.map +1 -0
- package/dist/components/hooks/useSeoApi.js +31 -0
- package/dist/components/hooks/useSeoPageResults.d.ts +19 -0
- package/dist/components/hooks/useSeoPageResults.d.ts.map +1 -0
- package/dist/components/hooks/useSeoPageResults.js +62 -0
- package/dist/components/hooks/useSeoSnapshot.d.ts +8 -0
- package/dist/components/hooks/useSeoSnapshot.d.ts.map +1 -0
- package/dist/components/hooks/useSeoSnapshot.js +39 -0
- package/dist/components/hooks/useSeoTrend.d.ts +8 -0
- package/dist/components/hooks/useSeoTrend.d.ts.map +1 -0
- package/dist/components/hooks/useSeoTrend.js +38 -0
- package/dist/components/layout/SeoReportHeader.d.ts +10 -0
- package/dist/components/layout/SeoReportHeader.d.ts.map +1 -0
- package/dist/components/layout/SeoReportHeader.js +18 -0
- package/dist/components/layout/SeoReportShell.d.ts +9 -0
- package/dist/components/layout/SeoReportShell.d.ts.map +1 -0
- package/dist/components/layout/SeoReportShell.js +17 -0
- package/dist/components/pdf/PdfDownloadButton.d.ts +9 -0
- package/dist/components/pdf/PdfDownloadButton.d.ts.map +1 -0
- package/dist/components/pdf/PdfDownloadButton.js +80 -0
- package/dist/components/tables/IssueTable.d.ts +11 -0
- package/dist/components/tables/IssueTable.d.ts.map +1 -0
- package/dist/components/tables/IssueTable.js +121 -0
- package/dist/components/tables/PageResultsTable.d.ts +18 -0
- package/dist/components/tables/PageResultsTable.d.ts.map +1 -0
- package/dist/components/tables/PageResultsTable.js +96 -0
- package/dist/components/types.d.ts +107 -0
- package/dist/components/types.d.ts.map +1 -0
- package/dist/components/types.js +22 -0
- package/dist/components/utils/formatters.d.ts +15 -0
- package/dist/components/utils/formatters.d.ts.map +1 -0
- package/dist/components/utils/formatters.js +98 -0
- package/dist/components/utils/scoreHelpers.d.ts +17 -0
- package/dist/components/utils/scoreHelpers.d.ts.map +1 -0
- package/dist/components/utils/scoreHelpers.js +139 -0
- package/dist/components/views/SeoDashboard.d.ts +3 -0
- package/dist/components/views/SeoDashboard.d.ts.map +1 -0
- package/dist/components/views/SeoDashboard.js +239 -0
- package/dist/components/views/SeoPageReport.d.ts +3 -0
- package/dist/components/views/SeoPageReport.d.ts.map +1 -0
- package/dist/components/views/SeoPageReport.js +234 -0
- package/dist/components/views/SeoSnapshotReport.d.ts +3 -0
- package/dist/components/views/SeoSnapshotReport.d.ts.map +1 -0
- package/dist/components/views/SeoSnapshotReport.js +224 -0
- package/dist/components/visualization/CategoryScoreCard.d.ts +11 -0
- package/dist/components/visualization/CategoryScoreCard.d.ts.map +1 -0
- package/dist/components/visualization/CategoryScoreCard.js +17 -0
- package/dist/components/visualization/CategoryScoreGrid.d.ts +9 -0
- package/dist/components/visualization/CategoryScoreGrid.d.ts.map +1 -0
- package/dist/components/visualization/CategoryScoreGrid.js +32 -0
- package/dist/components/visualization/IssueCategoryChart.d.ts +8 -0
- package/dist/components/visualization/IssueCategoryChart.d.ts.map +1 -0
- package/dist/components/visualization/IssueCategoryChart.js +47 -0
- package/dist/components/visualization/MetricCard.d.ts +11 -0
- package/dist/components/visualization/MetricCard.d.ts.map +1 -0
- package/dist/components/visualization/MetricCard.js +17 -0
- package/dist/components/visualization/MetricCardRow.d.ts +7 -0
- package/dist/components/visualization/MetricCardRow.d.ts.map +1 -0
- package/dist/components/visualization/MetricCardRow.js +12 -0
- package/dist/components/visualization/ScoreBar.d.ts +11 -0
- package/dist/components/visualization/ScoreBar.d.ts.map +1 -0
- package/dist/components/visualization/ScoreBar.js +34 -0
- package/dist/components/visualization/ScoreGauge.d.ts +11 -0
- package/dist/components/visualization/ScoreGauge.d.ts.map +1 -0
- package/dist/components/visualization/ScoreGauge.js +28 -0
- package/dist/components/visualization/ScoreTrendChart.d.ts +9 -0
- package/dist/components/visualization/ScoreTrendChart.d.ts.map +1 -0
- package/dist/components/visualization/ScoreTrendChart.js +43 -0
- package/dist/components/visualization/SeverityBadge.d.ts +8 -0
- package/dist/components/visualization/SeverityBadge.d.ts.map +1 -0
- package/dist/components/visualization/SeverityBadge.js +14 -0
- package/dist/config.d.ts +38 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +36 -0
- package/dist/exports/components.d.ts +4 -0
- package/dist/exports/components.d.ts.map +1 -0
- package/dist/exports/components.js +9 -0
- package/dist/globals/SeoDashboard.d.ts +3 -0
- package/dist/globals/SeoDashboard.d.ts.map +1 -0
- package/dist/globals/SeoDashboard.js +25 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +39 -0
- package/dist/utilities/access.d.ts +8 -0
- package/dist/utilities/access.d.ts.map +1 -0
- package/dist/utilities/access.js +11 -0
- package/dist/utilities/auth.d.ts +7 -0
- package/dist/utilities/auth.d.ts.map +1 -0
- package/dist/utilities/auth.js +28 -0
- package/dist/utilities/checks.d.ts +3 -0
- package/dist/utilities/checks.d.ts.map +1 -0
- package/dist/utilities/checks.js +255 -0
- package/dist/utilities/crawler.d.ts +14 -0
- package/dist/utilities/crawler.d.ts.map +1 -0
- package/dist/utilities/crawler.js +152 -0
- package/dist/utilities/gsc.d.ts +15 -0
- package/dist/utilities/gsc.d.ts.map +1 -0
- package/dist/utilities/gsc.js +69 -0
- package/dist/utilities/helpers.d.ts +7 -0
- package/dist/utilities/helpers.d.ts.map +1 -0
- package/dist/utilities/helpers.js +44 -0
- package/dist/utilities/pagespeed.d.ts +3 -0
- package/dist/utilities/pagespeed.d.ts.map +1 -0
- package/dist/utilities/pagespeed.js +49 -0
- package/dist/utilities/providers.d.ts +3 -0
- package/dist/utilities/providers.d.ts.map +1 -0
- package/dist/utilities/providers.js +18 -0
- package/dist/utilities/runAudit.d.ts +14 -0
- package/dist/utilities/runAudit.d.ts.map +1 -0
- package/dist/utilities/runAudit.js +224 -0
- package/dist/utilities/scoring.d.ts +3 -0
- package/dist/utilities/scoring.d.ts.map +1 -0
- package/dist/utilities/scoring.js +45 -0
- package/dist/utilities/triggers.d.ts +3 -0
- package/dist/utilities/triggers.d.ts.map +1 -0
- package/dist/utilities/triggers.js +39 -0
- package/dist/utilities/types.d.ts +87 -0
- package/dist/utilities/types.d.ts.map +1 -0
- package/dist/utilities/types.js +2 -0
- package/package.json +63 -0
package/README.md
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# @orion-studios/payload-seo-audit
|
|
2
|
+
|
|
3
|
+
Professional SEO audit system for Payload CMS 3.0 + Next.js 15 projects.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 🔍 **Comprehensive SEO Analysis** - Crawls your site and checks 50+ SEO factors
|
|
8
|
+
- 📊 **Visual Dashboard** - Beautiful admin UI with scores, trends, and issue tracking
|
|
9
|
+
- 🎯 **Category Scoring** - Metadata, indexability, structure, links, media, structured data, performance
|
|
10
|
+
- 📈 **Trend Tracking** - Monitor your SEO improvements over time
|
|
11
|
+
- ⚡ **Performance Metrics** - Integrates with Google PageSpeed Insights for Core Web Vitals
|
|
12
|
+
- 🔄 **Auto-Trigger** - Optionally run audits when content is published
|
|
13
|
+
- 🌙 **Dark Mode** - Full dark mode support for comfortable viewing
|
|
14
|
+
- 📄 **PDF Export** - Download professional audit reports
|
|
15
|
+
- 🔌 **Easy Integration** - Simple plugin system with automated setup
|
|
16
|
+
|
|
17
|
+
## Quick Start
|
|
18
|
+
|
|
19
|
+
### Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npm install @orion-studios/payload-seo-audit
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Setup (10-15 minutes)
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
# 1. Run the automated setup tool
|
|
29
|
+
npx payload-seo-audit init
|
|
30
|
+
|
|
31
|
+
# 2. Configure your environment variables (.env.local)
|
|
32
|
+
SEO_AUDIT_SECRET=<random-secret-key>
|
|
33
|
+
NEXT_PUBLIC_SERVER_URL=https://your-site.com
|
|
34
|
+
|
|
35
|
+
# 3. Add plugin to payload.config.ts (see below)
|
|
36
|
+
|
|
37
|
+
# 4. Generate types and start dev server
|
|
38
|
+
npx payload generate:types
|
|
39
|
+
npm run dev
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Plugin Configuration
|
|
43
|
+
|
|
44
|
+
Add to your `payload.config.ts`:
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
import { seoAuditPlugin } from '@orion-studios/payload-seo-audit'
|
|
48
|
+
|
|
49
|
+
export default buildConfig({
|
|
50
|
+
plugins: [
|
|
51
|
+
seoAuditPlugin({
|
|
52
|
+
site: {
|
|
53
|
+
name: 'Your Site Name',
|
|
54
|
+
domain: 'www.yoursite.com',
|
|
55
|
+
canonicalHost: 'https://www.yoursite.com',
|
|
56
|
+
sitemapURL: 'https://www.yoursite.com/sitemap.xml',
|
|
57
|
+
keyURLs: [
|
|
58
|
+
'https://www.yoursite.com',
|
|
59
|
+
'https://www.yoursite.com/about',
|
|
60
|
+
],
|
|
61
|
+
},
|
|
62
|
+
crawl: {
|
|
63
|
+
maxPages: 120,
|
|
64
|
+
maxDepth: 2,
|
|
65
|
+
excludePatterns: ['/admin', '/api', '?preview='],
|
|
66
|
+
},
|
|
67
|
+
triggers: {
|
|
68
|
+
collections: ['posts', 'pages'],
|
|
69
|
+
globals: ['home'],
|
|
70
|
+
},
|
|
71
|
+
access: (user) => user?.role === 'admin',
|
|
72
|
+
}),
|
|
73
|
+
],
|
|
74
|
+
})
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Usage
|
|
78
|
+
|
|
79
|
+
1. Visit `/admin/globals/seo-dashboard`
|
|
80
|
+
2. Click **"Run Audit Now"**
|
|
81
|
+
3. View comprehensive SEO analysis
|
|
82
|
+
4. Click on snapshots to see detailed reports
|
|
83
|
+
5. Track your improvements over time
|
|
84
|
+
|
|
85
|
+
## Documentation
|
|
86
|
+
|
|
87
|
+
- **[Integration Guide](./INTEGRATION.md)** - Complete setup instructions with troubleshooting
|
|
88
|
+
- **[Configuration Options](./INTEGRATION.md#step-2-configure-payload-plugin)** - All plugin config options
|
|
89
|
+
- **[Troubleshooting](./INTEGRATION.md#troubleshooting)** - Common issues and solutions
|
|
90
|
+
|
|
91
|
+
## Troubleshooting
|
|
92
|
+
|
|
93
|
+
### Dark Mode Not Working (White Cards)
|
|
94
|
+
|
|
95
|
+
Ensure your `tailwind.config.ts` has BOTH:
|
|
96
|
+
|
|
97
|
+
1. **Plugin path in content array:**
|
|
98
|
+
```typescript
|
|
99
|
+
content: [
|
|
100
|
+
'./src/**/*.{js,ts,jsx,tsx}',
|
|
101
|
+
'./node_modules/@orion-studios/payload-seo-audit/dist/**/*.js',
|
|
102
|
+
]
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
2. **Correct darkMode configuration:**
|
|
106
|
+
```typescript
|
|
107
|
+
darkMode: ['selector', '[data-theme="dark"]'],
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
**If you have a custom theme toggle:** Update it to use `data-theme` attribute instead of classes:
|
|
111
|
+
```typescript
|
|
112
|
+
// ✅ Correct
|
|
113
|
+
document.documentElement.setAttribute('data-theme', 'dark')
|
|
114
|
+
|
|
115
|
+
// ❌ Wrong
|
|
116
|
+
document.documentElement.classList.add('dark')
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Then restart dev server and hard refresh. See [full troubleshooting guide](./INTEGRATION.md#troubleshooting).
|
|
120
|
+
|
|
121
|
+
## License
|
|
122
|
+
|
|
123
|
+
MIT License
|
|
124
|
+
|
|
125
|
+
## Author
|
|
126
|
+
|
|
127
|
+
Built by Orion Studios
|
package/bin/init.js
ADDED
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('fs')
|
|
4
|
+
const path = require('path')
|
|
5
|
+
|
|
6
|
+
const ROUTES = [
|
|
7
|
+
{ path: 'run/route.ts', method: 'POST', handler: 'handleSeoRun' },
|
|
8
|
+
{ path: 'run-stream/route.ts', method: 'POST', handler: 'handleSeoRunStream' },
|
|
9
|
+
{ path: 'snapshots/route.ts', method: 'GET', handler: 'handleSeoSnapshots' },
|
|
10
|
+
{ path: 'snapshot-report/route.ts', method: 'GET', handler: 'handleSeoSnapshotReport' },
|
|
11
|
+
{ path: 'page-results/route.ts', method: 'GET', handler: 'handleSeoPageResults' },
|
|
12
|
+
{ path: 'page-result/route.ts', method: 'GET', handler: 'handleSeoPageResult' },
|
|
13
|
+
{ path: 'trend/route.ts', method: 'GET', handler: 'handleSeoTrend' },
|
|
14
|
+
{ path: 'cron/route.ts', method: 'POST', handler: 'handleSeoCron' },
|
|
15
|
+
{ path: 'backlinks/import/route.ts', method: 'POST', handler: 'handleSeoBacklinksImport' },
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
const PLUGIN_PATH = './node_modules/@orion-studios/payload-seo-audit/dist/**/*.js'
|
|
19
|
+
|
|
20
|
+
console.log('🔧 SEO Audit Plugin Setup\n')
|
|
21
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n')
|
|
22
|
+
|
|
23
|
+
// Find the API directory
|
|
24
|
+
function findApiDirectory() {
|
|
25
|
+
const cwd = process.cwd()
|
|
26
|
+
const possiblePaths = [
|
|
27
|
+
path.join(cwd, 'src/app/api'),
|
|
28
|
+
path.join(cwd, 'app/api'),
|
|
29
|
+
path.join(cwd, 'src/api'),
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
for (const apiPath of possiblePaths) {
|
|
33
|
+
if (fs.existsSync(apiPath)) {
|
|
34
|
+
return apiPath
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Default to src/app/api
|
|
39
|
+
return path.join(cwd, 'src/app/api')
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Find Tailwind config file
|
|
43
|
+
function findTailwindConfig() {
|
|
44
|
+
const cwd = process.cwd()
|
|
45
|
+
const possibleConfigs = [
|
|
46
|
+
path.join(cwd, 'tailwind.config.ts'),
|
|
47
|
+
path.join(cwd, 'tailwind.config.js'),
|
|
48
|
+
path.join(cwd, 'tailwind.config.mjs'),
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
for (const configPath of possibleConfigs) {
|
|
52
|
+
if (fs.existsSync(configPath)) {
|
|
53
|
+
return configPath
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return null
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Update Tailwind config to include plugin path
|
|
61
|
+
function updateTailwindConfig(configPath) {
|
|
62
|
+
try {
|
|
63
|
+
let content = fs.readFileSync(configPath, 'utf-8')
|
|
64
|
+
let updated = false
|
|
65
|
+
|
|
66
|
+
// Check if plugin path already exists
|
|
67
|
+
if (content.includes(PLUGIN_PATH)) {
|
|
68
|
+
console.log('✓ Tailwind config already includes plugin path')
|
|
69
|
+
} else {
|
|
70
|
+
// Find the content array and add the plugin path
|
|
71
|
+
const contentArrayRegex = /content\s*:\s*\[([^\]]*)\]/s
|
|
72
|
+
const match = content.match(contentArrayRegex)
|
|
73
|
+
|
|
74
|
+
if (!match) {
|
|
75
|
+
console.log('⚠️ Could not find content array in Tailwind config')
|
|
76
|
+
console.log(' Please manually add to tailwind.config.ts:')
|
|
77
|
+
console.log(` content: [..., '${PLUGIN_PATH}']`)
|
|
78
|
+
return false
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Extract existing content array items
|
|
82
|
+
const existingContent = match[1].trim()
|
|
83
|
+
|
|
84
|
+
// Add plugin path to the end
|
|
85
|
+
const newContent = existingContent
|
|
86
|
+
? `${existingContent},\n '${PLUGIN_PATH}',`
|
|
87
|
+
: `'${PLUGIN_PATH}',`
|
|
88
|
+
|
|
89
|
+
// Replace the content array
|
|
90
|
+
content = content.replace(
|
|
91
|
+
contentArrayRegex,
|
|
92
|
+
`content: [\n ${newContent}\n ]`
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
updated = true
|
|
96
|
+
console.log('✓ Updated Tailwind config with plugin path')
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Check darkMode configuration
|
|
100
|
+
const hasDarkModeConfig = content.match(/darkMode\s*:\s*\[['"]selector['"]\s*,\s*['"]\[data-theme="dark"\]['"]\]/)
|
|
101
|
+
|
|
102
|
+
if (!hasDarkModeConfig) {
|
|
103
|
+
console.log('⚠️ Dark mode configuration may be incorrect')
|
|
104
|
+
console.log(' For proper dark mode support, your tailwind.config should have:')
|
|
105
|
+
console.log(` darkMode: ['selector', '[data-theme="dark"]'],`)
|
|
106
|
+
console.log('')
|
|
107
|
+
console.log(' This ensures compatibility with both your frontend and Payload admin.')
|
|
108
|
+
console.log(' See INTEGRATION.md for more details.')
|
|
109
|
+
updated = true // Flag that manual review is needed
|
|
110
|
+
} else {
|
|
111
|
+
console.log('✓ Dark mode configuration is correct')
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Write back to file if updated
|
|
115
|
+
if (updated && content !== fs.readFileSync(configPath, 'utf-8')) {
|
|
116
|
+
fs.writeFileSync(configPath, content)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return true
|
|
120
|
+
|
|
121
|
+
} catch (error) {
|
|
122
|
+
console.log('⚠️ Error updating Tailwind config:', error.message)
|
|
123
|
+
console.log(' Please manually add to tailwind.config.ts:')
|
|
124
|
+
console.log(` content: [..., '${PLUGIN_PATH}']`)
|
|
125
|
+
console.log(` darkMode: ['selector', '[data-theme="dark"]'],`)
|
|
126
|
+
return false
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Generate route template
|
|
131
|
+
function generateRouteTemplate(route) {
|
|
132
|
+
return `import { NextRequest } from 'next/server'
|
|
133
|
+
import { getPayload } from '@/utilities/getPayload'
|
|
134
|
+
import { ${route.handler} } from '@orion-studios/payload-seo-audit/api'
|
|
135
|
+
|
|
136
|
+
export async function ${route.method}(request: NextRequest) {
|
|
137
|
+
const payload = await getPayload()
|
|
138
|
+
// @ts-expect-error - Peer dependency type mismatch between plugin and parent payload
|
|
139
|
+
return ${route.handler}(payload, request)
|
|
140
|
+
}
|
|
141
|
+
`
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Check environment variables
|
|
145
|
+
function checkEnvironmentSetup() {
|
|
146
|
+
const envPath = path.join(process.cwd(), '.env.local')
|
|
147
|
+
const hasEnvFile = fs.existsSync(envPath)
|
|
148
|
+
|
|
149
|
+
if (hasEnvFile) {
|
|
150
|
+
const envContent = fs.readFileSync(envPath, 'utf-8')
|
|
151
|
+
const hasSecret = envContent.includes('SEO_AUDIT_SECRET')
|
|
152
|
+
const hasServerUrl = envContent.includes('NEXT_PUBLIC_SERVER_URL')
|
|
153
|
+
|
|
154
|
+
return { hasSecret, hasServerUrl }
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return { hasSecret: false, hasServerUrl: false }
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Main execution
|
|
161
|
+
try {
|
|
162
|
+
// Step 1: Create API routes
|
|
163
|
+
console.log('Step 1: Creating API routes\n')
|
|
164
|
+
|
|
165
|
+
const apiDir = findApiDirectory()
|
|
166
|
+
const seoDir = path.join(apiDir, 'seo')
|
|
167
|
+
|
|
168
|
+
console.log(` 📁 API directory: ${apiDir}`)
|
|
169
|
+
|
|
170
|
+
// Create SEO directory
|
|
171
|
+
fs.mkdirSync(seoDir, { recursive: true })
|
|
172
|
+
|
|
173
|
+
// Generate route files
|
|
174
|
+
let createdCount = 0
|
|
175
|
+
let skippedCount = 0
|
|
176
|
+
|
|
177
|
+
ROUTES.forEach(route => {
|
|
178
|
+
const routePath = path.join(seoDir, route.path)
|
|
179
|
+
|
|
180
|
+
// Create subdirectories if needed
|
|
181
|
+
fs.mkdirSync(path.dirname(routePath), { recursive: true })
|
|
182
|
+
|
|
183
|
+
// Check if file already exists
|
|
184
|
+
if (fs.existsSync(routePath)) {
|
|
185
|
+
skippedCount++
|
|
186
|
+
return
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Write route file
|
|
190
|
+
const template = generateRouteTemplate(route)
|
|
191
|
+
fs.writeFileSync(routePath, template)
|
|
192
|
+
createdCount++
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
console.log(` ✓ Created ${createdCount} API route${createdCount !== 1 ? 's' : ''}`)
|
|
196
|
+
if (skippedCount > 0) {
|
|
197
|
+
console.log(` ℹ Skipped ${skippedCount} existing route${skippedCount !== 1 ? 's' : ''}`)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Step 2: Update Tailwind config
|
|
201
|
+
console.log('\nStep 2: Updating Tailwind configuration\n')
|
|
202
|
+
|
|
203
|
+
const tailwindConfigPath = findTailwindConfig()
|
|
204
|
+
|
|
205
|
+
if (tailwindConfigPath) {
|
|
206
|
+
console.log(` 📄 Found: ${path.basename(tailwindConfigPath)}`)
|
|
207
|
+
const updated = updateTailwindConfig(tailwindConfigPath)
|
|
208
|
+
|
|
209
|
+
if (!updated) {
|
|
210
|
+
console.log('\n ⚠️ IMPORTANT: Dark mode styles will NOT work without this!')
|
|
211
|
+
}
|
|
212
|
+
} else {
|
|
213
|
+
console.log(' ⚠️ No Tailwind config found')
|
|
214
|
+
console.log(' If using Tailwind, manually add:')
|
|
215
|
+
console.log(` content: [..., '${PLUGIN_PATH}']`)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Step 3: Check environment setup
|
|
219
|
+
console.log('\nStep 3: Environment variable check\n')
|
|
220
|
+
|
|
221
|
+
const { hasSecret, hasServerUrl } = checkEnvironmentSetup()
|
|
222
|
+
|
|
223
|
+
if (hasSecret) {
|
|
224
|
+
console.log(' ✓ SEO_AUDIT_SECRET is set')
|
|
225
|
+
} else {
|
|
226
|
+
console.log(' ✗ SEO_AUDIT_SECRET not found in .env.local')
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (hasServerUrl) {
|
|
230
|
+
console.log(' ✓ NEXT_PUBLIC_SERVER_URL is set')
|
|
231
|
+
} else {
|
|
232
|
+
console.log(' ✗ NEXT_PUBLIC_SERVER_URL not found in .env.local')
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Final summary
|
|
236
|
+
console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n')
|
|
237
|
+
console.log('✅ Setup complete!\n')
|
|
238
|
+
|
|
239
|
+
// Next steps
|
|
240
|
+
console.log('📝 Next steps:\n')
|
|
241
|
+
|
|
242
|
+
if (!hasSecret || !hasServerUrl) {
|
|
243
|
+
console.log(' 1. Add missing environment variables to .env.local:')
|
|
244
|
+
if (!hasSecret) {
|
|
245
|
+
console.log(' SEO_AUDIT_SECRET=<generate-random-key>')
|
|
246
|
+
console.log(' # Generate with: node -e "console.log(require(\'crypto\').randomBytes(32).toString(\'hex\'))"')
|
|
247
|
+
}
|
|
248
|
+
if (!hasServerUrl) {
|
|
249
|
+
console.log(' NEXT_PUBLIC_SERVER_URL=https://your-domain.com')
|
|
250
|
+
}
|
|
251
|
+
console.log('')
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
console.log(` ${!hasSecret || !hasServerUrl ? '2' : '1'}. Configure the plugin in payload.config.ts`)
|
|
255
|
+
console.log(` ${!hasSecret || !hasServerUrl ? '3' : '2'}. Run: npx payload generate:types`)
|
|
256
|
+
console.log(` ${!hasSecret || !hasServerUrl ? '4' : '3'}. Start dev server: npm run dev`)
|
|
257
|
+
console.log(` ${!hasSecret || !hasServerUrl ? '5' : '4'}. Visit: /admin/globals/seo-dashboard`)
|
|
258
|
+
|
|
259
|
+
console.log('\n💡 Documentation: See INTEGRATION.md for detailed setup guide')
|
|
260
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n')
|
|
261
|
+
|
|
262
|
+
} catch (error) {
|
|
263
|
+
console.error('\n❌ Setup failed:')
|
|
264
|
+
console.error(error.message)
|
|
265
|
+
console.error('\nPlease report this issue or follow the manual setup guide in INTEGRATION.md')
|
|
266
|
+
process.exit(1)
|
|
267
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"backlinks-import.d.ts","sourceRoot":"","sources":["../../src/api/backlinks-import.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAA;AACtC,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AA0F5D,wBAAsB,wBAAwB,CAC5C,OAAO,EAAE,OAAO,EAChB,OAAO,EAAE,WAAW,GACnB,OAAO,CAAC,YAAY,CAAC,CAiGvB"}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.handleSeoBacklinksImport = handleSeoBacklinksImport;
|
|
37
|
+
const auth_1 = require("../utilities/auth");
|
|
38
|
+
const parseCSVLine = (line) => {
|
|
39
|
+
const result = [];
|
|
40
|
+
let current = '';
|
|
41
|
+
let inQuotes = false;
|
|
42
|
+
for (let i = 0; i < line.length; i += 1) {
|
|
43
|
+
const char = line[i];
|
|
44
|
+
if (char === '"' && line[i + 1] === '"') {
|
|
45
|
+
current += '"';
|
|
46
|
+
i += 1;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (char === '"') {
|
|
50
|
+
inQuotes = !inQuotes;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (char === ',' && !inQuotes) {
|
|
54
|
+
result.push(current.trim());
|
|
55
|
+
current = '';
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
current += char;
|
|
59
|
+
}
|
|
60
|
+
result.push(current.trim());
|
|
61
|
+
return result;
|
|
62
|
+
};
|
|
63
|
+
const parseCSV = (csv) => {
|
|
64
|
+
const rows = csv
|
|
65
|
+
.split(/\r?\n/)
|
|
66
|
+
.map((row) => row.trim())
|
|
67
|
+
.filter(Boolean);
|
|
68
|
+
if (rows.length < 2)
|
|
69
|
+
return [];
|
|
70
|
+
const headers = parseCSVLine(rows[0]);
|
|
71
|
+
return rows.slice(1).map((row) => {
|
|
72
|
+
const values = parseCSVLine(row);
|
|
73
|
+
return headers.reduce((acc, header, index) => {
|
|
74
|
+
acc[header] = values[index] || '';
|
|
75
|
+
return acc;
|
|
76
|
+
}, {});
|
|
77
|
+
});
|
|
78
|
+
};
|
|
79
|
+
const getCSVInput = async (request) => {
|
|
80
|
+
const contentType = request.headers.get('content-type') || '';
|
|
81
|
+
if (contentType.includes('application/json')) {
|
|
82
|
+
const body = (await request.json());
|
|
83
|
+
return {
|
|
84
|
+
label: body.label,
|
|
85
|
+
capturedAt: body.capturedAt,
|
|
86
|
+
csv: body.csv || '',
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
if (contentType.includes('multipart/form-data')) {
|
|
90
|
+
const formData = await request.formData();
|
|
91
|
+
const file = formData.get('file');
|
|
92
|
+
const csv = file instanceof File ? await file.text() : '';
|
|
93
|
+
return {
|
|
94
|
+
label: formData.get('label') || undefined,
|
|
95
|
+
capturedAt: formData.get('capturedAt') || undefined,
|
|
96
|
+
csv,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
const csv = await request.text();
|
|
100
|
+
return {
|
|
101
|
+
csv,
|
|
102
|
+
};
|
|
103
|
+
};
|
|
104
|
+
async function handleSeoBacklinksImport(payload, request) {
|
|
105
|
+
const { NextResponse } = await Promise.resolve().then(() => __importStar(require('next/server')));
|
|
106
|
+
try {
|
|
107
|
+
const user = await (0, auth_1.authenticateSEOAdmin)(payload, request);
|
|
108
|
+
if (!user) {
|
|
109
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
110
|
+
}
|
|
111
|
+
const parsedInput = await getCSVInput(request);
|
|
112
|
+
const rows = parseCSV(parsedInput.csv || '');
|
|
113
|
+
const maxRows = Math.max(10, Number(process.env.SEO_BACKLINK_IMPORT_MAX_ROWS || 5000));
|
|
114
|
+
if (rows.length === 0) {
|
|
115
|
+
return NextResponse.json({ error: 'No CSV rows found to import.' }, { status: 400 });
|
|
116
|
+
}
|
|
117
|
+
if (rows.length > maxRows) {
|
|
118
|
+
return NextResponse.json({
|
|
119
|
+
error: `CSV row count ${rows.length} exceeds SEO_BACKLINK_IMPORT_MAX_ROWS (${maxRows}).`,
|
|
120
|
+
}, { status: 400 });
|
|
121
|
+
}
|
|
122
|
+
const backlinks = rows.map((row) => {
|
|
123
|
+
const sourceURL = row.sourceURL || row.sourceUrl || row.source || '';
|
|
124
|
+
const targetURL = row.targetURL || row.targetUrl || row.target || '';
|
|
125
|
+
const anchorText = row.anchorText || row.anchor || '';
|
|
126
|
+
const firstSeen = row.firstSeen || '';
|
|
127
|
+
const lastSeen = row.lastSeen || '';
|
|
128
|
+
const linkTypeValue = (row.linkType || row.followType || row.type || 'unknown').toLowerCase();
|
|
129
|
+
const linkType = linkTypeValue === 'follow' || linkTypeValue === 'nofollow' ? linkTypeValue : 'unknown';
|
|
130
|
+
return {
|
|
131
|
+
sourceURL,
|
|
132
|
+
targetURL,
|
|
133
|
+
anchorText,
|
|
134
|
+
firstSeen: firstSeen || undefined,
|
|
135
|
+
lastSeen: lastSeen || undefined,
|
|
136
|
+
linkType,
|
|
137
|
+
};
|
|
138
|
+
});
|
|
139
|
+
const referringDomains = new Set(backlinks
|
|
140
|
+
.map((entry) => {
|
|
141
|
+
try {
|
|
142
|
+
return new URL(entry.sourceURL).hostname;
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
})
|
|
148
|
+
.filter(Boolean)).size;
|
|
149
|
+
const totalBacklinks = backlinks.length;
|
|
150
|
+
const domainAuthorityProxy = Math.min(100, Math.round(Math.log10(referringDomains + 1) * 30));
|
|
151
|
+
const label = parsedInput.label?.trim() ||
|
|
152
|
+
`Backlink Import ${new Date(parsedInput.capturedAt || Date.now()).toISOString()}`;
|
|
153
|
+
const capturedAt = parsedInput.capturedAt || new Date().toISOString();
|
|
154
|
+
const snapshot = await payload.create({
|
|
155
|
+
collection: 'seo-authority-snapshots',
|
|
156
|
+
data: {
|
|
157
|
+
label,
|
|
158
|
+
capturedAt,
|
|
159
|
+
source: 'manual-csv',
|
|
160
|
+
totalBacklinks,
|
|
161
|
+
referringDomains,
|
|
162
|
+
domainAuthorityProxy,
|
|
163
|
+
backlinks,
|
|
164
|
+
providerMetadata: {
|
|
165
|
+
importedRows: rows.length,
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
overrideAccess: true,
|
|
169
|
+
});
|
|
170
|
+
return NextResponse.json({
|
|
171
|
+
success: true,
|
|
172
|
+
snapshotID: snapshot.id,
|
|
173
|
+
totalBacklinks,
|
|
174
|
+
referringDomains,
|
|
175
|
+
domainAuthorityProxy,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
console.error('SEO backlinks import error:', error);
|
|
180
|
+
return NextResponse.json({ error: error instanceof Error ? error.message : 'Failed to import backlinks CSV.' }, { status: 500 });
|
|
181
|
+
}
|
|
182
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cron.d.ts","sourceRoot":"","sources":["../../src/api/cron.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAA;AACtC,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAS5D,wBAAsB,aAAa,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,YAAY,CAAC,CA6DjG"}
|
package/dist/api/cron.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.handleSeoCron = handleSeoCron;
|
|
37
|
+
const auth_1 = require("../utilities/auth");
|
|
38
|
+
const index_1 = require("../index");
|
|
39
|
+
const runAudit_1 = require("../utilities/runAudit");
|
|
40
|
+
async function handleSeoCron(payload, request) {
|
|
41
|
+
const { NextResponse } = await Promise.resolve().then(() => __importStar(require('next/server')));
|
|
42
|
+
if (!(0, auth_1.isValidSEOSecret)(request)) {
|
|
43
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
const seoConfig = (0, index_1.getSeoConfig)(payload);
|
|
47
|
+
if (!seoConfig) {
|
|
48
|
+
return NextResponse.json({ error: 'SEO Audit plugin not configured' }, { status: 500 });
|
|
49
|
+
}
|
|
50
|
+
const site = {
|
|
51
|
+
id: 1,
|
|
52
|
+
name: seoConfig.site.name,
|
|
53
|
+
domain: seoConfig.site.domain,
|
|
54
|
+
canonicalHost: seoConfig.site.canonicalHost,
|
|
55
|
+
sitemapURL: seoConfig.site.sitemapURL,
|
|
56
|
+
keyURLs: seoConfig.site.keyURLs.map((url) => ({ url })),
|
|
57
|
+
crawlSettings: {
|
|
58
|
+
maxPages: seoConfig.crawl.maxPages,
|
|
59
|
+
maxDepth: seoConfig.crawl.maxDepth,
|
|
60
|
+
requestTimeoutMs: seoConfig.crawl.requestTimeoutMs,
|
|
61
|
+
includePatterns: seoConfig.crawl.includePatterns.map((pattern) => ({ pattern })),
|
|
62
|
+
excludePatterns: seoConfig.crawl.excludePatterns.map((pattern) => ({ pattern })),
|
|
63
|
+
},
|
|
64
|
+
integrations: {
|
|
65
|
+
enablePageSpeed: seoConfig.integrations.enablePageSpeed,
|
|
66
|
+
enableSearchConsole: seoConfig.integrations.enableSearchConsole,
|
|
67
|
+
},
|
|
68
|
+
thresholds: {
|
|
69
|
+
targetLCPMs: seoConfig.thresholds.targetLCPMs,
|
|
70
|
+
targetCLS: seoConfig.thresholds.targetCLS,
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
const result = await (0, runAudit_1.runSEOSnapshot)({
|
|
74
|
+
payload,
|
|
75
|
+
site,
|
|
76
|
+
runType: 'scheduled',
|
|
77
|
+
});
|
|
78
|
+
return NextResponse.json({
|
|
79
|
+
success: true,
|
|
80
|
+
ranAt: new Date().toISOString(),
|
|
81
|
+
siteName: site.name,
|
|
82
|
+
...result,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
console.error('SEO cron error:', error);
|
|
87
|
+
return NextResponse.json({ error: error instanceof Error ? error.message : 'Failed cron SEO run.' }, { status: 500 });
|
|
88
|
+
}
|
|
89
|
+
}
|