From 5871b6ac194bbbbadb000458114edefcc4f1d032 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcus=20Bl=C3=A4ttermann?= Date: Wed, 11 Feb 2026 15:16:58 +0100 Subject: [PATCH 1/5] fix: replace npmjs links in html --- server/utils/readme.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/server/utils/readme.ts b/server/utils/readme.ts index 8f50a0350..7217d29b7 100644 --- a/server/utils/readme.ts +++ b/server/utils/readme.ts @@ -210,6 +210,16 @@ const isNpmJsUrlThatCanBeRedirected = (url: URL) => { return true } +const replaceHtmlLink = (html: string) => { + return html.replace(/href="([^"]+)"/g, (match, href) => { + if (isNpmJsUrlThatCanBeRedirected(new URL(href, 'https://www.npmjs.com'))) { + const newHref = href.replace(/^https?:\/\/(www\.)?npmjs\.com/, '') + return `href="${newHref}"` + } + return match + }) +} + /** * Resolve a relative URL to an absolute URL. * If repository info is available, resolve to provider's raw file URLs. @@ -436,7 +446,14 @@ ${html} return `
${body}
\n` } - marked.setOptions({ renderer }) + marked.setOptions({ + renderer, + walkTokens: token => { + if (token.type === 'html') { + token.text = replaceHtmlLink(token.text) + } + }, + }) const rawHtml = marked.parse(content) as string From ce5ec88594fda55fe3a1cf28e3655bc85d452b19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcus=20Bl=C3=A4ttermann?= Date: Wed, 11 Feb 2026 15:17:13 +0100 Subject: [PATCH 2/5] fix: remove external link marker from images --- app/components/Readme.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/Readme.vue b/app/components/Readme.vue index 488fce8c2..e613e76e3 100644 --- a/app/components/Readme.vue +++ b/app/components/Readme.vue @@ -144,7 +144,7 @@ function handleClick(event: MouseEvent) { @apply decoration-accent text-accent; } -.readme :deep(a[target='_blank']::after) { +.readme :deep(a[target='_blank']:not(:has(img))::after) { /* I don't know what kind of sorcery this is, but it ensures this icon can't wrap to a new line on its own. */ content: '__'; @apply inline i-carbon:launch rtl-flip ms-1 opacity-50; From 44e6b07a328074c1374e9e0860b527dcc96a452d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcus=20Bl=C3=A4ttermann?= Date: Wed, 11 Feb 2026 15:31:14 +0100 Subject: [PATCH 3/5] feat: add typescript playground links --- server/utils/readme.ts | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/server/utils/readme.ts b/server/utils/readme.ts index 7217d29b7..e9acb7832 100644 --- a/server/utils/readme.ts +++ b/server/utils/readme.ts @@ -13,6 +13,7 @@ interface PlaygroundProvider { id: string // Provider identifier name: string domains: string[] // Associated domains + path?: string icon?: string // Provider icon name } @@ -74,6 +75,13 @@ const PLAYGROUND_PROVIDERS: PlaygroundProvider[] = [ domains: ['vite.new'], icon: 'vite', }, + { + id: 'typescript-playground', + name: 'Typescript Playground', + domains: ['typescriptlang.org'], + path: '/play', + icon: 'typescript', + }, ] /** @@ -86,7 +94,10 @@ function matchPlaygroundProvider(url: string): PlaygroundProvider | null { for (const provider of PLAYGROUND_PROVIDERS) { for (const domain of provider.domains) { - if (hostname === domain || hostname.endsWith(`.${domain}`)) { + if ( + (hostname === domain || hostname.endsWith(`.${domain}`)) && + (!provider.path || parsed.pathname.startsWith(provider.path)) + ) { return provider } } @@ -406,10 +417,6 @@ ${html} const text = this.parser.parseInline(tokens) const titleAttr = title ? ` title="${title}"` : '' - const isExternal = resolvedHref.startsWith('http://') || resolvedHref.startsWith('https://') - const relAttr = isExternal ? ' rel="nofollow noreferrer noopener"' : '' - const targetAttr = isExternal ? ' target="_blank"' : '' - // Check if this is a playground link const provider = matchPlaygroundProvider(resolvedHref) if (provider && !seenUrls.has(resolvedHref)) { @@ -428,7 +435,7 @@ ${html} const hrefValue = resolvedHref.startsWith('#') ? resolvedHref.toLowerCase() : resolvedHref - return `${text}` + return `${text}` } // GitHub-style callouts: > [!NOTE], > [!TIP], etc. From 7bef5a11c23dba652d97214c7e6b39b53f2734a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcus=20Bl=C3=A4ttermann?= Date: Wed, 11 Feb 2026 18:06:32 +0100 Subject: [PATCH 4/5] fix: allow to parse all link data on html links as well --- server/utils/readme.ts | 50 ++++++++++++++++++++++++------------------ 1 file changed, 29 insertions(+), 21 deletions(-) diff --git a/server/utils/readme.ts b/server/utils/readme.ts index e9acb7832..74f758edf 100644 --- a/server/utils/readme.ts +++ b/server/utils/readme.ts @@ -411,31 +411,15 @@ ${html} return `` } - // Resolve link URLs, add security attributes, and collect playground links + // // Resolve link URLs, add security attributes, and collect playground links renderer.link = function ({ href, title, tokens }: Tokens.Link) { - const resolvedHref = resolveUrl(href, packageName, repoInfo) const text = this.parser.parseInline(tokens) const titleAttr = title ? ` title="${title}"` : '' + const plainText = text.replace(/<[^>]*>/g, '').trim() - // Check if this is a playground link - const provider = matchPlaygroundProvider(resolvedHref) - if (provider && !seenUrls.has(resolvedHref)) { - seenUrls.add(resolvedHref) - - // Extract label from link text (strip HTML tags for plain text) - const plainText = text.replace(/<[^>]*>/g, '').trim() - - collectedLinks.push({ - url: resolvedHref, - provider: provider.id, - providerName: provider.name, - label: plainText || title || provider.name, - }) - } - - const hrefValue = resolvedHref.startsWith('#') ? resolvedHref.toLowerCase() : resolvedHref + const intermediateTitleAttr = `${` data-title-intermediate="${plainText || title}"`}` - return `${text}` + return `${text}` } // GitHub-style callouts: > [!NOTE], > [!TIP], etc. @@ -518,11 +502,35 @@ ${html} return { tagName, attribs } }, a: (tagName, attribs) => { + if (!attribs.href) { + return { tagName, attribs } + } + + const resolvedHref = resolveUrl(attribs.href, packageName, repoInfo) + + const provider = matchPlaygroundProvider(resolvedHref) + if (provider && !seenUrls.has(resolvedHref)) { + seenUrls.add(resolvedHref) + + collectedLinks.push({ + url: resolvedHref, + provider: provider.id, + providerName: provider.name, + /** + * We need to set some data attribute before hand because `transformTags` doesn't + * provide the text of the element. This will automatically be removed, because there + * is an allow list for link attributes. + * */ + label: attribs['data-title-intermediate'] || provider.name, + }) + } + // Add security attributes for external links - if (attribs.href && hasProtocol(attribs.href, { acceptRelative: true })) { + if (resolvedHref && hasProtocol(resolvedHref, { acceptRelative: true })) { attribs.rel = 'nofollow noreferrer noopener' attribs.target = '_blank' } + attribs.href = resolvedHref return { tagName, attribs } }, div: prefixId, From 0eccef4606c0bc99bb88126a54c7e9a30ecf71d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcus=20Bl=C3=A4ttermann?= Date: Wed, 11 Feb 2026 18:21:17 +0100 Subject: [PATCH 5/5] fix(typo): capitalisation Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- server/utils/readme.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/utils/readme.ts b/server/utils/readme.ts index 74f758edf..decf44b66 100644 --- a/server/utils/readme.ts +++ b/server/utils/readme.ts @@ -77,7 +77,7 @@ const PLAYGROUND_PROVIDERS: PlaygroundProvider[] = [ }, { id: 'typescript-playground', - name: 'Typescript Playground', + name: 'TypeScript Playground', domains: ['typescriptlang.org'], path: '/play', icon: 'typescript',