Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 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
41 changes: 10 additions & 31 deletions app/components/Readme.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ defineProps<{
html: string
}>()

const router = useRouter()
const { copy } = useClipboard()

// Combined click handler for:
Expand Down Expand Up @@ -39,29 +38,6 @@ function handleClick(event: MouseEvent) {
}, 2000)
return
}

// Handle npmjs.com link clicks - route internally
const anchor = target.closest('a')
if (!anchor) return

const href = anchor.getAttribute('href')
if (!href) return

// Handle relative anchor links
if (href.startsWith('#')) {
event.preventDefault()
router.push(href)
return
}

const match = href.match(/^(?:https?:\/\/)?(?:www\.)?npmjs\.(?:com|org)(\/.+)$/)
if (!match || !match[1]) return

const route = router.resolve(match[1])
if (route) {
event.preventDefault()
router.push(route)
}
}
</script>

Expand Down Expand Up @@ -141,15 +117,18 @@ function handleClick(event: MouseEvent) {
}

.readme :deep(a) {
color: var(--fg);
text-decoration: underline;
text-underline-offset: 4px;
text-decoration-color: var(--fg-subtle);
transition: text-decoration-color 0.2s ease;
@apply underline-offset-[0.2rem] underline decoration-1 decoration-fg/30 font-mono text-fg transition-colors duration-200;
}

.readme :deep(a:hover) {
text-decoration-color: var(--accent);
@apply decoration-accent text-accent;
}
.readme :deep(a:focus-visible) {
@apply decoration-accent text-accent;
}

.readme :deep(a[target='_blank']::after) {
content: '';
@apply i-carbon:launch rtl-flip ms-1 -align-10% size-[1em] opacity-50;
}
Comment thread
essenmitsosse marked this conversation as resolved.

.readme :deep(code) {
Expand Down
31 changes: 31 additions & 0 deletions server/utils/readme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,33 @@ function slugify(text: string): string {
.replace(/^-|-$/g, '') // Trim leading/trailing hyphens
}

/** These path on npmjs.com don't belong to packages or search, so we shouldn't try to replace them with npmx.dev urls */
const reservedPathsNpmJs = [
'products',
'login',
'signup',
'advisories',
'blog',
'about',
'press',
'policies',
]

const isNpmJsUrlThatCanBeRedirected = (url: URL) => {
if (url.host !== 'www.npmjs.com' && url.host !== 'npmjs.com') {
return false
}

if (
url.pathname === '/' ||
reservedPathsNpmJs.some(path => url.pathname.startsWith(`/${path}`))
) {
return false
}

return true
}

/**
* Resolve a relative URL to an absolute URL.
* If repository info is available, resolve to provider's raw file URLs.
Expand All @@ -199,6 +226,10 @@ function resolveUrl(url: string, packageName: string, repoInfo?: RepositoryInfo)
try {
const parsed = new URL(url, 'https://example.com')
if (parsed.protocol === 'http:' || parsed.protocol === 'https:') {
// Redirect npmjs urls to ourself
if (isNpmJsUrlThatCanBeRedirected(parsed)) {
return parsed.pathname + parsed.search + parsed.hash
}
return url
Comment thread
essenmitsosse marked this conversation as resolved.
}
} catch {
Expand Down
23 changes: 23 additions & 0 deletions test/unit/server/utils/readme.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,29 @@ describe('Markdown File URL Resolution', () => {
)
})
})

describe('npm.js urls', () => {
it('redirects npmjs.com urls to local', async () => {
const markdown = `[Some npmjs.com link](https://www.npmjs.com/package/test-pkg)`
const result = await renderReadmeHtml(markdown, 'test-pkg')

expect(result.html).toContain('href="/package/test-pkg"')
})

it('redirects npmjs.com urls to local (no www and http)', async () => {
const markdown = `[Some npmjs.com link](http://npmjs.com/package/test-pkg)`
const result = await renderReadmeHtml(markdown, 'test-pkg')

expect(result.html).toContain('href="/package/test-pkg"')
})

it('does not redirect npmjs.com to local if they are in the list of exceptions', async () => {
const markdown = `[Root Contributing](https://www.npmjs.com/products)`
const result = await renderReadmeHtml(markdown, 'test-pkg')

expect(result.html).toContain('href="https://www.npmjs.com/products"')
})
})
})

describe('Markdown Content Extraction', () => {
Expand Down
Loading