diff --git a/src/adapters/default/discover.test.ts b/src/adapters/default/discover.test.ts
index 16ad50ab..5ca3da30 100644
--- a/src/adapters/default/discover.test.ts
+++ b/src/adapters/default/discover.test.ts
@@ -46,7 +46,8 @@ describe('discoverDefault', () => {
document.querySelector('#root').innerHTML = '';
+ 'Resume' +
+ 'ResourcesContactGuide';
`);
});
await new Promise((resolve) => server!.listen(0, '127.0.0.1', resolve));
@@ -76,7 +77,7 @@ describe('discoverDefault', () => {
}
response.setHeader('content-type', 'text/html');
response.end(`Example
-
+
`);
});
await new Promise((resolve) => server!.listen(0, '127.0.0.1', resolve));
@@ -95,5 +96,6 @@ describe('discoverDefault', () => {
`${origin}/five`,
`${origin}/aviso-legal`,
]);
+ expect(inventory.navigation).toContainEqual({ text: 'Resume', href: `${origin}/resume.pdf` });
});
});
diff --git a/src/adapters/default/discover.ts b/src/adapters/default/discover.ts
index 9121d5e1..e2832b5c 100644
--- a/src/adapters/default/discover.ts
+++ b/src/adapters/default/discover.ts
@@ -1,4 +1,4 @@
-import { fetchSitemapWithDiagnostics, classifyUrl, extractSameOriginLinks, routeKey } from '../../lib/extraction/sitemap.js';
+import { fetchSitemapWithDiagnostics, classifyUrl, extractSameOriginLinks, resolvePageLink, routeKey } from '../../lib/extraction/sitemap.js';
import { extractMeta, extractTitle, extractNavLinks } from '../../lib/html-extract/index.js';
import { sourceContextOptions, getPlaywright } from '../../lib/browser-kit/browser-kit.js';
import type { InventoryUrl } from '../shared.js';
@@ -78,13 +78,12 @@ export async function discoverDefault(url: string, _opts: Record {
const href = $(el).attr('href')?.trim();
if (!href || href.startsWith('#')) return;
- const resolved = resolveAndFilter(href, baseUrl, baseOrigin);
+ const resolved = resolvePageLink(href, baseUrl, baseOrigin);
if (resolved && !seen.has(resolved)) {
seen.add(resolved);
urls.push(resolved);
@@ -363,7 +363,7 @@ async function crawlRenderedNavLinks(baseUrl: string, baseOrigin: string): Promi
);
const seen = new Set();
return hrefs.flatMap((href) => {
- const resolved = resolveAndFilter(href, baseUrl, baseOrigin);
+ const resolved = resolvePageLink(href, baseUrl, baseOrigin);
if (!resolved || seen.has(resolved)) return [];
seen.add(resolved);
return [resolved];
@@ -376,7 +376,7 @@ async function crawlRenderedNavLinks(baseUrl: string, baseOrigin: string): Promi
}
}
-function resolveAndFilter(href: string, baseUrl: string, baseOrigin: string): string | null {
+export function resolvePageLink(href: string, baseUrl: string, baseOrigin: string): string | null {
try {
const resolved = new URL(href, baseUrl);
if (resolved.protocol !== 'http:' && resolved.protocol !== 'https:') return null;
diff --git a/src/lib/screenshot/resource-capture.test.ts b/src/lib/screenshot/resource-capture.test.ts
index 78374858..a0264c79 100644
--- a/src/lib/screenshot/resource-capture.test.ts
+++ b/src/lib/screenshot/resource-capture.test.ts
@@ -68,15 +68,18 @@ describe( 'CapturedResourceStore', () => {
expect( manifest.icons[ 0 ].src ).toBe( identity.logo.url );
} );
- it( 'exports a linked DOCX as portable bytes beside a missing HTML route', async () => {
+ it.each( [
+ [ 'docx', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'PK\x03\x04word document fixture' ],
+ [ 'PDF', 'application/pdf', '%PDF-1.7\npdf document fixture' ],
+ ] )( 'exports a linked %s as portable bytes beside a missing HTML route', async ( extension, contentType, content ) => {
const outputDir = mkdtempSync( join( tmpdir(), 'dla-linked-docx-' ) );
dirs.push( outputDir );
mkdirSync( join( outputDir, 'html' ) );
mkdirSync( join( outputDir, 'screenshots' ) );
const sourceUrl = 'https://example.com/';
- const download = 'https://example.com/_files/ugd/flyer.docx?dn=Fly%20fishing.docx';
+ const download = `https://example.com/_files/ugd/flyer.${ extension }?dn=Fly%20fishing.${ extension }`;
const missing = 'https://example.com/missing-page';
- const bytes = Buffer.from( 'PK\x03\x04word document fixture' );
+ const bytes = Buffer.from( content );
const html = `FlyerMissing`;
writeFileSync( join( outputDir, 'html', 'home.html' ), html );
writeFileSync( join( outputDir, 'screenshots', 'manifest.json' ), JSON.stringify( {
@@ -84,7 +87,7 @@ describe( 'CapturedResourceStore', () => {
} ) );
const fetchMedia = vi.fn( async ( url: string ) => ( {
finalUrl: url, status: 200,
- headers: new Headers( { 'content-type': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' } ),
+ headers: new Headers( { 'content-type': contentType } ),
body: bytes,
} ) );
const store = new CapturedResourceStore( outputDir, sourceUrl, fetchMedia );
diff --git a/src/lib/screenshot/resource-capture.ts b/src/lib/screenshot/resource-capture.ts
index 270fc68b..ad3f148b 100644
--- a/src/lib/screenshot/resource-capture.ts
+++ b/src/lib/screenshot/resource-capture.ts
@@ -305,11 +305,11 @@ export function isAudioLink( reference: string, documentUrl: string ): boolean {
}
}
-/** Linked Word files are downloads, not navigable HTML routes. */
+/** Linked documents are downloads, not navigable HTML routes. */
export function isDocumentDownloadLink( reference: string, documentUrl: string ): boolean {
try {
const url = new URL( reference.replace( /&/g, '&' ), documentUrl );
- return /^https?:$/.test( url.protocol ) && /\.docx?$/i.test( url.pathname );
+ return /^https?:$/.test( url.protocol ) && /\.(?:pdf|docx?)$/i.test( url.pathname );
} catch {
return false;
}
@@ -662,7 +662,7 @@ export class CapturedResourceStore {
if ( fetched.body.length === 0 && contentType !== 'text/css' )
throw new Error( 'render dependency response body is empty' );
if (
- ! /^(?:text\/css|image\/|audio\/|video\/|font\/|application\/(?:json|manifest\+json|font|x-font|font-woff|octet-stream|msword|vnd\.openxmlformats-officedocument\.wordprocessingml\.document))/i.test(
+ ! /^(?:text\/css|image\/|audio\/|video\/|font\/|application\/(?:json|manifest\+json|font|x-font|font-woff|octet-stream|pdf|msword|vnd\.openxmlformats-officedocument\.wordprocessingml\.document))/i.test(
contentType
)
)