Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/sites/opensea/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { resolveOpenSeaCollectionFromApi } from './pages/api';
import { parseOpenSeaCollection } from './pages/collection';
import { extractOpenSeaEmbeddedItems, extractOpenSeaEmbeddedItemTokens } from './pages/embedded-items';
import { parseOpenSeaItem } from './pages/item';
import { extractOpenSeaCollectionTitle } from './pages/title';
import { resolveOpenSeaArtworkSources } from './pages/source';

/**
Expand All @@ -26,6 +27,9 @@ export const openSeaAdapter: SourceSiteAdapter = {
}
return extractOpenSeaEmbeddedItems(html);
},
extractTitleFromHtml(url: URL, html: string, parsed: ParsedFindInput | null): string | null {
return extractOpenSeaCollectionTitle(url, html, parsed);
},
extractTokensFromHtml(url: URL, html: string): readonly ParsedFindInput[] {
if (parseOpenSeaCollection(url)?.kind !== 'os-collection') {
return [];
Expand Down
102 changes: 102 additions & 0 deletions src/sites/opensea/pages/title.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import type { ParsedFindInput } from '../../../types';

const NAME_WINDOW_CHARS = 500;
const NAME_PATTERN = /"name"\s*:\s*"((?:[^"\\]|\\.)*)"/g;

/**
* extractOpenSeaCollectionTitle returns the collection's own name for a
* collection page, never the page `<title>` verbatim.
*
* OpenSea's collection `<title>` embeds the floor price and page furniture
* ("PXL NET 0.1904 ETH - Collection | OpenSea"), so a title taken from it
* persists a price that is stale the moment it is stored. The collection's
* real name rides the same embedded JSON payloads the keyless item extraction
* already scans: the collection object carries adjacent `"name"` and
* `"slug"` fields, and the slug is known from the parsed URL.
*
* Fallback order matters: embedded name first (exact), then a `<title>`
* scrubbed of the price/"- Collection" furniture (close), then null so the
* caller's generic title chain keeps its existing behavior.
*/
export function extractOpenSeaCollectionTitle(
_url: URL,
html: string,
parsed: ParsedFindInput | null
): string | null {
if (parsed?.kind !== 'os-collection') {
return null;
}
return embeddedCollectionName(html, parsed.slug) ?? scrubbedCollectionHtmlTitle(html);
}

/**
* embeddedCollectionName finds the `"name"` nearest to `"slug":"<slug>"` in
* the page's embedded JSON. The window bound keeps a payment-token or item
* name elsewhere in the payload from being read as the collection's.
*/
function embeddedCollectionName(html: string, slug: string): string | null {
const slugMarker = `"slug":"${slug}"`;
let searchFrom = 0;
for (;;) {
const slugIndex = html.indexOf(slugMarker, searchFrom);
if (slugIndex < 0) {
return null;
}
const windowStart = Math.max(0, slugIndex - NAME_WINDOW_CHARS);
const window = html.slice(windowStart, slugIndex + slugMarker.length + NAME_WINDOW_CHARS);
const name = nearestName(window, slugIndex - windowStart);
if (name) {
return name;
}
searchFrom = slugIndex + slugMarker.length;
}
}

function nearestName(window: string, slugOffset: number): string | null {
let best: { distance: number; raw: string } | null = null;
NAME_PATTERN.lastIndex = 0;
for (;;) {
const match = NAME_PATTERN.exec(window);
if (!match) {
break;
}
const distance = Math.abs(match.index - slugOffset);
if (!best || distance < best.distance) {
best = { distance, raw: match[1] };
}
}
if (!best) {
return null;
}
try {
const decoded = JSON.parse(`"${best.raw}"`) as string;
const trimmed = decoded.trim();
return trimmed.length > 0 ? trimmed : null;
} catch {
return null;
}
}

/**
* scrubbedCollectionHtmlTitle strips the collection-page furniture from a
* `<title>` when no embedded name was found: the "| OpenSea" suffix, then a
* trailing floor price + "- Collection" segment.
*/
function scrubbedCollectionHtmlTitle(html: string): string | null {
const titleMatch = /<title[^>]*>([^<]*)<\/title>/i.exec(html);
if (!titleMatch) {
return null;
}
let title = titleMatch[1].trim();
title = title.replace(/\s*\|\s*OpenSea\s*$/i, '').trim();
const withoutFurniture = title
.replace(/\s+[\d.,]+\s*ETH\s*[-–—]\s*Collection\s*$/i, '')
.replace(/\s*[-–—]\s*Collection\s*$/i, '')
.trim();
if (withoutFurniture.length === 0 || withoutFurniture === title) {
// No collection furniture found: leave the generic title chain to apply
// its own marketplace suffix handling unchanged.
return null;
}
return withoutFurniture;
}
35 changes: 35 additions & 0 deletions tests/source-resolver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1083,6 +1083,41 @@ describe('resolveTokenInfos collection support', () => {
assert.equal(result.title, 'Chromie Squiggle by Snowfro');
});

test('OpenSea collection prefers the embedded collection name over the page title', async () => {
const html = [
'<html><head><title>PXL NET 0.1904 ETH - Collection | OpenSea</title></head><body>',
'<script>{"collection":{"name":"PXL NET","slug":"pxl-net","__typename":"CollectionType"}}</script>',
openSeaCollectionItems(openSeaItem('ethereum', OPENSEA_COLLECTION_CONTRACT, '97')),
'</body></html>',
].join('');
const result = await resolveTokenInfos('https://opensea.io/collection/pxl-net', {
fetch: htmlFetch(html) as typeof fetch,
});

assert.equal(result.kind, 'tokens');
if (result.kind !== 'tokens') {
throw new Error('narrowing');
}
assert.equal(result.title, 'PXL NET');
});

test('OpenSea collection scrubs floor price and furniture from a title fallback', async () => {
const html = [
'<html><head><title>PXL NET 0.1904 ETH - Collection | OpenSea</title></head><body>',
openSeaCollectionItems(openSeaItem('ethereum', OPENSEA_COLLECTION_CONTRACT, '97')),
'</body></html>',
].join('');
const result = await resolveTokenInfos('https://opensea.io/collection/pxl-net', {
fetch: htmlFetch(html) as typeof fetch,
});

assert.equal(result.kind, 'tokens');
if (result.kind !== 'tokens') {
throw new Error('narrowing');
}
assert.equal(result.title, 'PXL NET');
});

test('OpenSea collection does not call the authenticated collection NFTs API', async () => {
const html = openSeaCollectionItems(openSeaItem('ethereum', OPENSEA_COLLECTION_CONTRACT, '97'));
const calledUrls: string[] = [];
Expand Down
Loading