@pagebridge/core 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export { GSCClient, type GSCClientOptions, type IndexStatusResult, type IndexVerdict, } from "./gsc-client.js";
2
2
  export { SyncEngine, type SyncOptions, type SyncResult, type IndexStatusSyncResult, type SnapshotInsights, type PublishingImpact, type CannibalizationTarget, type DailyMetricPoint, } from "./sync-engine.js";
3
3
  export { DecayDetector, defaultRules, type DecayRule, type DecaySignal, type QuietPeriodConfig, } from "./decay-detector.js";
4
- export { URLMatcher, type MatchResult, type URLMatcherConfig, type UnmatchReason, type MatchDiagnostics, } from "./url-matcher.js";
4
+ export { URLMatcher, type MatchResult, type URLMatcherConfig, type ContentTypeUrlConfig, type UnmatchReason, type MatchDiagnostics, } from "./url-matcher.js";
5
5
  export { TaskGenerator, type TaskGeneratorOptions, type QueryContext, } from "./task-generator.js";
6
6
  export { QuickWinAnalyzer, type QuickWinQuery, type QuickWinConfig, } from "./quick-win-analyzer.js";
7
7
  export { CtrAnomalyAnalyzer, EXPECTED_CTR_BY_POSITION, type CtrAnomaly, type CtrAnomalySeverity, type CtrAnomalyConfig, type InsightAlert, } from "./ctr-anomaly-analyzer.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,SAAS,EACT,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACtB,KAAK,YAAY,GAClB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,UAAU,EACV,KAAK,WAAW,EAChB,KAAK,UAAU,EACf,KAAK,qBAAqB,EAC1B,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,EACrB,KAAK,qBAAqB,EAC1B,KAAK,gBAAgB,GACtB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,aAAa,EACb,YAAY,EACZ,KAAK,SAAS,EACd,KAAK,WAAW,EAChB,KAAK,iBAAiB,GACvB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,UAAU,EACV,KAAK,WAAW,EAChB,KAAK,gBAAgB,EACrB,KAAK,aAAa,EAClB,KAAK,gBAAgB,GACtB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,aAAa,EACb,KAAK,oBAAoB,EACzB,KAAK,YAAY,GAClB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,gBAAgB,EAChB,KAAK,aAAa,EAClB,KAAK,cAAc,GACpB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EACL,kBAAkB,EAClB,wBAAwB,EACxB,KAAK,UAAU,EACf,KAAK,kBAAkB,EACvB,KAAK,gBAAgB,EACrB,KAAK,YAAY,GAClB,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AACrE,OAAO,EACL,wBAAwB,EACxB,KAAK,YAAY,GAClB,MAAM,iCAAiC,CAAC;AACzC,OAAO,EACL,uBAAuB,EACvB,KAAK,oBAAoB,EACzB,KAAK,qBAAqB,GAC3B,MAAM,+BAA+B,CAAC;AACvC,OAAO,EACL,mBAAmB,EACnB,KAAK,eAAe,EACpB,KAAK,YAAY,EACjB,KAAK,aAAa,EAClB,KAAK,UAAU,EACf,KAAK,qBAAqB,GAC3B,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,SAAS,EACT,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACtB,KAAK,YAAY,GAClB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EACL,UAAU,EACV,KAAK,WAAW,EAChB,KAAK,UAAU,EACf,KAAK,qBAAqB,EAC1B,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,EACrB,KAAK,qBAAqB,EAC1B,KAAK,gBAAgB,GACtB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,aAAa,EACb,YAAY,EACZ,KAAK,SAAS,EACd,KAAK,WAAW,EAChB,KAAK,iBAAiB,GACvB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,UAAU,EACV,KAAK,WAAW,EAChB,KAAK,gBAAgB,EACrB,KAAK,oBAAoB,EACzB,KAAK,aAAa,EAClB,KAAK,gBAAgB,GACtB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,aAAa,EACb,KAAK,oBAAoB,EACzB,KAAK,YAAY,GAClB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,gBAAgB,EAChB,KAAK,aAAa,EAClB,KAAK,cAAc,GACpB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EACL,kBAAkB,EAClB,wBAAwB,EACxB,KAAK,UAAU,EACf,KAAK,kBAAkB,EACvB,KAAK,gBAAgB,EACrB,KAAK,YAAY,GAClB,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AACrE,OAAO,EACL,wBAAwB,EACxB,KAAK,YAAY,GAClB,MAAM,iCAAiC,CAAC;AACzC,OAAO,EACL,uBAAuB,EACvB,KAAK,oBAAoB,EACzB,KAAK,qBAAqB,GAC3B,MAAM,+BAA+B,CAAC;AACvC,OAAO,EACL,mBAAmB,EACnB,KAAK,eAAe,EACpB,KAAK,YAAY,EACjB,KAAK,aAAa,EAClB,KAAK,UAAU,EACf,KAAK,qBAAqB,GAC3B,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC"}
@@ -14,17 +14,37 @@ export interface MatchResult {
14
14
  matchedSlug?: string;
15
15
  unmatchReason: UnmatchReason;
16
16
  extractedSlug?: string;
17
+ matchedContentType?: string;
17
18
  diagnostics?: MatchDiagnostics;
18
19
  }
19
- export interface URLMatcherConfig {
20
- contentTypes: string[];
20
+ /**
21
+ * Configuration for a single content type's URL structure
22
+ */
23
+ export interface ContentTypeUrlConfig {
24
+ contentType: string;
25
+ pathPrefix?: string;
21
26
  slugField: string;
27
+ }
28
+ /**
29
+ * URLMatcher configuration with support for multiple content types and path structures
30
+ * @deprecated If using the old flat structure (contentTypes, slugField, pathPrefix),
31
+ * migrate to urlConfigs for more flexible URL path handling.
32
+ */
33
+ export interface URLMatcherConfig {
34
+ /** New format: array of per-content-type URL configs (recommended) */
35
+ urlConfigs?: ContentTypeUrlConfig[];
36
+ /** @deprecated Use urlConfigs instead. Kept for backward compatibility. */
37
+ contentTypes?: string[];
38
+ /** @deprecated Use urlConfigs instead. Kept for backward compatibility. */
39
+ slugField?: string;
40
+ /** @deprecated Use urlConfigs instead. Kept for backward compatibility. */
22
41
  pathPrefix?: string;
23
42
  baseUrl: string;
24
43
  }
25
44
  export declare class URLMatcher {
26
45
  private sanityClient;
27
46
  private config;
47
+ private normalizedConfigs;
28
48
  constructor(sanityClient: SanityClient, config: URLMatcherConfig);
29
49
  matchUrls(gscUrls: string[]): Promise<MatchResult[]>;
30
50
  /**
@@ -33,7 +53,6 @@ export declare class URLMatcher {
33
53
  getAvailableSlugs(): Promise<string[]>;
34
54
  private matchSingleUrl;
35
55
  private normalizeUrl;
36
- private extractSlug;
37
56
  private extractSlugWithDiagnostics;
38
57
  private escapeRegex;
39
58
  private normalizeSlug;
@@ -1 +1 @@
1
- {"version":3,"file":"url-matcher.d.ts","sourceRoot":"","sources":["../src/url-matcher.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAEnD,MAAM,MAAM,aAAa,GACrB,SAAS,GACT,mBAAmB,GACnB,sBAAsB,GACtB,qBAAqB,CAAC;AAE1B,MAAM,WAAW,gBAAgB;IAC/B,aAAa,EAAE,MAAM,CAAC;IACtB,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,mBAAmB,EAAE,MAAM,CAAC;IAC5B,YAAY,EAAE,MAAM,EAAE,CAAC;CACxB;AAED,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B,UAAU,EAAE,OAAO,GAAG,YAAY,GAAG,OAAO,GAAG,MAAM,CAAC;IACtD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,aAAa,CAAC;IAC7B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,WAAW,CAAC,EAAE,gBAAgB,CAAC;CAChC;AAED,MAAM,WAAW,gBAAgB;IAC/B,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;CACjB;AAQD,qBAAa,UAAU;IAEnB,OAAO,CAAC,YAAY;IACpB,OAAO,CAAC,MAAM;gBADN,YAAY,EAAE,YAAY,EAC1B,MAAM,EAAE,gBAAgB;IAG5B,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;IAyB1D;;OAEG;IACG,iBAAiB,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;IAc5C,OAAO,CAAC,cAAc;IAoGtB,OAAO,CAAC,YAAY;IAYpB,OAAO,CAAC,WAAW;IAInB,OAAO,CAAC,0BAA0B;IA0ClC,OAAO,CAAC,WAAW;IAInB,OAAO,CAAC,aAAa;IAIrB;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAiBxB;;OAEG;IACH,OAAO,CAAC,mBAAmB;CA8B5B"}
1
+ {"version":3,"file":"url-matcher.d.ts","sourceRoot":"","sources":["../src/url-matcher.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAEnD,MAAM,MAAM,aAAa,GACrB,SAAS,GACT,mBAAmB,GACnB,sBAAsB,GACtB,qBAAqB,CAAC;AAE1B,MAAM,WAAW,gBAAgB;IAC/B,aAAa,EAAE,MAAM,CAAC;IACtB,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,mBAAmB,EAAE,MAAM,CAAC;IAC5B,YAAY,EAAE,MAAM,EAAE,CAAC;CACxB;AAED,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B,UAAU,EAAE,OAAO,GAAG,YAAY,GAAG,OAAO,GAAG,MAAM,CAAC;IACtD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,aAAa,CAAC;IAC7B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,WAAW,CAAC,EAAE,gBAAgB,CAAC;CAChC;AAED;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;GAIG;AACH,MAAM,WAAW,gBAAgB;IAC/B,sEAAsE;IACtE,UAAU,CAAC,EAAE,oBAAoB,EAAE,CAAC;IACpC,2EAA2E;IAC3E,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,2EAA2E;IAC3E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,2EAA2E;IAC3E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;CACjB;AASD,qBAAa,UAAU;IAInB,OAAO,CAAC,YAAY;IACpB,OAAO,CAAC,MAAM;IAJhB,OAAO,CAAC,iBAAiB,CAAyB;gBAGxC,YAAY,EAAE,YAAY,EAC1B,MAAM,EAAE,gBAAgB;IAuB5B,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,WAAW,EAAE,CAAC;IAuC1D;;OAEG;IACG,iBAAiB,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;IAsB5C,OAAO,CAAC,cAAc;IA0GtB,OAAO,CAAC,YAAY;IAYpB,OAAO,CAAC,0BAA0B;IA2ClC,OAAO,CAAC,WAAW;IAInB,OAAO,CAAC,aAAa;IAIrB;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAiBxB;;OAEG;IACH,OAAO,CAAC,mBAAmB;CA8B5B"}
@@ -1,130 +1,152 @@
1
1
  export class URLMatcher {
2
2
  sanityClient;
3
3
  config;
4
+ normalizedConfigs;
4
5
  constructor(sanityClient, config) {
5
6
  this.sanityClient = sanityClient;
6
7
  this.config = config;
8
+ // Normalize config: convert old format to new format if needed
9
+ if (config.urlConfigs) {
10
+ this.normalizedConfigs = config.urlConfigs;
11
+ }
12
+ else if (config.contentTypes) {
13
+ // Backward compatibility: convert old flat format to new format
14
+ console.warn("[URLMatcher] Deprecated: contentTypes, slugField, and pathPrefix are deprecated. " +
15
+ "Please use urlConfigs instead for more flexible URL path handling per content type.");
16
+ this.normalizedConfigs = config.contentTypes.map((contentType) => ({
17
+ contentType,
18
+ slugField: config.slugField || "slug",
19
+ pathPrefix: config.pathPrefix,
20
+ }));
21
+ }
22
+ else {
23
+ throw new Error("URLMatcher requires either 'urlConfigs' (new format) or 'contentTypes' (deprecated format)");
24
+ }
7
25
  }
8
26
  async matchUrls(gscUrls) {
9
- const query = `*[_type in $types]{
10
- _id,
11
- _type,
12
- "${this.config.slugField}": ${this.config.slugField}.current,
13
- _createdAt
14
- }`;
15
- const documents = await this.sanityClient.fetch(query, {
16
- types: this.config.contentTypes,
17
- });
18
- const slugToDoc = new Map();
27
+ // Query documents for each content type with its configured slug field
28
+ const slugToDocMap = new Map();
19
29
  const allSlugs = [];
20
- for (const doc of documents) {
21
- const slug = doc[this.config.slugField];
22
- if (slug) {
23
- const normalized = this.normalizeSlug(slug);
24
- slugToDoc.set(normalized, doc);
25
- allSlugs.push(normalized);
30
+ for (const urlConfig of this.normalizedConfigs) {
31
+ const query = `*[_type == $type]{
32
+ _id,
33
+ _type,
34
+ "${urlConfig.slugField}": ${urlConfig.slugField}.current,
35
+ _createdAt
36
+ }`;
37
+ const documents = await this.sanityClient.fetch(query, {
38
+ type: urlConfig.contentType,
39
+ });
40
+ for (const doc of documents) {
41
+ const slug = doc[urlConfig.slugField];
42
+ if (slug) {
43
+ const normalized = this.normalizeSlug(slug);
44
+ const key = `${urlConfig.contentType}:${normalized}`;
45
+ slugToDocMap.set(key, {
46
+ _id: doc._id,
47
+ _createdAt: doc._createdAt,
48
+ contentType: urlConfig.contentType,
49
+ });
50
+ allSlugs.push(normalized);
51
+ }
26
52
  }
27
53
  }
28
- return gscUrls.map((url) => this.matchSingleUrl(url, slugToDoc, allSlugs));
54
+ return gscUrls.map((url) => this.matchSingleUrl(url, slugToDocMap, allSlugs));
29
55
  }
30
56
  /**
31
57
  * Get all available slugs from Sanity for diagnostic purposes
32
58
  */
33
59
  async getAvailableSlugs() {
34
- const query = `*[_type in $types]{
35
- "${this.config.slugField}": ${this.config.slugField}.current
36
- }`;
37
- const documents = await this.sanityClient.fetch(query, {
38
- types: this.config.contentTypes,
39
- });
40
- return documents
41
- .map((doc) => doc[this.config.slugField])
42
- .filter((slug) => !!slug)
43
- .map((slug) => this.normalizeSlug(slug));
60
+ const allSlugs = [];
61
+ for (const urlConfig of this.normalizedConfigs) {
62
+ const query = `*[_type == $type]{
63
+ "${urlConfig.slugField}": ${urlConfig.slugField}.current
64
+ }`;
65
+ const documents = await this.sanityClient.fetch(query, {
66
+ type: urlConfig.contentType,
67
+ });
68
+ const slugs = documents
69
+ .map((doc) => doc[urlConfig.slugField])
70
+ .filter((slug) => !!slug)
71
+ .map((slug) => this.normalizeSlug(slug));
72
+ allSlugs.push(...slugs);
73
+ }
74
+ return allSlugs;
44
75
  }
45
- matchSingleUrl(gscUrl, slugToDoc, allSlugs) {
76
+ matchSingleUrl(gscUrl, slugToDocMap, allSlugs) {
46
77
  const normalized = this.normalizeUrl(gscUrl);
47
- const extractionResult = this.extractSlugWithDiagnostics(normalized);
48
- // Check if URL is outside path prefix
49
- if (extractionResult.outsidePrefix) {
50
- return {
51
- gscUrl,
52
- sanityId: undefined,
53
- confidence: "none",
54
- unmatchReason: "outside_path_prefix",
55
- diagnostics: {
56
- normalizedUrl: normalized,
57
- pathAfterPrefix: null,
58
- configuredPrefix: this.config.pathPrefix ?? null,
59
- availableSlugsCount: slugToDoc.size,
60
- similarSlugs: [],
61
- },
62
- };
63
- }
64
- const slug = extractionResult.slug;
65
- if (!slug) {
66
- return {
67
- gscUrl,
68
- sanityId: undefined,
69
- confidence: "none",
70
- unmatchReason: "no_slug_extracted",
71
- diagnostics: {
72
- normalizedUrl: normalized,
73
- pathAfterPrefix: extractionResult.pathAfterPrefix,
74
- configuredPrefix: this.config.pathPrefix ?? null,
75
- availableSlugsCount: slugToDoc.size,
76
- similarSlugs: [],
77
- },
78
- };
79
- }
80
- const exactMatch = slugToDoc.get(slug);
81
- if (exactMatch) {
82
- return {
83
- gscUrl,
84
- sanityId: exactMatch._id,
85
- confidence: "exact",
86
- matchedSlug: slug,
87
- unmatchReason: "matched",
88
- extractedSlug: slug,
89
- };
90
- }
91
- const withoutTrailing = slug.replace(/\/$/, "");
92
- const trailingMatch = slugToDoc.get(withoutTrailing);
93
- if (trailingMatch) {
94
- return {
95
- gscUrl,
96
- sanityId: trailingMatch._id,
97
- confidence: "normalized",
98
- matchedSlug: withoutTrailing,
99
- unmatchReason: "matched",
100
- extractedSlug: slug,
101
- };
102
- }
103
- const withTrailing = slug + "/";
104
- const addedTrailingMatch = slugToDoc.get(withTrailing);
105
- if (addedTrailingMatch) {
106
- return {
107
- gscUrl,
108
- sanityId: addedTrailingMatch._id,
109
- confidence: "normalized",
110
- matchedSlug: withTrailing,
111
- unmatchReason: "matched",
112
- extractedSlug: slug,
113
- };
78
+ // Try to match against each content type's path prefix
79
+ for (const urlConfig of this.normalizedConfigs) {
80
+ const extractionResult = this.extractSlugWithDiagnostics(normalized, urlConfig.pathPrefix);
81
+ if (extractionResult.outsidePrefix) {
82
+ continue; // Try next content type
83
+ }
84
+ const slug = extractionResult.slug;
85
+ if (!slug) {
86
+ continue; // Try next content type
87
+ }
88
+ // Try exact match
89
+ const key = `${urlConfig.contentType}:${slug}`;
90
+ const exactMatch = slugToDocMap.get(key);
91
+ if (exactMatch) {
92
+ return {
93
+ gscUrl,
94
+ sanityId: exactMatch._id,
95
+ confidence: "exact",
96
+ matchedSlug: slug,
97
+ matchedContentType: urlConfig.contentType,
98
+ unmatchReason: "matched",
99
+ extractedSlug: slug,
100
+ };
101
+ }
102
+ // Try without trailing slash
103
+ const withoutTrailing = slug.replace(/\/$/, "");
104
+ const keyWithoutTrailing = `${urlConfig.contentType}:${withoutTrailing}`;
105
+ const trailingMatch = slugToDocMap.get(keyWithoutTrailing);
106
+ if (trailingMatch) {
107
+ return {
108
+ gscUrl,
109
+ sanityId: trailingMatch._id,
110
+ confidence: "normalized",
111
+ matchedSlug: withoutTrailing,
112
+ matchedContentType: urlConfig.contentType,
113
+ unmatchReason: "matched",
114
+ extractedSlug: slug,
115
+ };
116
+ }
117
+ // Try with trailing slash
118
+ const withTrailing = slug + "/";
119
+ const keyWithTrailing = `${urlConfig.contentType}:${withTrailing}`;
120
+ const addedTrailingMatch = slugToDocMap.get(keyWithTrailing);
121
+ if (addedTrailingMatch) {
122
+ return {
123
+ gscUrl,
124
+ sanityId: addedTrailingMatch._id,
125
+ confidence: "normalized",
126
+ matchedSlug: withTrailing,
127
+ matchedContentType: urlConfig.contentType,
128
+ unmatchReason: "matched",
129
+ extractedSlug: slug,
130
+ };
131
+ }
114
132
  }
115
- // No match found - find similar slugs for suggestions
116
- const similarSlugs = this.findSimilarSlugs(slug, allSlugs, 3);
133
+ // No match found across any content type - return diagnostic info
134
+ const firstConfig = this.normalizedConfigs[0];
135
+ const firstExtractionResult = this.extractSlugWithDiagnostics(normalized, firstConfig?.pathPrefix);
136
+ const similarSlugs = this.findSimilarSlugs(firstExtractionResult.slug || "", allSlugs, 3);
117
137
  return {
118
138
  gscUrl,
119
139
  sanityId: undefined,
120
140
  confidence: "none",
121
- unmatchReason: "no_matching_document",
122
- extractedSlug: slug,
141
+ unmatchReason: firstExtractionResult.outsidePrefix
142
+ ? "outside_path_prefix"
143
+ : "no_matching_document",
144
+ extractedSlug: firstExtractionResult.slug,
123
145
  diagnostics: {
124
146
  normalizedUrl: normalized,
125
- pathAfterPrefix: extractionResult.pathAfterPrefix,
126
- configuredPrefix: this.config.pathPrefix ?? null,
127
- availableSlugsCount: slugToDoc.size,
147
+ pathAfterPrefix: firstExtractionResult.pathAfterPrefix,
148
+ configuredPrefix: firstConfig?.pathPrefix ?? null,
149
+ availableSlugsCount: slugToDocMap.size,
128
150
  similarSlugs,
129
151
  },
130
152
  };
@@ -141,16 +163,13 @@ export class URLMatcher {
141
163
  return url.toLowerCase();
142
164
  }
143
165
  }
144
- extractSlug(normalizedUrl) {
145
- return this.extractSlugWithDiagnostics(normalizedUrl).slug;
146
- }
147
- extractSlugWithDiagnostics(normalizedUrl) {
166
+ extractSlugWithDiagnostics(normalizedUrl, pathPrefix) {
148
167
  try {
149
168
  const parsed = new URL(normalizedUrl);
150
169
  let path = parsed.pathname;
151
170
  // Check if the URL is outside the configured path prefix
152
- if (this.config.pathPrefix) {
153
- const prefixRegex = new RegExp(`^${this.escapeRegex(this.config.pathPrefix)}(/|$)`);
171
+ if (pathPrefix) {
172
+ const prefixRegex = new RegExp(`^${this.escapeRegex(pathPrefix)}(/|$)`);
154
173
  if (!prefixRegex.test(path)) {
155
174
  return {
156
175
  slug: undefined,
@@ -158,7 +177,7 @@ export class URLMatcher {
158
177
  outsidePrefix: true,
159
178
  };
160
179
  }
161
- path = path.replace(new RegExp(`^${this.escapeRegex(this.config.pathPrefix)}`), "");
180
+ path = path.replace(new RegExp(`^${this.escapeRegex(pathPrefix)}`), "");
162
181
  }
163
182
  const slug = path.replace(/^\/+|\/+$/g, "");
164
183
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pagebridge/core",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Core engine for PageBridge — GSC sync, URL matching, content decay detection, and task generation",
5
5
  "license": "MIT",
6
6
  "private": false,