@djangocfg/seo 2.1.50

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 (68) hide show
  1. package/README.md +192 -0
  2. package/dist/cli.d.ts +1 -0
  3. package/dist/cli.mjs +3780 -0
  4. package/dist/cli.mjs.map +1 -0
  5. package/dist/crawler/index.d.ts +88 -0
  6. package/dist/crawler/index.mjs +610 -0
  7. package/dist/crawler/index.mjs.map +1 -0
  8. package/dist/google-console/index.d.ts +95 -0
  9. package/dist/google-console/index.mjs +539 -0
  10. package/dist/google-console/index.mjs.map +1 -0
  11. package/dist/index.d.ts +285 -0
  12. package/dist/index.mjs +3236 -0
  13. package/dist/index.mjs.map +1 -0
  14. package/dist/link-checker/index.d.ts +76 -0
  15. package/dist/link-checker/index.mjs +326 -0
  16. package/dist/link-checker/index.mjs.map +1 -0
  17. package/dist/markdown-report-B3QdDzxE.d.ts +193 -0
  18. package/dist/reports/index.d.ts +24 -0
  19. package/dist/reports/index.mjs +836 -0
  20. package/dist/reports/index.mjs.map +1 -0
  21. package/dist/routes/index.d.ts +69 -0
  22. package/dist/routes/index.mjs +372 -0
  23. package/dist/routes/index.mjs.map +1 -0
  24. package/dist/scanner-Cz4Th2Pt.d.ts +60 -0
  25. package/dist/types/index.d.ts +144 -0
  26. package/dist/types/index.mjs +3 -0
  27. package/dist/types/index.mjs.map +1 -0
  28. package/package.json +114 -0
  29. package/src/analyzer.ts +256 -0
  30. package/src/cli/commands/audit.ts +260 -0
  31. package/src/cli/commands/content.ts +180 -0
  32. package/src/cli/commands/crawl.ts +32 -0
  33. package/src/cli/commands/index.ts +12 -0
  34. package/src/cli/commands/inspect.ts +60 -0
  35. package/src/cli/commands/links.ts +41 -0
  36. package/src/cli/commands/robots.ts +36 -0
  37. package/src/cli/commands/routes.ts +126 -0
  38. package/src/cli/commands/sitemap.ts +48 -0
  39. package/src/cli/index.ts +149 -0
  40. package/src/cli/types.ts +40 -0
  41. package/src/config.ts +207 -0
  42. package/src/content/index.ts +51 -0
  43. package/src/content/link-checker.ts +182 -0
  44. package/src/content/link-fixer.ts +188 -0
  45. package/src/content/scanner.ts +200 -0
  46. package/src/content/sitemap-generator.ts +321 -0
  47. package/src/content/types.ts +140 -0
  48. package/src/crawler/crawler.ts +425 -0
  49. package/src/crawler/index.ts +10 -0
  50. package/src/crawler/robots-parser.ts +171 -0
  51. package/src/crawler/sitemap-validator.ts +204 -0
  52. package/src/google-console/analyzer.ts +317 -0
  53. package/src/google-console/auth.ts +100 -0
  54. package/src/google-console/client.ts +281 -0
  55. package/src/google-console/index.ts +9 -0
  56. package/src/index.ts +144 -0
  57. package/src/link-checker/index.ts +461 -0
  58. package/src/reports/claude-context.ts +149 -0
  59. package/src/reports/generator.ts +244 -0
  60. package/src/reports/index.ts +27 -0
  61. package/src/reports/json-report.ts +320 -0
  62. package/src/reports/markdown-report.ts +246 -0
  63. package/src/reports/split-report.ts +252 -0
  64. package/src/routes/analyzer.ts +324 -0
  65. package/src/routes/index.ts +25 -0
  66. package/src/routes/scanner.ts +298 -0
  67. package/src/types/index.ts +222 -0
  68. package/src/utils/index.ts +154 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/routes/scanner.ts","../../src/routes/analyzer.ts"],"names":[],"mappings":";;;;;AAiDA,IAAM,UAAA,GAAa,CAAC,UAAA,EAAY,SAAA,EAAW,YAAY,SAAS,CAAA;AAChE,IAAM,WAAA,GAAc,CAAC,WAAA,EAAa,UAAA,EAAY,aAAa,UAAU,CAAA;AACrE,IAAM,gBAAgB,CAAC,QAAA,EAAU,SAAA,EAAW,OAAA,EAAS,aAAa,UAAU,CAAA;AAKrE,SAAS,UAAA,CAAW,QAAA,GAAmB,OAAA,CAAQ,GAAA,EAAI,EAAkB;AAC1E,EAAA,MAAM,UAAA,GAAa;AAAA,IACjB,IAAA,CAAK,UAAU,KAAK,CAAA;AAAA,IACpB,IAAA,CAAK,QAAA,EAAU,KAAA,EAAO,KAAK;AAAA,GAC7B;AAEA,EAAA,KAAA,MAAW,OAAO,UAAA,EAAY;AAC5B,IAAA,IAAI,WAAW,GAAG,CAAA,IAAK,SAAS,GAAG,CAAA,CAAE,aAAY,EAAG;AAClD,MAAA,OAAO,GAAA;AAAA,IACT;AAAA,EACF;AAEA,EAAA,OAAO,IAAA;AACT;AAKO,SAAS,UAAA,CAAW,OAAA,GAAuB,EAAC,EAAe;AAChE,EAAA,MAAM;AAAA,IACJ,MAAA,GAAS,YAAW,IAAK,OAAA;AAAA,IACzB,UAAA,GAAa,IAAA;AAAA,IACb,cAAA,GAAiB;AAAA,GACnB,GAAI,OAAA;AAEJ,EAAA,IAAI,CAAC,UAAA,CAAW,MAAM,CAAA,EAAG;AACvB,IAAA,MAAM,IAAI,KAAA,CAAM,CAAA,yBAAA,EAA4B,MAAM,CAAA,CAAE,CAAA;AAAA,EACtD;AAEA,EAAA,MAAM,SAAsB,EAAC;AAE7B,EAAA,aAAA,CAAc,QAAQ,EAAA,EAAI,MAAA,EAAQ,EAAE,UAAA,EAAY,gBAAgB,CAAA;AAEhE,EAAA,OAAO;AAAA,IACL,MAAA;AAAA,IACA,YAAA,EAAc,OAAO,MAAA,CAAO,CAAA,CAAA,KAAK,CAAC,CAAA,CAAE,SAAA,IAAa,CAAA,CAAE,IAAA,KAAS,MAAM,CAAA;AAAA,IAClE,aAAA,EAAe,OAAO,MAAA,CAAO,CAAA,CAAA,KAAK,EAAE,SAAA,IAAa,CAAA,CAAE,SAAS,MAAM,CAAA;AAAA,IAClE,WAAW,MAAA,CAAO,MAAA,CAAO,CAAA,CAAA,KAAK,CAAA,CAAE,SAAS,KAAK,CAAA;AAAA,IAC9C;AAAA,GACF;AACF;AAEA,SAAS,aAAA,CACP,GAAA,EACA,SAAA,EACA,MAAA,EACA,OAAA,EACM;AACN,EAAA,IAAI,OAAA;AAEJ,EAAA,IAAI;AACF,IAAA,OAAA,GAAU,YAAY,GAAG,CAAA;AAAA,EAC3B,CAAA,CAAA,MAAQ;AACN,IAAA;AAAA,EACF;AAEA,EAAA,KAAA,MAAW,SAAS,OAAA,EAAS;AAC3B,IAAA,MAAM,QAAA,GAAW,IAAA,CAAK,GAAA,EAAK,KAAK,CAAA;AAEhC,IAAA,IAAI,IAAA;AACJ,IAAA,IAAI;AACF,MAAA,IAAA,GAAO,SAAS,QAAQ,CAAA;AAAA,IAC1B,CAAA,CAAA,MAAQ;AACN,MAAA;AAAA,IACF;AAEA,IAAA,IAAI,IAAA,CAAK,aAAY,EAAG;AAEtB,MAAA,IAAI,MAAM,UAAA,CAAW,GAAG,KAAK,KAAA,CAAM,UAAA,CAAW,GAAG,CAAA,EAAG;AAGpD,MAAA,IAAI,MAAM,UAAA,CAAW,GAAG,KAAK,KAAA,CAAM,QAAA,CAAS,GAAG,CAAA,EAAG;AAEhD,QAAA,aAAA,CAAc,QAAA,EAAU,SAAA,EAAW,MAAA,EAAQ,OAAO,CAAA;AAClD,QAAA;AAAA,MACF;AAGA,MAAA,IAAI,KAAA,CAAM,UAAA,CAAW,GAAG,CAAA,EAAG;AAEzB,QAAA,aAAA,CAAc,QAAA,EAAU,SAAA,EAAW,MAAA,EAAQ,OAAO,CAAA;AAClD,QAAA;AAAA,MACF;AAGA,MAAA,IAAI,KAAA,CAAM,WAAW,GAAG,CAAA,IAAK,CAAC,KAAA,CAAM,QAAA,CAAS,GAAG,CAAA,EAAG;AACjD,QAAA;AAAA,MACF;AAGA,MAAA,MAAM,OAAA,GAAU,eAAe,KAAK,CAAA;AACpC,MAAA,MAAM,YAAA,GAAe,SAAA,GAAY,GAAA,GAAM,OAAA,CAAQ,UAAA;AAE/C,MAAA,aAAA,CAAc,QAAA,EAAU,YAAA,EAAc,MAAA,EAAQ,OAAO,CAAA;AAAA,IACvD,CAAA,MAAA,IAAW,IAAA,CAAK,MAAA,EAAO,EAAG;AAExB,MAAA,IAAI,UAAA,CAAW,QAAA,CAAS,KAAK,CAAA,EAAG;AAC9B,QAAA,MAAM,KAAA,GAAQ,eAAA,CAAgB,SAAA,IAAa,GAAA,EAAK,KAAK,MAAM,CAAA;AAC3D,QAAA,MAAA,CAAO,KAAK,KAAK,CAAA;AAAA,MACnB;AAGA,MAAA,IAAI,OAAA,CAAQ,UAAA,IAAc,WAAA,CAAY,QAAA,CAAS,KAAK,CAAA,EAAG;AACrD,QAAA,MAAM,KAAA,GAAQ,eAAA,CAAgB,SAAA,IAAa,GAAA,EAAK,KAAK,KAAK,CAAA;AAC1D,QAAA,MAAA,CAAO,KAAK,KAAK,CAAA;AAAA,MACnB;AAGA,MAAA,IAAI,QAAQ,cAAA,EAAgB;AAC1B,QAAA,MAAM,QAAA,GAAW,KAAA,CAAM,OAAA,CAAQ,mBAAA,EAAqB,EAAE,CAAA;AACtD,QAAA,IAAI,aAAA,CAAc,QAAA,CAAS,QAAQ,CAAA,EAAG;AACpC,UAAA,MAAM,KAAA,GAAQ,eAAA,CAAgB,SAAA,IAAa,GAAA,EAAK,KAAK,QAA6B,CAAA;AAClF,UAAA,MAAA,CAAO,KAAK,KAAK,CAAA;AAAA,QACnB;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;AAUA,SAAS,eAAe,OAAA,EAA8B;AAEpD,EAAA,IAAI,QAAQ,UAAA,CAAW,OAAO,KAAK,OAAA,CAAQ,QAAA,CAAS,IAAI,CAAA,EAAG;AACzD,IAAA,MAAM,SAAA,GAAY,OAAA,CAAQ,KAAA,CAAM,CAAA,EAAG,EAAE,CAAA;AACrC,IAAA,OAAO;AAAA,MACL,UAAA,EAAY,OAAA;AAAA,MACZ,SAAA,EAAW,IAAA;AAAA,MACX,SAAA;AAAA,MACA,UAAA,EAAY,IAAA;AAAA,MACZ,kBAAA,EAAoB;AAAA,KACtB;AAAA,EACF;AAGA,EAAA,IAAI,QAAQ,UAAA,CAAW,MAAM,KAAK,OAAA,CAAQ,QAAA,CAAS,GAAG,CAAA,EAAG;AACvD,IAAA,MAAM,SAAA,GAAY,OAAA,CAAQ,KAAA,CAAM,CAAA,EAAG,EAAE,CAAA;AACrC,IAAA,OAAO;AAAA,MACL,UAAA,EAAY,OAAA;AAAA,MACZ,SAAA,EAAW,IAAA;AAAA,MACX,SAAA;AAAA,MACA,UAAA,EAAY,IAAA;AAAA,MACZ,kBAAA,EAAoB;AAAA,KACtB;AAAA,EACF;AAGA,EAAA,IAAI,QAAQ,UAAA,CAAW,GAAG,KAAK,OAAA,CAAQ,QAAA,CAAS,GAAG,CAAA,EAAG;AACpD,IAAA,MAAM,SAAA,GAAY,OAAA,CAAQ,KAAA,CAAM,CAAA,EAAG,EAAE,CAAA;AACrC,IAAA,OAAO;AAAA,MACL,UAAA,EAAY,OAAA;AAAA,MACZ,SAAA,EAAW,IAAA;AAAA,MACX,SAAA;AAAA,MACA,UAAA,EAAY,KAAA;AAAA,MACZ,kBAAA,EAAoB;AAAA,KACtB;AAAA,EACF;AAGA,EAAA,OAAO;AAAA,IACL,UAAA,EAAY,OAAA;AAAA,IACZ,SAAA,EAAW,KAAA;AAAA,IACX,UAAA,EAAY,KAAA;AAAA,IACZ,kBAAA,EAAoB;AAAA,GACtB;AACF;AAEA,SAAS,eAAA,CAAgB,IAAA,EAAc,QAAA,EAAkB,IAAA,EAAoC;AAC3F,EAAA,MAAM,WAAW,IAAA,CAAK,KAAA,CAAM,GAAG,CAAA,CAAE,OAAO,OAAO,CAAA;AAC/C,EAAA,MAAM,kBAA4B,EAAC;AACnC,EAAA,IAAI,SAAA,GAAY,KAAA;AAChB,EAAA,IAAI,UAAA,GAAa,KAAA;AACjB,EAAA,IAAI,kBAAA,GAAqB,KAAA;AAEzB,EAAA,KAAA,MAAW,WAAW,QAAA,EAAU;AAC9B,IAAA,MAAM,IAAA,GAAO,eAAe,OAAO,CAAA;AACnC,IAAA,IAAI,KAAK,SAAA,EAAW;AAClB,MAAA,SAAA,GAAY,IAAA;AACZ,MAAA,IAAI,KAAK,SAAA,EAAW;AAClB,QAAA,eAAA,CAAgB,IAAA,CAAK,KAAK,SAAS,CAAA;AAAA,MACrC;AACA,MAAA,IAAI,IAAA,CAAK,YAAY,UAAA,GAAa,IAAA;AAClC,MAAA,IAAI,IAAA,CAAK,oBAAoB,kBAAA,GAAqB,IAAA;AAAA,IACpD;AAAA,EACF;AAGA,EAAA,IAAI,UAAA;AACJ,EAAA,MAAM,UAAA,GAAa,QAAA,CAAS,KAAA,CAAM,aAAa,CAAA;AAC/C,EAAA,IAAI,UAAA,EAAY;AACd,IAAA,UAAA,GAAa,WAAW,CAAC,CAAA;AAAA,EAC3B;AAEA,EAAA,OAAO;AAAA,IACL,MAAM,IAAA,IAAQ,GAAA;AAAA,IACd,QAAA;AAAA,IACA,IAAA;AAAA,IACA,SAAA;AAAA,IACA,eAAA;AAAA,IACA,UAAA;AAAA,IACA,kBAAA;AAAA,IACA;AAAA,GACF;AACF;AAKO,SAAS,UAAA,CAAW,OAAkB,MAAA,EAAyC;AACpF,EAAA,IAAI,MAAM,KAAA,CAAM,IAAA;AAEhB,EAAA,KAAA,MAAW,OAAA,IAAW,MAAM,eAAA,EAAiB;AAC3C,IAAA,MAAM,KAAA,GAAQ,MAAA,GAAS,OAAO,CAAA,IAAK,IAAI,OAAO,CAAA,CAAA,CAAA;AAG9C,IAAA,IAAI,MAAM,kBAAA,EAAoB;AAC5B,MAAA,GAAA,GAAM,GAAA,CAAI,OAAA,CAAQ,CAAA,KAAA,EAAQ,OAAO,MAAM,KAAK,CAAA;AAAA,IAC9C,CAAA,MAAA,IAAW,MAAM,UAAA,EAAY;AAC3B,MAAA,GAAA,GAAM,GAAA,CAAI,OAAA,CAAQ,CAAA,IAAA,EAAO,OAAO,KAAK,KAAK,CAAA;AAAA,IAC5C,CAAA,MAAO;AACL,MAAA,GAAA,GAAM,GAAA,CAAI,OAAA,CAAQ,CAAA,CAAA,EAAI,OAAO,KAAK,KAAK,CAAA;AAAA,IACzC;AAAA,EACF;AAEA,EAAA,OAAO,GAAA;AACT;AAKO,SAAS,aAAA,CAAc,YAAwB,OAAA,EAA2B;AAC/E,EAAA,OAAO,UAAA,CAAW,YAAA,CAAa,GAAA,CAAI,CAAA,KAAA,KAAS;AAC1C,IAAA,MAAM,GAAA,GAAM,IAAI,GAAA,CAAI,KAAA,CAAM,MAAM,OAAO,CAAA;AACvC,IAAA,OAAO,GAAA,CAAI,IAAA;AAAA,EACb,CAAC,CAAA;AACH;AC/OO,SAAS,kBAAA,CACd,UAAA,EACA,WAAA,EACA,OAAA,EACuB;AACvB,EAAA,MAAM,YAAY,UAAA,CAAW,MAAA,CAAO,OAAO,CAAA,CAAA,KAAK,CAAA,CAAE,SAAS,MAAM,CAAA;AAGjE,EAAA,MAAM,eAAe,IAAI,GAAA;AAAA,IACvB,WAAA,CAAY,IAAI,CAAA,GAAA,KAAO;AACrB,MAAA,IAAI;AACF,QAAA,OAAO,IAAI,GAAA,CAAI,GAAG,CAAA,CAAE,QAAA;AAAA,MACtB,CAAA,CAAA,MAAQ;AACN,QAAA,OAAO,GAAA;AAAA,MACT;AAAA,IACF,CAAC;AAAA,GACH;AAGA,EAAA,MAAM,qBAAkC,EAAC;AACzC,EAAA,MAAM,WAAwB,EAAC;AAE/B,EAAA,KAAA,MAAW,KAAA,IAAS,WAAW,YAAA,EAAc;AAC3C,IAAA,MAAM,IAAA,GAAO,KAAA,CAAM,IAAA,KAAS,GAAA,GAAM,MAAM,KAAA,CAAM,IAAA;AAE9C,IAAA,IAAI,aAAa,GAAA,CAAI,IAAI,CAAA,IAAK,YAAA,CAAa,IAAI,IAAA,GAAO,GAAG,CAAA,IAAK,YAAA,CAAa,IAAI,IAAA,CAAK,OAAA,CAAQ,KAAA,EAAO,EAAE,CAAC,CAAA,EAAG;AACvG,MAAA,QAAA,CAAS,KAAK,KAAK,CAAA;AAAA,IACrB,CAAA,MAAO;AACL,MAAA,kBAAA,CAAmB,KAAK,KAAK,CAAA;AAAA,IAC/B;AAAA,EACF;AAGA,EAAA,MAAM,WAAA,GAAc,IAAI,GAAA,CAAI,UAAA,CAAW,aAAa,GAAA,CAAI,CAAA,CAAA,KAAK,CAAA,CAAE,IAAI,CAAC,CAAA;AACpE,EAAA,MAAM,eAAA,GAAkB,WAAW,aAAA,CAAc,GAAA,CAAI,OAAK,YAAA,CAAa,CAAA,CAAE,IAAI,CAAC,CAAA;AAE9E,EAAA,MAAM,iBAA2B,EAAC;AAClC,EAAA,KAAA,MAAW,QAAQ,YAAA,EAAc;AAE/B,IAAA,IAAI,YAAY,GAAA,CAAI,IAAI,CAAA,IAAK,WAAA,CAAY,IAAI,IAAA,GAAO,GAAG,CAAA,IAAK,WAAA,CAAY,IAAI,IAAA,CAAK,OAAA,CAAQ,KAAA,EAAO,EAAE,CAAC,CAAA,EAAG;AACpG,MAAA;AAAA,IACF;AAGA,IAAA,MAAM,iBAAiB,eAAA,CAAgB,IAAA,CAAK,WAAS,KAAA,CAAM,IAAA,CAAK,IAAI,CAAC,CAAA;AACrE,IAAA,IAAI,cAAA,EAAgB;AAEpB,IAAA,cAAA,CAAe,KAAK,IAAI,CAAA;AAAA,EAC1B;AAEA,EAAA,OAAO;AAAA,IACL,SAAA;AAAA,IACA,WAAA;AAAA,IACA,kBAAA;AAAA,IACA,cAAA;AAAA,IACA;AAAA,GACF;AACF;AAKA,SAAS,aAAa,SAAA,EAA2B;AAC/C,EAAA,IAAI,UAAU,SAAA,CAEX,OAAA,CAAQ,iBAAA,EAAmB,MAAM,EAEjC,OAAA,CAAQ,yBAAA,EAA2B,UAAU,CAAA,CAE7C,QAAQ,qBAAA,EAAuB,KAAK,CAAA,CAEpC,OAAA,CAAQ,iBAAiB,QAAQ,CAAA;AAGpC,EAAA,OAAA,GAAU,IAAI,OAAO,CAAA,GAAA,CAAA;AAErB,EAAA,OAAO,IAAI,OAAO,OAAO,CAAA;AAC3B;AAKA,eAAsB,YAAA,CACpB,YACA,OAAA,EACoC;AACpC,EAAA,MAAM;AAAA,IACJ,OAAA;AAAA,IACA,OAAA,GAAU,GAAA;AAAA,IACV,WAAA,GAAc,CAAA;AAAA,IACd,UAAA,GAAa;AAAA,GACf,GAAI,OAAA;AAEJ,EAAA,MAAM,MAAA,GAAS,UAAA,GAAa,UAAA,CAAW,YAAA,GAAe,UAAA,CAAW,OAAO,MAAA,CAAO,CAAA,CAAA,KAAK,CAAA,CAAE,IAAA,KAAS,MAAM,CAAA;AACrG,EAAA,MAAM,KAAA,GAAQ,OAAO,WAAW,CAAA;AAEhC,EAAA,MAAM,OAAA,GAAU,MAAM,OAAA,CAAQ,GAAA;AAAA,IAC5B,MAAA,CAAO,GAAA;AAAA,MAAI,CAAA,KAAA,KACT,MAAM,YAAY;AAChB,QAAA,MAAM,MAAM,IAAI,GAAA,CAAI,KAAA,CAAM,IAAA,EAAM,OAAO,CAAA,CAAE,IAAA;AACzC,QAAA,MAAM,SAAA,GAAY,KAAK,GAAA,EAAI;AAE3B,QAAA,IAAI;AACF,UAAA,MAAM,UAAA,GAAa,IAAI,eAAA,EAAgB;AACvC,UAAA,MAAM,YAAY,UAAA,CAAW,MAAM,UAAA,CAAW,KAAA,IAAS,OAAO,CAAA;AAE9D,UAAA,MAAM,QAAA,GAAW,MAAM,KAAA,CAAM,GAAA,EAAK;AAAA,YAChC,MAAA,EAAQ,MAAA;AAAA,YACR,QAAQ,UAAA,CAAW,MAAA;AAAA,YACnB,QAAA,EAAU;AAAA,WACX,CAAA;AAED,UAAA,YAAA,CAAa,SAAS,CAAA;AAEtB,UAAA,OAAO;AAAA,YACL,KAAA;AAAA,YACA,GAAA;AAAA,YACA,YAAY,QAAA,CAAS,MAAA;AAAA,YACrB,YAAA,EAAc,QAAA,CAAS,MAAA,IAAU,GAAA,IAAO,SAAS,MAAA,GAAS,GAAA;AAAA,YAC1D,YAAA,EAAc,IAAA,CAAK,GAAA,EAAI,GAAI;AAAA,WAC7B;AAAA,QACF,SAAS,KAAA,EAAO;AACd,UAAA,OAAO;AAAA,YACL,KAAA;AAAA,YACA,GAAA;AAAA,YACA,UAAA,EAAY,CAAA;AAAA,YACZ,YAAA,EAAc,KAAA;AAAA,YACd,OAAQ,KAAA,CAAgB,OAAA;AAAA,YACxB,YAAA,EAAc,IAAA,CAAK,GAAA,EAAI,GAAI;AAAA,WAC7B;AAAA,QACF;AAAA,MACF,CAAC;AAAA;AACH,GACF;AAEA,EAAA,OAAO,OAAA;AACT;AAKO,SAAS,aAAA,CACd,UAAA,EACA,UAAA,EACA,YAAA,EACY;AACZ,EAAA,MAAM,SAAqB,EAAC;AAC5B,EAAA,MAAM,GAAA,GAAA,iBAAM,IAAI,IAAA,EAAK,EAAE,WAAA,EAAY;AAGnC,EAAA,IAAI,UAAA,IAAc,UAAA,CAAW,kBAAA,CAAmB,MAAA,GAAS,CAAA,EAAG;AAC1D,IAAA,KAAA,MAAW,KAAA,IAAS,WAAW,kBAAA,EAAoB;AACjD,MAAA,MAAA,CAAO,IAAA,CAAK;AAAA,QACV,EAAA,EAAI,CAAA,sBAAA,EAAyB,KAAA,CAAM,IAAI,CAAA,CAAA;AAAA,QACvC,QAAA,EAAU,UAAA;AAAA,QACV,QAAA,EAAU,SAAA;AAAA,QACV,KAAA,EAAO,4BAAA;AAAA,QACP,WAAA,EAAa,CAAA,aAAA,EAAgB,KAAA,CAAM,IAAI,CAAA,sBAAA,CAAA;AAAA,QACvC,KAAK,KAAA,CAAM,IAAA;AAAA,QACX,cAAA,EAAgB,oDAAA;AAAA,QAChB,UAAA,EAAY;AAAA,OACb,CAAA;AAAA,IACH;AAAA,EACF;AAGA,EAAA,IAAI,UAAA,IAAc,UAAA,CAAW,cAAA,CAAe,MAAA,GAAS,CAAA,EAAG;AACtD,IAAA,KAAA,MAAW,QAAQ,UAAA,CAAW,cAAA,CAAe,KAAA,CAAM,CAAA,EAAG,EAAE,CAAA,EAAG;AACzD,MAAA,MAAA,CAAO,IAAA,CAAK;AAAA,QACV,EAAA,EAAI,kBAAkB,IAAI,CAAA,CAAA;AAAA,QAC1B,QAAA,EAAU,UAAA;AAAA,QACV,QAAA,EAAU,MAAA;AAAA,QACV,KAAA,EAAO,oCAAA;AAAA,QACP,WAAA,EAAa,OAAO,IAAI,CAAA,wCAAA,CAAA;AAAA,QACxB,GAAA,EAAK,IAAA;AAAA,QACL,cAAA,EAAgB,uDAAA;AAAA,QAChB,UAAA,EAAY;AAAA,OACb,CAAA;AAAA,IACH;AAAA,EACF;AAGA,EAAA,IAAI,YAAA,EAAc;AAChB,IAAA,KAAA,MAAW,UAAU,YAAA,EAAc;AACjC,MAAA,IAAI,CAAC,OAAO,YAAA,EAAc;AACxB,QAAA,MAAA,CAAO,IAAA,CAAK;AAAA,UACV,EAAA,EAAI,CAAA,mBAAA,EAAsB,MAAA,CAAO,KAAA,CAAM,IAAI,CAAA,CAAA;AAAA,UAC3C,QAAA,EAAU,WAAA;AAAA,UACV,QAAA,EAAU,MAAA,CAAO,UAAA,KAAe,GAAA,GAAM,OAAA,GAAU,SAAA;AAAA,UAChD,KAAA,EAAO,CAAA,cAAA,EAAiB,MAAA,CAAO,UAAA,IAAc,OAAO,CAAA,CAAA;AAAA,UACpD,WAAA,EAAa,OAAO,KAAA,IAAS,CAAA,MAAA,EAAS,OAAO,KAAA,CAAM,IAAI,CAAA,iBAAA,EAAoB,MAAA,CAAO,UAAU,CAAA,CAAA;AAAA,UAC5F,KAAK,MAAA,CAAO,GAAA;AAAA,UACZ,cAAA,EAAgB,sCAAA;AAAA,UAChB,UAAA,EAAY;AAAA,SACb,CAAA;AAAA,MACH;AAAA,IACF;AAAA,EACF;AAGA,EAAA,IAAI,WAAW,aAAA,CAAc,MAAA,GAAS,UAAA,CAAW,YAAA,CAAa,SAAS,CAAA,EAAG;AACxE,IAAA,MAAA,CAAO,IAAA,CAAK;AAAA,MACV,EAAA,EAAI,yBAAA;AAAA,MACJ,QAAA,EAAU,SAAA;AAAA,MACV,QAAA,EAAU,MAAA;AAAA,MACV,KAAA,EAAO,8BAAA;AAAA,MACP,WAAA,EAAa,GAAG,UAAA,CAAW,aAAA,CAAc,MAAM,CAAA,YAAA,EAAe,UAAA,CAAW,aAAa,MAAM,CAAA,cAAA,CAAA;AAAA,MAC5F,GAAA,EAAK,GAAA;AAAA,MACL,cAAA,EAAgB,gEAAA;AAAA,MAChB,UAAA,EAAY;AAAA,KACb,CAAA;AAAA,EACH;AAEA,EAAA,OAAO,MAAA;AACT;AAKO,SAAS,qBAAA,CACd,UAAA,EACA,UAAA,EACA,YAAA,EACQ;AACR,EAAA,MAAM,QAAkB,EAAC;AAEzB,EAAA,KAAA,CAAM,KAAK,mBAAmB,CAAA;AAC9B,EAAA,KAAA,CAAM,KAAK,EAAE,CAAA;AACb,EAAA,KAAA,CAAM,IAAA,CAAK,CAAA,cAAA,EAAiB,UAAA,CAAW,MAAA,CAAO,MAAM,CAAA,CAAE,CAAA;AACtD,EAAA,KAAA,CAAM,IAAA,CAAK,CAAA,2BAAA,EAAe,UAAA,CAAW,YAAA,CAAa,MAAM,CAAA,CAAE,CAAA;AAC1D,EAAA,KAAA,CAAM,IAAA,CAAK,CAAA,4BAAA,EAAgB,UAAA,CAAW,aAAA,CAAc,MAAM,CAAA,CAAE,CAAA;AAC5D,EAAA,KAAA,CAAM,IAAA,CAAK,CAAA,wBAAA,EAAY,UAAA,CAAW,SAAA,CAAU,MAAM,CAAA,CAAE,CAAA;AACpD,EAAA,KAAA,CAAM,KAAK,EAAE,CAAA;AAEb,EAAA,IAAI,UAAA,EAAY;AACd,IAAA,KAAA,CAAM,KAAK,wBAAwB,CAAA;AACnC,IAAA,KAAA,CAAM,KAAK,EAAE,CAAA;AACb,IAAA,KAAA,CAAM,IAAA,CAAK,CAAA,cAAA,EAAiB,UAAA,CAAW,WAAA,CAAY,MAAM,CAAA,CAAE,CAAA;AAC3D,IAAA,KAAA,CAAM,IAAA,CAAK,CAAA,6BAAA,EAAiB,UAAA,CAAW,QAAA,CAAS,MAAM,CAAA,CAAE,CAAA;AACxD,IAAA,KAAA,CAAM,IAAA,CAAK,CAAA,yCAAA,EAA6B,UAAA,CAAW,kBAAA,CAAmB,MAAM,CAAA,CAAE,CAAA;AAC9E,IAAA,KAAA,CAAM,IAAA,CAAK,CAAA,qCAAA,EAAyB,UAAA,CAAW,cAAA,CAAe,MAAM,CAAA,CAAE,CAAA;AACtE,IAAA,KAAA,CAAM,KAAK,EAAE,CAAA;AAAA,EACf;AAEA,EAAA,IAAI,YAAA,EAAc;AAChB,IAAA,MAAM,aAAa,YAAA,CAAa,MAAA,CAAO,CAAA,CAAA,KAAK,CAAA,CAAE,YAAY,CAAA,CAAE,MAAA;AAC5D,IAAA,MAAM,SAAS,YAAA,CAAa,MAAA,CAAO,CAAA,CAAA,KAAK,CAAC,EAAE,YAAY,CAAA;AAEvD,IAAA,KAAA,CAAM,KAAK,wBAAwB,CAAA;AACnC,IAAA,KAAA,CAAM,KAAK,EAAE,CAAA;AACb,IAAA,KAAA,CAAM,KAAK,CAAA,YAAA,EAAe,UAAU,CAAA,CAAA,EAAI,YAAA,CAAa,MAAM,CAAA,CAAE,CAAA;AAE7D,IAAA,IAAI,MAAA,CAAO,SAAS,CAAA,EAAG;AACrB,MAAA,KAAA,CAAM,KAAK,EAAE,CAAA;AACb,MAAA,KAAA,CAAM,KAAK,gBAAgB,CAAA;AAC3B,MAAA,KAAA,MAAW,CAAA,IAAK,MAAA,CAAO,KAAA,CAAM,CAAA,EAAG,EAAE,CAAA,EAAG;AACnC,QAAA,KAAA,CAAM,IAAA,CAAK,CAAA,IAAA,EAAO,CAAA,CAAE,KAAA,CAAM,IAAI,WAAM,CAAA,CAAE,UAAA,IAAc,CAAA,CAAE,KAAK,CAAA,CAAE,CAAA;AAAA,MAC/D;AACA,MAAA,IAAI,MAAA,CAAO,SAAS,EAAA,EAAI;AACtB,QAAA,KAAA,CAAM,IAAA,CAAK,CAAA,SAAA,EAAY,MAAA,CAAO,MAAA,GAAS,EAAE,CAAA,KAAA,CAAO,CAAA;AAAA,MAClD;AAAA,IACF;AAAA,EACF;AAEA,EAAA,OAAO,KAAA,CAAM,KAAK,IAAI,CAAA;AACxB","file":"index.mjs","sourcesContent":["/**\n * @djangocfg/seo - Next.js App Router Scanner\n * Scans app/ directory to extract all routes\n */\n\nimport { readdirSync, statSync, existsSync } from 'node:fs';\nimport { join, relative } from 'node:path';\n\nexport interface RouteInfo {\n /** Route path (e.g., /blog/[slug]) */\n path: string;\n /** File path relative to app/ */\n filePath: string;\n /** Route type */\n type: 'page' | 'api' | 'layout' | 'loading' | 'error';\n /** Is dynamic route */\n isDynamic: boolean;\n /** Dynamic segments (e.g., ['slug', '...path']) */\n dynamicSegments: string[];\n /** Is catch-all route */\n isCatchAll: boolean;\n /** Is optional catch-all route */\n isOptionalCatchAll: boolean;\n /** Route group (if any) */\n routeGroup?: string;\n}\n\nexport interface ScanResult {\n /** All discovered routes */\n routes: RouteInfo[];\n /** Static routes (no dynamic segments) */\n staticRoutes: RouteInfo[];\n /** Dynamic routes (with [param]) */\n dynamicRoutes: RouteInfo[];\n /** API routes */\n apiRoutes: RouteInfo[];\n /** App directory path */\n appDir: string;\n}\n\nexport interface ScanOptions {\n /** Path to app directory */\n appDir?: string;\n /** Include API routes */\n includeApi?: boolean;\n /** Include layouts, loading, error pages */\n includeSpecial?: boolean;\n}\n\nconst PAGE_FILES = ['page.tsx', 'page.ts', 'page.jsx', 'page.js'];\nconst ROUTE_FILES = ['route.tsx', 'route.ts', 'route.jsx', 'route.js'];\nconst SPECIAL_FILES = ['layout', 'loading', 'error', 'not-found', 'template'];\n\n/**\n * Find app directory by searching common locations\n */\nexport function findAppDir(startDir: string = process.cwd()): string | null {\n const candidates = [\n join(startDir, 'app'),\n join(startDir, 'src', 'app'),\n ];\n\n for (const dir of candidates) {\n if (existsSync(dir) && statSync(dir).isDirectory()) {\n return dir;\n }\n }\n\n return null;\n}\n\n/**\n * Scan Next.js app directory for routes\n */\nexport function scanRoutes(options: ScanOptions = {}): ScanResult {\n const {\n appDir = findAppDir() || './app',\n includeApi = true,\n includeSpecial = false,\n } = options;\n\n if (!existsSync(appDir)) {\n throw new Error(`App directory not found: ${appDir}`);\n }\n\n const routes: RouteInfo[] = [];\n\n scanDirectory(appDir, '', routes, { includeApi, includeSpecial });\n\n return {\n routes,\n staticRoutes: routes.filter(r => !r.isDynamic && r.type === 'page'),\n dynamicRoutes: routes.filter(r => r.isDynamic && r.type === 'page'),\n apiRoutes: routes.filter(r => r.type === 'api'),\n appDir,\n };\n}\n\nfunction scanDirectory(\n dir: string,\n routePath: string,\n routes: RouteInfo[],\n options: { includeApi: boolean; includeSpecial: boolean }\n): void {\n let entries: string[];\n\n try {\n entries = readdirSync(dir);\n } catch {\n return;\n }\n\n for (const entry of entries) {\n const fullPath = join(dir, entry);\n\n let stat;\n try {\n stat = statSync(fullPath);\n } catch {\n continue;\n }\n\n if (stat.isDirectory()) {\n // Skip private folders\n if (entry.startsWith('_') || entry.startsWith('.')) continue;\n\n // Handle route groups (folder)\n if (entry.startsWith('(') && entry.endsWith(')')) {\n // Route groups don't add to URL path\n scanDirectory(fullPath, routePath, routes, options);\n continue;\n }\n\n // Handle parallel routes @folder\n if (entry.startsWith('@')) {\n // Parallel routes don't add to URL path\n scanDirectory(fullPath, routePath, routes, options);\n continue;\n }\n\n // Handle intercepting routes (.)folder, (..)folder, (...)folder\n if (entry.startsWith('(') && !entry.endsWith(')')) {\n continue; // Skip intercepting routes for now\n }\n\n // Build new route path\n const segment = processSegment(entry);\n const newRoutePath = routePath + '/' + segment.urlSegment;\n\n scanDirectory(fullPath, newRoutePath, routes, options);\n } else if (stat.isFile()) {\n // Check for page files\n if (PAGE_FILES.includes(entry)) {\n const route = createRouteInfo(routePath || '/', dir, 'page');\n routes.push(route);\n }\n\n // Check for API route files\n if (options.includeApi && ROUTE_FILES.includes(entry)) {\n const route = createRouteInfo(routePath || '/', dir, 'api');\n routes.push(route);\n }\n\n // Check for special files\n if (options.includeSpecial) {\n const baseName = entry.replace(/\\.(tsx?|jsx?|js)$/, '');\n if (SPECIAL_FILES.includes(baseName)) {\n const route = createRouteInfo(routePath || '/', dir, baseName as RouteInfo['type']);\n routes.push(route);\n }\n }\n }\n }\n}\n\ninterface SegmentInfo {\n urlSegment: string;\n isDynamic: boolean;\n paramName?: string;\n isCatchAll: boolean;\n isOptionalCatchAll: boolean;\n}\n\nfunction processSegment(segment: string): SegmentInfo {\n // Optional catch-all: [[...slug]]\n if (segment.startsWith('[[...') && segment.endsWith(']]')) {\n const paramName = segment.slice(5, -2);\n return {\n urlSegment: segment,\n isDynamic: true,\n paramName,\n isCatchAll: true,\n isOptionalCatchAll: true,\n };\n }\n\n // Catch-all: [...slug]\n if (segment.startsWith('[...') && segment.endsWith(']')) {\n const paramName = segment.slice(4, -1);\n return {\n urlSegment: segment,\n isDynamic: true,\n paramName,\n isCatchAll: true,\n isOptionalCatchAll: false,\n };\n }\n\n // Dynamic: [slug]\n if (segment.startsWith('[') && segment.endsWith(']')) {\n const paramName = segment.slice(1, -1);\n return {\n urlSegment: segment,\n isDynamic: true,\n paramName,\n isCatchAll: false,\n isOptionalCatchAll: false,\n };\n }\n\n // Static segment\n return {\n urlSegment: segment,\n isDynamic: false,\n isCatchAll: false,\n isOptionalCatchAll: false,\n };\n}\n\nfunction createRouteInfo(path: string, filePath: string, type: RouteInfo['type']): RouteInfo {\n const segments = path.split('/').filter(Boolean);\n const dynamicSegments: string[] = [];\n let isDynamic = false;\n let isCatchAll = false;\n let isOptionalCatchAll = false;\n\n for (const segment of segments) {\n const info = processSegment(segment);\n if (info.isDynamic) {\n isDynamic = true;\n if (info.paramName) {\n dynamicSegments.push(info.paramName);\n }\n if (info.isCatchAll) isCatchAll = true;\n if (info.isOptionalCatchAll) isOptionalCatchAll = true;\n }\n }\n\n // Extract route group from path\n let routeGroup: string | undefined;\n const groupMatch = filePath.match(/\\(([^)]+)\\)/);\n if (groupMatch) {\n routeGroup = groupMatch[1];\n }\n\n return {\n path: path || '/',\n filePath,\n type,\n isDynamic,\n dynamicSegments,\n isCatchAll,\n isOptionalCatchAll,\n routeGroup,\n };\n}\n\n/**\n * Convert route path to URL (replace dynamic segments with example values)\n */\nexport function routeToUrl(route: RouteInfo, params?: Record<string, string>): string {\n let url = route.path;\n\n for (const segment of route.dynamicSegments) {\n const value = params?.[segment] || `{${segment}}`;\n\n // Handle different dynamic segment types\n if (route.isOptionalCatchAll) {\n url = url.replace(`[[...${segment}]]`, value);\n } else if (route.isCatchAll) {\n url = url.replace(`[...${segment}]`, value);\n } else {\n url = url.replace(`[${segment}]`, value);\n }\n }\n\n return url;\n}\n\n/**\n * Get static routes as URLs (ready to check)\n */\nexport function getStaticUrls(scanResult: ScanResult, baseUrl: string): string[] {\n return scanResult.staticRoutes.map(route => {\n const url = new URL(route.path, baseUrl);\n return url.href;\n });\n}\n","/**\n * @djangocfg/seo - Routes Analyzer\n * Compare routes with sitemap and verify accessibility\n */\n\nimport pLimit from 'p-limit';\nimport type { ScanResult, RouteInfo } from './scanner.js';\nimport type { SeoIssue } from '../types/index.js';\n\nexport interface SitemapUrl {\n loc: string;\n lastmod?: string;\n changefreq?: string;\n priority?: string;\n}\n\nexport interface RouteComparisonResult {\n /** Routes found in app/ */\n appRoutes: RouteInfo[];\n /** URLs found in sitemap */\n sitemapUrls: string[];\n /** Static routes missing from sitemap */\n missingFromSitemap: RouteInfo[];\n /** Sitemap URLs that don't match any route */\n extraInSitemap: string[];\n /** Matching routes */\n matching: RouteInfo[];\n}\n\nexport interface RouteVerificationResult {\n /** Route being verified */\n route: RouteInfo;\n /** Full URL */\n url: string;\n /** HTTP status code */\n statusCode: number;\n /** Is accessible (2xx) */\n isAccessible: boolean;\n /** Error message if failed */\n error?: string;\n /** Response time in ms */\n responseTime: number;\n}\n\nexport interface VerifyOptions {\n /** Base URL (e.g., http://localhost:3000) */\n baseUrl: string;\n /** Request timeout in ms */\n timeout?: number;\n /** Max concurrent requests */\n concurrency?: number;\n /** Only verify static routes */\n staticOnly?: boolean;\n}\n\n/**\n * Compare routes with sitemap URLs\n */\nexport function compareWithSitemap(\n scanResult: ScanResult,\n sitemapUrls: string[],\n baseUrl: string\n): RouteComparisonResult {\n const appRoutes = scanResult.routes.filter(r => r.type === 'page');\n\n // Normalize sitemap URLs to paths\n const sitemapPaths = new Set(\n sitemapUrls.map(url => {\n try {\n return new URL(url).pathname;\n } catch {\n return url;\n }\n })\n );\n\n // Find static routes missing from sitemap\n const missingFromSitemap: RouteInfo[] = [];\n const matching: RouteInfo[] = [];\n\n for (const route of scanResult.staticRoutes) {\n const path = route.path === '/' ? '/' : route.path;\n // Check with and without trailing slash\n if (sitemapPaths.has(path) || sitemapPaths.has(path + '/') || sitemapPaths.has(path.replace(/\\/$/, ''))) {\n matching.push(route);\n } else {\n missingFromSitemap.push(route);\n }\n }\n\n // Find sitemap URLs that don't match any static route\n const staticPaths = new Set(scanResult.staticRoutes.map(r => r.path));\n const dynamicPatterns = scanResult.dynamicRoutes.map(r => routeToRegex(r.path));\n\n const extraInSitemap: string[] = [];\n for (const path of sitemapPaths) {\n // Skip if matches static route\n if (staticPaths.has(path) || staticPaths.has(path + '/') || staticPaths.has(path.replace(/\\/$/, ''))) {\n continue;\n }\n\n // Skip if matches dynamic route pattern\n const matchesDynamic = dynamicPatterns.some(regex => regex.test(path));\n if (matchesDynamic) continue;\n\n extraInSitemap.push(path);\n }\n\n return {\n appRoutes,\n sitemapUrls,\n missingFromSitemap,\n extraInSitemap,\n matching,\n };\n}\n\n/**\n * Convert route path to regex for matching\n */\nfunction routeToRegex(routePath: string): RegExp {\n let pattern = routePath\n // Escape special regex chars except [ ]\n .replace(/[.+?^${}()|\\\\]/g, '\\\\$&')\n // Optional catch-all: [[...slug]] matches zero or more segments\n .replace(/\\[\\[\\.\\.\\.([^\\]]+)\\]\\]/g, '(?:/.*)?')\n // Catch-all: [...slug] matches one or more segments\n .replace(/\\[\\.\\.\\.([^\\]]+)\\]/g, '/.+')\n // Dynamic: [slug] matches one segment\n .replace(/\\[([^\\]]+)\\]/g, '/[^/]+');\n\n // Handle trailing slash optionally\n pattern = `^${pattern}/?$`;\n\n return new RegExp(pattern);\n}\n\n/**\n * Verify routes are accessible\n */\nexport async function verifyRoutes(\n scanResult: ScanResult,\n options: VerifyOptions\n): Promise<RouteVerificationResult[]> {\n const {\n baseUrl,\n timeout = 10000,\n concurrency = 5,\n staticOnly = true,\n } = options;\n\n const routes = staticOnly ? scanResult.staticRoutes : scanResult.routes.filter(r => r.type === 'page');\n const limit = pLimit(concurrency);\n\n const results = await Promise.all(\n routes.map(route =>\n limit(async () => {\n const url = new URL(route.path, baseUrl).href;\n const startTime = Date.now();\n\n try {\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), timeout);\n\n const response = await fetch(url, {\n method: 'HEAD',\n signal: controller.signal,\n redirect: 'follow',\n });\n\n clearTimeout(timeoutId);\n\n return {\n route,\n url,\n statusCode: response.status,\n isAccessible: response.status >= 200 && response.status < 400,\n responseTime: Date.now() - startTime,\n };\n } catch (error) {\n return {\n route,\n url,\n statusCode: 0,\n isAccessible: false,\n error: (error as Error).message,\n responseTime: Date.now() - startTime,\n };\n }\n })\n )\n );\n\n return results;\n}\n\n/**\n * Analyze routes and generate SEO issues\n */\nexport function analyzeRoutes(\n scanResult: ScanResult,\n comparison?: RouteComparisonResult,\n verification?: RouteVerificationResult[]\n): SeoIssue[] {\n const issues: SeoIssue[] = [];\n const now = new Date().toISOString();\n\n // Issue: Static routes missing from sitemap\n if (comparison && comparison.missingFromSitemap.length > 0) {\n for (const route of comparison.missingFromSitemap) {\n issues.push({\n id: `route-missing-sitemap-${route.path}`,\n category: 'indexing',\n severity: 'warning',\n title: 'Route missing from sitemap',\n description: `Static route ${route.path} is not in sitemap.xml`,\n url: route.path,\n recommendation: 'Add this route to your sitemap for better indexing',\n detectedAt: now,\n });\n }\n }\n\n // Issue: Sitemap URLs that don't match routes (potential dead links)\n if (comparison && comparison.extraInSitemap.length > 0) {\n for (const path of comparison.extraInSitemap.slice(0, 20)) {\n issues.push({\n id: `sitemap-orphan-${path}`,\n category: 'indexing',\n severity: 'info',\n title: 'Sitemap URL without matching route',\n description: `URL ${path} in sitemap doesn't match any app/ route`,\n url: path,\n recommendation: 'Verify this URL is still valid or remove from sitemap',\n detectedAt: now,\n });\n }\n }\n\n // Issue: Inaccessible routes\n if (verification) {\n for (const result of verification) {\n if (!result.isAccessible) {\n issues.push({\n id: `route-inaccessible-${result.route.path}`,\n category: 'technical',\n severity: result.statusCode === 404 ? 'error' : 'warning',\n title: `Route returns ${result.statusCode || 'error'}`,\n description: result.error || `Route ${result.route.path} returned status ${result.statusCode}`,\n url: result.url,\n recommendation: 'Fix the route or remove it from app/',\n detectedAt: now,\n });\n }\n }\n }\n\n // Issue: Too many dynamic routes (SEO concern)\n if (scanResult.dynamicRoutes.length > scanResult.staticRoutes.length * 2) {\n issues.push({\n id: 'too-many-dynamic-routes',\n category: 'content',\n severity: 'info',\n title: 'High ratio of dynamic routes',\n description: `${scanResult.dynamicRoutes.length} dynamic vs ${scanResult.staticRoutes.length} static routes`,\n url: '/',\n recommendation: 'Ensure dynamic routes have proper generateStaticParams for SSG',\n detectedAt: now,\n });\n }\n\n return issues;\n}\n\n/**\n * Generate routes summary\n */\nexport function generateRoutesSummary(\n scanResult: ScanResult,\n comparison?: RouteComparisonResult,\n verification?: RouteVerificationResult[]\n): string {\n const lines: string[] = [];\n\n lines.push('## Routes Summary');\n lines.push('');\n lines.push(`Total routes: ${scanResult.routes.length}`);\n lines.push(`├── Static: ${scanResult.staticRoutes.length}`);\n lines.push(`├── Dynamic: ${scanResult.dynamicRoutes.length}`);\n lines.push(`└── API: ${scanResult.apiRoutes.length}`);\n lines.push('');\n\n if (comparison) {\n lines.push('### Sitemap Comparison');\n lines.push('');\n lines.push(`Sitemap URLs: ${comparison.sitemapUrls.length}`);\n lines.push(`├── Matching: ${comparison.matching.length}`);\n lines.push(`├── Missing from sitemap: ${comparison.missingFromSitemap.length}`);\n lines.push(`└── Extra in sitemap: ${comparison.extraInSitemap.length}`);\n lines.push('');\n }\n\n if (verification) {\n const accessible = verification.filter(r => r.isAccessible).length;\n const broken = verification.filter(r => !r.isAccessible);\n\n lines.push('### Route Verification');\n lines.push('');\n lines.push(`Accessible: ${accessible}/${verification.length}`);\n\n if (broken.length > 0) {\n lines.push('');\n lines.push('Broken routes:');\n for (const r of broken.slice(0, 10)) {\n lines.push(` - ${r.route.path} → ${r.statusCode || r.error}`);\n }\n if (broken.length > 10) {\n lines.push(` - ... +${broken.length - 10} more`);\n }\n }\n }\n\n return lines.join('\\n');\n}\n"]}
@@ -0,0 +1,60 @@
1
+ /**
2
+ * @djangocfg/seo - Next.js App Router Scanner
3
+ * Scans app/ directory to extract all routes
4
+ */
5
+ interface RouteInfo {
6
+ /** Route path (e.g., /blog/[slug]) */
7
+ path: string;
8
+ /** File path relative to app/ */
9
+ filePath: string;
10
+ /** Route type */
11
+ type: 'page' | 'api' | 'layout' | 'loading' | 'error';
12
+ /** Is dynamic route */
13
+ isDynamic: boolean;
14
+ /** Dynamic segments (e.g., ['slug', '...path']) */
15
+ dynamicSegments: string[];
16
+ /** Is catch-all route */
17
+ isCatchAll: boolean;
18
+ /** Is optional catch-all route */
19
+ isOptionalCatchAll: boolean;
20
+ /** Route group (if any) */
21
+ routeGroup?: string;
22
+ }
23
+ interface ScanResult {
24
+ /** All discovered routes */
25
+ routes: RouteInfo[];
26
+ /** Static routes (no dynamic segments) */
27
+ staticRoutes: RouteInfo[];
28
+ /** Dynamic routes (with [param]) */
29
+ dynamicRoutes: RouteInfo[];
30
+ /** API routes */
31
+ apiRoutes: RouteInfo[];
32
+ /** App directory path */
33
+ appDir: string;
34
+ }
35
+ interface ScanOptions {
36
+ /** Path to app directory */
37
+ appDir?: string;
38
+ /** Include API routes */
39
+ includeApi?: boolean;
40
+ /** Include layouts, loading, error pages */
41
+ includeSpecial?: boolean;
42
+ }
43
+ /**
44
+ * Find app directory by searching common locations
45
+ */
46
+ declare function findAppDir(startDir?: string): string | null;
47
+ /**
48
+ * Scan Next.js app directory for routes
49
+ */
50
+ declare function scanRoutes(options?: ScanOptions): ScanResult;
51
+ /**
52
+ * Convert route path to URL (replace dynamic segments with example values)
53
+ */
54
+ declare function routeToUrl(route: RouteInfo, params?: Record<string, string>): string;
55
+ /**
56
+ * Get static routes as URLs (ready to check)
57
+ */
58
+ declare function getStaticUrls(scanResult: ScanResult, baseUrl: string): string[];
59
+
60
+ export { type RouteInfo as R, type ScanResult as S, type ScanOptions as a, findAppDir as f, getStaticUrls as g, routeToUrl as r, scanRoutes as s };
@@ -0,0 +1,144 @@
1
+ /**
2
+ * @djangocfg/seo - Types
3
+ * Shared types for SEO module
4
+ */
5
+ type IndexingVerdict = 'PASS' | 'PARTIAL' | 'FAIL' | 'NEUTRAL' | 'VERDICT_UNSPECIFIED';
6
+ type CoverageState = 'SUBMITTED_AND_INDEXED' | 'DUPLICATE_WITHOUT_USER_SELECTED_CANONICAL' | 'DUPLICATE_GOOGLE_CHOSE_DIFFERENT_CANONICAL' | 'NOT_INDEXED' | 'URL_NOT_FOUND' | 'CRAWLED_CURRENTLY_NOT_INDEXED' | 'DISCOVERED_CURRENTLY_NOT_INDEXED' | 'BLOCKED_DUE_TO_UNAUTHORIZED_REQUEST' | 'BLOCKED_BY_ROBOTS_TXT' | 'INDEXED_NOT_SUBMITTED_IN_SITEMAP' | 'COVERAGE_STATE_UNSPECIFIED';
7
+ type IndexingState = 'INDEXING_ALLOWED' | 'BLOCKED_BY_META_TAG' | 'BLOCKED_BY_HTTP_HEADER' | 'BLOCKED_BY_ROBOTS_TXT' | 'INDEXING_STATE_UNSPECIFIED';
8
+ type RobotsTxtState = 'ALLOWED' | 'DISALLOWED' | 'ROBOTS_TXT_STATE_UNSPECIFIED';
9
+ type PageFetchState = 'SUCCESSFUL' | 'SOFT_404' | 'BLOCKED_ROBOTS_TXT' | 'NOT_FOUND' | 'ACCESS_DENIED' | 'SERVER_ERROR' | 'REDIRECT_ERROR' | 'ACCESS_FORBIDDEN' | 'BLOCKED_4XX' | 'INTERNAL_CRAWL_ERROR' | 'INVALID_URL' | 'PAGE_FETCH_STATE_UNSPECIFIED';
10
+ interface UrlInspectionResult {
11
+ url: string;
12
+ inspectionResultLink?: string;
13
+ indexStatusResult: {
14
+ verdict: IndexingVerdict;
15
+ coverageState: CoverageState;
16
+ indexingState: IndexingState;
17
+ robotsTxtState: RobotsTxtState;
18
+ pageFetchState: PageFetchState;
19
+ lastCrawlTime?: string;
20
+ crawledAs?: 'DESKTOP' | 'MOBILE';
21
+ googleCanonical?: string;
22
+ userCanonical?: string;
23
+ sitemap?: string[];
24
+ referringUrls?: string[];
25
+ };
26
+ mobileUsabilityResult?: {
27
+ verdict: IndexingVerdict;
28
+ issues?: Array<{
29
+ issueType: string;
30
+ message: string;
31
+ }>;
32
+ };
33
+ richResultsResult?: {
34
+ verdict: IndexingVerdict;
35
+ detectedItems?: Array<{
36
+ richResultType: string;
37
+ items?: Array<{
38
+ name: string;
39
+ issues?: Array<{
40
+ issueMessage: string;
41
+ severity: 'ERROR' | 'WARNING';
42
+ }>;
43
+ }>;
44
+ }>;
45
+ };
46
+ }
47
+ type IssueSeverity = 'critical' | 'error' | 'warning' | 'info';
48
+ type IssueCategory = 'indexing' | 'crawling' | 'content' | 'technical' | 'mobile' | 'performance' | 'structured-data' | 'security';
49
+ interface SeoIssue {
50
+ id: string;
51
+ url: string;
52
+ category: IssueCategory;
53
+ severity: IssueSeverity;
54
+ title: string;
55
+ description: string;
56
+ recommendation: string;
57
+ detectedAt: string;
58
+ metadata?: Record<string, unknown>;
59
+ }
60
+ interface CrawlResult {
61
+ url: string;
62
+ statusCode: number;
63
+ contentType?: string;
64
+ title?: string;
65
+ metaDescription?: string;
66
+ metaRobots?: string;
67
+ canonicalUrl?: string;
68
+ h1?: string[];
69
+ h2?: string[];
70
+ links: {
71
+ internal: string[];
72
+ external: string[];
73
+ };
74
+ images: Array<{
75
+ src: string;
76
+ alt?: string;
77
+ hasAlt: boolean;
78
+ }>;
79
+ loadTime: number;
80
+ /** Time to first byte (ms) */
81
+ ttfb?: number;
82
+ contentLength?: number;
83
+ errors: string[];
84
+ warnings: string[];
85
+ crawledAt: string;
86
+ }
87
+ interface CrawlerConfig {
88
+ maxPages?: number;
89
+ maxDepth?: number;
90
+ concurrency?: number;
91
+ timeout?: number;
92
+ userAgent?: string;
93
+ respectRobotsTxt?: boolean;
94
+ includePatterns?: string[];
95
+ excludePatterns?: string[];
96
+ }
97
+ interface SeoReport {
98
+ id: string;
99
+ siteUrl: string;
100
+ generatedAt: string;
101
+ summary: ReportSummary;
102
+ issues: SeoIssue[];
103
+ urlInspections: UrlInspectionResult[];
104
+ crawlResults: CrawlResult[];
105
+ recommendations: Recommendation[];
106
+ }
107
+ interface ReportSummary {
108
+ totalUrls: number;
109
+ indexedUrls: number;
110
+ notIndexedUrls: number;
111
+ issuesByCategory: Record<IssueCategory, number>;
112
+ issuesBySeverity: Record<IssueSeverity, number>;
113
+ healthScore: number;
114
+ }
115
+ interface Recommendation {
116
+ priority: 1 | 2 | 3 | 4 | 5;
117
+ category: IssueCategory;
118
+ title: string;
119
+ description: string;
120
+ affectedUrls: string[];
121
+ estimatedImpact: 'high' | 'medium' | 'low';
122
+ actionItems: string[];
123
+ }
124
+ interface GoogleConsoleConfig {
125
+ serviceAccountPath?: string;
126
+ serviceAccountJson?: {
127
+ client_email: string;
128
+ private_key: string;
129
+ project_id?: string;
130
+ };
131
+ siteUrl: string;
132
+ /** GSC property format: 'sc-domain:example.com' or 'https://example.com'. Auto-detected if not provided. */
133
+ gscSiteUrl?: string;
134
+ }
135
+ interface SeoModuleConfig {
136
+ googleConsole?: GoogleConsoleConfig;
137
+ crawler?: CrawlerConfig;
138
+ reports?: {
139
+ outputDir: string;
140
+ formats: ('json' | 'markdown')[];
141
+ };
142
+ }
143
+
144
+ export type { CoverageState, CrawlResult, CrawlerConfig, GoogleConsoleConfig, IndexingState, IndexingVerdict, IssueCategory, IssueSeverity, PageFetchState, Recommendation, ReportSummary, RobotsTxtState, SeoIssue, SeoModuleConfig, SeoReport, UrlInspectionResult };
@@ -0,0 +1,3 @@
1
+
2
+ //# sourceMappingURL=index.mjs.map
3
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"index.mjs"}
package/package.json ADDED
@@ -0,0 +1,114 @@
1
+ {
2
+ "name": "@djangocfg/seo",
3
+ "version": "2.1.50",
4
+ "description": "SEO analytics and indexing diagnostics module with Google Search Console integration and AI-ready reports",
5
+ "keywords": [
6
+ "seo",
7
+ "google-search-console",
8
+ "indexing",
9
+ "crawler",
10
+ "site-audit",
11
+ "ai-reports",
12
+ "nextjs",
13
+ "typescript"
14
+ ],
15
+ "author": {
16
+ "name": "DjangoCFG",
17
+ "url": "https://djangocfg.com"
18
+ },
19
+ "homepage": "https://djangocfg.com",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "https://github.com/markolofsen/django-cfg.git",
23
+ "directory": "packages/seo"
24
+ },
25
+ "bugs": {
26
+ "url": "https://github.com/markolofsen/django-cfg/issues"
27
+ },
28
+ "license": "MIT",
29
+ "type": "module",
30
+ "main": "./dist/index.mjs",
31
+ "types": "./dist/index.d.mts",
32
+ "exports": {
33
+ ".": {
34
+ "types": "./dist/index.d.mts",
35
+ "import": "./dist/index.mjs",
36
+ "default": "./dist/index.mjs"
37
+ },
38
+ "./google-console": {
39
+ "types": "./dist/google-console/index.d.mts",
40
+ "import": "./dist/google-console/index.mjs",
41
+ "default": "./dist/google-console/index.mjs"
42
+ },
43
+ "./crawler": {
44
+ "types": "./dist/crawler/index.d.mts",
45
+ "import": "./dist/crawler/index.mjs",
46
+ "default": "./dist/crawler/index.mjs"
47
+ },
48
+ "./link-checker": {
49
+ "types": "./dist/link-checker/index.d.mts",
50
+ "import": "./dist/link-checker/index.mjs",
51
+ "default": "./dist/link-checker/index.mjs"
52
+ },
53
+ "./reports": {
54
+ "types": "./dist/reports/index.d.mts",
55
+ "import": "./dist/reports/index.mjs",
56
+ "default": "./dist/reports/index.mjs"
57
+ },
58
+ "./types": {
59
+ "types": "./dist/types/index.d.mts",
60
+ "import": "./dist/types/index.mjs",
61
+ "default": "./dist/types/index.mjs"
62
+ },
63
+ "./routes": {
64
+ "types": "./dist/routes/index.d.mts",
65
+ "import": "./dist/routes/index.mjs",
66
+ "default": "./dist/routes/index.mjs"
67
+ }
68
+ },
69
+ "files": [
70
+ "dist",
71
+ "src",
72
+ "README.md",
73
+ "LICENSE"
74
+ ],
75
+ "bin": {
76
+ "djangocfg-seo": "./dist/cli.mjs"
77
+ },
78
+ "scripts": {
79
+ "build": "tsup",
80
+ "dev": "tsup --watch",
81
+ "clean": "rm -rf dist",
82
+ "lint": "eslint .",
83
+ "check": "tsc --noEmit",
84
+ "test": "vitest run",
85
+ "test:watch": "vitest",
86
+ "test:coverage": "vitest run --coverage",
87
+ "inspect": "tsx src/cli.ts inspect",
88
+ "report": "tsx src/cli.ts report"
89
+ },
90
+ "dependencies": {
91
+ "@googleapis/searchconsole": "^1.0.0",
92
+ "google-auth-library": "^9.15.1",
93
+ "chalk": "^5.3.0",
94
+ "consola": "^3.4.2",
95
+ "cheerio": "^1.0.0",
96
+ "linkinator": "^7.5.0",
97
+ "p-limit": "^6.2.0",
98
+ "p-retry": "^7.0.0",
99
+ "picocolors": "^1.1.1",
100
+ "prompts": "^2.4.2",
101
+ "robots-parser": "^3.0.1"
102
+ },
103
+ "devDependencies": {
104
+ "@djangocfg/typescript-config": "^2.1.50",
105
+ "@types/node": "^24.7.2",
106
+ "tsup": "^8.5.0",
107
+ "tsx": "^4.19.2",
108
+ "typescript": "^5.9.3",
109
+ "vitest": "^3.1.4"
110
+ },
111
+ "publishConfig": {
112
+ "access": "public"
113
+ }
114
+ }
@@ -0,0 +1,256 @@
1
+ /**
2
+ * @djangocfg/seo - Main Analyzer
3
+ * High-level SEO analysis orchestrator
4
+ */
5
+
6
+ import consola from 'consola';
7
+ import { GoogleConsoleClient, analyzeInspectionResults } from './google-console/index.js';
8
+ import {
9
+ SiteCrawler,
10
+ analyzeCrawlResults,
11
+ analyzeRobotsTxt,
12
+ analyzeAllSitemaps,
13
+ } from './crawler/index.js';
14
+ import {
15
+ generateAndSaveReports,
16
+ generateJsonReport,
17
+ printReportSummary,
18
+ } from './reports/index.js';
19
+ import type {
20
+ SeoModuleConfig,
21
+ SeoReport,
22
+ SeoIssue,
23
+ UrlInspectionResult,
24
+ CrawlResult,
25
+ } from './types/index.js';
26
+
27
+ export interface AnalyzeOptions {
28
+ includeGoogleConsole?: boolean;
29
+ includeCrawler?: boolean;
30
+ includeRobotsTxt?: boolean;
31
+ includeSitemap?: boolean;
32
+ crawlerMaxPages?: number;
33
+ crawlerMaxDepth?: number;
34
+ urlsToInspect?: string[];
35
+ }
36
+
37
+ export interface AnalyzeResult {
38
+ report: SeoReport;
39
+ issues: SeoIssue[];
40
+ urlInspections: UrlInspectionResult[];
41
+ crawlResults: CrawlResult[];
42
+ }
43
+
44
+ /**
45
+ * Main SEO Analyzer class
46
+ * Combines all modules for comprehensive SEO analysis
47
+ */
48
+ export class SeoAnalyzer {
49
+ private config: SeoModuleConfig;
50
+ private siteUrl: string;
51
+ private gscClient?: GoogleConsoleClient;
52
+
53
+ constructor(config: SeoModuleConfig & { siteUrl: string }) {
54
+ this.config = config;
55
+ this.siteUrl = config.siteUrl || config.googleConsole?.siteUrl || '';
56
+
57
+ if (!this.siteUrl) {
58
+ throw new Error('siteUrl is required');
59
+ }
60
+
61
+ // Initialize Google Console client if configured
62
+ if (config.googleConsole) {
63
+ this.gscClient = new GoogleConsoleClient({
64
+ ...config.googleConsole,
65
+ siteUrl: this.siteUrl,
66
+ });
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Run full SEO analysis
72
+ */
73
+ async analyze(options: AnalyzeOptions = {}): Promise<AnalyzeResult> {
74
+ const {
75
+ includeGoogleConsole = true,
76
+ includeCrawler = true,
77
+ includeRobotsTxt = true,
78
+ includeSitemap = true,
79
+ crawlerMaxPages = this.config.crawler?.maxPages || 100,
80
+ crawlerMaxDepth = this.config.crawler?.maxDepth || 3,
81
+ urlsToInspect,
82
+ } = options;
83
+
84
+ consola.start(`Starting SEO analysis for ${this.siteUrl}`);
85
+
86
+ const allIssues: SeoIssue[] = [];
87
+ const allInspections: UrlInspectionResult[] = [];
88
+ const allCrawlResults: CrawlResult[] = [];
89
+
90
+ // 1. Analyze robots.txt
91
+ if (includeRobotsTxt) {
92
+ consola.info('Analyzing robots.txt...');
93
+ try {
94
+ const robotsAnalysis = await analyzeRobotsTxt(this.siteUrl);
95
+ allIssues.push(...robotsAnalysis.issues);
96
+ consola.success(`robots.txt: ${robotsAnalysis.exists ? 'found' : 'not found'}`);
97
+ } catch (error) {
98
+ consola.warn('Failed to analyze robots.txt:', error);
99
+ }
100
+ }
101
+
102
+ // 2. Analyze sitemaps
103
+ if (includeSitemap) {
104
+ consola.info('Analyzing sitemaps...');
105
+ try {
106
+ const sitemapUrl = new URL('/sitemap.xml', this.siteUrl).href;
107
+ const sitemapAnalyses = await analyzeAllSitemaps(sitemapUrl);
108
+
109
+ let totalUrls = 0;
110
+ for (const analysis of sitemapAnalyses) {
111
+ allIssues.push(...analysis.issues);
112
+ totalUrls += analysis.urls.length;
113
+ }
114
+
115
+ consola.success(`Sitemaps: ${sitemapAnalyses.length} found, ${totalUrls} URLs`);
116
+ } catch (error) {
117
+ consola.warn('Failed to analyze sitemaps:', error);
118
+ }
119
+ }
120
+
121
+ // 3. Crawl site
122
+ if (includeCrawler) {
123
+ consola.info('Crawling site...');
124
+ try {
125
+ const crawler = new SiteCrawler(this.siteUrl, {
126
+ maxPages: crawlerMaxPages,
127
+ maxDepth: crawlerMaxDepth,
128
+ ...this.config.crawler,
129
+ });
130
+
131
+ const crawlResults = await crawler.crawl();
132
+ const crawlIssues = analyzeCrawlResults(crawlResults);
133
+
134
+ allCrawlResults.push(...crawlResults);
135
+ allIssues.push(...crawlIssues);
136
+
137
+ consola.success(`Crawled ${crawlResults.length} pages, found ${crawlIssues.length} issues`);
138
+ } catch (error) {
139
+ consola.error('Failed to crawl site:', error);
140
+ }
141
+ }
142
+
143
+ // 4. Google Search Console inspection
144
+ if (includeGoogleConsole && this.gscClient) {
145
+ consola.info('Inspecting URLs via Google Search Console...');
146
+ try {
147
+ const isAuth = await this.gscClient.verify();
148
+
149
+ if (isAuth) {
150
+ const urls =
151
+ urlsToInspect ||
152
+ allCrawlResults
153
+ .filter((r) => r.statusCode === 200)
154
+ .slice(0, 50)
155
+ .map((r) => r.url);
156
+
157
+ if (urls.length > 0) {
158
+ const inspections = await this.gscClient.inspectUrls(urls);
159
+ const gscIssues = analyzeInspectionResults(inspections);
160
+
161
+ allInspections.push(...inspections);
162
+ allIssues.push(...gscIssues);
163
+
164
+ consola.success(`Inspected ${inspections.length} URLs, found ${gscIssues.length} issues`);
165
+ }
166
+ }
167
+ } catch (error) {
168
+ consola.warn('Google Search Console inspection failed:', error);
169
+ }
170
+ }
171
+
172
+ // Generate report
173
+ const report = generateJsonReport(
174
+ this.siteUrl,
175
+ {
176
+ issues: allIssues,
177
+ urlInspections: allInspections,
178
+ crawlResults: allCrawlResults,
179
+ },
180
+ { includeRawData: true }
181
+ );
182
+
183
+ consola.success(`Analysis complete. Health score: ${report.summary.healthScore}/100`);
184
+
185
+ return {
186
+ report,
187
+ issues: allIssues,
188
+ urlInspections: allInspections,
189
+ crawlResults: allCrawlResults,
190
+ };
191
+ }
192
+
193
+ /**
194
+ * Save report to files
195
+ */
196
+ async saveReport(outputDir?: string): Promise<void> {
197
+ const result = await this.analyze();
198
+
199
+ await generateAndSaveReports(
200
+ this.siteUrl,
201
+ {
202
+ issues: result.issues,
203
+ urlInspections: result.urlInspections,
204
+ crawlResults: result.crawlResults,
205
+ },
206
+ {
207
+ outputDir: outputDir || this.config.reports?.outputDir || './seo-reports',
208
+ formats: this.config.reports?.formats || ['json', 'markdown'],
209
+ includeRawData: true,
210
+ }
211
+ );
212
+
213
+ printReportSummary(result.report);
214
+ }
215
+
216
+ /**
217
+ * Quick health check
218
+ */
219
+ async healthCheck(): Promise<{
220
+ healthScore: number;
221
+ criticalIssues: number;
222
+ errors: number;
223
+ warnings: number;
224
+ }> {
225
+ const result = await this.analyze({
226
+ includeGoogleConsole: false,
227
+ crawlerMaxPages: 20,
228
+ crawlerMaxDepth: 2,
229
+ });
230
+
231
+ return {
232
+ healthScore: result.report.summary.healthScore,
233
+ criticalIssues: result.report.summary.issuesBySeverity.critical || 0,
234
+ errors: result.report.summary.issuesBySeverity.error || 0,
235
+ warnings: result.report.summary.issuesBySeverity.warning || 0,
236
+ };
237
+ }
238
+
239
+ /**
240
+ * Inspect specific URLs
241
+ */
242
+ async inspectUrls(urls: string[]): Promise<UrlInspectionResult[]> {
243
+ if (!this.gscClient) {
244
+ throw new Error('Google Console is not configured');
245
+ }
246
+
247
+ return this.gscClient.inspectUrls(urls);
248
+ }
249
+
250
+ /**
251
+ * Get Google Console client
252
+ */
253
+ getGoogleConsoleClient(): GoogleConsoleClient | undefined {
254
+ return this.gscClient;
255
+ }
256
+ }