Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
680bebf
chore: check initial data on package page
alexdln Feb 25, 2026
84d2234
chore: check more details
alexdln Feb 25, 2026
8c7061f
chore: check more details
alexdln Feb 25, 2026
d67ac0c
chore: check client hook
alexdln Feb 25, 2026
efcfb46
chore: check package hook
alexdln Feb 25, 2026
f49069c
fix: compute version outside of package hook deps
alexdln Feb 25, 2026
b88a97c
fix: compute version outside of package hook deps with stable ref
alexdln Feb 25, 2026
573221c
fix: check data on refresh
alexdln Feb 25, 2026
0451045
fix: update skeleton condition
alexdln Feb 25, 2026
b699286
fix: update article condition
alexdln Feb 25, 2026
bf24ec7
fix: update server rendered package condition
alexdln Feb 25, 2026
1c4269a
fix: update skeleton condition on package page
alexdln Feb 25, 2026
4934123
fix: update skeleton condition on package page
alexdln Feb 25, 2026
0f87ec4
fix: update initial state on package page
alexdln Feb 25, 2026
687e639
fix: update initial state on package page
alexdln Feb 25, 2026
0632594
fix: update initial state on package page
alexdln Feb 25, 2026
373f486
fix: check server content only var
alexdln Feb 25, 2026
54b3e09
fix: wait readme data as well
alexdln Feb 25, 2026
d9acec6
fix: check readme data
alexdln Feb 25, 2026
2e115c1
fix: wait for readme loading
alexdln Feb 25, 2026
bb4015b
fix: wait for readme loading
alexdln Feb 25, 2026
4660e64
fix: polish ssr result preview for scenario 2
alexdln Feb 25, 2026
fbefb1c
fix: update readme response types
alexdln Feb 25, 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
39 changes: 23 additions & 16 deletions app/pages/package/[[org]]/[name].vue
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,15 @@ const { data: readmeData } = useLazyFetch<ReadmeResponse>(
const version = requestedVersion.value
return version ? `${base}/v/${version}` : base
},
{ default: () => ({ html: '', mdExists: false, playgroundLinks: [], toc: [] }) },
{
default: () => ({
html: '',
mdExists: false,
playgroundLinks: [],
toc: [],
defaultValue: true,
}),
},
)
const playgroundLinks = computed(() => [
Expand Down Expand Up @@ -259,18 +267,19 @@ const nuxtApp = useNuxtApp()
const route = useRoute()
const hasEmptyPayload =
import.meta.client &&
nuxtApp.isHydrating &&
nuxtApp.payload.serverRendered &&
!Object.keys(nuxtApp.payload.data ?? {}).length
const isSpaFallback = shallowRef(hasEmptyPayload && !nuxtApp.payload.path)
const isSpaFallback = shallowRef(nuxtApp.isHydrating && hasEmptyPayload && !nuxtApp.payload.path)
const isHydratingWithServerContent = shallowRef(
hasEmptyPayload && nuxtApp.payload.path === route.path,
nuxtApp.isHydrating && hasEmptyPayload && nuxtApp.payload.path === route.path,
)
const hasServerContentOnly = shallowRef(hasEmptyPayload && nuxtApp.payload.path === route.path)
// When we have server-rendered content but no payload data, capture the server
// DOM before Vue's hydration replaces it. This lets us show the server-rendered
// HTML as a static snapshot while data refetches, avoiding any visual flash.
const serverRenderedHtml = shallowRef<string | null>(
isHydratingWithServerContent.value
hasServerContentOnly.value
? (document.getElementById('package-article')?.innerHTML ?? null)
: null,
)
Expand All @@ -279,7 +288,6 @@ if (isSpaFallback.value || isHydratingWithServerContent.value) {
nuxtApp.hooks.hookOnce('app:suspense:resolve', () => {
isSpaFallback.value = false
isHydratingWithServerContent.value = false
serverRenderedHtml.value = null
})
}
Expand Down Expand Up @@ -733,27 +741,26 @@ const showSkeleton = shallowRef(false)
<!-- Scenario 1: SPA fallback — show skeleton (no real content to preserve) -->
<!-- Scenario 2: SSR with missing payload — preserve server DOM, skip skeleton -->
<PackageSkeleton
v-if="
isSpaFallback || (!isHydratingWithServerContent && (showSkeleton || status === 'pending'))
"
v-if="isSpaFallback || (!hasServerContentOnly && (showSkeleton || status === 'pending'))"
/>

<!-- During hydration without payload, show captured server HTML as a static snapshot.
This avoids a visual flash: the user sees the server content while data refetches.
v-html is safe here: the content originates from the server's own SSR output,
captured from the DOM before hydration — it is not user-controlled input. -->
captured from the DOM before hydration — it is not user-controlled input.
We also show SSR output until critical data is loaded, so that after rendering dynamic
content, the user receives the same result as he received from the server-->
<article
v-else-if="isHydratingWithServerContent && serverRenderedHtml"
v-else-if="
isHydratingWithServerContent ||
(hasServerContentOnly && serverRenderedHtml && (!pkg || readmeData?.defaultValue))
"
id="package-article"
:class="$style.packagePage"
v-html="serverRenderedHtml"
/>

<article
v-else-if="status === 'success' && pkg"
id="package-article"
:class="$style.packagePage"
>
<article v-else-if="pkg" id="package-article" :class="$style.packagePage">
<!-- Package header -->
<header
class="sticky top-14 z-1 bg-[--bg] py-2 border-border"
Expand Down
2 changes: 2 additions & 0 deletions shared/types/readme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ export interface TocItem {
* Response from README API endpoint
*/
export interface ReadmeResponse {
/** Whether the response is the default value */
defaultValue?: boolean
/** Whether the README exists */
mdExists?: boolean
/** Rendered HTML content */
Expand Down
Loading