gscdump 1.5.0 → 1.5.3
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/index.d.mts +2 -2
- package/dist/index.mjs +1 -1
- package/dist/sitemap-compat.d.mts +8 -0
- package/dist/sitemap-compat.mjs +7 -0
- package/dist/sitemap.d.mts +170 -17
- package/dist/sitemap.mjs +663 -63
- package/package.json +8 -2
package/dist/index.d.mts
CHANGED
|
@@ -18,7 +18,7 @@ import { hasGscReadScope, hasGscWriteScope, hasIndexingScope } from "./core/scop
|
|
|
18
18
|
import { ParsedGscSiteUrl, normalizeGscSiteUrl, normalizeRegistrationTarget, parseGscSiteUrl } from "./core/site-url.mjs";
|
|
19
19
|
import { normalizeUrl } from "./normalize.mjs";
|
|
20
20
|
import { AccountNextAction, AccountStatus, AnalyticsNextAction, AnalyticsStatus, GSCDUMP_ONBOARDING_CONTRACT_VERSION, GSCDUMP_OPTIONAL_INDEXING_SCOPE, GSCDUMP_REQUIRED_ANALYTICS_SCOPE, GSCDUMP_WRITE_ANALYTICS_SCOPE, IndexingNextAction, IndexingStatus, LifecycleError, LifecycleErrorCode, LifecycleProgress, LifecycleWebhookEnvelope, LifecycleWebhookEvent, PartnerLifecycleAccount, PartnerLifecycleResponse, PartnerLifecycleSite, PropertyNextAction, PropertyStatus, QuerySourceMode, SitemapNextAction, SitemapStatus, accountNextActions, accountStatuses, analyticsNextActions, analyticsStatuses, hasOptionalIndexingScope, hasRequiredAnalyticsScope, indexingNextActions, indexingStatuses, lifecycleErrorCodes, parseGrantedScopes, propertyNextActions, propertyStatuses, querySourceModes, sitemapNextActions, sitemapStatuses } from "./onboarding.mjs";
|
|
21
|
-
import {
|
|
21
|
+
import { discoverSitemap, fetchSitemapUrls } from "./sitemap-compat.mjs";
|
|
22
22
|
import { decodeSiteId, encodeSiteId, normalizeSiteUrl } from "./tenant.mjs";
|
|
23
23
|
import { urlMatchKey } from "./url.mjs";
|
|
24
|
-
export { type AccountNextAction, type AccountStatus, AmpInspectionResult, AmpIssue, type AnalyticsNextAction, type AnalyticsStatus, ApiSite, ApiSitemap, ApiSitemapContent, Auth, AuthClient, AuthOptions, type BackfillProgress, BatchInspectUrlsFlatSettledOptions, CallOptions, CanonicalDifferenceKind, DAYS_PER_RANGE, DEFAULT_GSC_REQUEST_TIMEOUT_MS, DataRow, DimensionFilter, DimensionFilterGroup,
|
|
24
|
+
export { type AccountNextAction, type AccountStatus, AmpInspectionResult, AmpIssue, type AnalyticsNextAction, type AnalyticsStatus, ApiSite, ApiSitemap, ApiSitemapContent, Auth, AuthClient, AuthOptions, type BackfillProgress, BatchInspectUrlsFlatSettledOptions, CallOptions, CanonicalDifferenceKind, DAYS_PER_RANGE, DEFAULT_GSC_REQUEST_TIMEOUT_MS, DataRow, DimensionFilter, DimensionFilterGroup, Err, FetchSitesWithSitemapsOptions, GSCDUMP_ONBOARDING_CONTRACT_VERSION, GSCDUMP_OPTIONAL_INDEXING_SCOPE, GSCDUMP_REQUIRED_ANALYTICS_SCOPE, GSCDUMP_WRITE_ANALYTICS_SCOPE, GSC_FINALIZED_LAG_DAYS, GSC_FRESHEST_LAG_DAYS, GSC_INDEXING_SCOPE, GSC_READ_SCOPE, GSC_RETENTION_MONTHS, GSC_SITE_VERIFICATION_SCOPE, GSC_WRITE_SCOPE, GoogleSearchConsoleClient, GoogleSearchConsoleClientOptions, GscApiError, type GscApiErrorInfo, type GscError, type GscErrorKind, GscPropertyCandidate, INDEXING_ISSUE_FILTERS, INDEXING_ISSUE_LABELS, INDEXING_ISSUE_SEVERITY, IndexStatusResult, IndexingEligibility, IndexingIneligibleReason, IndexingIssueType, IndexingMetadata, type IndexingNextAction, IndexingNotificationType, IndexingResult, type IndexingStatus, InspectUrlFlatSettledResult, InspectUrlIndexResponse, InspectUrlResult, InspectionPriority, KNOWN_COVERAGE_STATES, KNOWN_PAGE_FETCH_STATES, LegacyInspectionPriority, type LifecycleError, type LifecycleErrorCode, type LifecycleProgress, type LifecycleWebhookEnvelope, type LifecycleWebhookEvent, MAX_FLAT_INSPECTION_BATCH_CONCURRENCY, MS_PER_DAY, MobileUsabilityIssue, MobileUsabilityResult, OAuthTokenInfo, OAuthTokens, Ok, ParsedGscSiteUrl, ParsedIndexingResult, type PartnerLifecycleAccount, type PartnerLifecycleResponse, type PartnerLifecycleSite, Period, type PropertyNextAction, type PropertyStatus, PublishUrlNotificationResponse, QueryReturn, type QuerySourceMode, RequiredNonNullable, ResolvedAnalyticsRange, Result, RichResultsDetectedItem, RichResultsIssue, RichResultsItem, RichResultsResult, SearchAnalyticsMetadata, SearchAnalyticsQuery, SearchAnalyticsResponse, Site, SiteAnalytics, type SitemapNextAction, type SitemapStatus, URL_INSPECTION_EFFECTIVE_LIMIT, UnmappedInspectionReasons, UrlInspectionResult, UrlNotification, UrlNotificationMetadata, ValueWeightedInspectionPriority, VerificationMethod, VerificationSite, VerificationSiteType, VerificationToken, VerificationWebResource, accountNextActions, accountStatuses, addDays, addSite, analyticsNextActions, analyticsStatuses, batchInspectUrls, batchInspectUrlsFlatSettled, batchRequestIndexing, canUseUrlInspection, classifyCanonicalDifference, classifyError, countDays, createAuth, createFetch, decodeSiteId, deleteSite, deleteSitemap, discoverSitemap, encodeSiteId, err, exchangeAuthCodeResult, fetchSitemap, fetchSitemapUrls, fetchSitemaps, fetchSites, fetchSitesWithSitemaps, findBestGscProperty, findExactGscProperty, formatGscPropertyCandidates, generateGscDateRange, getBackfillProgress, getDateRange, getFreshestGscDate, getIndexingEligibility, getIndexingMetadata, getLatestGscDate, getNextCheckAfter, getNextCheckPriority, getNextDate, getOldestGscDate, getPendingDates, getPreviousDate, getPstDate, getVerificationToken, getVerifiedSite, googleSearchConsole, groupIntoRanges, gscErrorToException, gscPropertyMatchesTarget, hasGoogleScope, hasGscReadScope, hasGscWriteScope, hasIndexingScope, hasOptionalIndexingScope, hasRequiredAnalyticsScope, indexingNextActions, indexingStatuses, inspectUrl, inspectUrlFlat, introspectAccessToken, introspectAccessTokenResult, isCanonicalMismatch, isErr, isFragmentUrl, isOk, isPermissionDeniedError, isValidGscDate, isVerifiedGscPermission, isVerifiedGscProperty, lifecycleErrorCodes, listVerifiedSites, matchGscSite, normalizeGscSiteUrl, normalizeRegistrationTarget, normalizeSiteUrl, normalizeUrl, ok, parseGoogleError, parseGrantedScopes, parseGscSiteUrl, pickBestGscProperty, propertyNextActions, propertyStatuses, querySourceModes, refreshAccessToken, requestIndexing, resolveVerificationTarget, rethrowAsGscApiError, revokeOAuthToken, revokeOAuthTokenResult, runSequentialBatch, siteUrlToVerificationSite, sitemapNextActions, sitemapStatuses, submitSitemap, toIsoDate, unmappedInspectionReasons, unverifySite, unwrapResult, urlMatchKey, verificationMethodsFor, verifySite };
|
package/dist/index.mjs
CHANGED
|
@@ -18,7 +18,7 @@ import { URL_INSPECTION_EFFECTIVE_LIMIT } from "./core/quota.mjs";
|
|
|
18
18
|
import "./core/types.mjs";
|
|
19
19
|
import { normalizeUrl } from "./normalize.mjs";
|
|
20
20
|
import { GSCDUMP_ONBOARDING_CONTRACT_VERSION, GSCDUMP_OPTIONAL_INDEXING_SCOPE, GSCDUMP_REQUIRED_ANALYTICS_SCOPE, GSCDUMP_WRITE_ANALYTICS_SCOPE, accountNextActions, accountStatuses, analyticsNextActions, analyticsStatuses, hasOptionalIndexingScope, hasRequiredAnalyticsScope, indexingNextActions, indexingStatuses, lifecycleErrorCodes, parseGrantedScopes, propertyNextActions, propertyStatuses, querySourceModes, sitemapNextActions, sitemapStatuses } from "./onboarding.mjs";
|
|
21
|
-
import { discoverSitemap, fetchSitemapUrls } from "./sitemap.mjs";
|
|
21
|
+
import { discoverSitemap, fetchSitemapUrls } from "./sitemap-compat.mjs";
|
|
22
22
|
import { decodeSiteId, encodeSiteId, normalizeSiteUrl } from "./tenant.mjs";
|
|
23
23
|
import { urlMatchKey } from "./url.mjs";
|
|
24
24
|
export { DAYS_PER_RANGE, DEFAULT_GSC_REQUEST_TIMEOUT_MS, GSCDUMP_ONBOARDING_CONTRACT_VERSION, GSCDUMP_OPTIONAL_INDEXING_SCOPE, GSCDUMP_REQUIRED_ANALYTICS_SCOPE, GSCDUMP_WRITE_ANALYTICS_SCOPE, GSC_FINALIZED_LAG_DAYS, GSC_FRESHEST_LAG_DAYS, GSC_INDEXING_SCOPE, GSC_READ_SCOPE, GSC_RETENTION_MONTHS, GSC_SITE_VERIFICATION_SCOPE, GSC_WRITE_SCOPE, GscApiError, INDEXING_ISSUE_FILTERS, INDEXING_ISSUE_LABELS, INDEXING_ISSUE_SEVERITY, KNOWN_COVERAGE_STATES, KNOWN_PAGE_FETCH_STATES, MAX_FLAT_INSPECTION_BATCH_CONCURRENCY, MS_PER_DAY, URL_INSPECTION_EFFECTIVE_LIMIT, accountNextActions, accountStatuses, addDays, addSite, analyticsNextActions, analyticsStatuses, batchInspectUrls, batchInspectUrlsFlatSettled, batchRequestIndexing, canUseUrlInspection, classifyCanonicalDifference, classifyError, countDays, createAuth, createFetch, decodeSiteId, deleteSite, deleteSitemap, discoverSitemap, encodeSiteId, err, exchangeAuthCodeResult, fetchSitemap, fetchSitemapUrls, fetchSitemaps, fetchSites, fetchSitesWithSitemaps, findBestGscProperty, findExactGscProperty, formatGscPropertyCandidates, generateGscDateRange, getBackfillProgress, getDateRange, getFreshestGscDate, getIndexingEligibility, getIndexingMetadata, getLatestGscDate, getNextCheckAfter, getNextCheckPriority, getNextDate, getOldestGscDate, getPendingDates, getPreviousDate, getPstDate, getVerificationToken, getVerifiedSite, googleSearchConsole, groupIntoRanges, gscErrorToException, gscPropertyMatchesTarget, hasGoogleScope, hasGscReadScope, hasGscWriteScope, hasIndexingScope, hasOptionalIndexingScope, hasRequiredAnalyticsScope, indexingNextActions, indexingStatuses, inspectUrl, inspectUrlFlat, introspectAccessToken, introspectAccessTokenResult, isCanonicalMismatch, isErr, isFragmentUrl, isOk, isPermissionDeniedError, isValidGscDate, isVerifiedGscPermission, isVerifiedGscProperty, lifecycleErrorCodes, listVerifiedSites, matchGscSite, normalizeGscSiteUrl, normalizeRegistrationTarget, normalizeSiteUrl, normalizeUrl, ok, parseGoogleError, parseGrantedScopes, parseGscSiteUrl, pickBestGscProperty, propertyNextActions, propertyStatuses, querySourceModes, refreshAccessToken, requestIndexing, resolveVerificationTarget, rethrowAsGscApiError, revokeOAuthToken, revokeOAuthTokenResult, runSequentialBatch, siteUrlToVerificationSite, sitemapNextActions, sitemapStatuses, submitSitemap, toIsoDate, unmappedInspectionReasons, unverifySite, unwrapResult, urlMatchKey, verificationMethodsFor, verifySite };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { DiscoverSitemapOptions, FetchSitemapUrlsOptions } from "./sitemap.mjs";
|
|
2
|
+
/**
|
|
3
|
+
* Keep the v1 root exports without making every `gscdump` import initialize
|
|
4
|
+
* Nuxt Sitemap. The parser remains isolated behind the `./sitemap` entry.
|
|
5
|
+
*/
|
|
6
|
+
declare function discoverSitemap(site: string, options?: DiscoverSitemapOptions): Promise<string | null>;
|
|
7
|
+
declare function fetchSitemapUrls(sitemapUrl: string, options?: FetchSitemapUrlsOptions): Promise<string[]>;
|
|
8
|
+
export { discoverSitemap, fetchSitemapUrls };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
async function discoverSitemap(site, options = {}) {
|
|
2
|
+
return (await import("./sitemap.mjs")).discoverSitemap(site, options);
|
|
3
|
+
}
|
|
4
|
+
async function fetchSitemapUrls(sitemapUrl, options = {}) {
|
|
5
|
+
return (await import("./sitemap.mjs")).fetchSitemapUrls(sitemapUrl, options);
|
|
6
|
+
}
|
|
7
|
+
export { discoverSitemap, fetchSitemapUrls };
|
package/dist/sitemap.d.mts
CHANGED
|
@@ -1,26 +1,179 @@
|
|
|
1
|
-
interface
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
interface SitemapUrlEntry {
|
|
2
|
+
loc: string;
|
|
3
|
+
lastmod?: string;
|
|
4
|
+
changefreq?: string;
|
|
5
|
+
priority?: number;
|
|
6
|
+
}
|
|
7
|
+
interface SitemapIndexEntry {
|
|
8
|
+
loc: string;
|
|
9
|
+
lastmod?: string;
|
|
10
|
+
}
|
|
11
|
+
interface SitemapDocumentMeta {
|
|
12
|
+
bytesRead: number;
|
|
13
|
+
complete: boolean;
|
|
14
|
+
hasBom: boolean;
|
|
15
|
+
hasXmlDeclaration: boolean;
|
|
16
|
+
namespace: string | null;
|
|
17
|
+
}
|
|
18
|
+
type SitemapDocumentResult = {
|
|
19
|
+
_tag: 'urlset';
|
|
20
|
+
entries: SitemapUrlEntry[];
|
|
21
|
+
meta: SitemapDocumentMeta;
|
|
22
|
+
} | {
|
|
23
|
+
_tag: 'index';
|
|
24
|
+
entries: SitemapIndexEntry[];
|
|
25
|
+
meta: SitemapDocumentMeta;
|
|
26
|
+
} | {
|
|
27
|
+
_tag: 'empty_body';
|
|
28
|
+
} | {
|
|
29
|
+
_tag: 'html';
|
|
30
|
+
metaRefreshUrl?: string;
|
|
31
|
+
} | {
|
|
32
|
+
_tag: 'byte_limit';
|
|
33
|
+
bytesRead: number;
|
|
34
|
+
maxBytes: number;
|
|
35
|
+
} | {
|
|
36
|
+
_tag: 'parse_error';
|
|
37
|
+
error: string;
|
|
38
|
+
} | {
|
|
39
|
+
_tag: 'unsupported_document';
|
|
40
|
+
root: string | null;
|
|
41
|
+
};
|
|
42
|
+
interface ParseSitemapDocumentOptions {
|
|
43
|
+
maxBytes?: number;
|
|
44
|
+
maxEntries?: number;
|
|
45
|
+
acceptUrl?: (entry: SitemapUrlEntry) => boolean;
|
|
46
|
+
}
|
|
47
|
+
type SitemapBody = string | Uint8Array | ReadableStream<Uint8Array> | AsyncIterable<Uint8Array>;
|
|
48
|
+
interface SitemapFetchOptions extends ParseSitemapDocumentOptions {
|
|
49
|
+
fetcher?: typeof fetch;
|
|
50
|
+
headers?: HeadersInit;
|
|
5
51
|
signal?: AbortSignal;
|
|
52
|
+
timeoutMs?: number;
|
|
53
|
+
}
|
|
54
|
+
type SitemapFetchResult = {
|
|
55
|
+
_tag: 'ok';
|
|
56
|
+
url: string;
|
|
57
|
+
document: Extract<SitemapDocumentResult, {
|
|
58
|
+
_tag: 'urlset' | 'index';
|
|
59
|
+
}>;
|
|
60
|
+
} | {
|
|
61
|
+
_tag: 'not_found';
|
|
62
|
+
url: string;
|
|
63
|
+
status: 404 | 410;
|
|
64
|
+
} | {
|
|
65
|
+
_tag: 'http_error';
|
|
66
|
+
url: string;
|
|
67
|
+
status: number;
|
|
68
|
+
statusText: string;
|
|
69
|
+
} | {
|
|
70
|
+
_tag: 'network_error';
|
|
71
|
+
url: string;
|
|
72
|
+
error: string;
|
|
73
|
+
} | {
|
|
74
|
+
_tag: 'document_error';
|
|
75
|
+
url: string;
|
|
76
|
+
error: Exclude<SitemapDocumentResult, {
|
|
77
|
+
_tag: 'urlset' | 'index';
|
|
78
|
+
}>;
|
|
79
|
+
};
|
|
80
|
+
interface DiscoverSitemapOptions extends SitemapFetchOptions {
|
|
81
|
+
userAgent?: string;
|
|
82
|
+
}
|
|
83
|
+
interface SitemapDiscoveryFailure {
|
|
84
|
+
url: string;
|
|
85
|
+
kind: 'http' | 'network' | 'document';
|
|
86
|
+
detail: string;
|
|
87
|
+
}
|
|
88
|
+
type SitemapDiscoveryResult = {
|
|
89
|
+
_tag: 'found';
|
|
90
|
+
url: string;
|
|
91
|
+
source: 'common_path' | 'robots';
|
|
92
|
+
} | {
|
|
93
|
+
_tag: 'not_found';
|
|
94
|
+
} | {
|
|
95
|
+
_tag: 'incomplete';
|
|
96
|
+
failures: SitemapDiscoveryFailure[];
|
|
97
|
+
};
|
|
98
|
+
interface WalkSitemapsOptions extends SitemapFetchOptions {
|
|
99
|
+
maxDepth?: number;
|
|
100
|
+
maxDocuments?: number;
|
|
101
|
+
maxUrls?: number;
|
|
6
102
|
}
|
|
7
|
-
/**
|
|
8
|
-
* Try to discover a sitemap for `domain` by checking robots.txt for a
|
|
9
|
-
* `Sitemap:` directive, then a small set of common paths. Returns the first
|
|
10
|
-
* URL that responds with a 2xx, or `null`.
|
|
11
|
-
*/
|
|
12
|
-
declare function discoverSitemap(domain: string, options?: DiscoverSitemapOptions): Promise<string | null>;
|
|
13
103
|
interface FetchSitemapUrlsOptions extends DiscoverSitemapOptions {
|
|
14
|
-
/** Maximum nested sitemap-index depth to follow. Default 3. */
|
|
15
104
|
maxDepth?: number;
|
|
16
|
-
/** Stop after this many URLs (across all nested sitemaps). Default unlimited. */
|
|
17
105
|
limit?: number;
|
|
18
106
|
}
|
|
107
|
+
interface SitemapWalkFailure {
|
|
108
|
+
url: string;
|
|
109
|
+
depth: number;
|
|
110
|
+
error: Exclude<SitemapFetchResult, {
|
|
111
|
+
_tag: 'ok';
|
|
112
|
+
}>;
|
|
113
|
+
}
|
|
114
|
+
type SitemapWalkResult = {
|
|
115
|
+
_tag: 'ok';
|
|
116
|
+
entries: SitemapUrlEntry[];
|
|
117
|
+
documentsRead: number;
|
|
118
|
+
complete: boolean;
|
|
119
|
+
failures: SitemapWalkFailure[];
|
|
120
|
+
} | {
|
|
121
|
+
_tag: 'not_found';
|
|
122
|
+
} | {
|
|
123
|
+
_tag: 'error';
|
|
124
|
+
failures: SitemapWalkFailure[];
|
|
125
|
+
};
|
|
126
|
+
type SitemapIdentityResult = {
|
|
127
|
+
_tag: 'ok';
|
|
128
|
+
url: string;
|
|
129
|
+
} | {
|
|
130
|
+
_tag: 'invalid';
|
|
131
|
+
reason: 'empty' | 'invalid_url' | 'off_origin';
|
|
132
|
+
};
|
|
133
|
+
interface SitemapEvidenceRecord {
|
|
134
|
+
path: string;
|
|
135
|
+
lastDownloaded?: string | null;
|
|
136
|
+
fetchedAt?: number | null;
|
|
137
|
+
}
|
|
138
|
+
interface ScopedSitemapRecords<T extends SitemapEvidenceRecord> {
|
|
139
|
+
sitemaps: Array<T & {
|
|
140
|
+
path: string;
|
|
141
|
+
}>;
|
|
142
|
+
excludedCount: number;
|
|
143
|
+
duplicateCount: number;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Streaming sitemap parser backed by the same utility Nuxt Sitemap consumers
|
|
147
|
+
* previously used. Expected failures are tagged values.
|
|
148
|
+
* Entry limits produce a useful partial document with `meta.complete = false`;
|
|
149
|
+
* byte limits fail closed because the document boundary was not observed.
|
|
150
|
+
*/
|
|
151
|
+
declare function parseSitemapDocument(body: SitemapBody, options?: ParseSitemapDocumentOptions): Promise<SitemapDocumentResult>;
|
|
152
|
+
declare function fetchSitemapDocument(url: string, options?: SitemapFetchOptions): Promise<SitemapFetchResult>;
|
|
153
|
+
declare function parseRobotsSitemapUrls(robots: string, baseUrl?: string): string[];
|
|
154
|
+
declare function canonicalSitemapIdentity(path: string, site?: string): SitemapIdentityResult;
|
|
155
|
+
declare function sameSitemapIdentity(left: string, right: string, site?: string): boolean;
|
|
156
|
+
declare function scopeSitemapRecords<T extends SitemapEvidenceRecord>(records: readonly T[], site?: string): ScopedSitemapRecords<T>;
|
|
157
|
+
/**
|
|
158
|
+
* Discover a real sitemap document. A 200 HTML shell is rejected, and
|
|
159
|
+
* transport failures remain distinguishable from an authoritative miss.
|
|
160
|
+
*/
|
|
161
|
+
declare function discoverSitemapResult(site: string, options?: DiscoverSitemapOptions): Promise<SitemapDiscoveryResult>;
|
|
162
|
+
/**
|
|
163
|
+
* Compatibility surface retained at the package root. New callers that need
|
|
164
|
+
* explicit incomplete-discovery evidence should use `discoverSitemapResult`.
|
|
165
|
+
*/
|
|
166
|
+
declare function discoverSitemap(site: string, options?: DiscoverSitemapOptions): Promise<string | null>;
|
|
167
|
+
/**
|
|
168
|
+
* Bounded sitemap and sitemap-index walk. Partial results are explicit through
|
|
169
|
+
* `complete` and `failures`; callers must not infer removals from an incomplete
|
|
170
|
+
* walk.
|
|
171
|
+
*/
|
|
172
|
+
declare function walkSitemaps(roots: string | readonly string[], options?: WalkSitemapsOptions): Promise<SitemapWalkResult>;
|
|
19
173
|
/**
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* de-duplicated. The XML parser is regex-based — it handles the common
|
|
23
|
-
* `<loc>https://...</loc>` shape but doesn't validate the schema.
|
|
174
|
+
* Compatibility surface retained at the package root. The shared parser and
|
|
175
|
+
* traversal own the work while the original string-array contract remains.
|
|
24
176
|
*/
|
|
25
177
|
declare function fetchSitemapUrls(sitemapUrl: string, options?: FetchSitemapUrlsOptions): Promise<string[]>;
|
|
26
|
-
|
|
178
|
+
declare function sitemapContentHash(entries: readonly Pick<SitemapUrlEntry, 'loc'>[]): Promise<string>;
|
|
179
|
+
export { DiscoverSitemapOptions, FetchSitemapUrlsOptions, ParseSitemapDocumentOptions, ScopedSitemapRecords, SitemapBody, SitemapDiscoveryFailure, SitemapDiscoveryResult, SitemapDocumentMeta, SitemapDocumentResult, SitemapEvidenceRecord, SitemapFetchOptions, SitemapFetchResult, SitemapIdentityResult, SitemapIndexEntry, SitemapUrlEntry, SitemapWalkFailure, SitemapWalkResult, WalkSitemapsOptions, canonicalSitemapIdentity, discoverSitemap, discoverSitemapResult, fetchSitemapDocument, fetchSitemapUrls, parseRobotsSitemapUrls, parseSitemapDocument, sameSitemapIdentity, scopeSitemapRecords, sitemapContentHash, walkSitemaps };
|
package/dist/sitemap.mjs
CHANGED
|
@@ -1,70 +1,670 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
1
|
+
import { urlMatchKey } from "./url.mjs";
|
|
2
|
+
import { parseSitemapStream } from "@nuxtjs/sitemap/utils";
|
|
3
|
+
const DEFAULT_FETCH_TIMEOUT_MS = 1e4;
|
|
4
|
+
const DEFAULT_MAX_BYTES = 50 * 1024 * 1024;
|
|
5
|
+
const DEFAULT_MAX_ENTRIES = 5e4;
|
|
6
|
+
const DEFAULT_MAX_DEPTH = 3;
|
|
7
|
+
const DEFAULT_MAX_DOCUMENTS = 100;
|
|
8
|
+
const SNIFF_BYTES = 65536;
|
|
9
|
+
const COMMON_SITEMAP_PATHS = [
|
|
10
|
+
"/sitemap.xml",
|
|
11
|
+
"/sitemap_index.xml",
|
|
12
|
+
"/sitemap-index.xml",
|
|
13
|
+
"/sitemaps.xml"
|
|
14
|
+
];
|
|
15
|
+
function errorMessage(error) {
|
|
16
|
+
return error instanceof Error ? error.message : String(error);
|
|
17
|
+
}
|
|
18
|
+
function looksLikeHtml(prefix) {
|
|
19
|
+
const start = prefix.trimStart().replace(/^\uFEFF/, "").replace(/^<\?xml[\s\S]*?\?>\s*/i, "").replace(/^(?:<!--[\s\S]*?-->\s*)+/, "");
|
|
20
|
+
return /^<!doctype\s+html\b|^<html\b/i.test(start);
|
|
21
|
+
}
|
|
22
|
+
function extractMetaRefreshUrl(html) {
|
|
23
|
+
return /content=["']?\d+;\s*url=([^"'\s>]+)/i.exec(html)?.[1];
|
|
24
|
+
}
|
|
25
|
+
function isAsyncIterable(value) {
|
|
26
|
+
return typeof value === "object" && value !== null && Symbol.asyncIterator in value;
|
|
27
|
+
}
|
|
28
|
+
async function* readableStreamChunks(stream) {
|
|
29
|
+
const reader = stream.getReader();
|
|
30
|
+
let done = false;
|
|
31
|
+
try {
|
|
32
|
+
while (!done) {
|
|
33
|
+
const next = await reader.read();
|
|
34
|
+
done = next.done;
|
|
35
|
+
if (next.value) yield next.value;
|
|
36
|
+
}
|
|
37
|
+
} finally {
|
|
38
|
+
if (!done) await reader.cancel("sitemap reader stopped before the response body completed");
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
async function* bodyChunks(body) {
|
|
42
|
+
if (typeof body === "string") {
|
|
43
|
+
yield new TextEncoder().encode(body);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (body instanceof Uint8Array) {
|
|
47
|
+
yield body;
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (isAsyncIterable(body)) {
|
|
51
|
+
yield* body;
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
yield* readableStreamChunks(body);
|
|
55
|
+
}
|
|
56
|
+
function replayStream(reader, initial) {
|
|
57
|
+
let offset = 0;
|
|
58
|
+
return new ReadableStream({
|
|
59
|
+
async pull(controller) {
|
|
60
|
+
if (offset < initial.length) {
|
|
61
|
+
controller.enqueue(initial[offset++]);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const next = await reader.read();
|
|
65
|
+
if (next.done) {
|
|
66
|
+
controller.close();
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (next.value) controller.enqueue(next.value);
|
|
70
|
+
},
|
|
71
|
+
async cancel(reason) {
|
|
72
|
+
await reader.cancel(reason);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
async function bodyWithDetectedCompression(body) {
|
|
77
|
+
const reader = body.getReader();
|
|
78
|
+
const initial = [];
|
|
79
|
+
let prefixLength = 0;
|
|
80
|
+
while (prefixLength < 2) {
|
|
81
|
+
const next = await reader.read();
|
|
82
|
+
if (next.done) break;
|
|
83
|
+
if (next.value) {
|
|
84
|
+
initial.push(next.value);
|
|
85
|
+
prefixLength += next.value.byteLength;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const prefix = initial.length === 1 ? initial[0] : Uint8Array.from(initial.flatMap((chunk) => [...chunk]));
|
|
89
|
+
const replay = replayStream(reader, initial);
|
|
90
|
+
if (!(prefix.length >= 2 && prefix[0] === 31 && prefix[1] === 139)) return replay;
|
|
91
|
+
if (typeof DecompressionStream === "undefined") {
|
|
92
|
+
await replay.cancel("gzip decompression is unavailable in this runtime");
|
|
93
|
+
throw new Error("gzip sitemap body requires DecompressionStream support");
|
|
94
|
+
}
|
|
95
|
+
return replay.pipeThrough(new DecompressionStream("gzip"));
|
|
96
|
+
}
|
|
97
|
+
function sitemapInputBoundaryError(bytesRead, maxBytes) {
|
|
98
|
+
return Object.assign(/* @__PURE__ */ new Error(`Sitemap exceeds ${maxBytes} bytes`), {
|
|
99
|
+
_tag: "byte_limit",
|
|
100
|
+
bytesRead,
|
|
101
|
+
maxBytes
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
function isSitemapInputBoundaryError(error) {
|
|
105
|
+
return error !== null && typeof error === "object" && "_tag" in error && error._tag === "byte_limit";
|
|
106
|
+
}
|
|
107
|
+
async function* replaySitemapInput(initial, iterator, stats, maxBytes) {
|
|
108
|
+
let done = false;
|
|
109
|
+
try {
|
|
110
|
+
yield* initial;
|
|
111
|
+
while (!done) {
|
|
112
|
+
const next = await iterator.next();
|
|
113
|
+
done = Boolean(next.done);
|
|
114
|
+
if (!next.value) continue;
|
|
115
|
+
const bytesRead = stats.bytesRead + next.value.byteLength;
|
|
116
|
+
if (bytesRead > maxBytes) throw sitemapInputBoundaryError(bytesRead, maxBytes);
|
|
117
|
+
stats.bytesRead = bytesRead;
|
|
118
|
+
yield next.value;
|
|
119
|
+
}
|
|
120
|
+
} finally {
|
|
121
|
+
if (!done) await iterator.return?.(void 0);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
async function prepareSitemapInput(body, maxBytes) {
|
|
125
|
+
const iterator = bodyChunks(body)[Symbol.asyncIterator]();
|
|
126
|
+
const decoder = new TextDecoder("utf-8", { ignoreBOM: true });
|
|
127
|
+
const initial = [];
|
|
128
|
+
const stats = {
|
|
129
|
+
bytesRead: 0,
|
|
130
|
+
prefix: "",
|
|
131
|
+
hasContent: false,
|
|
132
|
+
hasBom: false
|
|
133
|
+
};
|
|
134
|
+
let done = false;
|
|
135
|
+
while (stats.prefix.length < SNIFF_BYTES && !done) {
|
|
136
|
+
const next = await iterator.next();
|
|
137
|
+
done = Boolean(next.done);
|
|
138
|
+
if (!next.value) continue;
|
|
139
|
+
const bytesRead = stats.bytesRead + next.value.byteLength;
|
|
140
|
+
if (bytesRead > maxBytes) {
|
|
141
|
+
await iterator.return?.(void 0);
|
|
142
|
+
return {
|
|
143
|
+
_tag: "byte_limit",
|
|
144
|
+
bytesRead,
|
|
145
|
+
maxBytes
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
initial.push(next.value);
|
|
149
|
+
stats.bytesRead = bytesRead;
|
|
150
|
+
const text = decoder.decode(next.value, { stream: true });
|
|
151
|
+
stats.prefix += text.slice(0, SNIFF_BYTES - stats.prefix.length);
|
|
152
|
+
}
|
|
153
|
+
if (done && stats.prefix.length < SNIFF_BYTES) stats.prefix += decoder.decode();
|
|
154
|
+
stats.hasContent = Boolean(stats.prefix.trim());
|
|
155
|
+
stats.hasBom = stats.prefix.charCodeAt(0) === 65279;
|
|
156
|
+
if (!stats.hasContent) {
|
|
157
|
+
await iterator.return?.(void 0);
|
|
158
|
+
return { _tag: "empty_body" };
|
|
159
|
+
}
|
|
160
|
+
if (looksLikeHtml(stats.prefix)) {
|
|
161
|
+
await iterator.return?.(void 0);
|
|
162
|
+
const metaRefreshUrl = extractMetaRefreshUrl(stats.prefix);
|
|
163
|
+
return {
|
|
164
|
+
_tag: "html",
|
|
165
|
+
...metaRefreshUrl ? { metaRefreshUrl } : {}
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
return {
|
|
169
|
+
_tag: "ready",
|
|
170
|
+
input: replaySitemapInput(initial, iterator, stats, maxBytes),
|
|
171
|
+
stats
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
function normalizeSitemapUrlInput(input) {
|
|
175
|
+
if (typeof input === "string") return { loc: input };
|
|
176
|
+
if (!input.loc) return null;
|
|
177
|
+
const lastmod = input.lastmod instanceof Date ? input.lastmod.toISOString() : input.lastmod;
|
|
178
|
+
return {
|
|
179
|
+
loc: input.loc,
|
|
180
|
+
...lastmod ? { lastmod } : {},
|
|
181
|
+
...input.changefreq ? { changefreq: input.changefreq } : {},
|
|
182
|
+
...input.priority !== void 0 ? { priority: input.priority } : {}
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
function rootMetadata(prefix) {
|
|
186
|
+
const rootMatch = /<(?:[\w.-]+:)?(urlset|sitemapindex)\b([^>]*)>/i.exec(prefix);
|
|
187
|
+
if (!rootMatch) return {
|
|
188
|
+
root: /<(?:[\w.-]+:)?([\w.-]+)\b/.exec(prefix)?.[1]?.toLowerCase() ?? null,
|
|
189
|
+
namespace: null
|
|
190
|
+
};
|
|
191
|
+
const attributes = rootMatch[2] ?? "";
|
|
192
|
+
const namespace = /\sxmlns(?::[\w.-]+)?=(["'])(.*?)\1/i.exec(attributes)?.[2] ?? null;
|
|
193
|
+
return {
|
|
194
|
+
root: rootMatch[1].toLowerCase(),
|
|
195
|
+
namespace
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
async function parseSitemapDocument(body, options = {}) {
|
|
199
|
+
const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
200
|
+
const maxEntries = options.maxEntries ?? DEFAULT_MAX_ENTRIES;
|
|
201
|
+
const prepared = await prepareSitemapInput(body, maxBytes);
|
|
202
|
+
if (prepared._tag !== "ready") return prepared;
|
|
203
|
+
let root = null;
|
|
204
|
+
let complete = true;
|
|
205
|
+
const urls = [];
|
|
206
|
+
const sitemaps = [];
|
|
207
|
+
try {
|
|
208
|
+
for await (const event of parseSitemapStream(prepared.input)) {
|
|
209
|
+
if (event._tag === "kind") {
|
|
210
|
+
root = event.kind === "index" ? "sitemapindex" : "urlset";
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
if (event._tag === "warning") continue;
|
|
214
|
+
if (event._tag === "url") {
|
|
215
|
+
const entry = normalizeSitemapUrlInput(event.url);
|
|
216
|
+
if (!entry || options.acceptUrl && !options.acceptUrl(entry)) continue;
|
|
217
|
+
if (urls.length >= maxEntries) {
|
|
218
|
+
complete = false;
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
urls.push(entry);
|
|
222
|
+
} else if (event._tag === "sitemap") {
|
|
223
|
+
if (sitemaps.length >= maxEntries) {
|
|
224
|
+
complete = false;
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
sitemaps.push(event.sitemap);
|
|
228
|
+
}
|
|
229
|
+
if (!complete) break;
|
|
230
|
+
}
|
|
231
|
+
} catch (error) {
|
|
232
|
+
if (isSitemapInputBoundaryError(error)) return error;
|
|
233
|
+
const metadata = rootMetadata(prepared.stats.prefix);
|
|
234
|
+
if (metadata.root && metadata.root !== "urlset" && metadata.root !== "sitemapindex") return {
|
|
235
|
+
_tag: "unsupported_document",
|
|
236
|
+
root: metadata.root
|
|
237
|
+
};
|
|
238
|
+
return {
|
|
239
|
+
_tag: "parse_error",
|
|
240
|
+
error: `Not a valid sitemap: ${errorMessage(error)}`
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
const metadata = rootMetadata(prepared.stats.prefix);
|
|
244
|
+
const meta = {
|
|
245
|
+
bytesRead: prepared.stats.bytesRead,
|
|
246
|
+
complete,
|
|
247
|
+
hasBom: prepared.stats.hasBom,
|
|
248
|
+
hasXmlDeclaration: /^\s*<\?xml\b/i.test(prepared.stats.prefix),
|
|
249
|
+
namespace: metadata.namespace
|
|
250
|
+
};
|
|
251
|
+
if (root === "urlset") return {
|
|
252
|
+
_tag: "urlset",
|
|
253
|
+
entries: urls,
|
|
254
|
+
meta
|
|
255
|
+
};
|
|
256
|
+
if (root === "sitemapindex") return {
|
|
257
|
+
_tag: "index",
|
|
258
|
+
entries: sitemaps,
|
|
259
|
+
meta
|
|
260
|
+
};
|
|
261
|
+
return {
|
|
262
|
+
_tag: "unsupported_document",
|
|
263
|
+
root
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
function requestSignal(signal, timeoutMs) {
|
|
267
|
+
if (signal) return signal;
|
|
268
|
+
return AbortSignal.timeout(timeoutMs);
|
|
269
|
+
}
|
|
270
|
+
async function fetchSitemapDocument(url, options = {}) {
|
|
271
|
+
const fetcher = options.fetcher ?? globalThis.fetch;
|
|
272
|
+
let response;
|
|
273
|
+
try {
|
|
274
|
+
response = await fetcher(url, {
|
|
275
|
+
headers: {
|
|
276
|
+
accept: "application/xml,text/xml,application/rss+xml,*/*;q=0.1",
|
|
277
|
+
...options.headers
|
|
278
|
+
},
|
|
279
|
+
redirect: "follow",
|
|
280
|
+
signal: requestSignal(options.signal, options.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS)
|
|
281
|
+
});
|
|
282
|
+
} catch (error) {
|
|
283
|
+
return {
|
|
284
|
+
_tag: "network_error",
|
|
285
|
+
url,
|
|
286
|
+
error: errorMessage(error)
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
if (response.status === 404 || response.status === 410) return {
|
|
290
|
+
_tag: "not_found",
|
|
291
|
+
url,
|
|
292
|
+
status: response.status
|
|
293
|
+
};
|
|
294
|
+
if (!response.ok) return {
|
|
295
|
+
_tag: "http_error",
|
|
296
|
+
url,
|
|
297
|
+
status: response.status,
|
|
298
|
+
statusText: response.statusText
|
|
299
|
+
};
|
|
300
|
+
if (!response.body) return {
|
|
301
|
+
_tag: "document_error",
|
|
302
|
+
url,
|
|
303
|
+
error: { _tag: "empty_body" }
|
|
304
|
+
};
|
|
305
|
+
let body;
|
|
306
|
+
try {
|
|
307
|
+
body = await bodyWithDetectedCompression(response.body);
|
|
308
|
+
} catch (error) {
|
|
309
|
+
return {
|
|
310
|
+
_tag: "document_error",
|
|
311
|
+
url,
|
|
312
|
+
error: {
|
|
313
|
+
_tag: "parse_error",
|
|
314
|
+
error: errorMessage(error)
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
const document = await parseSitemapDocument(body, options);
|
|
319
|
+
if (document._tag === "urlset" || document._tag === "index") return {
|
|
320
|
+
_tag: "ok",
|
|
321
|
+
url: response.url || url,
|
|
322
|
+
document
|
|
323
|
+
};
|
|
324
|
+
return {
|
|
325
|
+
_tag: "document_error",
|
|
326
|
+
url,
|
|
327
|
+
error: document
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
function parseRobotsSitemapUrls(robots, baseUrl) {
|
|
331
|
+
const urls = [];
|
|
38
332
|
const seen = /* @__PURE__ */ new Set();
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
if (
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
333
|
+
for (const match of robots.matchAll(/^\s*sitemap:\s*(\S+)/gim)) {
|
|
334
|
+
const raw = match[1];
|
|
335
|
+
if (!raw) continue;
|
|
336
|
+
let value = raw;
|
|
337
|
+
if (baseUrl) try {
|
|
338
|
+
value = new URL(raw, baseUrl).toString();
|
|
339
|
+
} catch {
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
if (!seen.has(value)) {
|
|
343
|
+
seen.add(value);
|
|
344
|
+
urls.push(value);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return urls;
|
|
348
|
+
}
|
|
349
|
+
function normalizeSiteOrigin(input) {
|
|
350
|
+
const value = input.trim().replace(/^sc-domain:/i, "");
|
|
351
|
+
try {
|
|
352
|
+
const url = new URL(/^https?:\/\//i.test(value) ? value : `https://${value}`);
|
|
353
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") return null;
|
|
354
|
+
return url.origin;
|
|
355
|
+
} catch {
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
function canonicalSitemapIdentity(path, site = "") {
|
|
360
|
+
const value = path.trim();
|
|
361
|
+
if (!value) return {
|
|
362
|
+
_tag: "invalid",
|
|
363
|
+
reason: "empty"
|
|
364
|
+
};
|
|
365
|
+
const siteOrigin = normalizeSiteOrigin(site);
|
|
366
|
+
let parsed;
|
|
367
|
+
try {
|
|
368
|
+
parsed = new URL(value, siteOrigin ? `${siteOrigin}/` : void 0);
|
|
369
|
+
} catch {
|
|
370
|
+
return {
|
|
371
|
+
_tag: "invalid",
|
|
372
|
+
reason: "invalid_url"
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return {
|
|
376
|
+
_tag: "invalid",
|
|
377
|
+
reason: "invalid_url"
|
|
378
|
+
};
|
|
379
|
+
if (siteOrigin && parsed.origin !== siteOrigin) return {
|
|
380
|
+
_tag: "invalid",
|
|
381
|
+
reason: "off_origin"
|
|
382
|
+
};
|
|
383
|
+
parsed.hash = "";
|
|
384
|
+
return {
|
|
385
|
+
_tag: "ok",
|
|
386
|
+
url: `${parsed.origin}${parsed.pathname}${parsed.search}`
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
function sameSitemapIdentity(left, right, site = "") {
|
|
390
|
+
const leftIdentity = canonicalSitemapIdentity(left, site);
|
|
391
|
+
if (leftIdentity._tag !== "ok") return false;
|
|
392
|
+
const rightIdentity = canonicalSitemapIdentity(right, site);
|
|
393
|
+
return rightIdentity._tag === "ok" && leftIdentity.url === rightIdentity.url;
|
|
394
|
+
}
|
|
395
|
+
function evidenceTime(record) {
|
|
396
|
+
const downloadedAt = record.lastDownloaded ? Date.parse(record.lastDownloaded) : NaN;
|
|
397
|
+
if (Number.isFinite(downloadedAt)) return downloadedAt;
|
|
398
|
+
return record.fetchedAt ?? 0;
|
|
399
|
+
}
|
|
400
|
+
function scopeSitemapRecords(records, site = "") {
|
|
401
|
+
const byIdentity = /* @__PURE__ */ new Map();
|
|
402
|
+
let excludedCount = 0;
|
|
403
|
+
let duplicateCount = 0;
|
|
404
|
+
for (const record of records) {
|
|
405
|
+
const identity = canonicalSitemapIdentity(record.path, site);
|
|
406
|
+
if (identity._tag !== "ok") {
|
|
407
|
+
excludedCount++;
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
const normalized = {
|
|
411
|
+
...record,
|
|
412
|
+
path: identity.url
|
|
413
|
+
};
|
|
414
|
+
const existing = byIdentity.get(identity.url);
|
|
415
|
+
if (!existing) {
|
|
416
|
+
byIdentity.set(identity.url, normalized);
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
duplicateCount++;
|
|
420
|
+
if (evidenceTime(normalized) >= evidenceTime(existing)) byIdentity.set(identity.url, normalized);
|
|
421
|
+
}
|
|
422
|
+
return {
|
|
423
|
+
sitemaps: [...byIdentity.values()],
|
|
424
|
+
excludedCount,
|
|
425
|
+
duplicateCount
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
function discoveryFailure(result) {
|
|
429
|
+
if (result._tag === "network_error") return {
|
|
430
|
+
url: result.url,
|
|
431
|
+
kind: "network",
|
|
432
|
+
detail: result.error
|
|
433
|
+
};
|
|
434
|
+
if (result._tag === "http_error") return {
|
|
435
|
+
url: result.url,
|
|
436
|
+
kind: "http",
|
|
437
|
+
detail: `HTTP ${result.status}: ${result.statusText}`
|
|
438
|
+
};
|
|
439
|
+
return {
|
|
440
|
+
url: result.url,
|
|
441
|
+
kind: "document",
|
|
442
|
+
detail: result.error._tag
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
async function readRobots(origin, options) {
|
|
446
|
+
const url = `${origin}/robots.txt`;
|
|
447
|
+
const fetcher = options.fetcher ?? globalThis.fetch;
|
|
448
|
+
let response;
|
|
449
|
+
try {
|
|
450
|
+
response = await fetcher(url, {
|
|
451
|
+
headers: { "user-agent": options.userAgent ?? "gscdump sitemap fetcher" },
|
|
452
|
+
signal: requestSignal(options.signal, options.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS)
|
|
453
|
+
});
|
|
454
|
+
} catch (error) {
|
|
455
|
+
return {
|
|
456
|
+
urls: [],
|
|
457
|
+
failure: {
|
|
458
|
+
url,
|
|
459
|
+
kind: "network",
|
|
460
|
+
detail: errorMessage(error)
|
|
461
|
+
}
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
if (response.status === 404 || response.status === 410) return { urls: [] };
|
|
465
|
+
if (!response.ok) return {
|
|
466
|
+
urls: [],
|
|
467
|
+
failure: {
|
|
468
|
+
url,
|
|
469
|
+
kind: "http",
|
|
470
|
+
detail: `HTTP ${response.status}: ${response.statusText}`
|
|
471
|
+
}
|
|
472
|
+
};
|
|
473
|
+
const maxBytes = Math.min(options.maxBytes ?? DEFAULT_MAX_BYTES, 1024 * 1024);
|
|
474
|
+
const body = response.body;
|
|
475
|
+
if (!body) return { urls: [] };
|
|
476
|
+
const decoder = new TextDecoder();
|
|
477
|
+
let bytes = 0;
|
|
478
|
+
let text = "";
|
|
479
|
+
try {
|
|
480
|
+
for await (const chunk of readableStreamChunks(body)) {
|
|
481
|
+
bytes += chunk.byteLength;
|
|
482
|
+
if (bytes > maxBytes) return {
|
|
483
|
+
urls: [],
|
|
484
|
+
failure: {
|
|
485
|
+
url,
|
|
486
|
+
kind: "document",
|
|
487
|
+
detail: `robots.txt exceeds ${maxBytes} bytes`
|
|
488
|
+
}
|
|
489
|
+
};
|
|
490
|
+
text += decoder.decode(chunk, { stream: true });
|
|
491
|
+
}
|
|
492
|
+
text += decoder.decode();
|
|
493
|
+
} catch (error) {
|
|
494
|
+
return {
|
|
495
|
+
urls: [],
|
|
496
|
+
failure: {
|
|
497
|
+
url,
|
|
498
|
+
kind: "network",
|
|
499
|
+
detail: errorMessage(error)
|
|
500
|
+
}
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
return { urls: parseRobotsSitemapUrls(text, origin) };
|
|
504
|
+
}
|
|
505
|
+
async function discoverSitemapResult(site, options = {}) {
|
|
506
|
+
const origin = normalizeSiteOrigin(site);
|
|
507
|
+
if (!origin) return {
|
|
508
|
+
_tag: "incomplete",
|
|
509
|
+
failures: [{
|
|
510
|
+
url: site,
|
|
511
|
+
kind: "document",
|
|
512
|
+
detail: "invalid site URL"
|
|
513
|
+
}]
|
|
514
|
+
};
|
|
515
|
+
const headers = {
|
|
516
|
+
"user-agent": options.userAgent ?? "gscdump sitemap fetcher",
|
|
517
|
+
...options.headers
|
|
518
|
+
};
|
|
519
|
+
const failures = [];
|
|
520
|
+
for (const path of COMMON_SITEMAP_PATHS) {
|
|
521
|
+
const result = await fetchSitemapDocument(`${origin}${path}`, {
|
|
522
|
+
...options,
|
|
523
|
+
headers
|
|
524
|
+
});
|
|
525
|
+
if (result._tag === "ok") return {
|
|
526
|
+
_tag: "found",
|
|
527
|
+
url: result.url,
|
|
528
|
+
source: "common_path"
|
|
529
|
+
};
|
|
530
|
+
if (result._tag !== "not_found" && !(result._tag === "document_error" && (result.error._tag === "html" || result.error._tag === "unsupported_document"))) failures.push(discoveryFailure(result));
|
|
531
|
+
}
|
|
532
|
+
const robots = await readRobots(origin, options);
|
|
533
|
+
if (robots.failure) failures.push(robots.failure);
|
|
534
|
+
for (const url of robots.urls) {
|
|
535
|
+
const result = await fetchSitemapDocument(url, {
|
|
536
|
+
...options,
|
|
537
|
+
headers
|
|
46
538
|
});
|
|
47
|
-
if (
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
539
|
+
if (result._tag === "ok") return {
|
|
540
|
+
_tag: "found",
|
|
541
|
+
url: result.url,
|
|
542
|
+
source: "robots"
|
|
543
|
+
};
|
|
544
|
+
if (result._tag !== "not_found") failures.push(discoveryFailure(result));
|
|
545
|
+
}
|
|
546
|
+
return failures.length > 0 ? {
|
|
547
|
+
_tag: "incomplete",
|
|
548
|
+
failures
|
|
549
|
+
} : { _tag: "not_found" };
|
|
550
|
+
}
|
|
551
|
+
async function discoverSitemap(site, options = {}) {
|
|
552
|
+
const result = await discoverSitemapResult(site, options);
|
|
553
|
+
return result._tag === "found" ? result.url : null;
|
|
554
|
+
}
|
|
555
|
+
function walkFailure(url, depth, error) {
|
|
556
|
+
return {
|
|
557
|
+
url,
|
|
558
|
+
depth,
|
|
559
|
+
error
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
async function walkSitemaps(roots, options = {}) {
|
|
563
|
+
const maxDepth = options.maxDepth ?? DEFAULT_MAX_DEPTH;
|
|
564
|
+
const maxDocuments = options.maxDocuments ?? DEFAULT_MAX_DOCUMENTS;
|
|
565
|
+
const maxUrls = options.maxUrls ?? DEFAULT_MAX_ENTRIES;
|
|
566
|
+
const queue = (typeof roots === "string" ? [roots] : [...roots]).map((url) => ({
|
|
567
|
+
url,
|
|
568
|
+
depth: 0
|
|
569
|
+
}));
|
|
570
|
+
const rootCount = queue.length;
|
|
571
|
+
const seenDocuments = /* @__PURE__ */ new Set();
|
|
572
|
+
const seenUrls = /* @__PURE__ */ new Set();
|
|
573
|
+
const entries = [];
|
|
574
|
+
const failures = [];
|
|
575
|
+
let documentsRead = 0;
|
|
576
|
+
let complete = true;
|
|
577
|
+
let loadedRoot = false;
|
|
578
|
+
let missingRoots = 0;
|
|
579
|
+
while (queue.length > 0) {
|
|
580
|
+
if (documentsRead >= maxDocuments) {
|
|
581
|
+
complete = false;
|
|
582
|
+
break;
|
|
583
|
+
}
|
|
584
|
+
const next = queue.shift();
|
|
585
|
+
if (seenDocuments.has(next.url)) continue;
|
|
586
|
+
seenDocuments.add(next.url);
|
|
587
|
+
const result = await fetchSitemapDocument(next.url, {
|
|
588
|
+
...options,
|
|
589
|
+
maxEntries: Math.max(maxUrls - entries.length, 1)
|
|
590
|
+
});
|
|
591
|
+
if (result._tag !== "ok") {
|
|
592
|
+
if (next.depth === 0 && result._tag === "not_found") {
|
|
593
|
+
missingRoots++;
|
|
594
|
+
continue;
|
|
595
|
+
} else failures.push(walkFailure(next.url, next.depth, result));
|
|
596
|
+
complete = false;
|
|
597
|
+
continue;
|
|
598
|
+
}
|
|
599
|
+
if (next.depth === 0) loadedRoot = true;
|
|
600
|
+
documentsRead++;
|
|
601
|
+
complete &&= result.document.meta.complete;
|
|
602
|
+
if (result.document._tag === "index") {
|
|
603
|
+
if (next.depth >= maxDepth && result.document.entries.length > 0) {
|
|
604
|
+
complete = false;
|
|
605
|
+
continue;
|
|
606
|
+
}
|
|
607
|
+
for (const child of result.document.entries) queue.push({
|
|
608
|
+
url: child.loc,
|
|
609
|
+
depth: next.depth + 1
|
|
610
|
+
});
|
|
611
|
+
continue;
|
|
612
|
+
}
|
|
613
|
+
for (const entry of result.document.entries) {
|
|
614
|
+
if (seenUrls.has(entry.loc)) continue;
|
|
615
|
+
if (entries.length >= maxUrls) {
|
|
616
|
+
complete = false;
|
|
617
|
+
break;
|
|
55
618
|
}
|
|
56
|
-
|
|
619
|
+
seenUrls.add(entry.loc);
|
|
620
|
+
entries.push(entry);
|
|
57
621
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
if (seen.has(u)) continue;
|
|
62
|
-
seen.add(u);
|
|
63
|
-
out.push(u);
|
|
64
|
-
if (limit != null && out.length >= limit) return;
|
|
622
|
+
if (entries.length >= maxUrls && queue.length > 0) {
|
|
623
|
+
complete = false;
|
|
624
|
+
break;
|
|
65
625
|
}
|
|
626
|
+
}
|
|
627
|
+
if (!loadedRoot && missingRoots === rootCount && failures.length === 0) return { _tag: "not_found" };
|
|
628
|
+
if (!loadedRoot) return {
|
|
629
|
+
_tag: "error",
|
|
630
|
+
failures
|
|
631
|
+
};
|
|
632
|
+
return {
|
|
633
|
+
_tag: "ok",
|
|
634
|
+
entries,
|
|
635
|
+
documentsRead,
|
|
636
|
+
complete,
|
|
637
|
+
failures
|
|
66
638
|
};
|
|
67
|
-
await visit(sitemapUrl, 0);
|
|
68
|
-
return out;
|
|
69
639
|
}
|
|
70
|
-
|
|
640
|
+
function legacyWalkFailureMessage(failure) {
|
|
641
|
+
const result = failure.error;
|
|
642
|
+
if (result._tag === "not_found" || result._tag === "http_error") return `Fetch ${failure.url} failed: ${result.status}`;
|
|
643
|
+
if (result._tag === "network_error") return result.error;
|
|
644
|
+
if (result.error._tag === "parse_error") return result.error.error;
|
|
645
|
+
return `Sitemap ${failure.url} failed: ${result.error._tag}`;
|
|
646
|
+
}
|
|
647
|
+
async function fetchSitemapUrls(sitemapUrl, options = {}) {
|
|
648
|
+
const { limit, maxDepth, userAgent, ...fetchOptions } = options;
|
|
649
|
+
const result = await walkSitemaps(sitemapUrl, {
|
|
650
|
+
...fetchOptions,
|
|
651
|
+
headers: {
|
|
652
|
+
"user-agent": userAgent ?? "gscdump sitemap fetcher",
|
|
653
|
+
...fetchOptions.headers
|
|
654
|
+
},
|
|
655
|
+
maxDepth,
|
|
656
|
+
maxDocuments: Number.MAX_SAFE_INTEGER,
|
|
657
|
+
maxUrls: limit ?? Number.MAX_SAFE_INTEGER
|
|
658
|
+
});
|
|
659
|
+
if (result._tag === "not_found") throw new Error(`Fetch ${sitemapUrl} failed: 404`);
|
|
660
|
+
if (result._tag === "error") throw new Error(result.failures[0] ? legacyWalkFailureMessage(result.failures[0]) : "Sitemap fetch failed");
|
|
661
|
+
if (result.failures[0]) throw new Error(legacyWalkFailureMessage(result.failures[0]));
|
|
662
|
+
return result.entries.map((entry) => entry.loc);
|
|
663
|
+
}
|
|
664
|
+
async function sitemapContentHash(entries) {
|
|
665
|
+
const normalized = entries.map((entry) => urlMatchKey(entry.loc)).filter((url) => Boolean(url)).sort();
|
|
666
|
+
const input = new TextEncoder().encode(normalized.join("\n"));
|
|
667
|
+
const hash = await crypto.subtle.digest("SHA-256", input);
|
|
668
|
+
return Array.from(new Uint8Array(hash), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
669
|
+
}
|
|
670
|
+
export { canonicalSitemapIdentity, discoverSitemap, discoverSitemapResult, fetchSitemapDocument, fetchSitemapUrls, parseRobotsSitemapUrls, parseSitemapDocument, sameSitemapIdentity, scopeSitemapRecords, sitemapContentHash, walkSitemaps };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gscdump",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.5.
|
|
4
|
+
"version": "1.5.3",
|
|
5
5
|
"description": "Direct Google Search Console client with typed queries, streaming pagination, URL inspection, and indexing helpers",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Harlan Wilton",
|
|
@@ -52,6 +52,11 @@
|
|
|
52
52
|
"import": "./dist/core/result.mjs",
|
|
53
53
|
"default": "./dist/core/result.mjs"
|
|
54
54
|
},
|
|
55
|
+
"./sitemap": {
|
|
56
|
+
"types": "./dist/sitemap.d.mts",
|
|
57
|
+
"import": "./dist/sitemap.mjs",
|
|
58
|
+
"default": "./dist/sitemap.mjs"
|
|
59
|
+
},
|
|
55
60
|
"./contracts": {
|
|
56
61
|
"types": "./dist/contracts.d.mts",
|
|
57
62
|
"import": "./dist/contracts.mjs",
|
|
@@ -82,8 +87,9 @@
|
|
|
82
87
|
"node": ">=22"
|
|
83
88
|
},
|
|
84
89
|
"dependencies": {
|
|
90
|
+
"@nuxtjs/sitemap": "8.3.0",
|
|
85
91
|
"ofetch": "^1.5.1",
|
|
86
|
-
"@gscdump/contracts": "^1.5.
|
|
92
|
+
"@gscdump/contracts": "^1.5.3"
|
|
87
93
|
},
|
|
88
94
|
"scripts": {
|
|
89
95
|
"dev": "obuild --stub",
|