@djangocfg/seo 2.1.50 → 2.1.51

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/dist/cli.mjs CHANGED
@@ -1003,6 +1003,22 @@ async function analyzeRobotsTxt(siteUrl) {
1003
1003
  }
1004
1004
  analysis.exists = true;
1005
1005
  analysis.content = await response.text();
1006
+ if (analysis.content.includes("content-signal") || analysis.content.includes("Content-Signal") || analysis.content.includes("ai-input") || analysis.content.includes("ai-train")) {
1007
+ analysis.issues.push({
1008
+ id: "cloudflare-managed-robots",
1009
+ url: robotsUrl,
1010
+ category: "technical",
1011
+ severity: "warning",
1012
+ title: "Cloudflare managed robots.txt detected",
1013
+ description: `Your robots.txt is being overwritten by Cloudflare's "Content Signals Policy". Your app/robots.ts file is not being served.`,
1014
+ recommendation: 'Disable in Cloudflare Dashboard: Security \u2192 Settings \u2192 "Manage your robots.txt" \u2192 Set to "Off".',
1015
+ detectedAt: (/* @__PURE__ */ new Date()).toISOString(),
1016
+ metadata: {
1017
+ cloudflareFeature: "Managed robots.txt",
1018
+ docsUrl: "https://developers.cloudflare.com/bots/additional-configurations/managed-robots-txt/"
1019
+ }
1020
+ });
1021
+ }
1006
1022
  const robots = robotsParser(robotsUrl, analysis.content);
1007
1023
  analysis.sitemaps = robots.getSitemaps();
1008
1024
  if (analysis.sitemaps.length === 0) {
@@ -2097,6 +2113,65 @@ function generateClaudeContext(report) {
2097
2113
  lines.push("- Parallel `@folder` - skipped");
2098
2114
  lines.push("- Private `_folder` - skipped");
2099
2115
  lines.push("");
2116
+ lines.push("## SEO Files (Next.js App Router)");
2117
+ lines.push("");
2118
+ lines.push("**Required files in `app/`:**");
2119
+ lines.push("");
2120
+ lines.push("### sitemap.xml/route.ts");
2121
+ lines.push("```typescript");
2122
+ lines.push("import { createSitemapHandler } from '@djangocfg/nextjs/sitemap';");
2123
+ lines.push("");
2124
+ lines.push('export const dynamic = "force-static";');
2125
+ lines.push("export const { GET } = createSitemapHandler({");
2126
+ lines.push(" siteUrl,");
2127
+ lines.push(" staticPages: [");
2128
+ lines.push(' { loc: "/", priority: 1.0, changefreq: "daily" },');
2129
+ lines.push(' { loc: "/about", priority: 0.8 },');
2130
+ lines.push(" ],");
2131
+ lines.push(" dynamicPages: async () => fetchPagesFromAPI(),");
2132
+ lines.push("});");
2133
+ lines.push("```");
2134
+ lines.push("");
2135
+ lines.push("### robots.ts");
2136
+ lines.push("```typescript");
2137
+ lines.push("import type { MetadataRoute } from 'next';");
2138
+ lines.push("");
2139
+ lines.push("export default function robots(): MetadataRoute.Robots {");
2140
+ lines.push(" return {");
2141
+ lines.push(' rules: { userAgent: "*", allow: "/" },');
2142
+ lines.push(" sitemap: `${siteUrl}/sitemap.xml`,");
2143
+ lines.push(" };");
2144
+ lines.push("}");
2145
+ lines.push("```");
2146
+ lines.push("");
2147
+ lines.push("### Cloudflare Override");
2148
+ lines.push("");
2149
+ lines.push('If robots.txt shows "Content-Signal" or "ai-train" \u2014 Cloudflare is overriding your file.');
2150
+ lines.push('**Fix:** Dashboard \u2192 Security \u2192 Settings \u2192 "Manage your robots.txt" \u2192 Set to "Off"');
2151
+ lines.push("");
2152
+ lines.push("### Declarative Routes with SEO");
2153
+ lines.push("");
2154
+ lines.push("Create `app/_routes/` with SEO metadata for sitemap:");
2155
+ lines.push("```typescript");
2156
+ lines.push("import { defineRoute } from '@djangocfg/nextjs/navigation';");
2157
+ lines.push("");
2158
+ lines.push("export const home = defineRoute('/', {");
2159
+ lines.push(" label: 'Home',");
2160
+ lines.push(" protected: false,");
2161
+ lines.push(" priority: 1.0, // Sitemap priority 0.0-1.0");
2162
+ lines.push(" changefreq: 'daily', // always|hourly|daily|weekly|monthly|yearly|never");
2163
+ lines.push(" noindex: false, // Exclude from sitemap");
2164
+ lines.push("});");
2165
+ lines.push("");
2166
+ lines.push("export const staticRoutes = [home, about, contact];");
2167
+ lines.push("```");
2168
+ lines.push("");
2169
+ lines.push("Then in `sitemap.xml/route.ts`:");
2170
+ lines.push("```typescript");
2171
+ lines.push("import { routes } from '@/app/_routes';");
2172
+ lines.push("routes.getAllStaticRoutes().filter(r => !r.metadata.noindex)");
2173
+ lines.push("```");
2174
+ lines.push("");
2100
2175
  lines.push("## Link Guidelines");
2101
2176
  lines.push("");
2102
2177
  lines.push("### Nextra/MDX Projects (content/)");
@@ -2414,28 +2489,29 @@ function createRouteInfo(path6, filePath, type) {
2414
2489
  function compareWithSitemap(scanResult, sitemapUrls, baseUrl) {
2415
2490
  const appRoutes = scanResult.routes.filter((r) => r.type === "page");
2416
2491
  const sitemapPaths = new Set(
2417
- sitemapUrls.map((url) => {
2492
+ sitemapUrls.filter((url) => url).map((url) => {
2418
2493
  try {
2419
2494
  return new URL(url).pathname;
2420
2495
  } catch {
2421
2496
  return url;
2422
2497
  }
2423
- })
2498
+ }).filter(Boolean)
2424
2499
  );
2425
2500
  const missingFromSitemap = [];
2426
2501
  const matching = [];
2427
2502
  for (const route of scanResult.staticRoutes) {
2428
- const path6 = route.path === "/" ? "/" : route.path;
2503
+ const path6 = route.path || "/";
2429
2504
  if (sitemapPaths.has(path6) || sitemapPaths.has(path6 + "/") || sitemapPaths.has(path6.replace(/\/$/, ""))) {
2430
2505
  matching.push(route);
2431
2506
  } else {
2432
2507
  missingFromSitemap.push(route);
2433
2508
  }
2434
2509
  }
2435
- const staticPaths = new Set(scanResult.staticRoutes.map((r) => r.path));
2436
- const dynamicPatterns = scanResult.dynamicRoutes.map((r) => routeToRegex(r.path));
2510
+ const staticPaths = new Set(scanResult.staticRoutes.map((r) => r.path).filter(Boolean));
2511
+ const dynamicPatterns = scanResult.dynamicRoutes.filter((r) => r.path).map((r) => routeToRegex(r.path));
2437
2512
  const extraInSitemap = [];
2438
2513
  for (const path6 of sitemapPaths) {
2514
+ if (!path6) continue;
2439
2515
  if (staticPaths.has(path6) || staticPaths.has(path6 + "/") || staticPaths.has(path6.replace(/\/$/, ""))) {
2440
2516
  continue;
2441
2517
  }
@@ -2452,6 +2528,7 @@ function compareWithSitemap(scanResult, sitemapUrls, baseUrl) {
2452
2528
  };
2453
2529
  }
2454
2530
  function routeToRegex(routePath) {
2531
+ if (!routePath) return /^$/;
2455
2532
  let pattern = routePath.replace(/[.+?^${}()|\\]/g, "\\$&").replace(/\[\[\.\.\.([^\]]+)\]\]/g, "(?:/.*)?").replace(/\[\.\.\.([^\]]+)\]/g, "/.+").replace(/\[([^\]]+)\]/g, "/[^/]+");
2456
2533
  pattern = `^${pattern}/?$`;
2457
2534
  return new RegExp(pattern);