|
@@ -1,6 +1,7 @@
|
|
|
import axios from 'axios';
|
|
import axios from 'axios';
|
|
|
import * as cheerio from 'cheerio';
|
|
import * as cheerio from 'cheerio';
|
|
|
import { logger } from '../utils/logger';
|
|
import { logger } from '../utils/logger';
|
|
|
|
|
+import { normalizeUrl } from '../utils/url';
|
|
|
|
|
|
|
|
export interface DiscoveredUrl {
|
|
export interface DiscoveredUrl {
|
|
|
url: string;
|
|
url: string;
|
|
@@ -8,11 +9,6 @@ export interface DiscoveredUrl {
|
|
|
foundOn: string;
|
|
foundOn: string;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-const TRACKING_PARAMS = new Set([
|
|
|
|
|
- 'utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',
|
|
|
|
|
- 'fbclid', 'gclid', 'ref',
|
|
|
|
|
-]);
|
|
|
|
|
-
|
|
|
|
|
const SKIP_EXTENSIONS = new Set([
|
|
const SKIP_EXTENSIONS = new Set([
|
|
|
'.js', '.css', '.png', '.jpg', '.jpeg', '.gif', '.svg', '.ico', '.pdf',
|
|
'.js', '.css', '.png', '.jpg', '.jpeg', '.gif', '.svg', '.ico', '.pdf',
|
|
|
'.zip', '.woff', '.woff2', '.ttf', '.eot', '.mp4', '.mp3', '.webp',
|
|
'.zip', '.woff', '.woff2', '.ttf', '.eot', '.mp4', '.mp3', '.webp',
|
|
@@ -28,37 +24,13 @@ const SKIP_PATH_PATTERNS = [
|
|
|
const PAGINATION_RE = /\/page\/\d+/;
|
|
const PAGINATION_RE = /\/page\/\d+/;
|
|
|
|
|
|
|
|
export class LinkDiscovery {
|
|
export class LinkDiscovery {
|
|
|
|
|
+ // Normalize relative-to-origin and then constrain to same-origin. Normalization
|
|
|
|
|
+ // rules live in src/utils/url.ts so sitemap and link-discovery URLs share a form.
|
|
|
private normalizeUrl(raw: string, origin: string): string | null {
|
|
private normalizeUrl(raw: string, origin: string): string | null {
|
|
|
- let parsed: URL;
|
|
|
|
|
- try {
|
|
|
|
|
- parsed = new URL(raw, origin);
|
|
|
|
|
- } catch {
|
|
|
|
|
- return null;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (parsed.origin !== origin) {
|
|
|
|
|
- return null;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Strip fragment
|
|
|
|
|
- parsed.hash = '';
|
|
|
|
|
-
|
|
|
|
|
- // Remove tracking params
|
|
|
|
|
- for (const param of TRACKING_PARAMS) {
|
|
|
|
|
- parsed.searchParams.delete(param);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Sort remaining params for consistency
|
|
|
|
|
- parsed.searchParams.sort();
|
|
|
|
|
-
|
|
|
|
|
- let href = parsed.href;
|
|
|
|
|
-
|
|
|
|
|
- // Collapse trailing slashes but keep root /
|
|
|
|
|
- if (href !== origin + '/' && href.endsWith('/')) {
|
|
|
|
|
- href = href.replace(/\/+$/, '');
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- return href;
|
|
|
|
|
|
|
+ const normalized = normalizeUrl(raw, origin);
|
|
|
|
|
+ if (!normalized) return null;
|
|
|
|
|
+ if (new URL(normalized).origin !== origin) return null;
|
|
|
|
|
+ return normalized;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private shouldSkip(url: string): boolean {
|
|
private shouldSkip(url: string): boolean {
|