@houseofmvps/claude-rank 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/.claude-plugin/plugin.json +16 -0
- package/CLAUDE.md +65 -0
- package/LICENSE +21 -0
- package/README.md +151 -0
- package/agents/aeo-auditor.md +26 -0
- package/agents/geo-auditor.md +27 -0
- package/agents/schema-auditor.md +17 -0
- package/agents/seo-auditor.md +28 -0
- package/bin/claude-rank.mjs +66 -0
- package/commands/rank-aeo.md +3 -0
- package/commands/rank-audit.md +3 -0
- package/commands/rank-fix.md +3 -0
- package/commands/rank-geo.md +3 -0
- package/commands/rank-schema.md +3 -0
- package/commands/rank.md +5 -0
- package/hooks/hooks.json +10 -0
- package/llms.txt +21 -0
- package/package.json +58 -0
- package/research/geo-research.md +106 -0
- package/research/schema-catalog.md +170 -0
- package/research/seo-benchmarks.md +75 -0
- package/skills/rank/SKILL.md +48 -0
- package/skills/rank-aeo/SKILL.md +37 -0
- package/skills/rank-audit/SKILL.md +78 -0
- package/skills/rank-fix/SKILL.md +48 -0
- package/skills/rank-geo/SKILL.md +42 -0
- package/skills/rank-schema/SKILL.md +42 -0
- package/tools/aeo-scanner.mjs +394 -0
- package/tools/audit-history.mjs +117 -0
- package/tools/geo-scanner.mjs +531 -0
- package/tools/lib/html-parser.mjs +490 -0
- package/tools/lib/security.mjs +204 -0
- package/tools/llms-txt-generator.mjs +92 -0
- package/tools/robots-analyzer.mjs +190 -0
- package/tools/schema-engine.mjs +294 -0
- package/tools/seo-scanner.mjs +514 -0
- package/tools/sitemap-analyzer.mjs +224 -0
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sitemap-analyzer.mjs — Generate sitemaps and discover routes from HTML files.
|
|
3
|
+
* Supports Next.js app/pages router and static HTML builds.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// generateSitemap
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Generate a valid sitemap XML string.
|
|
15
|
+
* Deduplicates paths and always includes the root URL.
|
|
16
|
+
* @param {string} baseUrl
|
|
17
|
+
* @param {string[]} paths
|
|
18
|
+
* @returns {string}
|
|
19
|
+
*/
|
|
20
|
+
export function generateSitemap(baseUrl, paths) {
|
|
21
|
+
// Normalize base URL: remove trailing slash
|
|
22
|
+
const base = baseUrl.replace(/\/$/, '');
|
|
23
|
+
|
|
24
|
+
// Always include root, deduplicate
|
|
25
|
+
const allPaths = ['/', ...paths];
|
|
26
|
+
const seen = new Set();
|
|
27
|
+
const uniquePaths = [];
|
|
28
|
+
|
|
29
|
+
for (const p of allPaths) {
|
|
30
|
+
// Normalize: ensure leading slash
|
|
31
|
+
const normalized = p.startsWith('/') ? p : `/${p}`;
|
|
32
|
+
if (!seen.has(normalized)) {
|
|
33
|
+
seen.add(normalized);
|
|
34
|
+
uniquePaths.push(normalized);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const today = new Date().toISOString().split('T')[0];
|
|
39
|
+
|
|
40
|
+
const urlEntries = uniquePaths.map((p) => {
|
|
41
|
+
const loc = `${base}${p}`;
|
|
42
|
+
return ` <url>\n <loc>${loc}</loc>\n <lastmod>${today}</lastmod>\n <changefreq>weekly</changefreq>\n <priority>${p === '/' ? '1.0' : '0.8'}</priority>\n </url>`;
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
return [
|
|
46
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
47
|
+
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',
|
|
48
|
+
...urlEntries,
|
|
49
|
+
'</urlset>',
|
|
50
|
+
'',
|
|
51
|
+
].join('\n');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
// findRoutes
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Recursively collect all .html files under a directory.
|
|
60
|
+
* @param {string} dir
|
|
61
|
+
* @param {string} rootDir
|
|
62
|
+
* @param {string[]} acc
|
|
63
|
+
* @returns {string[]}
|
|
64
|
+
*/
|
|
65
|
+
function collectHtmlFiles(dir, rootDir, acc = []) {
|
|
66
|
+
let entries;
|
|
67
|
+
try {
|
|
68
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
69
|
+
} catch {
|
|
70
|
+
return acc;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
for (const entry of entries) {
|
|
74
|
+
const fullPath = path.join(dir, entry.name);
|
|
75
|
+
if (entry.isDirectory()) {
|
|
76
|
+
// Skip node_modules and hidden dirs
|
|
77
|
+
if (entry.name === 'node_modules' || entry.name.startsWith('.')) continue;
|
|
78
|
+
collectHtmlFiles(fullPath, rootDir, acc);
|
|
79
|
+
} else if (entry.isFile() && entry.name.endsWith('.html')) {
|
|
80
|
+
acc.push(fullPath);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return acc;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Convert a file path to a URL path.
|
|
89
|
+
* index.html → /
|
|
90
|
+
* about/index.html → /about/
|
|
91
|
+
* about.html → /about
|
|
92
|
+
* @param {string} filePath relative path from search root
|
|
93
|
+
* @returns {string}
|
|
94
|
+
*/
|
|
95
|
+
function filePathToRoute(filePath) {
|
|
96
|
+
// Normalize separators
|
|
97
|
+
const normalized = filePath.replace(/\\/g, '/');
|
|
98
|
+
// Strip .html
|
|
99
|
+
let route = normalized.replace(/\.html$/, '');
|
|
100
|
+
// index → directory
|
|
101
|
+
if (route === 'index') return '/';
|
|
102
|
+
if (route.endsWith('/index')) return route.slice(0, -'index'.length);
|
|
103
|
+
return `/${route}`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Find routes from a project directory.
|
|
108
|
+
* Auto-detects Next.js app/pages or static HTML in public/dist/build.
|
|
109
|
+
* @param {string} dir
|
|
110
|
+
* @returns {string[]}
|
|
111
|
+
*/
|
|
112
|
+
export function findRoutes(dir) {
|
|
113
|
+
const absDir = path.resolve(dir);
|
|
114
|
+
|
|
115
|
+
// Priority: Next.js app router, then pages router, then static dirs
|
|
116
|
+
const searchRoots = [];
|
|
117
|
+
|
|
118
|
+
const appDir = path.join(absDir, 'app');
|
|
119
|
+
const pagesDir = path.join(absDir, 'pages');
|
|
120
|
+
const srcAppDir = path.join(absDir, 'src', 'app');
|
|
121
|
+
const srcPagesDir = path.join(absDir, 'src', 'pages');
|
|
122
|
+
const publicDir = path.join(absDir, 'public');
|
|
123
|
+
const distDir = path.join(absDir, 'dist');
|
|
124
|
+
const buildDir = path.join(absDir, 'build');
|
|
125
|
+
const outDir = path.join(absDir, 'out');
|
|
126
|
+
|
|
127
|
+
for (const candidate of [appDir, srcAppDir, pagesDir, srcPagesDir]) {
|
|
128
|
+
if (fs.existsSync(candidate)) {
|
|
129
|
+
searchRoots.push(candidate);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Static output dirs
|
|
134
|
+
for (const candidate of [publicDir, distDir, buildDir, outDir]) {
|
|
135
|
+
if (fs.existsSync(candidate)) {
|
|
136
|
+
searchRoots.push(candidate);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Fallback: scan root
|
|
141
|
+
if (searchRoots.length === 0) {
|
|
142
|
+
searchRoots.push(absDir);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const routes = new Set();
|
|
146
|
+
|
|
147
|
+
for (const root of searchRoots) {
|
|
148
|
+
const htmlFiles = collectHtmlFiles(root, root);
|
|
149
|
+
for (const file of htmlFiles) {
|
|
150
|
+
const rel = path.relative(root, file);
|
|
151
|
+
const route = filePathToRoute(rel);
|
|
152
|
+
routes.add(route);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Also discover Next.js routes from directory structure (page.tsx, page.jsx, page.js)
|
|
157
|
+
for (const candidate of [appDir, srcAppDir]) {
|
|
158
|
+
if (fs.existsSync(candidate)) {
|
|
159
|
+
discoverNextAppRoutes(candidate, candidate, routes);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return [...routes];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Discover routes from Next.js App Router (page.tsx / page.jsx / page.js / page.ts)
|
|
168
|
+
* @param {string} dir
|
|
169
|
+
* @param {string} rootDir
|
|
170
|
+
* @param {Set<string>} routes
|
|
171
|
+
*/
|
|
172
|
+
function discoverNextAppRoutes(dir, rootDir, routes) {
|
|
173
|
+
let entries;
|
|
174
|
+
try {
|
|
175
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
176
|
+
} catch {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
for (const entry of entries) {
|
|
181
|
+
const fullPath = path.join(dir, entry.name);
|
|
182
|
+
if (entry.isDirectory()) {
|
|
183
|
+
if (entry.name === 'node_modules' || entry.name.startsWith('.')) continue;
|
|
184
|
+
// Skip route groups (parentheses), keep dynamic routes
|
|
185
|
+
discoverNextAppRoutes(fullPath, rootDir, routes);
|
|
186
|
+
} else if (
|
|
187
|
+
entry.isFile() &&
|
|
188
|
+
/^page\.(tsx?|jsx?)$/.test(entry.name)
|
|
189
|
+
) {
|
|
190
|
+
let rel = path.relative(rootDir, dir).replace(/\\/g, '/');
|
|
191
|
+
// Strip route groups like (marketing)
|
|
192
|
+
rel = rel.replace(/\([^/]+\)\//g, '').replace(/\([^/]+\)$/, '');
|
|
193
|
+
// Replace dynamic segments [slug] with :slug placeholder → keep as-is for sitemap
|
|
194
|
+
const route = rel === '' ? '/' : `/${rel}/`;
|
|
195
|
+
routes.add(route);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ---------------------------------------------------------------------------
|
|
201
|
+
// CLI
|
|
202
|
+
// ---------------------------------------------------------------------------
|
|
203
|
+
|
|
204
|
+
if (process.argv[1] === new URL(import.meta.url).pathname) {
|
|
205
|
+
const [,, command, dir, baseUrl] = process.argv;
|
|
206
|
+
|
|
207
|
+
if (command === 'generate') {
|
|
208
|
+
if (!dir || !baseUrl) {
|
|
209
|
+
console.error('Usage: node tools/sitemap-analyzer.mjs generate <dir> <base-url>');
|
|
210
|
+
process.exit(1);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const absDir = path.resolve(dir);
|
|
214
|
+
const routes = findRoutes(absDir);
|
|
215
|
+
const xml = generateSitemap(baseUrl, routes);
|
|
216
|
+
const outPath = path.join(absDir, 'sitemap.xml');
|
|
217
|
+
fs.writeFileSync(outPath, xml, 'utf8');
|
|
218
|
+
console.log(`sitemap.xml written to ${outPath}`);
|
|
219
|
+
console.log(`Routes found: ${routes.join(', ') || '(none beyond root)'}`);
|
|
220
|
+
} else {
|
|
221
|
+
console.error('Usage: node tools/sitemap-analyzer.mjs generate <dir> <base-url>');
|
|
222
|
+
process.exit(1);
|
|
223
|
+
}
|
|
224
|
+
}
|