@intlayer/engine 9.5.9 → 9.5.11
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/cjs/cli.cjs +7 -0
- package/dist/cjs/docReview/alignBlocks.cjs +53 -36
- package/dist/cjs/docReview/alignBlocks.cjs.map +1 -1
- package/dist/cjs/docReview/rebuildDocument.cjs +3 -1
- package/dist/cjs/docReview/rebuildDocument.cjs.map +1 -1
- package/dist/cjs/docReview/reviewReport.cjs.map +1 -1
- package/dist/cjs/docReview/segmentDocument.cjs +8 -5
- package/dist/cjs/docReview/segmentDocument.cjs.map +1 -1
- package/dist/cjs/init/cms.cjs +35 -0
- package/dist/cjs/init/cms.cjs.map +1 -1
- package/dist/cjs/init/documentationRouter.cjs +141 -0
- package/dist/cjs/init/documentationRouter.cjs.map +1 -0
- package/dist/cjs/init/frameworkSetup/backend/index.cjs +138 -0
- package/dist/cjs/init/frameworkSetup/backend/index.cjs.map +1 -0
- package/dist/cjs/init/frameworkSetup/backend/transforms.cjs +223 -0
- package/dist/cjs/init/frameworkSetup/backend/transforms.cjs.map +1 -0
- package/dist/cjs/init/frameworkSetup/index.cjs +9 -3
- package/dist/cjs/init/frameworkSetup/index.cjs.map +1 -1
- package/dist/cjs/init/frameworkSetup/localeRouting.cjs +13 -0
- package/dist/cjs/init/frameworkSetup/localeRouting.cjs.map +1 -0
- package/dist/cjs/init/frameworkSetup/nextAppRouter/index.cjs +34 -7
- package/dist/cjs/init/frameworkSetup/nextAppRouter/index.cjs.map +1 -1
- package/dist/cjs/init/frameworkSetup/nextAppRouter/templates.cjs +56 -0
- package/dist/cjs/init/frameworkSetup/nextAppRouter/templates.cjs.map +1 -1
- package/dist/cjs/init/frameworkSetup/nextAppRouter/transforms.cjs +6 -3
- package/dist/cjs/init/frameworkSetup/nextAppRouter/transforms.cjs.map +1 -1
- package/dist/cjs/init/frameworkSetup/tanstackStart/index.cjs +32 -3
- package/dist/cjs/init/frameworkSetup/tanstackStart/index.cjs.map +1 -1
- package/dist/cjs/init/frameworkSetup/tanstackStart/templates.cjs +58 -0
- package/dist/cjs/init/frameworkSetup/tanstackStart/templates.cjs.map +1 -1
- package/dist/cjs/init/frameworkSetup/tanstackStart/transforms.cjs +94 -14
- package/dist/cjs/init/frameworkSetup/tanstackStart/transforms.cjs.map +1 -1
- package/dist/cjs/init/index.cjs +46 -159
- package/dist/cjs/init/index.cjs.map +1 -1
- package/dist/cjs/init/utils/configManipulation.cjs +39 -0
- package/dist/cjs/init/utils/configManipulation.cjs.map +1 -1
- package/dist/cjs/init/utils/index.cjs +10 -0
- package/dist/cjs/init/utils/lintConfig.cjs +52 -0
- package/dist/cjs/init/utils/lintConfig.cjs.map +1 -0
- package/dist/cjs/init/utils/packageManager.cjs +62 -13
- package/dist/cjs/init/utils/packageManager.cjs.map +1 -1
- package/dist/cjs/init/utils/urlRoutingFramework.cjs +41 -0
- package/dist/cjs/init/utils/urlRoutingFramework.cjs.map +1 -0
- package/dist/cjs/initConfig/index.cjs +4 -1
- package/dist/cjs/initConfig/index.cjs.map +1 -1
- package/dist/cjs/installMCP/installMCP.cjs +1 -1
- package/dist/cjs/installMCP/installMCP.cjs.map +1 -1
- package/dist/cjs/loadDictionaries/loadLocalDictionaries.cjs +3 -1
- package/dist/cjs/loadDictionaries/loadLocalDictionaries.cjs.map +1 -1
- package/dist/cjs/loadDictionaries/loadRemoteDictionaries.cjs +1 -1
- package/dist/cjs/loadDictionaries/loadRemoteDictionaries.cjs.map +1 -1
- package/dist/cjs/loadDictionaries/log.cjs +1 -1
- package/dist/cjs/loadDictionaries/log.cjs.map +1 -1
- package/dist/cjs/loadDictionaries/logTypeScriptErrors.cjs +111 -43
- package/dist/cjs/loadDictionaries/logTypeScriptErrors.cjs.map +1 -1
- package/dist/cjs/scan/analyzeBundleContent.cjs +26 -12
- package/dist/cjs/scan/analyzeBundleContent.cjs.map +1 -1
- package/dist/cjs/scan/calculateScore.cjs +4 -1
- package/dist/cjs/scan/calculateScore.cjs.map +1 -1
- package/dist/cjs/scan/checks.cjs +394 -221
- package/dist/cjs/scan/checks.cjs.map +1 -1
- package/dist/cjs/scan/detection/checkDetails.cjs +56 -0
- package/dist/cjs/scan/detection/checkDetails.cjs.map +1 -0
- package/dist/cjs/scan/detection/classifyInternalLinks.cjs +47 -0
- package/dist/cjs/scan/detection/classifyInternalLinks.cjs.map +1 -0
- package/dist/cjs/scan/detection/detectRoutingStrategy.cjs +143 -0
- package/dist/cjs/scan/detection/detectRoutingStrategy.cjs.map +1 -0
- package/dist/cjs/scan/detection/detectTechnologies.cjs +89 -0
- package/dist/cjs/scan/detection/detectTechnologies.cjs.map +1 -0
- package/dist/cjs/scan/detection/index.cjs +36 -0
- package/dist/cjs/scan/detection/localeCode.cjs +118 -0
- package/dist/cjs/scan/detection/localeCode.cjs.map +1 -0
- package/dist/cjs/scan/detection/localizedPages.cjs +48 -0
- package/dist/cjs/scan/detection/localizedPages.cjs.map +1 -0
- package/dist/cjs/scan/detection/technologySignatures.cjs +564 -0
- package/dist/cjs/scan/detection/technologySignatures.cjs.map +1 -0
- package/dist/cjs/scan/detection/url.cjs +53 -0
- package/dist/cjs/scan/detection/url.cjs.map +1 -0
- package/dist/cjs/scan/fetchText.cjs +56 -0
- package/dist/cjs/scan/fetchText.cjs.map +1 -0
- package/dist/cjs/scan/index.cjs +59 -3
- package/dist/cjs/scan/parseHtml.cjs +55 -10
- package/dist/cjs/scan/parseHtml.cjs.map +1 -1
- package/dist/cjs/scan/robots.cjs +65 -0
- package/dist/cjs/scan/robots.cjs.map +1 -0
- package/dist/cjs/scan/runScanChecks.cjs +89 -0
- package/dist/cjs/scan/runScanChecks.cjs.map +1 -0
- package/dist/cjs/scan/scanWebsite.cjs +58 -52
- package/dist/cjs/scan/scanWebsite.cjs.map +1 -1
- package/dist/cjs/scan/sitemap.cjs +109 -0
- package/dist/cjs/scan/sitemap.cjs.map +1 -0
- package/dist/cjs/utils/chunkJSON.cjs +24 -9
- package/dist/cjs/utils/chunkJSON.cjs.map +1 -1
- package/dist/cjs/utils/getChunk.cjs +2 -2
- package/dist/cjs/utils/getChunk.cjs.map +1 -1
- package/dist/cjs/utils/runParallel/index.cjs +1 -1
- package/dist/cjs/utils/runParallel/index.cjs.map +1 -1
- package/dist/cjs/utils/runParallel/pidTree.cjs +23 -11
- package/dist/cjs/utils/runParallel/pidTree.cjs.map +1 -1
- package/dist/cjs/utils/runParallel/ps.cjs +8 -3
- package/dist/cjs/utils/runParallel/ps.cjs.map +1 -1
- package/dist/cjs/utils/runParallel/runTask.cjs +2 -1
- package/dist/cjs/utils/runParallel/runTask.cjs.map +1 -1
- package/dist/cjs/utils/runParallel/wmic.cjs +8 -3
- package/dist/cjs/utils/runParallel/wmic.cjs.map +1 -1
- package/dist/cjs/writeContentDeclaration/detectExportedComponentName.cjs +7 -4
- package/dist/cjs/writeContentDeclaration/detectExportedComponentName.cjs.map +1 -1
- package/dist/cjs/writeContentDeclaration/writeMarkdownFile.cjs +2 -2
- package/dist/cjs/writeContentDeclaration/writeMarkdownFile.cjs.map +1 -1
- package/dist/esm/cli.mjs +5 -3
- package/dist/esm/docReview/alignBlocks.mjs +53 -36
- package/dist/esm/docReview/alignBlocks.mjs.map +1 -1
- package/dist/esm/docReview/rebuildDocument.mjs +3 -1
- package/dist/esm/docReview/rebuildDocument.mjs.map +1 -1
- package/dist/esm/docReview/reviewReport.mjs.map +1 -1
- package/dist/esm/docReview/segmentDocument.mjs +8 -5
- package/dist/esm/docReview/segmentDocument.mjs.map +1 -1
- package/dist/esm/init/cms.mjs +35 -2
- package/dist/esm/init/cms.mjs.map +1 -1
- package/dist/esm/init/documentationRouter.mjs +139 -0
- package/dist/esm/init/documentationRouter.mjs.map +1 -0
- package/dist/esm/init/frameworkSetup/backend/index.mjs +130 -0
- package/dist/esm/init/frameworkSetup/backend/index.mjs.map +1 -0
- package/dist/esm/init/frameworkSetup/backend/transforms.mjs +216 -0
- package/dist/esm/init/frameworkSetup/backend/transforms.mjs.map +1 -0
- package/dist/esm/init/frameworkSetup/index.mjs +9 -3
- package/dist/esm/init/frameworkSetup/index.mjs.map +1 -1
- package/dist/esm/init/frameworkSetup/localeRouting.mjs +12 -0
- package/dist/esm/init/frameworkSetup/localeRouting.mjs.map +1 -0
- package/dist/esm/init/frameworkSetup/nextAppRouter/index.mjs +36 -9
- package/dist/esm/init/frameworkSetup/nextAppRouter/index.mjs.map +1 -1
- package/dist/esm/init/frameworkSetup/nextAppRouter/templates.mjs +54 -1
- package/dist/esm/init/frameworkSetup/nextAppRouter/templates.mjs.map +1 -1
- package/dist/esm/init/frameworkSetup/nextAppRouter/transforms.mjs +6 -3
- package/dist/esm/init/frameworkSetup/nextAppRouter/transforms.mjs.map +1 -1
- package/dist/esm/init/frameworkSetup/tanstackStart/index.mjs +35 -6
- package/dist/esm/init/frameworkSetup/tanstackStart/index.mjs.map +1 -1
- package/dist/esm/init/frameworkSetup/tanstackStart/templates.mjs +56 -1
- package/dist/esm/init/frameworkSetup/tanstackStart/templates.mjs.map +1 -1
- package/dist/esm/init/frameworkSetup/tanstackStart/transforms.mjs +94 -15
- package/dist/esm/init/frameworkSetup/tanstackStart/transforms.mjs.map +1 -1
- package/dist/esm/init/index.mjs +42 -158
- package/dist/esm/init/index.mjs.map +1 -1
- package/dist/esm/init/utils/configManipulation.mjs +38 -1
- package/dist/esm/init/utils/configManipulation.mjs.map +1 -1
- package/dist/esm/init/utils/index.mjs +5 -3
- package/dist/esm/init/utils/lintConfig.mjs +50 -0
- package/dist/esm/init/utils/lintConfig.mjs.map +1 -0
- package/dist/esm/init/utils/packageManager.mjs +60 -14
- package/dist/esm/init/utils/packageManager.mjs.map +1 -1
- package/dist/esm/init/utils/urlRoutingFramework.mjs +40 -0
- package/dist/esm/init/utils/urlRoutingFramework.mjs.map +1 -0
- package/dist/esm/initConfig/index.mjs +4 -1
- package/dist/esm/initConfig/index.mjs.map +1 -1
- package/dist/esm/installMCP/installMCP.mjs +1 -1
- package/dist/esm/installMCP/installMCP.mjs.map +1 -1
- package/dist/esm/loadDictionaries/loadLocalDictionaries.mjs +3 -1
- package/dist/esm/loadDictionaries/loadLocalDictionaries.mjs.map +1 -1
- package/dist/esm/loadDictionaries/loadRemoteDictionaries.mjs +1 -1
- package/dist/esm/loadDictionaries/loadRemoteDictionaries.mjs.map +1 -1
- package/dist/esm/loadDictionaries/log.mjs +1 -1
- package/dist/esm/loadDictionaries/log.mjs.map +1 -1
- package/dist/esm/loadDictionaries/logTypeScriptErrors.mjs +112 -44
- package/dist/esm/loadDictionaries/logTypeScriptErrors.mjs.map +1 -1
- package/dist/esm/scan/analyzeBundleContent.mjs +26 -12
- package/dist/esm/scan/analyzeBundleContent.mjs.map +1 -1
- package/dist/esm/scan/calculateScore.mjs +4 -1
- package/dist/esm/scan/calculateScore.mjs.map +1 -1
- package/dist/esm/scan/checks.mjs +387 -220
- package/dist/esm/scan/checks.mjs.map +1 -1
- package/dist/esm/scan/detection/checkDetails.mjs +54 -0
- package/dist/esm/scan/detection/checkDetails.mjs.map +1 -0
- package/dist/esm/scan/detection/classifyInternalLinks.mjs +46 -0
- package/dist/esm/scan/detection/classifyInternalLinks.mjs.map +1 -0
- package/dist/esm/scan/detection/detectRoutingStrategy.mjs +141 -0
- package/dist/esm/scan/detection/detectRoutingStrategy.mjs.map +1 -0
- package/dist/esm/scan/detection/detectTechnologies.mjs +87 -0
- package/dist/esm/scan/detection/detectTechnologies.mjs.map +1 -0
- package/dist/esm/scan/detection/index.mjs +10 -0
- package/dist/esm/scan/detection/localeCode.mjs +108 -0
- package/dist/esm/scan/detection/localeCode.mjs.map +1 -0
- package/dist/esm/scan/detection/localizedPages.mjs +46 -0
- package/dist/esm/scan/detection/localizedPages.mjs.map +1 -0
- package/dist/esm/scan/detection/technologySignatures.mjs +563 -0
- package/dist/esm/scan/detection/technologySignatures.mjs.map +1 -0
- package/dist/esm/scan/detection/url.mjs +47 -0
- package/dist/esm/scan/detection/url.mjs.map +1 -0
- package/dist/esm/scan/fetchText.mjs +55 -0
- package/dist/esm/scan/fetchText.mjs.map +1 -0
- package/dist/esm/scan/index.mjs +15 -3
- package/dist/esm/scan/parseHtml.mjs +52 -11
- package/dist/esm/scan/parseHtml.mjs.map +1 -1
- package/dist/esm/scan/robots.mjs +63 -0
- package/dist/esm/scan/robots.mjs.map +1 -0
- package/dist/esm/scan/runScanChecks.mjs +88 -0
- package/dist/esm/scan/runScanChecks.mjs.map +1 -0
- package/dist/esm/scan/scanWebsite.mjs +59 -53
- package/dist/esm/scan/scanWebsite.mjs.map +1 -1
- package/dist/esm/scan/sitemap.mjs +105 -0
- package/dist/esm/scan/sitemap.mjs.map +1 -0
- package/dist/esm/utils/chunkJSON.mjs +24 -9
- package/dist/esm/utils/chunkJSON.mjs.map +1 -1
- package/dist/esm/utils/getChunk.mjs +2 -2
- package/dist/esm/utils/getChunk.mjs.map +1 -1
- package/dist/esm/utils/runParallel/index.mjs +1 -1
- package/dist/esm/utils/runParallel/index.mjs.map +1 -1
- package/dist/esm/utils/runParallel/pidTree.mjs +23 -11
- package/dist/esm/utils/runParallel/pidTree.mjs.map +1 -1
- package/dist/esm/utils/runParallel/ps.mjs +8 -3
- package/dist/esm/utils/runParallel/ps.mjs.map +1 -1
- package/dist/esm/utils/runParallel/runTask.mjs +2 -1
- package/dist/esm/utils/runParallel/runTask.mjs.map +1 -1
- package/dist/esm/utils/runParallel/wmic.mjs +8 -3
- package/dist/esm/utils/runParallel/wmic.mjs.map +1 -1
- package/dist/esm/writeContentDeclaration/detectExportedComponentName.mjs +7 -4
- package/dist/esm/writeContentDeclaration/detectExportedComponentName.mjs.map +1 -1
- package/dist/esm/writeContentDeclaration/writeMarkdownFile.mjs +2 -2
- package/dist/esm/writeContentDeclaration/writeMarkdownFile.mjs.map +1 -1
- package/dist/types/cli.d.ts +5 -3
- package/dist/types/docReview/rebuildDocument.d.ts.map +1 -1
- package/dist/types/docReview/reviewReport.d.ts.map +1 -1
- package/dist/types/docReview/segmentDocument.d.ts.map +1 -1
- package/dist/types/formatDictionary.d.ts +2 -2
- package/dist/types/init/cms.d.ts +11 -0
- package/dist/types/init/cms.d.ts.map +1 -1
- package/dist/types/init/documentationRouter.d.ts +64 -0
- package/dist/types/init/documentationRouter.d.ts.map +1 -0
- package/dist/types/init/frameworkSetup/backend/index.d.ts +16 -0
- package/dist/types/init/frameworkSetup/backend/index.d.ts.map +1 -0
- package/dist/types/init/frameworkSetup/backend/transforms.d.ts +37 -0
- package/dist/types/init/frameworkSetup/backend/transforms.d.ts.map +1 -0
- package/dist/types/init/frameworkSetup/index.d.ts.map +1 -1
- package/dist/types/init/frameworkSetup/localeRouting.d.ts +11 -0
- package/dist/types/init/frameworkSetup/localeRouting.d.ts.map +1 -0
- package/dist/types/init/frameworkSetup/nextAppRouter/index.d.ts +5 -5
- package/dist/types/init/frameworkSetup/nextAppRouter/index.d.ts.map +1 -1
- package/dist/types/init/frameworkSetup/nextAppRouter/templates.d.ts +12 -0
- package/dist/types/init/frameworkSetup/nextAppRouter/templates.d.ts.map +1 -1
- package/dist/types/init/frameworkSetup/nextAppRouter/transforms.d.ts +10 -1
- package/dist/types/init/frameworkSetup/nextAppRouter/transforms.d.ts.map +1 -1
- package/dist/types/init/frameworkSetup/tanstackStart/index.d.ts +3 -2
- package/dist/types/init/frameworkSetup/tanstackStart/index.d.ts.map +1 -1
- package/dist/types/init/frameworkSetup/tanstackStart/templates.d.ts +10 -0
- package/dist/types/init/frameworkSetup/tanstackStart/templates.d.ts.map +1 -1
- package/dist/types/init/frameworkSetup/tanstackStart/transforms.d.ts +12 -0
- package/dist/types/init/frameworkSetup/tanstackStart/transforms.d.ts.map +1 -1
- package/dist/types/init/frameworkSetup/types.d.ts +6 -0
- package/dist/types/init/frameworkSetup/types.d.ts.map +1 -1
- package/dist/types/init/index.d.ts +15 -1
- package/dist/types/init/index.d.ts.map +1 -1
- package/dist/types/init/utils/configManipulation.d.ts +14 -0
- package/dist/types/init/utils/configManipulation.d.ts.map +1 -1
- package/dist/types/init/utils/index.d.ts +5 -3
- package/dist/types/init/utils/lintConfig.d.ts +28 -0
- package/dist/types/init/utils/lintConfig.d.ts.map +1 -0
- package/dist/types/init/utils/packageManager.d.ts +29 -6
- package/dist/types/init/utils/packageManager.d.ts.map +1 -1
- package/dist/types/init/utils/urlRoutingFramework.d.ts +9 -0
- package/dist/types/init/utils/urlRoutingFramework.d.ts.map +1 -0
- package/dist/types/initConfig/index.d.ts +3 -1
- package/dist/types/initConfig/index.d.ts.map +1 -1
- package/dist/types/loadDictionaries/logTypeScriptErrors.d.ts +4 -0
- package/dist/types/loadDictionaries/logTypeScriptErrors.d.ts.map +1 -1
- package/dist/types/scan/analyzeBundleContent.d.ts.map +1 -1
- package/dist/types/scan/calculateScore.d.ts +4 -1
- package/dist/types/scan/calculateScore.d.ts.map +1 -1
- package/dist/types/scan/checks.d.ts +114 -21
- package/dist/types/scan/checks.d.ts.map +1 -1
- package/dist/types/scan/detection/checkDetails.d.ts +29 -0
- package/dist/types/scan/detection/checkDetails.d.ts.map +1 -0
- package/dist/types/scan/detection/classifyInternalLinks.d.ts +44 -0
- package/dist/types/scan/detection/classifyInternalLinks.d.ts.map +1 -0
- package/dist/types/scan/detection/detectRoutingStrategy.d.ts +60 -0
- package/dist/types/scan/detection/detectRoutingStrategy.d.ts.map +1 -0
- package/dist/types/scan/detection/detectTechnologies.d.ts +60 -0
- package/dist/types/scan/detection/detectTechnologies.d.ts.map +1 -0
- package/dist/types/scan/detection/index.d.ts +9 -0
- package/dist/types/scan/detection/localeCode.d.ts +49 -0
- package/dist/types/scan/detection/localeCode.d.ts.map +1 -0
- package/dist/types/scan/detection/localizedPages.d.ts +40 -0
- package/dist/types/scan/detection/localizedPages.d.ts.map +1 -0
- package/dist/types/scan/detection/technologySignatures.d.ts +48 -0
- package/dist/types/scan/detection/technologySignatures.d.ts.map +1 -0
- package/dist/types/scan/detection/url.d.ts +27 -0
- package/dist/types/scan/detection/url.d.ts.map +1 -0
- package/dist/types/scan/fetchText.d.ts +30 -0
- package/dist/types/scan/fetchText.d.ts.map +1 -0
- package/dist/types/scan/index.d.ts +16 -4
- package/dist/types/scan/parseHtml.d.ts +17 -0
- package/dist/types/scan/parseHtml.d.ts.map +1 -1
- package/dist/types/scan/robots.d.ts +23 -0
- package/dist/types/scan/robots.d.ts.map +1 -0
- package/dist/types/scan/runScanChecks.d.ts +43 -0
- package/dist/types/scan/runScanChecks.d.ts.map +1 -0
- package/dist/types/scan/scanWebsite.d.ts.map +1 -1
- package/dist/types/scan/sitemap.d.ts +49 -0
- package/dist/types/scan/sitemap.d.ts.map +1 -0
- package/dist/types/scan/types.d.ts +15 -0
- package/dist/types/scan/types.d.ts.map +1 -1
- package/dist/types/utils/chunkJSON.d.ts.map +1 -1
- package/dist/types/utils/runParallel/pidTree.d.ts.map +1 -1
- package/dist/types/writeContentDeclaration/writeMarkdownFile.d.ts.map +1 -1
- package/package.json +16 -8
package/dist/esm/scan/checks.mjs
CHANGED
|
@@ -1,265 +1,432 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { fetchText } from "./fetchText.mjs";
|
|
2
|
+
import { isAbsoluteUrl, normalizeUrl, parseUrl } from "./detection/url.mjs";
|
|
3
|
+
import { extractHreflangs, extractHtmlLang } from "./parseHtml.mjs";
|
|
2
4
|
import { analyzeBundleContent } from "./analyzeBundleContent.mjs";
|
|
3
|
-
import {
|
|
5
|
+
import { getLanguageCode, isRightToLeftLocale, isSameLanguage, isValidLocaleCode, isValidOpenGraphLocale, normalizeLocaleCode, toOpenGraphLocale } from "./detection/localeCode.mjs";
|
|
6
|
+
import { getUrlLocale } from "./detection/detectRoutingStrategy.mjs";
|
|
7
|
+
import { classifyInternalLinks } from "./detection/classifyInternalLinks.mjs";
|
|
8
|
+
import { isPathAllowedByRobots, parseRobots } from "./robots.mjs";
|
|
9
|
+
import { collectSitemapEntries, getDefaultSitemapUrls } from "./sitemap.mjs";
|
|
4
10
|
|
|
5
11
|
//#region src/scan/checks.ts
|
|
12
|
+
/** Maximum number of links listed in a check's details. */
|
|
13
|
+
const MAX_LISTED_LINKS = 30;
|
|
14
|
+
/** Maximum number of hreflang alternates fetched for the reciprocity check. */
|
|
15
|
+
const MAX_FETCHED_ALTERNATES = 8;
|
|
16
|
+
/** Statuses meaning the site refuses automated requests, not a broken page. */
|
|
17
|
+
const BOT_BLOCKING_STATUSES = /* @__PURE__ */ new Set([
|
|
18
|
+
401,
|
|
19
|
+
403,
|
|
20
|
+
429
|
|
21
|
+
]);
|
|
6
22
|
/** Format a byte count as a human-readable size. */
|
|
7
23
|
const formatSize = (bytes) => {
|
|
8
24
|
if (bytes >= 1048576) return `${(bytes / 1048576).toFixed(2)} MB`;
|
|
9
25
|
if (bytes >= 1024) return `${(bytes / 1024).toFixed(2)} KB`;
|
|
10
26
|
return `${bytes} B`;
|
|
11
27
|
};
|
|
28
|
+
/** Build a URL-scoped check type (`url_htmlLang\https://…`). */
|
|
29
|
+
const urlCheckType = (checkName, targetUrl) => `${checkName}\\${targetUrl}`;
|
|
30
|
+
/** Emit an event whose details are attached to its own status. */
|
|
31
|
+
const emitCheck = (emit, type, status, details) => emit({
|
|
32
|
+
type,
|
|
33
|
+
status,
|
|
34
|
+
details: details === void 0 ? void 0 : { [status]: details }
|
|
35
|
+
});
|
|
36
|
+
/** Render an anchor as an HTML line for the details popover. */
|
|
37
|
+
const formatAnchor = ({ href, text }) => `<a href="${href}">${text.slice(0, 60) || "Link"}</a>`;
|
|
38
|
+
/** `url_htmlLang`: `<html lang>` is present and a valid BCP 47 tag. */
|
|
39
|
+
const checkHtmlLang = ({ targetUrl, langTag }, emit) => {
|
|
40
|
+
const type = urlCheckType("url_htmlLang", targetUrl);
|
|
41
|
+
if (!langTag) emitCheck(emit, type, "error", "Missing html lang attribute");
|
|
42
|
+
else if (!isValidLocaleCode(langTag)) emitCheck(emit, type, "error", `"${langTag}" is not a valid BCP 47 language tag (expected e.g. "en" or "en-US")`);
|
|
43
|
+
else emitCheck(emit, type, "success", langTag);
|
|
44
|
+
};
|
|
12
45
|
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
46
|
+
* `url_htmlDir`: the text direction matches the language. `dir` is only
|
|
47
|
+
* required for right-to-left languages, `ltr` being the browser default.
|
|
15
48
|
*/
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
type:
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
status: langTag ? "success" : "warning",
|
|
30
|
-
details: {
|
|
31
|
-
success: langTag,
|
|
32
|
-
warning: langTag ? void 0 : "No locale detected"
|
|
33
|
-
}
|
|
34
|
-
});
|
|
35
|
-
events.push({
|
|
36
|
-
type: `url_htmlDir\\${targetUrl}`,
|
|
37
|
-
status: dirTag ? "success" : "warning",
|
|
38
|
-
details: {
|
|
39
|
-
success: dirTag,
|
|
40
|
-
warning: dirTag ? void 0 : "Missing html dir attribute"
|
|
41
|
-
}
|
|
42
|
-
});
|
|
43
|
-
return { langTag };
|
|
49
|
+
const checkHtmlDir = ({ targetUrl, langTag, dirTag }, emit) => {
|
|
50
|
+
const type = urlCheckType("url_htmlDir", targetUrl);
|
|
51
|
+
const direction = dirTag?.toLowerCase();
|
|
52
|
+
if (!langTag) {
|
|
53
|
+
emitCheck(emit, type, "warning", "Cannot verify the text direction: html lang is missing");
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const expectedDirection = isRightToLeftLocale(langTag) ? "rtl" : "ltr";
|
|
57
|
+
if (direction === expectedDirection || !direction && expectedDirection === "ltr") {
|
|
58
|
+
emitCheck(emit, type, "success", direction ?? "ltr (browser default)");
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
emitCheck(emit, type, "error", direction ? `dir="${direction}" does not match the ${expectedDirection.toUpperCase()} language "${langTag}"` : `Missing dir="rtl" for the right-to-left language "${langTag}"`);
|
|
44
62
|
};
|
|
45
|
-
/**
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
63
|
+
/**
|
|
64
|
+
* `url_currentLocale`: the locale announced by `<html lang>`, the URL and the
|
|
65
|
+
* self hreflang entry agree.
|
|
66
|
+
*/
|
|
67
|
+
const checkLocaleConsistency = ({ targetUrl, langTag, hreflangs, routing }, emit) => {
|
|
68
|
+
const type = urlCheckType("url_currentLocale", targetUrl);
|
|
69
|
+
const normalizedTargetUrl = normalizeUrl(targetUrl);
|
|
70
|
+
const selfHreflang = hreflangs.find(({ hreflang, href }) => hreflang.toLowerCase() !== "x-default" && normalizeUrl(href, targetUrl) === normalizedTargetUrl)?.hreflang;
|
|
71
|
+
const signals = {
|
|
72
|
+
htmlLang: langTag,
|
|
73
|
+
urlLocale: routing.urlLocale,
|
|
74
|
+
selfHreflang
|
|
75
|
+
};
|
|
76
|
+
const definedLocales = Object.values(signals).filter((locale) => Boolean(locale));
|
|
77
|
+
if (definedLocales.length === 0) {
|
|
78
|
+
emitCheck(emit, type, "warning", "No locale detected on the page");
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const firstLocale = definedLocales[0];
|
|
82
|
+
const isConsistent = definedLocales.every((locale) => isSameLanguage(locale, firstLocale));
|
|
83
|
+
emitCheck(emit, type, isConsistent ? "success" : "error", isConsistent ? signals : {
|
|
84
|
+
message: "The page announces different locales in html lang, its URL and its hreflang entry",
|
|
85
|
+
...signals
|
|
52
86
|
});
|
|
53
87
|
};
|
|
88
|
+
/** Describe an invalid Open Graph locale, with the value to use instead. */
|
|
89
|
+
const formatInvalidOpenGraphLocale = (localeCode, langTag, property) => {
|
|
90
|
+
const suggestion = toOpenGraphLocale(localeCode, langTag);
|
|
91
|
+
return `${property} "${localeCode}" is not a valid Open Graph locale (expected language_TERRITORY${suggestion ? `, e.g. "${suggestion}"` : ""}): crawlers ignore it and fall back to en_US`;
|
|
92
|
+
};
|
|
54
93
|
/**
|
|
55
|
-
*
|
|
56
|
-
* `
|
|
94
|
+
* `url_ogLocale`: `og:locale` is set, uses the Open Graph `language_TERRITORY`
|
|
95
|
+
* format (`en_GB`, not `en` nor `en-GB`) and matches `<html lang>`.
|
|
96
|
+
* `og:locale:alternate` values must use the same format.
|
|
57
97
|
*/
|
|
58
|
-
const
|
|
59
|
-
const
|
|
60
|
-
if (
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
type:
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
98
|
+
const checkOgLocale = ({ targetUrl, ogLocale, ogLocaleAlternates = [], langTag }, emit) => {
|
|
99
|
+
const type = urlCheckType("url_ogLocale", targetUrl);
|
|
100
|
+
if (!ogLocale) {
|
|
101
|
+
emitCheck(emit, type, "warning", "Missing <meta property=\"og:locale\">: social previews default to en_US");
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (!isValidOpenGraphLocale(ogLocale)) {
|
|
105
|
+
emitCheck(emit, type, "error", formatInvalidOpenGraphLocale(ogLocale, langTag, "og:locale"));
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (langTag && !isSameLanguage(ogLocale, langTag)) {
|
|
109
|
+
emitCheck(emit, type, "warning", `og:locale "${ogLocale}" does not match html lang "${langTag}"`);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const invalidAlternates = ogLocaleAlternates.filter((alternate) => !isValidOpenGraphLocale(alternate));
|
|
113
|
+
if (invalidAlternates.length > 0) {
|
|
114
|
+
emitCheck(emit, type, "warning", invalidAlternates.map((alternate) => formatInvalidOpenGraphLocale(alternate, void 0, "og:locale:alternate")).join("\n"));
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
emitCheck(emit, type, "success", ogLocale);
|
|
118
|
+
};
|
|
119
|
+
/**
|
|
120
|
+
* `url_hasCanonical`: a canonical link exists and does not point to another
|
|
121
|
+
* locale version (which would de-index the current one).
|
|
122
|
+
*/
|
|
123
|
+
const checkCanonical = ({ targetUrl, canonicalHref, hreflangs, routing, pageLocale }, emit) => {
|
|
124
|
+
const type = urlCheckType("url_hasCanonical", targetUrl);
|
|
125
|
+
if (canonicalHref === void 0 || canonicalHref === "") {
|
|
126
|
+
emitCheck(emit, type, "warning", "Missing canonical link");
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const canonicalUrl = normalizeUrl(canonicalHref, targetUrl);
|
|
130
|
+
const otherLocaleAlternate = hreflangs.find(({ hreflang, href }) => hreflang.toLowerCase() !== "x-default" && normalizeUrl(href, targetUrl) === canonicalUrl && !isSameLanguage(hreflang, pageLocale));
|
|
131
|
+
const canonicalLocale = getUrlLocale(canonicalUrl, routing);
|
|
132
|
+
if (otherLocaleAlternate || canonicalLocale && pageLocale && !isSameLanguage(canonicalLocale, pageLocale)) {
|
|
133
|
+
emitCheck(emit, type, "error", {
|
|
134
|
+
message: `The canonical URL points to the "${otherLocaleAlternate?.hreflang ?? canonicalLocale}" version instead of this "${pageLocale}" page: search engines will drop this page from their index`,
|
|
135
|
+
canonical: canonicalUrl
|
|
136
|
+
});
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
emitCheck(emit, type, "success", canonicalUrl === normalizeUrl(targetUrl) ? canonicalUrl : {
|
|
140
|
+
message: "Canonical points to another URL of the same locale",
|
|
141
|
+
canonical: canonicalUrl
|
|
76
142
|
});
|
|
77
143
|
};
|
|
78
|
-
const normalizeHost = (host) => host.startsWith("www.") ? host.slice(4) : host;
|
|
79
|
-
const localeValues = Object.values(ALL_LOCALES);
|
|
80
144
|
/**
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
* live DOM.
|
|
145
|
+
* `url_hreflang`: hreflang tags exist, use valid codes and absolute URLs, have
|
|
146
|
+
* no duplicates, and include a self reference.
|
|
84
147
|
*/
|
|
85
|
-
const
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
for (const { href } of anchors) {
|
|
91
|
-
if (!href || href.startsWith("#") || href.startsWith("javascript:")) continue;
|
|
92
|
-
try {
|
|
93
|
-
const url = new URL(href, origin);
|
|
94
|
-
if (normalizeHost(url.hostname) !== targetHostname) continue;
|
|
95
|
-
totalInternalCount++;
|
|
96
|
-
const path = url.pathname.toLowerCase();
|
|
97
|
-
const hostname = url.hostname.toLowerCase();
|
|
98
|
-
const hasLocaleInPath = localeValues.some((locale) => {
|
|
99
|
-
const l = locale.toLowerCase();
|
|
100
|
-
return path === `/${l}` || path.includes(`/${l}/`);
|
|
101
|
-
});
|
|
102
|
-
const hasLocaleInSubdomain = localeValues.some((locale) => {
|
|
103
|
-
const l = locale.toLowerCase();
|
|
104
|
-
return hostname.startsWith(`${l}.`) || hostname.includes(`.${l}.`);
|
|
105
|
-
});
|
|
106
|
-
if (hasLocaleInPath || hasLocaleInSubdomain) localizedCount++;
|
|
107
|
-
else nonLocalizedLinks.push(href);
|
|
108
|
-
} catch {}
|
|
148
|
+
const checkHreflang = ({ targetUrl, hreflangs, canonicalHref }, emit) => {
|
|
149
|
+
const type = urlCheckType("url_hreflang", targetUrl);
|
|
150
|
+
if (hreflangs.length === 0) {
|
|
151
|
+
emitCheck(emit, type, "warning", "No hreflang tags found");
|
|
152
|
+
return;
|
|
109
153
|
}
|
|
110
|
-
const
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
154
|
+
const issues = [];
|
|
155
|
+
const invalidCodes = hreflangs.map(({ hreflang }) => hreflang).filter((hreflang) => !isValidLocaleCode(hreflang));
|
|
156
|
+
if (invalidCodes.length > 0) issues.push(`Invalid hreflang codes: ${invalidCodes.join(", ")}`);
|
|
157
|
+
const ukCodes = hreflangs.map(({ hreflang }) => hreflang).filter((hreflang) => /-uk$/i.test(hreflang));
|
|
158
|
+
if (ukCodes.length > 0) issues.push(`"${ukCodes.join(", ")}": the United Kingdom region code is "GB" (e.g. en-GB)`);
|
|
159
|
+
const relativeHrefs = hreflangs.filter(({ href }) => !isAbsoluteUrl(href));
|
|
160
|
+
if (relativeHrefs.length > 0) issues.push(`hreflang URLs must be absolute (https://…), search engines ignore: ${relativeHrefs.map(({ hreflang, href }) => `${hreflang} → ${href}`).join(", ")}`);
|
|
161
|
+
const urlsByCode = /* @__PURE__ */ new Map();
|
|
162
|
+
for (const { hreflang, href } of hreflangs) {
|
|
163
|
+
const code = normalizeLocaleCode(hreflang);
|
|
164
|
+
const urls = urlsByCode.get(code) ?? /* @__PURE__ */ new Set();
|
|
165
|
+
urls.add(normalizeUrl(href, targetUrl));
|
|
166
|
+
urlsByCode.set(code, urls);
|
|
167
|
+
}
|
|
168
|
+
const conflictingCodes = [...urlsByCode.entries()].filter(([, urls]) => urls.size > 1).map(([code]) => code);
|
|
169
|
+
if (conflictingCodes.length > 0) issues.push(`Same hreflang declared with different URLs: ${conflictingCodes.join(", ")}`);
|
|
170
|
+
const selfUrls = /* @__PURE__ */ new Set([normalizeUrl(targetUrl)]);
|
|
171
|
+
if (canonicalHref) selfUrls.add(normalizeUrl(canonicalHref, targetUrl));
|
|
172
|
+
if (!hreflangs.some(({ hreflang, href }) => hreflang.toLowerCase() !== "x-default" && selfUrls.has(normalizeUrl(href, targetUrl)))) issues.push("No hreflang entry points to the page itself (self reference)");
|
|
173
|
+
const status = relativeHrefs.length > 0 ? "error" : issues.length > 0 ? "warning" : "success";
|
|
174
|
+
emitCheck(emit, type, status, issues.length > 0 ? {
|
|
175
|
+
issues,
|
|
176
|
+
hreflangs
|
|
177
|
+
} : hreflangs);
|
|
178
|
+
};
|
|
179
|
+
/** `url_hasXDefault`: an `x-default` hreflang is declared. */
|
|
180
|
+
const checkXDefault = ({ targetUrl, hreflangs }, emit) => {
|
|
181
|
+
const type = urlCheckType("url_hasXDefault", targetUrl);
|
|
182
|
+
const xDefault = hreflangs.find(({ hreflang }) => hreflang.toLowerCase() === "x-default");
|
|
183
|
+
if (xDefault) emitCheck(emit, type, "success", xDefault.href);
|
|
184
|
+
else if (hreflangs.length === 0) emitCheck(emit, type, "warning", "No hreflang tags, so no x-default either");
|
|
185
|
+
else emitCheck(emit, type, "error", "Missing x-default hreflang link");
|
|
186
|
+
};
|
|
187
|
+
/** Fetch an alternate and check it forms a valid hreflang pair with the page. */
|
|
188
|
+
const verifyAlternate = async (alternateUrl, hreflang, selfUrls, fetchOptions) => {
|
|
189
|
+
const response = await fetchText(alternateUrl, fetchOptions);
|
|
190
|
+
const verification = {
|
|
191
|
+
url: alternateUrl,
|
|
192
|
+
hreflang
|
|
193
|
+
};
|
|
194
|
+
if (!response || BOT_BLOCKING_STATUSES.has(response.status)) return {
|
|
195
|
+
...verification,
|
|
196
|
+
outcome: "unverifiable",
|
|
197
|
+
issues: []
|
|
198
|
+
};
|
|
199
|
+
if (!response.ok) return {
|
|
200
|
+
...verification,
|
|
201
|
+
outcome: "broken",
|
|
202
|
+
issues: [`HTTP ${response.status}`]
|
|
203
|
+
};
|
|
204
|
+
const issues = [];
|
|
205
|
+
if (response.redirected) issues.push(`redirects to ${response.finalUrl}`);
|
|
206
|
+
if (!extractHreflangs(response.text).some(({ href }) => selfUrls.has(normalizeUrl(href, response.finalUrl)))) issues.push("no hreflang link back to this page");
|
|
207
|
+
const alternateLang = extractHtmlLang(response.text);
|
|
208
|
+
if (alternateLang && !isSameLanguage(alternateLang, hreflang)) issues.push(`html lang is "${alternateLang}"`);
|
|
209
|
+
return {
|
|
210
|
+
...verification,
|
|
211
|
+
outcome: issues.length > 0 ? "invalid" : "valid",
|
|
212
|
+
issues
|
|
213
|
+
};
|
|
214
|
+
};
|
|
215
|
+
/**
|
|
216
|
+
* `url_hreflangReciprocal`: every alternate answers with a 200, is not
|
|
217
|
+
* redirected, links back to this page and declares the expected language.
|
|
218
|
+
* Search engines ignore hreflang pairs without a return link.
|
|
219
|
+
*/
|
|
220
|
+
const checkHreflangReciprocity = async ({ targetUrl, hreflangs, canonicalHref }, fetchOptions, emit) => {
|
|
221
|
+
const type = urlCheckType("url_hreflangReciprocal", targetUrl);
|
|
222
|
+
const selfUrls = /* @__PURE__ */ new Set([normalizeUrl(targetUrl)]);
|
|
223
|
+
if (canonicalHref) selfUrls.add(normalizeUrl(canonicalHref, targetUrl));
|
|
224
|
+
const alternatesByUrl = /* @__PURE__ */ new Map();
|
|
225
|
+
for (const { hreflang, href } of hreflangs) {
|
|
226
|
+
if (hreflang.toLowerCase() === "x-default") continue;
|
|
227
|
+
const absoluteUrl = parseUrl(href, targetUrl)?.href;
|
|
228
|
+
if (!absoluteUrl || selfUrls.has(normalizeUrl(absoluteUrl))) continue;
|
|
229
|
+
if (!alternatesByUrl.has(absoluteUrl)) alternatesByUrl.set(absoluteUrl, hreflang);
|
|
230
|
+
}
|
|
231
|
+
if (alternatesByUrl.size === 0) return;
|
|
232
|
+
const verifications = await Promise.all([...alternatesByUrl.entries()].slice(0, MAX_FETCHED_ALTERNATES).map(([alternateUrl, hreflang]) => verifyAlternate(alternateUrl, hreflang, selfUrls, fetchOptions)));
|
|
233
|
+
const unverifiableUrls = verifications.filter(({ outcome }) => outcome === "unverifiable").map(({ url }) => url);
|
|
234
|
+
const failingVerifications = verifications.filter(({ outcome }) => outcome === "broken" || outcome === "invalid");
|
|
235
|
+
const verifiedCount = verifications.length - unverifiableUrls.length;
|
|
236
|
+
if (verifiedCount === 0) {
|
|
237
|
+
emitCheck(emit, type, "warning", {
|
|
238
|
+
message: "The alternates could not be verified: they are unreachable or block automated requests",
|
|
239
|
+
urls: unverifiableUrls
|
|
240
|
+
});
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
if (failingVerifications.length === 0) {
|
|
244
|
+
emitCheck(emit, type, "success", `${verifiedCount} alternates checked, all reciprocal`);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
emitCheck(emit, type, failingVerifications.some(({ outcome }) => outcome === "broken") ? "error" : "warning", {
|
|
248
|
+
message: `${failingVerifications.length} of ${verifiedCount} checked hreflang alternates are not valid return pairs`,
|
|
249
|
+
alternates: failingVerifications.map(({ hreflang, url, issues }) => `${hreflang} ${url}: ${issues.join(", ")}`),
|
|
250
|
+
unverifiableUrls
|
|
119
251
|
});
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
252
|
+
};
|
|
253
|
+
/**
|
|
254
|
+
* Link checks, interpreted through the detected routing strategy (so an
|
|
255
|
+
* unprefixed link is correct on the default locale of a `prefix-no-default`
|
|
256
|
+
* site, and `?lang=` links are understood on `search-params` sites):
|
|
257
|
+
*
|
|
258
|
+
* - `url_hasLocalizedLinks`: internal links keep the page locale
|
|
259
|
+
* - `url_allAnchorsLocalized`: no internal link loses or switches the locale
|
|
260
|
+
* - `url_hasLangSelector`: crawlable links to the other locale versions exist
|
|
261
|
+
*/
|
|
262
|
+
const checkInternalLinks = (signals, emit) => {
|
|
263
|
+
const { targetUrl, routing, pageLocale } = signals;
|
|
264
|
+
const { internalLinks, sameLocaleLinks, otherLocaleLinks, unlocalizedLinks, switcherLinks } = classifyInternalLinks(signals);
|
|
265
|
+
const listLinks = (links) => links.slice(0, MAX_LISTED_LINKS).map(formatAnchor);
|
|
266
|
+
const localizedLinksType = urlCheckType("url_hasLocalizedLinks", targetUrl);
|
|
267
|
+
const isLocaleInUrl = routing.strategy !== "no-prefix" && routing.strategy !== "unknown";
|
|
268
|
+
if (internalLinks.length === 0) emitCheck(emit, localizedLinksType, "warning", {
|
|
269
|
+
message: "No internal links found on the page",
|
|
270
|
+
links: listLinks(switcherLinks)
|
|
271
|
+
});
|
|
272
|
+
else if (!isLocaleInUrl || !pageLocale) emitCheck(emit, localizedLinksType, "warning", {
|
|
273
|
+
message: `The locale is not reflected in the URLs (routing: ${routing.strategy}). Each locale needs its own crawlable URL to be indexed.`,
|
|
274
|
+
links: listLinks(internalLinks)
|
|
275
|
+
});
|
|
276
|
+
else if (sameLocaleLinks.length === 0) emitCheck(emit, localizedLinksType, "warning", {
|
|
277
|
+
message: `None of the ${internalLinks.length} internal links points to the "${pageLocale}" version (routing: ${routing.strategy})`,
|
|
278
|
+
links: listLinks(internalLinks)
|
|
279
|
+
});
|
|
280
|
+
else emitCheck(emit, localizedLinksType, "success", {
|
|
281
|
+
message: `${sameLocaleLinks.length} of ${internalLinks.length} internal links point to the "${pageLocale}" version (routing: ${routing.strategy})`,
|
|
282
|
+
links: listLinks(sameLocaleLinks)
|
|
127
283
|
});
|
|
284
|
+
if (isLocaleInUrl && pageLocale && internalLinks.length > 0) {
|
|
285
|
+
const brokenLinks = [...otherLocaleLinks, ...unlocalizedLinks];
|
|
286
|
+
emitCheck(emit, urlCheckType("url_allAnchorsLocalized", targetUrl), brokenLinks.length === 0 ? "success" : "warning", brokenLinks.length === 0 ? `All ${internalLinks.length} internal links keep the "${pageLocale}" locale` : {
|
|
287
|
+
message: `${brokenLinks.length} internal links leave the "${pageLocale}" locale (${otherLocaleLinks.length} to another locale, ${unlocalizedLinks.length} without locale)`,
|
|
288
|
+
links: listLinks(brokenLinks)
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
if (routing.locales.length > 1) emitCheck(emit, urlCheckType("url_hasLangSelector", targetUrl), switcherLinks.length > 0 ? "success" : "warning", switcherLinks.length > 0 ? {
|
|
292
|
+
message: `${switcherLinks.length} crawlable links to other locale versions`,
|
|
293
|
+
links: listLinks(switcherLinks)
|
|
294
|
+
} : "No crawlable <a href> to the other locale versions: search engines only discover them through hreflang");
|
|
128
295
|
};
|
|
296
|
+
const getFilename = (url) => url.split("/").pop()?.split("?")[0] ?? url;
|
|
297
|
+
const formatChunk = (chunk) => ({
|
|
298
|
+
filename: getFilename(chunk.url),
|
|
299
|
+
url: chunk.url,
|
|
300
|
+
fileSize: formatSize(chunk.fileSize),
|
|
301
|
+
totalLocaleSize: formatSize(chunk.totalLocaleSize),
|
|
302
|
+
usedLocaleSize: formatSize(chunk.usedLocaleSize),
|
|
303
|
+
unusedLocaleSize: formatSize(chunk.unusedLocaleSize),
|
|
304
|
+
dictionariesFound: chunk.dictionariesFound,
|
|
305
|
+
unusedPercent: `${chunk.unusedPercent}%`
|
|
306
|
+
});
|
|
307
|
+
/** Human-readable version of a {@link BundleContentAnalysis}, for display. */
|
|
308
|
+
const formatBundleAnalysis = (analysis) => ({
|
|
309
|
+
currentLocale: analysis.currentLocale,
|
|
310
|
+
totalPageSize: formatSize(analysis.totalPageSize),
|
|
311
|
+
renderedContentSize: formatSize(analysis.renderedContentSize),
|
|
312
|
+
contentSize: formatSize(analysis.contentSize),
|
|
313
|
+
totalLocaleSize: formatSize(analysis.totalLocaleSize),
|
|
314
|
+
totalUnusedLocaleSize: formatSize(analysis.totalUnusedLocaleSize),
|
|
315
|
+
unusedPercentOfLocale: `${analysis.unusedPercentOfLocale}%`,
|
|
316
|
+
mainBundleChunks: analysis.mainBundleChunks.map(formatChunk),
|
|
317
|
+
lazyBundleChunks: analysis.lazyBundleChunks.map(formatChunk)
|
|
318
|
+
});
|
|
129
319
|
/**
|
|
130
|
-
*
|
|
131
|
-
*
|
|
320
|
+
* `url_unusedBundleContent`: share of other-locale translations shipped in the
|
|
321
|
+
* eagerly loaded bundles. Returns the raw analysis for reporting.
|
|
132
322
|
*/
|
|
133
|
-
const checkBundleContent = (chunks, html, currentLocale, targetUrl, totalPageSize,
|
|
323
|
+
const checkBundleContent = (chunks, html, currentLocale, targetUrl, totalPageSize, emit) => {
|
|
324
|
+
const type = urlCheckType("url_unusedBundleContent", targetUrl);
|
|
134
325
|
if (!currentLocale) {
|
|
135
|
-
|
|
136
|
-
type: `url_unusedBundleContent\\${targetUrl}`,
|
|
137
|
-
status: "warning",
|
|
138
|
-
details: { warning: "Cannot analyse bundle content: page locale not detected" }
|
|
139
|
-
});
|
|
326
|
+
emitCheck(emit, type, "warning", "Cannot analyse bundle content: page locale not detected");
|
|
140
327
|
return;
|
|
141
328
|
}
|
|
142
329
|
const analysis = analyzeBundleContent(chunks, html, currentLocale, totalPageSize);
|
|
143
|
-
const mainBundleMaxUnused = analysis.mainBundleChunks.reduce((max,
|
|
144
|
-
|
|
145
|
-
events.push({
|
|
146
|
-
type: `url_unusedBundleContent\\${targetUrl}`,
|
|
147
|
-
status,
|
|
148
|
-
details: { [status]: analysis }
|
|
149
|
-
});
|
|
330
|
+
const mainBundleMaxUnused = analysis.mainBundleChunks.reduce((max, chunk) => Math.max(max, chunk.unusedPercent), 0);
|
|
331
|
+
emitCheck(emit, type, mainBundleMaxUnused === 0 ? "success" : mainBundleMaxUnused <= 30 ? "warning" : "error", formatBundleAnalysis(analysis));
|
|
150
332
|
return analysis;
|
|
151
333
|
};
|
|
152
|
-
/**
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
} catch (error) {
|
|
171
|
-
errors.push(`Failed to fetch robots.txt: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
334
|
+
/**
|
|
335
|
+
* Fetch `robots.txt` and check that neither the site nor its localized URLs
|
|
336
|
+
* are blocked for Googlebot.
|
|
337
|
+
*
|
|
338
|
+
* - `robots_robotsPresent`
|
|
339
|
+
* - `robots_noLocalizedUrlsForgotten`
|
|
340
|
+
*
|
|
341
|
+
* @param localizedUrls - URLs of every locale version (hreflang alternates).
|
|
342
|
+
* @returns The sitemaps declared in `robots.txt`.
|
|
343
|
+
*/
|
|
344
|
+
const checkRobots = async (origin, localizedUrls, fetchOptions, emit) => {
|
|
345
|
+
const response = await fetchText(`${origin}/robots.txt`, fetchOptions);
|
|
346
|
+
if (!response?.ok) {
|
|
347
|
+
emitCheck(emit, "robots_robotsPresent", "warning", response ? `No robots.txt found (HTTP ${response.status})` : "robots.txt is unreachable");
|
|
348
|
+
return [];
|
|
172
349
|
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
warning: robotsPresent ? void 0 : "No robots.txt found",
|
|
178
|
-
error: errors.length > 0 ? errors : void 0
|
|
179
|
-
}
|
|
350
|
+
const robots = parseRobots(response.text);
|
|
351
|
+
emitCheck(emit, "robots_robotsPresent", "success", {
|
|
352
|
+
disallow: robots.disallowedPaths,
|
|
353
|
+
sitemaps: robots.sitemapUrls
|
|
180
354
|
});
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
details: { error: noLocalizedUrlsForgotten ? void 0 : errors }
|
|
355
|
+
const blockedUrls = [origin, ...localizedUrls].filter((url) => {
|
|
356
|
+
const parsedUrl = parseUrl(url);
|
|
357
|
+
return parsedUrl?.origin === origin && !isPathAllowedByRobots(`${parsedUrl.pathname}${parsedUrl.search}`, robots);
|
|
185
358
|
});
|
|
359
|
+
const isSiteBlocked = !isPathAllowedByRobots("/", robots);
|
|
360
|
+
emitCheck(emit, "robots_noLocalizedUrlsForgotten", blockedUrls.length === 0 ? "success" : "error", blockedUrls.length === 0 ? true : isSiteBlocked ? "robots.txt blocks the whole site for Googlebot (Disallow: /)" : {
|
|
361
|
+
message: "Localized URLs blocked by robots.txt",
|
|
362
|
+
urls: [...new Set(blockedUrls)]
|
|
363
|
+
});
|
|
364
|
+
return robots.sitemapUrls;
|
|
186
365
|
};
|
|
187
|
-
/**
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
366
|
+
/**
|
|
367
|
+
* Read the sitemaps (declared in `robots.txt`, else the default locations,
|
|
368
|
+
* following sitemap indexes) and check their locale coverage.
|
|
369
|
+
*
|
|
370
|
+
* - `sitemap_sitemapPresent`
|
|
371
|
+
* - `sitemap_noLocalizedUrlsForgotten`: every locale is listed, and entries
|
|
372
|
+
* declaring alternates declare all of them
|
|
373
|
+
* - `sitemap_hasAlternates` / `sitemap_hasXDefault`
|
|
374
|
+
*/
|
|
375
|
+
const checkSitemap = async (origin, declaredSitemapUrls, routing, fetchOptions, emit) => {
|
|
376
|
+
const { fetchedSitemapUrls, entries, isTruncated } = await collectSitemapEntries(declaredSitemapUrls.length > 0 ? declaredSitemapUrls : getDefaultSitemapUrls(origin), fetchOptions);
|
|
377
|
+
if (fetchedSitemapUrls.length === 0) {
|
|
378
|
+
emitCheck(emit, "sitemap_sitemapPresent", "warning", declaredSitemapUrls.length > 0 ? `Sitemaps declared in robots.txt are unreachable: ${declaredSitemapUrls.join(", ")}` : "No sitemap found (robots.txt Sitemap directive, /sitemap.xml, /sitemap_index.xml)");
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
emitCheck(emit, "sitemap_sitemapPresent", "success", {
|
|
382
|
+
sitemaps: fetchedSitemapUrls,
|
|
383
|
+
urlCount: entries.length,
|
|
384
|
+
isTruncated
|
|
385
|
+
});
|
|
386
|
+
const knownLocales = routing.locales;
|
|
387
|
+
const foundLanguages = /* @__PURE__ */ new Set();
|
|
388
|
+
const entriesWithoutSelfReference = [];
|
|
389
|
+
const relativeAlternates = [];
|
|
390
|
+
let partiallyTranslatedCount = 0;
|
|
191
391
|
let hasAlternates = false;
|
|
192
|
-
let
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
if (
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
const allFoundLocales = /* @__PURE__ */ new Set();
|
|
205
|
-
let anyUrlMissingLocale = false;
|
|
206
|
-
for (const block of urlBlocks) {
|
|
207
|
-
const localesInUrl = /* @__PURE__ */ new Set();
|
|
208
|
-
for (const hreflang of block.match(/hreflang\s*=\s*"([^"]+)"/gi) ?? []) {
|
|
209
|
-
const value = hreflang.replace(/hreflang\s*=\s*"/i, "").replace(/"$/, "");
|
|
210
|
-
if (value !== "x-default") {
|
|
211
|
-
localesInUrl.add(value);
|
|
212
|
-
allFoundLocales.add(value);
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
const loc = block.match(/<loc>([\s\S]*?)<\/loc>/i)?.[1]?.trim();
|
|
216
|
-
if (loc) try {
|
|
217
|
-
const firstSegment = new URL(loc).pathname.split("/").filter(Boolean)[0];
|
|
218
|
-
if (firstSegment && discoveredLocales.has(firstSegment)) {
|
|
219
|
-
localesInUrl.add(firstSegment);
|
|
220
|
-
allFoundLocales.add(firstSegment);
|
|
221
|
-
}
|
|
222
|
-
} catch {}
|
|
223
|
-
const missing = [...discoveredLocales].filter((locale) => !localesInUrl.has(locale));
|
|
224
|
-
if (missing.length > 0 && missing.length < discoveredLocales.size) anyUrlMissingLocale = true;
|
|
225
|
-
}
|
|
226
|
-
const completelyMissing = [...discoveredLocales].filter((locale) => !allFoundLocales.has(locale));
|
|
227
|
-
if (anyUrlMissingLocale || completelyMissing.length > 0) {
|
|
228
|
-
noLocalizedUrlsForgotten = false;
|
|
229
|
-
if (completelyMissing.length > 0) errors.push(`The following locales are completely missing from the sitemap: ${completelyMissing.join(", ")}`);
|
|
230
|
-
}
|
|
392
|
+
let hasXDefault = false;
|
|
393
|
+
for (const { loc, alternates } of entries) {
|
|
394
|
+
const locLocale = getUrlLocale(loc, routing);
|
|
395
|
+
if (locLocale) foundLanguages.add(getLanguageCode(locLocale));
|
|
396
|
+
if (alternates.length === 0) continue;
|
|
397
|
+
hasAlternates = true;
|
|
398
|
+
const entryLanguages = /* @__PURE__ */ new Set();
|
|
399
|
+
for (const { hreflang, href } of alternates) {
|
|
400
|
+
if (!isAbsoluteUrl(href)) relativeAlternates.push(`${loc}: ${hreflang} → ${href}`);
|
|
401
|
+
if (hreflang.toLowerCase() === "x-default") {
|
|
402
|
+
hasXDefault = true;
|
|
403
|
+
continue;
|
|
231
404
|
}
|
|
405
|
+
entryLanguages.add(getLanguageCode(hreflang));
|
|
406
|
+
foundLanguages.add(getLanguageCode(hreflang));
|
|
232
407
|
}
|
|
233
|
-
|
|
234
|
-
|
|
408
|
+
const normalizedLoc = normalizeUrl(loc);
|
|
409
|
+
if (!alternates.some(({ href }) => normalizeUrl(href) === normalizedLoc)) entriesWithoutSelfReference.push(loc);
|
|
410
|
+
if (entryLanguages.size < knownLocales.length) partiallyTranslatedCount++;
|
|
235
411
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
if (sitemapPresent) {
|
|
245
|
-
events.push({
|
|
246
|
-
type: "sitemap_noLocalizedUrlsForgotten",
|
|
247
|
-
status: noLocalizedUrlsForgotten ? "success" : "warning",
|
|
248
|
-
details: { warning: noLocalizedUrlsForgotten ? void 0 : errors }
|
|
249
|
-
});
|
|
250
|
-
events.push({
|
|
251
|
-
type: "sitemap_hasXDefault",
|
|
252
|
-
status: hasXDefault ? "success" : "warning",
|
|
253
|
-
details: { warning: hasXDefault ? void 0 : "No x-default hreflang in sitemap" }
|
|
254
|
-
});
|
|
255
|
-
events.push({
|
|
256
|
-
type: "sitemap_hasAlternates",
|
|
257
|
-
status: hasAlternates ? "success" : "warning",
|
|
258
|
-
details: { warning: hasAlternates ? void 0 : "No alternate language links found in sitemap" }
|
|
259
|
-
});
|
|
412
|
+
const isSingleOriginRouting = routing.strategy !== "subdomain" && routing.strategy !== "domain";
|
|
413
|
+
if (knownLocales.length > 1) {
|
|
414
|
+
const missingLocales = isSingleOriginRouting ? knownLocales.filter((locale) => !foundLanguages.has(getLanguageCode(locale))) : [];
|
|
415
|
+
const issues = [...missingLocales.length > 0 ? [`Locales missing from the sitemap: ${missingLocales.join(", ")}`] : [], ...entriesWithoutSelfReference.slice(0, MAX_LISTED_LINKS).map((loc) => `${loc} is not listed among its own alternates`)];
|
|
416
|
+
emitCheck(emit, "sitemap_noLocalizedUrlsForgotten", issues.length === 0 ? "success" : "warning", issues.length === 0 ? {
|
|
417
|
+
message: `Every locale (${knownLocales.join(", ")}) is listed`,
|
|
418
|
+
partiallyTranslatedUrlCount: partiallyTranslatedCount
|
|
419
|
+
} : issues);
|
|
260
420
|
}
|
|
421
|
+
if (!hasAlternates) emitCheck(emit, "sitemap_hasAlternates", "warning", "No alternate language links found in the sitemap");
|
|
422
|
+
else if (relativeAlternates.length > 0) emitCheck(emit, "sitemap_hasAlternates", "error", {
|
|
423
|
+
message: `${relativeAlternates.length} sitemap hreflang alternates are not absolute URLs (https://…): search engines ignore them`,
|
|
424
|
+
alternates: relativeAlternates.slice(0, MAX_LISTED_LINKS)
|
|
425
|
+
});
|
|
426
|
+
else emitCheck(emit, "sitemap_hasAlternates", "success", true);
|
|
427
|
+
emitCheck(emit, "sitemap_hasXDefault", hasXDefault ? "success" : "warning", hasXDefault ? true : "No x-default hreflang in the sitemap");
|
|
261
428
|
};
|
|
262
429
|
|
|
263
430
|
//#endregion
|
|
264
|
-
export { checkBundleContent, checkCanonical,
|
|
431
|
+
export { checkBundleContent, checkCanonical, checkHreflang, checkHreflangReciprocity, checkHtmlDir, checkHtmlLang, checkInternalLinks, checkLocaleConsistency, checkOgLocale, checkRobots, checkSitemap, checkXDefault, formatBundleAnalysis, formatSize };
|
|
265
432
|
//# sourceMappingURL=checks.mjs.map
|