@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.
Files changed (165) hide show
  1. package/README.md +127 -0
  2. package/bin/init.js +267 -0
  3. package/dist/api/backlinks-import.d.ts +4 -0
  4. package/dist/api/backlinks-import.d.ts.map +1 -0
  5. package/dist/api/backlinks-import.js +182 -0
  6. package/dist/api/cron.d.ts +4 -0
  7. package/dist/api/cron.d.ts.map +1 -0
  8. package/dist/api/cron.js +89 -0
  9. package/dist/api/index.d.ts +10 -0
  10. package/dist/api/index.d.ts.map +1 -0
  11. package/dist/api/index.js +21 -0
  12. package/dist/api/page-result.d.ts +4 -0
  13. package/dist/api/page-result.d.ts.map +1 -0
  14. package/dist/api/page-result.js +93 -0
  15. package/dist/api/page-results.d.ts +4 -0
  16. package/dist/api/page-results.d.ts.map +1 -0
  17. package/dist/api/page-results.js +83 -0
  18. package/dist/api/run-stream.d.ts +4 -0
  19. package/dist/api/run-stream.d.ts.map +1 -0
  20. package/dist/api/run-stream.js +273 -0
  21. package/dist/api/run.d.ts +4 -0
  22. package/dist/api/run.d.ts.map +1 -0
  23. package/dist/api/run.js +102 -0
  24. package/dist/api/snapshot-report.d.ts +4 -0
  25. package/dist/api/snapshot-report.d.ts.map +1 -0
  26. package/dist/api/snapshot-report.js +130 -0
  27. package/dist/api/snapshots.d.ts +4 -0
  28. package/dist/api/snapshots.d.ts.map +1 -0
  29. package/dist/api/snapshots.js +138 -0
  30. package/dist/api/trend.d.ts +4 -0
  31. package/dist/api/trend.d.ts.map +1 -0
  32. package/dist/api/trend.js +71 -0
  33. package/dist/collections/SeoAuthoritySnapshots.d.ts +3 -0
  34. package/dist/collections/SeoAuthoritySnapshots.d.ts.map +1 -0
  35. package/dist/collections/SeoAuthoritySnapshots.js +83 -0
  36. package/dist/collections/SeoKeywordVisibility.d.ts +3 -0
  37. package/dist/collections/SeoKeywordVisibility.d.ts.map +1 -0
  38. package/dist/collections/SeoKeywordVisibility.js +65 -0
  39. package/dist/collections/SeoPageResults.d.ts +3 -0
  40. package/dist/collections/SeoPageResults.d.ts.map +1 -0
  41. package/dist/collections/SeoPageResults.js +170 -0
  42. package/dist/collections/SeoSnapshots.d.ts +3 -0
  43. package/dist/collections/SeoSnapshots.d.ts.map +1 -0
  44. package/dist/collections/SeoSnapshots.js +131 -0
  45. package/dist/components/hooks/useSeoApi.d.ts +7 -0
  46. package/dist/components/hooks/useSeoApi.d.ts.map +1 -0
  47. package/dist/components/hooks/useSeoApi.js +31 -0
  48. package/dist/components/hooks/useSeoPageResults.d.ts +19 -0
  49. package/dist/components/hooks/useSeoPageResults.d.ts.map +1 -0
  50. package/dist/components/hooks/useSeoPageResults.js +62 -0
  51. package/dist/components/hooks/useSeoSnapshot.d.ts +8 -0
  52. package/dist/components/hooks/useSeoSnapshot.d.ts.map +1 -0
  53. package/dist/components/hooks/useSeoSnapshot.js +39 -0
  54. package/dist/components/hooks/useSeoTrend.d.ts +8 -0
  55. package/dist/components/hooks/useSeoTrend.d.ts.map +1 -0
  56. package/dist/components/hooks/useSeoTrend.js +38 -0
  57. package/dist/components/layout/SeoReportHeader.d.ts +10 -0
  58. package/dist/components/layout/SeoReportHeader.d.ts.map +1 -0
  59. package/dist/components/layout/SeoReportHeader.js +18 -0
  60. package/dist/components/layout/SeoReportShell.d.ts +9 -0
  61. package/dist/components/layout/SeoReportShell.d.ts.map +1 -0
  62. package/dist/components/layout/SeoReportShell.js +17 -0
  63. package/dist/components/pdf/PdfDownloadButton.d.ts +9 -0
  64. package/dist/components/pdf/PdfDownloadButton.d.ts.map +1 -0
  65. package/dist/components/pdf/PdfDownloadButton.js +80 -0
  66. package/dist/components/tables/IssueTable.d.ts +11 -0
  67. package/dist/components/tables/IssueTable.d.ts.map +1 -0
  68. package/dist/components/tables/IssueTable.js +121 -0
  69. package/dist/components/tables/PageResultsTable.d.ts +18 -0
  70. package/dist/components/tables/PageResultsTable.d.ts.map +1 -0
  71. package/dist/components/tables/PageResultsTable.js +96 -0
  72. package/dist/components/types.d.ts +107 -0
  73. package/dist/components/types.d.ts.map +1 -0
  74. package/dist/components/types.js +22 -0
  75. package/dist/components/utils/formatters.d.ts +15 -0
  76. package/dist/components/utils/formatters.d.ts.map +1 -0
  77. package/dist/components/utils/formatters.js +98 -0
  78. package/dist/components/utils/scoreHelpers.d.ts +17 -0
  79. package/dist/components/utils/scoreHelpers.d.ts.map +1 -0
  80. package/dist/components/utils/scoreHelpers.js +139 -0
  81. package/dist/components/views/SeoDashboard.d.ts +3 -0
  82. package/dist/components/views/SeoDashboard.d.ts.map +1 -0
  83. package/dist/components/views/SeoDashboard.js +239 -0
  84. package/dist/components/views/SeoPageReport.d.ts +3 -0
  85. package/dist/components/views/SeoPageReport.d.ts.map +1 -0
  86. package/dist/components/views/SeoPageReport.js +234 -0
  87. package/dist/components/views/SeoSnapshotReport.d.ts +3 -0
  88. package/dist/components/views/SeoSnapshotReport.d.ts.map +1 -0
  89. package/dist/components/views/SeoSnapshotReport.js +224 -0
  90. package/dist/components/visualization/CategoryScoreCard.d.ts +11 -0
  91. package/dist/components/visualization/CategoryScoreCard.d.ts.map +1 -0
  92. package/dist/components/visualization/CategoryScoreCard.js +17 -0
  93. package/dist/components/visualization/CategoryScoreGrid.d.ts +9 -0
  94. package/dist/components/visualization/CategoryScoreGrid.d.ts.map +1 -0
  95. package/dist/components/visualization/CategoryScoreGrid.js +32 -0
  96. package/dist/components/visualization/IssueCategoryChart.d.ts +8 -0
  97. package/dist/components/visualization/IssueCategoryChart.d.ts.map +1 -0
  98. package/dist/components/visualization/IssueCategoryChart.js +47 -0
  99. package/dist/components/visualization/MetricCard.d.ts +11 -0
  100. package/dist/components/visualization/MetricCard.d.ts.map +1 -0
  101. package/dist/components/visualization/MetricCard.js +17 -0
  102. package/dist/components/visualization/MetricCardRow.d.ts +7 -0
  103. package/dist/components/visualization/MetricCardRow.d.ts.map +1 -0
  104. package/dist/components/visualization/MetricCardRow.js +12 -0
  105. package/dist/components/visualization/ScoreBar.d.ts +11 -0
  106. package/dist/components/visualization/ScoreBar.d.ts.map +1 -0
  107. package/dist/components/visualization/ScoreBar.js +34 -0
  108. package/dist/components/visualization/ScoreGauge.d.ts +11 -0
  109. package/dist/components/visualization/ScoreGauge.d.ts.map +1 -0
  110. package/dist/components/visualization/ScoreGauge.js +28 -0
  111. package/dist/components/visualization/ScoreTrendChart.d.ts +9 -0
  112. package/dist/components/visualization/ScoreTrendChart.d.ts.map +1 -0
  113. package/dist/components/visualization/ScoreTrendChart.js +43 -0
  114. package/dist/components/visualization/SeverityBadge.d.ts +8 -0
  115. package/dist/components/visualization/SeverityBadge.d.ts.map +1 -0
  116. package/dist/components/visualization/SeverityBadge.js +14 -0
  117. package/dist/config.d.ts +38 -0
  118. package/dist/config.d.ts.map +1 -0
  119. package/dist/config.js +36 -0
  120. package/dist/exports/components.d.ts +4 -0
  121. package/dist/exports/components.d.ts.map +1 -0
  122. package/dist/exports/components.js +9 -0
  123. package/dist/globals/SeoDashboard.d.ts +3 -0
  124. package/dist/globals/SeoDashboard.d.ts.map +1 -0
  125. package/dist/globals/SeoDashboard.js +25 -0
  126. package/dist/index.d.ts +6 -0
  127. package/dist/index.d.ts.map +1 -0
  128. package/dist/index.js +39 -0
  129. package/dist/utilities/access.d.ts +8 -0
  130. package/dist/utilities/access.d.ts.map +1 -0
  131. package/dist/utilities/access.js +11 -0
  132. package/dist/utilities/auth.d.ts +7 -0
  133. package/dist/utilities/auth.d.ts.map +1 -0
  134. package/dist/utilities/auth.js +28 -0
  135. package/dist/utilities/checks.d.ts +3 -0
  136. package/dist/utilities/checks.d.ts.map +1 -0
  137. package/dist/utilities/checks.js +255 -0
  138. package/dist/utilities/crawler.d.ts +14 -0
  139. package/dist/utilities/crawler.d.ts.map +1 -0
  140. package/dist/utilities/crawler.js +152 -0
  141. package/dist/utilities/gsc.d.ts +15 -0
  142. package/dist/utilities/gsc.d.ts.map +1 -0
  143. package/dist/utilities/gsc.js +69 -0
  144. package/dist/utilities/helpers.d.ts +7 -0
  145. package/dist/utilities/helpers.d.ts.map +1 -0
  146. package/dist/utilities/helpers.js +44 -0
  147. package/dist/utilities/pagespeed.d.ts +3 -0
  148. package/dist/utilities/pagespeed.d.ts.map +1 -0
  149. package/dist/utilities/pagespeed.js +49 -0
  150. package/dist/utilities/providers.d.ts +3 -0
  151. package/dist/utilities/providers.d.ts.map +1 -0
  152. package/dist/utilities/providers.js +18 -0
  153. package/dist/utilities/runAudit.d.ts +14 -0
  154. package/dist/utilities/runAudit.d.ts.map +1 -0
  155. package/dist/utilities/runAudit.js +224 -0
  156. package/dist/utilities/scoring.d.ts +3 -0
  157. package/dist/utilities/scoring.d.ts.map +1 -0
  158. package/dist/utilities/scoring.js +45 -0
  159. package/dist/utilities/triggers.d.ts +3 -0
  160. package/dist/utilities/triggers.d.ts.map +1 -0
  161. package/dist/utilities/triggers.js +39 -0
  162. package/dist/utilities/types.d.ts +87 -0
  163. package/dist/utilities/types.d.ts.map +1 -0
  164. package/dist/utilities/types.js +2 -0
  165. 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,4 @@
1
+ import type { Payload } from 'payload';
2
+ import type { NextRequest, NextResponse } from 'next/server';
3
+ export declare function handleSeoBacklinksImport(payload: Payload, request: NextRequest): Promise<NextResponse>;
4
+ //# sourceMappingURL=backlinks-import.d.ts.map
@@ -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,4 @@
1
+ import type { Payload } from 'payload';
2
+ import type { NextRequest, NextResponse } from 'next/server';
3
+ export declare function handleSeoCron(payload: Payload, request: NextRequest): Promise<NextResponse>;
4
+ //# sourceMappingURL=cron.d.ts.map
@@ -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"}
@@ -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
+ }