@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.
@@ -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
+ }