Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
c0a23b4
now able to detect whether gh releases are biing used by a package.
WilcoSp Feb 4, 2026
4873b1c
Merge branch 'npmx-dev:main' into feat/changelog-1
WilcoSp Feb 4, 2026
92193b5
moved has changelog to it's own api endpoint
WilcoSp Feb 7, 2026
2a397bf
added detection of a changelog file on the root of the github repo
WilcoSp Feb 7, 2026
4cfbbfe
Merge branch main into feat/changelog-1
WilcoSp Feb 7, 2026
42802dc
Merge branch 'npmx-dev:main' into feat/changelog-1
WilcoSp Feb 7, 2026
9c130bd
github releases are now being show at /package-changes (rendering of …
WilcoSp Feb 8, 2026
a424459
Merge branch 'feat/changelog-1' of github.com:WilcoSp/npmx.dev into f…
WilcoSp Feb 8, 2026
e487292
Merge branch 'npmx-dev:main' into feat/changelog-1
WilcoSp Feb 8, 2026
6c7d817
Merge branch 'npmx-dev:main' into feat/changelog-1
WilcoSp Feb 11, 2026
ba81215
fixed issue where markdown from github releases could be null
WilcoSp Feb 11, 2026
8d22cf2
some refactoring
WilcoSp Feb 11, 2026
f8b32d4
adding a11y test for changelog card.
WilcoSp Feb 11, 2026
b345df1
markdown is now being rendered (settings need to be added still)
WilcoSp Feb 12, 2026
f4c84ec
Merge branch 'npmx-dev:main' into feat/changelog-1
WilcoSp Feb 12, 2026
bc0fa2f
added headings handling and toc (only scroll behavior needs to be cha…
WilcoSp Feb 12, 2026
5e32d04
Merge branch 'feat/changelog-1' of github.com:WilcoSp/npmx.dev into f…
WilcoSp Feb 12, 2026
faf6b3b
fallback to tag for title if name is null
WilcoSp Feb 13, 2026
14c50da
removing   from toc slug and text
WilcoSp Feb 13, 2026
5cd51af
adding id for navigation for when it gets implemented
WilcoSp Feb 16, 2026
53cd46f
code blocks, blockquote & links are now handled by the markdown renderer
WilcoSp Feb 16, 2026
30d2663
new lines rendering is now closing to how it's on github. at least fo…
WilcoSp Feb 16, 2026
8d5ee71
enableling gfm for changelogs
WilcoSp Feb 20, 2026
34faad0
the changelog info api endpoint will now give the info when a changel…
WilcoSp Feb 20, 2026
55d45f3
now also able to render changelog markdown files
WilcoSp Feb 20, 2026
9c36c60
Merge branch 'main' into feat/changelog-1
WilcoSp Feb 20, 2026
95e3699
added heading links for changelog markdown
WilcoSp Feb 21, 2026
9f9195d
release card header links can now also be clicked to navigate to
WilcoSp Feb 21, 2026
f2d5f2b
Merge branch 'npmx-dev:main' into feat/changelog-1
WilcoSp Feb 22, 2026
cab3cc6
when the latest gh release contains a link to a changelog.md the chan…
WilcoSp Feb 22, 2026
342adca
Merge branch 'main' into feat/changelog-1
WilcoSp Feb 22, 2026
1a8c342
re exporting slugify
WilcoSp Feb 22, 2026
fbbff33
Merge branch 'npmx-dev:main' into feat/changelog-1
WilcoSp Feb 23, 2026
08717bc
adding back removing non bracking spaces, :emoji: support now also ad…
WilcoSp Feb 23, 2026
e07af6c
added indication that release is pre-release/draft
WilcoSp Feb 23, 2026
8225654
Merge branch 'npmx-dev:main' into feat/changelog-1
WilcoSp Feb 24, 2026
013654c
preventing package@version from being turned into mailto
WilcoSp Feb 24, 2026
6cbe6e8
changed how content-none is applied for changelog.md
WilcoSp Feb 24, 2026
1d93405
for changelog.md now resolving url
WilcoSp Feb 24, 2026
f6ec86e
relative/absolute resolved urls will stay within the base url
WilcoSp Feb 24, 2026
6198a71
id with release id will now be applied to all elements
WilcoSp Feb 24, 2026
2c057b9
Added button to view changelog/releases on the provider website.
WilcoSp Feb 25, 2026
ce34993
added a div which reservers the space for toc
WilcoSp Feb 25, 2026
fd15403
Merge branch 'main' into feat/changelog-1
WilcoSp Feb 25, 2026
04b2a99
adding back .tolowercase in slugify to have test pass again
WilcoSp Feb 25, 2026
746464f
Merge branch 'main' into feat/changelog-1
WilcoSp Feb 26, 2026
750072c
fixing some (minor) things caught by coderabbitai
WilcoSp Feb 26, 2026
6e6ee3a
..
WilcoSp Feb 26, 2026
0cab1e8
improving intermediateTitleAttr
WilcoSp Feb 26, 2026
4de0348
removing unneeded comment
WilcoSp Feb 26, 2026
5f4520f
removing resolving explicitly ./ & ../ comment
WilcoSp Feb 26, 2026
81f68b6
Merge branch 'main' into feat/changelog-1
WilcoSp Feb 26, 2026
6d0edfd
[autofix.ci] apply automated fixes
autofix-ci[bot] Feb 26, 2026
f9c0736
adding view_on translation to kn_IN (autofix removed it)
WilcoSp Feb 26, 2026
d48654a
Merge branch 'main' into feat/changelog-1
WilcoSp Feb 27, 2026
a67f039
Merge branch 'main' into feat/changelog-1
WilcoSp Feb 27, 2026
08828b8
adding ogImage to package changes page
WilcoSp Feb 27, 2026
5f1be8a
fixed version selector by adding latestVersion
WilcoSp Feb 27, 2026
d71dfd9
Merge branch 'npmx-dev:main' into feat/changelog-1
WilcoSp Feb 28, 2026
503b4df
for releases the date is now being used to send you as close as possi…
WilcoSp Feb 28, 2026
cfc8612
removing unused pending variable
WilcoSp Feb 28, 2026
e2fc2d2
removing console log
WilcoSp Feb 28, 2026
47431f5
switching from encodeURIComponent to encodeURI for changelog card navid
WilcoSp Feb 28, 2026
c43514b
Merge branch 'main' into feat/changelog-1
WilcoSp Mar 1, 2026
ef72002
disableling ogImage for package changes page as it has been changed a…
WilcoSp Mar 1, 2026
886dd9f
Merge branch 'main' into feat/changelog-1
WilcoSp Mar 1, 2026
40e8f2b
[autofix.ci] apply automated fixes
autofix-ci[bot] Mar 1, 2026
299b45c
adding view_on to fr_TR
WilcoSp Mar 1, 2026
2de6f9f
Merge branch 'main' into feat/changelog-1
WilcoSp Mar 2, 2026
6915b33
Merge branch 'npmx-dev:main' into feat/changelog-1
WilcoSp Mar 3, 2026
89cae7e
for releases now also matching on version in release title for scrolling
WilcoSp Mar 3, 2026
bdcf1a7
Merge branch 'feat/changelog-1' of github.com:WilcoSp/npmx.dev into f…
WilcoSp Mar 3, 2026
133aa39
now also matching to version with changelog.md
WilcoSp Mar 3, 2026
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
66 changes: 66 additions & 0 deletions app/components/Changelog/Card.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<script setup lang="ts">
import type { ReleaseData } from '~~/shared/types/changelog'

const { release } = defineProps<{
release: ReleaseData
}>()
const formattedDate = computed(() => {
if (!release.publishedAt) {
return
}
return new Date(release.publishedAt).toISOString().split('T')[0]
})

const cardId = computed(() => (release.publishedAt ? `date-${formattedDate.value}` : undefined))

const navId = computed(() => `release-${encodeURI(release.title)}`)

function navigateToTitle() {
navigateTo(`#${navId.value}`)
}
</script>
<template>
<section class="border border-border rounded-lg p-4 sm:p-6 scroll-mt-18" :id="cardId">
<div class="flex gap-2 items-center">
<h2
class="text-1xl sm:text-2xl font-medium min-w-0 break-words py-2 scroll-mt-20"
:id="navId"
>
<a
class="hover:decoration-accent hover:text-accent focus-visible:decoration-accent focus-visible:text-accent transition-colors duration-200"
:class="$style.linkTitle"
:href="`#${navId}`"
@click.prevent="navigateToTitle()"
>
{{ release.title }}
</a>
</h2>
<TagStatic v-if="release.prerelease" variant="default" class="h-unset">
{{ $t('changelog.pre_release') }}
</TagStatic>
<TagStatic v-if="release.draft" variant="default" class="h-unset">
{{ $t('changelog.draft') }}
</TagStatic>
<div class="flex-1" aria-hidden="true"></div>
<ReadmeTocDropdown
v-if="release?.toc && release.toc.length > 1"
:toc="release.toc"
class="ms-auto"
/>
<!-- :active-id="activeTocId" -->
</div>
<DateTime v-if="release.publishedAt" :datetime="release.publishedAt" date-style="medium" />
<Readme v-if="release.html" :html="release.html"></Readme>
</section>
</template>

<style module>
.linkTitle::after {
content: '__';
@apply inline i-lucide:link rtl-flip ms-1 opacity-0;
}

.linkTitle:hover:after {
@apply opacity-100;
}
</style>
41 changes: 41 additions & 0 deletions app/components/Changelog/Markdown.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<script setup lang="ts">
const { info, requestedVersion, tpTarget } = defineProps<{
info: ChangelogMarkdownInfo
requestedVersion: string | null
tpTarget?: HTMLElement | null
}>()

const route = useRoute()

const { data } = useLazyFetch(() => `/api/changelog/md/${info.provider}/${info.repo}/${info.path}`)

watch(
[() => data.value?.toc, () => requestedVersion?.toLowerCase(), () => route.hash],
([toc, rv, hash]) => {
if (toc && hash) {
navigateTo(hash)
return
}

if (!toc || !rv || hash) {
return
}

for (const item of toc) {
if (item.text.toLowerCase().includes(rv)) {
navigateTo(`#${item.id}`)
return
}
}
},
{
immediate: true,
},
)
</script>
<template>
<Teleport v-if="data?.toc && data.toc.length > 1 && !!tpTarget" :to="tpTarget">
<ReadmeTocDropdown :toc="data.toc" class="justify-self-end" />
</Teleport>
<Readme v-if="data?.html" :html="data.html"></Readme>
</template>
62 changes: 62 additions & 0 deletions app/components/Changelog/Releases.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<script setup lang="ts">
const { info, requestedDate, requestedVersion } = defineProps<{
info: ChangelogReleaseInfo
requestedDate?: string
requestedVersion?: string | null | undefined
}>()

const { data: releases } = useFetch<ReleaseData[]>(
() => `/api/changelog/releases/${info.provider}/${info.repo}`,
)

const route = useRoute()

const matchingDateReleases = computed(() => {
if (!requestedDate || !releases.value) {
return
}

return releases.value.filter(release => {
if (!release.publishedAt) {
return
}
const date = new Date(release.publishedAt).toISOString().split('T')[0]

return date == requestedDate
})
})

watch(
[() => route.hash, () => requestedDate?.toLowerCase(), releases, () => requestedVersion],
([hash, date, r, rv]) => {
if (hash && r) {
// ensures the user is scrolled to the hash
navigateTo(hash)
return
}
if (hash || !date || !r) {
return
}
if (rv) {
for (const match of matchingDateReleases.value ?? []) {
if (match.title.toLowerCase().includes(rv)) {
navigateTo(`#release-${encodeURI(match.title)}`)
return
}
}
}

navigateTo(`#date-${date}`)
},
{
immediate: true,
},
)
</script>
<template>
<div class="flex flex-col gap-2 py-3" v-if="releases">
<ClientOnly>
<ChangelogCard v-for="release of releases" :release :key="release.id" />
</ClientOnly>
</div>
</template>
2 changes: 1 addition & 1 deletion app/components/Readme.vue
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ function handleClick(event: MouseEvent) {
@apply inline i-lucide:external-link rtl-flip ms-1 opacity-50;
}

.readme :deep(:is(h1, h2, h3, h4, h5, h6) a[href^='#']::after) {
.readme :deep(:is(h1, h2, h3, h4, h5, h6) a[href^='#']:not([content-none])::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-lucide:link rtl-flip ms-1 opacity-0;
Expand Down
13 changes: 13 additions & 0 deletions app/composables/usePackageChangelog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { ChangelogInfo } from '~~/shared/types/changelog'

export function usePackageChangelog(
packageName: MaybeRefOrGetter<string>,
version?: MaybeRefOrGetter<string | null | undefined>,
) {
return useLazyFetch<ChangelogInfo>(() => {
const name = toValue(packageName)
const ver = toValue(version)
const base = `/api/changelog/info/${name}`
return ver ? `${base}/v/${ver}` : base
})
}
24 changes: 24 additions & 0 deletions app/composables/useProviderIcon.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { ProviderId } from '#imports'
import type { IconClass } from '~/types/icon'
import { computed, toValue } from 'vue'

const PROVIDER_ICONS: Record<ProviderId, IconClass> = {
github: 'i-simple-icons:github',
gitlab: 'i-simple-icons:gitlab',
bitbucket: 'i-simple-icons:bitbucket',
codeberg: 'i-simple-icons:codeberg',
gitea: 'i-simple-icons:gitea',
forgejo: 'i-simple-icons:forgejo',
gitee: 'i-simple-icons:gitee',
sourcehut: 'i-simple-icons:sourcehut',
tangled: 'i-custom:tangled',
radicle: 'i-lucide:network', // Radicle is a P2P network, using network icon
}

export function useProviderIcon(provider: MaybeRefOrGetter<ProviderId | null | undefined>) {
return computed((): IconClass => {
const uProvider = toValue(provider)
if (!uProvider) return 'i-simple-icons:github'
return PROVIDER_ICONS[uProvider] ?? 'i-lucide:code'
})
}
2 changes: 1 addition & 1 deletion app/pages/org/[org].vue
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ defineOgImageComponent('Default', {
target="_blank"
rel="noopener noreferrer"
class="link-subtle font-mono text-sm inline-flex items-center gap-1.5"
:title="$t('common.view_on_npm')"
:title="$t('common.view_on', { site: 'npm' })"
>
<span class="i-simple-icons:npm w-4 h-4" aria-hidden="true" />
npm
Expand Down
141 changes: 141 additions & 0 deletions app/pages/package-changes/[...path].vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
<script setup lang="ts">
import { useProviderIcon } from '~/composables/useProviderIcon'

definePageMeta({
name: 'changes',
path: '/package-changes/:path+',
alias: ['/package/changes/:path+', '/changes/:path+'],
scrollMargin: 150,
})

/// routing

const route = useRoute('changes')
const router = useRouter()
// Parse package name, version, and file path from URL
// Patterns:
// /code/nuxt/v/4.2.0 → packageName: "nuxt", version: "4.2.0", filePath: null (show tree)
// /code/nuxt/v/4.2.0/src/index.ts → packageName: "nuxt", version: "4.2.0", filePath: "src/index.ts"
// /code/@nuxt/kit/v/1.0.0 → packageName: "@nuxt/kit", version: "1.0.0", filePath: null
const parsedRoute = computed(() => {
const segments = route.params.path

// Find the /v/ separator for version
const vIndex = segments.indexOf('v')
if (vIndex === -1 || vIndex >= segments.length - 1) {
// No version specified - redirect or error
return {
packageName: segments.join('/'),
version: null as string | null,
filePath: null as string | null,
}
}

const packageName = segments.slice(0, vIndex).join('/')
const afterVersion = segments.slice(vIndex + 1)
const version = afterVersion[0] ?? null
const filePath = afterVersion.length > 1 ? afterVersion.slice(1).join('/') : null

return { packageName, version, filePath }
})

const packageName = computed(() => parsedRoute.value.packageName)
const version = computed(() => parsedRoute.value.version)
const filePath = computed(() => parsedRoute.value.filePath?.replace(/\/$/, ''))

const { data: pkg } = usePackage(packageName, version)

const versionUrlPattern = computed(() => {
const base = `/package-changes/${packageName.value}/v/{version}`
return filePath.value ? `${base}/${filePath.value}` : base
})

const latestVersion = computed(() => pkg.value?.['dist-tags']?.latest ?? null)

watch(
[version, latestVersion, packageName],
([v, latest, name]) => {
if (!v && latest && name) {
const pathSegments = [...name.split('/'), 'v', latest]
router.replace({ name: 'changes', params: { path: pathSegments as [string, ...string[]] } })
}
},
{ immediate: true },
)

// getting info
const { data: changelog, pending } = usePackageChangelog(packageName, version)

const repoProviderIcon = useProviderIcon(() => changelog.value?.provider)
const tptoc = useTemplateRef('tptoc')

const versionDate = computed(() => {
if (!version.value) {
return
}
const time = pkg.value?.time[version.value]
if (time) {
return new Date(time).toISOString().split('T')[0]
}
})

// defineOgImageComponent('Default', {
// title: () => `${pkg.value?.name ?? 'Package'} - Changelogs`,
// description: () => pkg.value?.license ?? '',
// primaryColor: '#60a5fa',
// })
</script>
<template>
<main class="flex-1 flex flex-col">
<header class="border-b border-border bg-bg sticky top-14 z-20">
<div class="container pt-4 pb-3">
<div class="flex items-center gap-3 mb-3 flex-wrap min-w-0">
<h1
class="font-mono text-lg sm:text-xl font-semibold text-fg hover:text-fg-muted transition-colors truncate"
>
<NuxtLink v-if="packageName" :to="packageRoute(packageName, version)">
{{ packageName }}
</NuxtLink>
</h1>

<VersionSelector
v-if="(version || latestVersion) && pkg?.versions && pkg?.['dist-tags']"
:package-name="packageName"
:current-version="version ?? latestVersion!"
:versions="pkg.versions"
:dist-tags="pkg['dist-tags']"
:url-pattern="versionUrlPattern"
/>
<div class="flex-1"></div>
<LinkBase
v-if="changelog?.link"
:to="changelog?.link"
:classicon="repoProviderIcon"
:title="$t('common.view_on', { site: changelog.provider })"
>
{{ changelog.provider }}
</LinkBase>

<div v-if="changelog?.type == 'md'" ref="tptoc" class="w-14 h-8">
<!-- prevents layout shift while loading -->
</div>
</div>
</div>
</header>
<section class="container w-full" v-if="!pending">
<LazyChangelogReleases
v-if="changelog?.type == 'release'"
:info="changelog"
:requestedDate="versionDate"
:requested-version="version || latestVersion"
/>
<LazyChangelogMarkdown
v-else-if="changelog?.type == 'md'"
:info="changelog"
:tpTarget="tptoc"
:requested-version="version || latestVersion"
/>
<p class="mt-5" v-else>{{ $t('changelog.no_logs') }}</p>
</section>
</main>
</template>
Loading
Loading