게시물 이동 로딩 후 진입으로 복귀

This commit is contained in:
2026-07-06 13:30:21 +09:00
parent 34d2d8e2ea
commit 7685f82c77
12 changed files with 81 additions and 233 deletions

57
app.vue
View File

@@ -18,6 +18,31 @@ const siteAccentStyle = computed(() => ({
'--site-accent': siteAccentColor.value
}))
const isSitePageNavigating = ref(false)
if (import.meta.client) {
const nuxtApp = useNuxtApp()
/**
* 공개 페이지 이동 로딩 오버레이를 표시한다.
* @returns {void}
*/
const showSitePageNavigationOverlay = () => {
isSitePageNavigating.value = true
}
/**
* 공개 페이지 이동 로딩 오버레이를 숨긴다.
* @returns {void}
*/
const hideSitePageNavigationOverlay = () => {
isSitePageNavigating.value = false
}
nuxtApp.hook('page:loading:start', showSitePageNavigationOverlay)
nuxtApp.hook('page:loading:end', hideSitePageNavigationOverlay)
}
useHead(() => ({
titleTemplate: (titleChunk) => titleChunk || appSiteSettings.value.title,
link: appSiteSettings.value.faviconUrl
@@ -35,8 +60,40 @@ useHead(() => ({
<template>
<div class="site-app" :style="siteAccentStyle">
<NuxtLoadingIndicator :color="siteAccentColor" :height="3" :throttle="80" :duration="5000" />
<Transition
enter-active-class="transition duration-100 ease-out"
enter-from-class="opacity-0"
enter-to-class="opacity-100"
leave-active-class="transition duration-150 ease-in"
leave-from-class="opacity-100"
leave-to-class="opacity-0"
>
<div
v-if="isSitePageNavigating"
class="site-page-loading fixed inset-0 z-[90] grid place-items-center bg-[var(--site-bg)]/78 text-[var(--site-text)] backdrop-blur-[2px]"
role="status"
aria-live="polite"
>
<div class="site-page-loading__panel grid place-items-center gap-3 text-center">
<span class="site-page-loading__spinner size-8 rounded-full border-2 border-[var(--site-line)] border-t-[var(--site-accent)]" aria-hidden="true" />
<span class="text-xs font-semibold uppercase tracking-[0.18em] opacity-70">Loading</span>
</div>
</div>
</Transition>
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
</div>
</template>
<style scoped>
.site-page-loading__spinner {
animation: site-page-loading-spin 0.8s linear infinite;
}
@keyframes site-page-loading-spin {
to {
transform: rotate(360deg);
}
}
</style>

View File

@@ -1,16 +0,0 @@
/**
* Vue Router 기본 스크롤 이동을 비활성화한다.
*
* 기본 동작은 클라이언트 페이지 이동 중 이전 화면이 먼저 최상단으로
* 이동하는 순간을 만들 수 있어, 실제 스크롤 처리는 클라이언트 플러그인에서
* Nuxt 페이지 렌더링 이후 새 화면 기준으로만 수행한다.
*/
export default {
/**
* 라우터 내장 스크롤 처리를 끈다.
* @returns {false} 기본 스크롤 처리 비활성화
*/
scrollBehavior() {
return false
}
}

View File

@@ -104,15 +104,6 @@
pointer-events: none;
}
html.site-route-veil body::before {
content: '';
position: fixed;
inset: 0;
z-index: 2147483647;
background: var(--site-bg);
pointer-events: none;
}
body {
min-width: 320px;
margin: 0;

View File

@@ -1,6 +1,6 @@
# 배포 가이드
> 로컬 기준 v1.10.46에서 `npm run lint`와 `npm run build` 검증을 통과했다. NAS 실제 컨테이너 기동과 도메인/프록시 접속 검증은 운영 배포 단계에서 진행한다.
> 로컬 기준 v1.10.47에서 `npm run lint`와 `npm run build` 검증을 통과했다. NAS 실제 컨테이너 기동과 도메인/프록시 접속 검증은 운영 배포 단계에서 진행한다.
## 빌드 유형

View File

@@ -1,5 +1,11 @@
# 의사결정 이력
## 2026-07-06 v1.10.47 — 게시물 이동은 로딩 후 진입으로 확정한다
즉시 전환, 라우터 스크롤 비활성화, Nuxt `page:finish` 이후 스크롤 적용, 짧은 전환 베일을 차례로 시도했지만 홈에서 게시물로 이동할 때 이전 홈 헤더 이미지가 순간 노출되는 현상이 남았다. 홈 화면의 큰 헤더 이미지와 문서 스크롤, Nuxt 클라이언트 라우팅의 페인트 타이밍이 맞물리는 문제라 즉시 전환을 더 고집하면 코드 복잡도만 커지고 사용자 체감은 안정되지 않는다.
따라서 게시물 상세 이동은 본문을 블로킹으로 먼저 조회하고, 이동 중에는 전역 로딩 오버레이로 이전 화면을 가린 뒤 준비된 상세 화면으로 진입하는 방식을 다시 채택한다. 문서형 사이트에서는 약간의 대기보다 화면이 튀지 않는 안정성이 더 중요하다고 판단했다.
## 2026-07-06 v1.10.46 — 즉시 전환을 유지하되 화면 교체 구간만 가린다
라우터 기본 스크롤을 끄고 Nuxt `page:finish` 이후로 스크롤 적용을 늦춰도, 홈에서 게시물로 이동할 때 이전 홈 화면의 헤더 이미지가 순간적으로 보일 수 있었다. 이는 페이지 컴포넌트 교체와 브라우저 페인트 사이의 짧은 구간에서 발생하는 시각적 문제이므로, 데이터 준비 완료까지 기다리는 방식으로 돌아가기 전에 화면 교체·스크롤 정리 구간만 덮는 전환 베일을 추가한다.

View File

@@ -10,8 +10,7 @@
| layouts/post.vue | 개별 게시물 — `default`와 동일, 오른쪽 컬럼 전체 높이 구분선 |
| layouts/admin.vue | 관리자 전체, 320px 밝은 Ghost형 사이드바(대시보드 `/admin` 활성 링크·사이트 보기 새 창·콘텐츠 구간 여백), 우측 전체 높이 캔버스, 멤버 수 표시, 하단 사용자 드롭다운·설정, `내 프로필` 멤버 편집 이동, 게시글 `+` 새 글 진입, 글 작성/수정 화면의 전체 화면 편집 모드와 문서 스크롤 잠금, **`/admin/settings`에서는 사이드바 숨김·본문 전체 화면·설정용 문서 스크롤 잠금** |
| layouts/page.vue | 고정 페이지 전체 화면 |
| app.vue | 공개 사이트 설정 기반 파비콘 head 링크, 페이지 제목이 있으면 제목만 쓰고 없으면 사이트 이름을 쓰는 title template, 브랜드 컬러 `--site-accent` 적용, 페이지 이동 로딩 진행바(`NuxtLoadingIndicator`, 브랜드 컬러) |
| app/router.options.js | Vue Router 기본 스크롤 이동 비활성화, 클라이언트 플러그인이 새 화면 기준으로 스크롤 적용 |
| app.vue | 공개 사이트 설정 기반 파비콘 head 링크, 페이지 제목이 있으면 제목만 쓰고 없으면 사이트 이름을 쓰는 title template, 브랜드 컬러 `--site-accent` 적용, 페이지 이동 로딩 진행바(`NuxtLoadingIndicator`, 브랜드 컬러)와 전역 이동 로딩 오버레이 |
| error.vue | 공개 404/서버 오류 화면, 홈 이동 버튼 |
## Composables
@@ -219,7 +218,7 @@
| pages/index.vue | 홈, `site_settings` 커버가 있을 때만 라이트·다크 이미지 지원 `HomeHero`, Featured/Latest, 사이트 설정 표시개수 기준 Latest·Featured·Pages 데이터 제한, 홈 표시 페이지를 섹션별 도서 진열장형 Pages 선반으로 노출, Pages 추천 카드는 왼쪽 위 SVG 아이콘 배지 표시, Featured와 Pages 선반의 독립 이전·다음 버튼 및 문서 레벨 포인터 추적 기반 드래그 슬라이드·스냅(이동 임계값 미만 클릭은 링크 이동, 포인터 캡처는 드래그 확정 뒤 적용), Pages 선반 ResizeObserver 기반 버튼 상태 갱신, Pages 카드 hover 상단 여백 보정, Latest 피드 Compact 기본값·List·Cards 보기, Compact 모바일 썸네일 80px 정사각형·Compact/List 설명 2줄 제한, Latest 메타는 태그가 있을 때만 링크 배지 표시, 카드 오른쪽 아래 화살표는 게시물 상세 링크, Featured는 추천 글이 있을 때만 표시하며 이미지 없는 추천 글은 제목 placeholder 썸네일과 모바일 터치 가로 스크롤·스냅, 끝에서 화살표 비활성 |
| pages/posts/index.vue | 게시물 전체 목록, 사이트 설정 Posts 표시개수 기준 서버 페이지네이션, 공용 번호형 페이지네이션, 태그는 있을 때만 카드 메타에 링크로 표시 |
| pages/posts/[slug].vue | `/post/:slug` 리다이렉트 |
| pages/post/[slug].vue | 블로그 글 상세, SSR 직접 접속은 본문 선조회·클라이언트 내부 이동은 즉시 화면 전환 후 상세 스켈레톤 표시, 첫 태그가 있을 때만 메타 행에 태그 링크, H1~H3 본문 제목을 오른쪽 TOC 상태로 제공, 공개 회원 로그인 세션의 회원이 글쓴이(`author_id`)이면 공유 버튼 옆 새 탭 편집 링크 표시, 게시물 SEO/OG 메타 출력(요약 없으면 본문 fallback), 공유 모달(X/Bluesky/Facebook/LinkedIn/Email/링크복사), 회원 댓글 섹션 |
| pages/post/[slug].vue | 블로그 글 상세, 본문 블로킹 조회 후 완성 화면 진입, 첫 태그가 있을 때만 메타 행에 태그 링크, H1~H3 본문 제목을 오른쪽 TOC 상태로 제공, 공개 회원 로그인 세션의 회원이 글쓴이(`author_id`)이면 공유 버튼 옆 새 탭 편집 링크 표시, 게시물 SEO/OG 메타 출력(요약 없으면 본문 fallback), 공유 모달(X/Bluesky/Facebook/LinkedIn/Email/링크복사), 회원 댓글 섹션 |
| pages/tags/index.vue | 태그 전체 목록, 중앙 히어로와 3열 태그 카드, 좌측 컬러 보더/hover 오버레이 |
| pages/tags/[slug].vue | `/tag/:slug` 리다이렉트 |
| pages/tag/[slug].vue | 태그별 글 목록, 사이트 설정 태그 게시물 표시개수 기준 서버 태그 필터·공용 번호형 페이지네이션, 태그 헤더 이미지가 있으면 상단 이미지 헤더 표시, 없으면 기본 태그명·설명 + 공통 섹션 패딩을 쓰는 리스트형 게시물 카드, 카드 태그 배지와 오른쪽 아래 화살표 링크 |
@@ -363,7 +362,6 @@
| plugins/site-analytics.client.js | 공개 라우트 pageview·heartbeat·read 클라이언트 전송 |
| plugins/bfcache-page-restore.client.js | 뒤로가기 캐시(bfcache) 복원 시 라우터 경로와 실제 URL 어긋남을 감지해 새로고침(HTML 문서 모드 페이지 이동 후 뒤로가기 잔상 방지) |
| plugins/navigation-lock.client.js | 페이지 이동 중 `html.site-navigating` 클래스로 링크·버튼 클릭을 잠가 중복 클릭 차단(안전 타임아웃 포함) |
| plugins/site-route-scroll.client.js | 라우트 변경 정보를 저장한 뒤 Nuxt `page:finish` 이후 새 화면에서 최상단·해시 위치 스크롤 적용, 이동 중 짧은 전환 베일로 이전 화면 상단 노출 방지 |
## 데이터베이스
@@ -415,7 +413,7 @@
| package.json | Nuxt 실행 스크립트와 의존성 |
| nuxt.config.js | Nuxt 앱 설정, `tailwindcss.cssPath`로 `main.css` 단일 엔트리, Tailwind 모듈, 관리자 QA를 위한 개발 도구 비활성화, 회원 썸네일 최소/최대 해상도·품질 런타임 설정 |
| tailwind.config.js | Tailwind 테마 설정 |
| assets/css/main.css | 전역 스타일, 공개 배경·사이드바 배경 통일 기준, 관리자 라이트 테마 격리(`admin-layout`), 좌측 사이드바/그리드 전환 애니메이션, 네비게이션 세로 바 hover 효과, 게시물 카드 제목 공통 인터랙션(`post-title-link`), 이동 중 클릭 잠금(`site-navigating`)·전환 베일(`site-route-veil`) |
| assets/css/main.css | 전역 스타일, 공개 배경·사이드바 배경 통일 기준, 관리자 라이트 테마 격리(`admin-layout`), 좌측 사이드바/그리드 전환 애니메이션, 네비게이션 세로 바 hover 효과, 게시물 카드 제목 공통 인터랙션(`post-title-link`), 이동 중 클릭 잠금(`site-navigating`) |
| composables/useMenuState.js | 좌측 메뉴 열림 상태·`closeMenu`(모바일 백드롭 등), 모바일은 마운트 시 항상 닫힘 시작·`menuMounted`로 첫 페인트 슬라이드 플래시 방지 |
| composables/useThemeMode.js | 사용자 화면 라이트/다크 테마 상태 관리 |
| middleware/admin-auth.global.js | 관리자 페이지 접근 인증 |

View File

@@ -144,7 +144,7 @@
### 공개 URL 구조
- `/posts` - 게시물 전체 목록. 사이트 설정 POST 설정의 `postListPageSize` 값 기준으로 서버 페이지네이션을 사용하며, 페이지 번호가 바뀌면 화면을 상단으로 이동한다. 하단 페이지네이션은 이전·다음 화살표와 페이지 번호 직접 이동을 함께 제공하며, 전체 페이지가 많으면 첫 페이지·끝 페이지·현재 페이지 주변 번호를 유지하고 중간 구간은 `...`으로 접는다.
- `/post/:slug` - 개별 게시물 상세. 직접 접속·SSR은 본문을 먼저 조회해 SEO 메타를 완성하고, 클라이언트 내부 이동은 새 상세 화면으로 즉시 전환한 뒤 데이터 준비 전에는 제목·메타·이미지·본문 자리 스켈레톤을 표시한다. 이동 중에는 `NuxtLoadingIndicator` 상단 진행바와 중복 클릭 잠금으로 피드백을 제공한다. Vue Router 기본 스크롤 처리는 끄고 Nuxt 페이지 렌더링 완료 후 새 화면 기준으로 최상단 또는 해시 위치를 적용해 이동 전 이전 화면이 먼저 위로 튀지 않게 한다. 화면 교체와 스크롤 정리 사이에는 배경색 전환 베일을 짧게 표시해 이전 화면 상단 노출을 숨긴다. 이전·다음 글은 상세 API 응답의 `previous`·`next`로 표시하며, `previous`는 더 과거 글, `next`는 더 최신 글을 뜻한다.
- `/post/:slug` - 개별 게시물 상세. 직접 접속·SSR과 클라이언트 내부 이동 모두 본문을 먼저 조회한 뒤 완성된 상세 화면으로 진입한다. 이동 중에는 `NuxtLoadingIndicator` 상단 진행바, 중복 클릭 잠금, 전역 페이지 이동 로딩 오버레이로 피드백을 제공해 이전 홈 화면이 상단으로 튀어 보이지 않게 한다. 이전·다음 글은 상세 API 응답의 `previous`·`next`로 표시하며, `previous`는 더 과거 글, `next`는 더 최신 글을 뜻한다.
- `/` - 홈. 클라이언트 복귀 시에도 주요 데이터를 블로킹 조회한 뒤 전환한다.
- `/sitemap.xml` - 공개 sitemap XML. 홈, 게시물 목록, 태그 목록, 공개 발행글, 공개 고정 페이지, 글이 있는 태그 URL을 요청 시점 DB 기준으로 자동 생성한다. 멤버십·비공개·초안·예약 대기 글과 `noindex` 글은 포함하지 않는다.
- `/robots.txt` - 크롤러 안내 파일. 관리자·API·회원 설정·로그인 경로를 제외하고 `Sitemap: /sitemap.xml` 절대 URL을 제공한다.

View File

@@ -1,5 +1,12 @@
# 업데이트 이력
## v1.10.47
- 게시물 상세 클라이언트 이동을 즉시 전환 방식에서 로딩 후 진입 방식으로 복귀.
- 홈에서 게시물로 이동할 때 이전 홈 헤더 이미지가 순간 노출되는 문제를 피하기 위해 게시물 본문을 블로킹 조회하도록 수정.
- `app/router.options.js`와 `plugins/site-route-scroll.client.js`, 전환 베일 스타일 제거.
- 전역 페이지 이동 로딩 오버레이를 다시 사용해 이전 화면 중간 상태를 가리고 준비된 상세 화면만 표시하도록 정리.
## v1.10.46
- 페이지 이동 시작부터 새 화면 스크롤 정리 완료까지 짧은 전환 베일을 표시하도록 추가.

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "sori.studio",
"version": "1.10.46",
"version": "1.10.47",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "sori.studio",
"version": "1.10.46",
"version": "1.10.47",
"hasInstallScript": true,
"dependencies": {
"@nuxtjs/tailwindcss": "^6.14.0",

View File

@@ -1,6 +1,6 @@
{
"name": "sori.studio",
"version": "1.10.46",
"version": "1.10.47",
"private": true,
"type": "module",
"imports": {

View File

@@ -9,17 +9,10 @@ const route = useRoute()
const slug = computed(() => String(route.params.slug || ''))
/**
* SSR 직접 접속은 본문을 먼저 받아 SEO를 유지하고, 클라이언트 내부 이동은 즉시 화면을 전환한다.
* 본문은 블로킹으로 받아 로딩 표시 후 완성된 화면으로 전환한다.
* 이전·다음 글은 상세 응답에 포함되어 별도 목록 요청 없이 표시된다.
*/
const {
data: fetchedPost,
pending: isPostPending,
error: postFetchError
} = await useFetch(() => `/api/posts/${slug.value}`, {
lazy: import.meta.client,
default: () => null
})
const { data: post } = await useFetch(() => `/api/posts/${slug.value}`)
const { data: tags } = useLazyFetch('/api/tags?tagType=all', {
default: () => []
})
@@ -29,15 +22,7 @@ const { data: siteSettings } = useLazyFetch('/api/site-settings', {
})
const postToc = useState('post-detail-toc', () => [])
const post = computed(() => (
fetchedPost.value?.slug === slug.value
? fetchedPost.value
: null
))
const isPostLoading = computed(() => isPostPending.value || (!post.value && !postFetchError.value))
if (import.meta.server && !post.value) {
if (!post.value) {
throw createError({
statusCode: 404,
statusMessage: '게시물을 찾을 수 없습니다.'
@@ -148,17 +133,6 @@ onMounted(() => {
fetchCurrentViewer()
})
watch([isPostPending, postFetchError, post], () => {
if (!import.meta.client || isPostPending.value || post.value) {
return
}
showError(createError({
statusCode: postFetchError.value?.statusCode || 404,
statusMessage: '게시물을 찾을 수 없습니다.'
}))
})
watchEffect(() => {
postToc.value = tableOfContents.value
})
@@ -279,7 +253,6 @@ useHead(() => ({
<template>
<div class="post-detail">
<template v-if="post">
<section class="mt-6 mb-8">
<div class="mx-auto flex max-w-[720px] flex-col gap-2.5 px-4 sm:px-5">
<h1 class="text-xl font-semibold leading-[1.125] sm:text-2xl">
@@ -547,49 +520,5 @@ useHead(() => ({
</div>
</div>
</Transition>
</template>
<section v-else class="post-detail__loading mt-6 mb-12" aria-live="polite" aria-busy="true">
<div class="mx-auto flex min-h-[70vh] max-w-[720px] flex-col gap-5 px-4 sm:px-5">
<div class="grid gap-3">
<div class="post-detail__loading-shimmer h-8 w-11/12 rounded bg-[var(--site-panel-strong)] sm:h-10" />
<div class="post-detail__loading-shimmer h-4 w-2/5 rounded bg-[var(--site-panel-strong)]" />
</div>
<div class="border-b border-[var(--site-line)] pb-4">
<div class="post-detail__loading-shimmer h-3 w-3/5 rounded bg-[var(--site-panel-strong)]" />
</div>
<div class="post-detail__loading-shimmer aspect-video w-full rounded-[10px] bg-[var(--site-panel-strong)]" />
<div class="mt-4 grid gap-3">
<div class="post-detail__loading-shimmer h-4 w-full rounded bg-[var(--site-panel-strong)]" />
<div class="post-detail__loading-shimmer h-4 w-[94%] rounded bg-[var(--site-panel-strong)]" />
<div class="post-detail__loading-shimmer h-4 w-[88%] rounded bg-[var(--site-panel-strong)]" />
<div class="post-detail__loading-shimmer h-4 w-[96%] rounded bg-[var(--site-panel-strong)]" />
<div class="post-detail__loading-shimmer h-4 w-[72%] rounded bg-[var(--site-panel-strong)]" />
</div>
<span class="sr-only">{{ isPostLoading ? '게시물을 불러오는 중입니다.' : '게시물을 표시할 수 없습니다.' }}</span>
</div>
</section>
</div>
</template>
<style scoped>
.post-detail__loading-shimmer {
position: relative;
overflow: hidden;
}
.post-detail__loading-shimmer::after {
position: absolute;
inset: 0;
content: '';
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.42), transparent);
transform: translateX(-100%);
animation: post-detail-loading-shimmer 1.2s ease-in-out infinite;
}
@keyframes post-detail-loading-shimmer {
to {
transform: translateX(100%);
}
}
</style>

View File

@@ -1,124 +0,0 @@
/**
* 공개 라우트 이동 후 새 화면 기준으로 스크롤 위치를 정리한다.
*
* Vue Router 기본 scrollBehavior는 전환 중 이전 화면을 먼저 움직일 수 있어
* `app/router.options.js`에서 끄고, 여기서는 Nuxt 페이지 렌더링이 끝난 뒤
* 새 화면에서만 최상단 또는 해시 위치로 이동한다. 전환 중에는 짧은 베일을
* 씌워 이전 화면이 상단으로 튀는 순간이 사용자에게 보이지 않게 한다.
*/
const ROUTE_VEIL_CLASS = 'site-route-veil'
const ROUTE_VEIL_TIMEOUT_MS = 8000
export default defineNuxtPlugin((nuxtApp) => {
const router = nuxtApp.$router
const root = document.documentElement
/** @type {{ id: number, to: import('vue-router').RouteLocationNormalizedLoaded, from: import('vue-router').RouteLocationNormalizedLoaded } | null} */
let pendingRouteScroll = null
let routeScrollId = 0
/** @type {ReturnType<typeof setTimeout> | null} */
let veilTimer = null
/**
* 브라우저 렌더 프레임을 기다린다.
* @returns {Promise<void>} 프레임 대기 Promise
*/
const waitForFrame = () => new Promise((resolve) => {
requestAnimationFrame(() => {
requestAnimationFrame(resolve)
})
})
/**
* 페이지 전환 베일을 표시한다.
* @returns {void}
*/
const showRouteVeil = () => {
root.classList.add(ROUTE_VEIL_CLASS)
clearTimeout(veilTimer)
veilTimer = setTimeout(() => {
root.classList.remove(ROUTE_VEIL_CLASS)
}, ROUTE_VEIL_TIMEOUT_MS)
}
/**
* 페이지 전환 베일을 숨긴다.
* @returns {void}
*/
const hideRouteVeil = () => {
clearTimeout(veilTimer)
veilTimer = null
root.classList.remove(ROUTE_VEIL_CLASS)
}
/**
* 이동 완료 후 적용할 스크롤 정보를 저장한다.
* @param {import('vue-router').RouteLocationNormalizedLoaded} to - 이동 대상 라우트
* @param {import('vue-router').RouteLocationNormalizedLoaded} from - 이전 라우트
* @returns {void}
*/
const queueRouteScroll = (to, from) => {
if (to.fullPath === from.fullPath) {
return
}
routeScrollId += 1
pendingRouteScroll = {
id: routeScrollId,
to,
from
}
}
/**
* 새 페이지 렌더링 이후 스크롤 위치를 적용한다.
* @returns {Promise<void>}
*/
const applyPendingRouteScroll = async () => {
const routeScroll = pendingRouteScroll
if (!routeScroll) {
hideRouteVeil()
return
}
await waitForFrame()
if (!pendingRouteScroll || pendingRouteScroll.id !== routeScroll.id) {
return
}
pendingRouteScroll = null
const { to } = routeScroll
if (to.hash) {
const targetId = decodeURIComponent(to.hash.slice(1))
const target = document.getElementById(targetId)
if (target) {
target.scrollIntoView({ block: 'start' })
await waitForFrame()
hideRouteVeil()
return
}
}
window.scrollTo({
top: 0,
left: 0,
behavior: 'auto'
})
await waitForFrame()
hideRouteVeil()
}
router.afterEach((to, from) => {
queueRouteScroll(to, from)
})
nuxtApp.hook('page:loading:start', showRouteVeil)
nuxtApp.hook('page:finish', applyPendingRouteScroll)
nuxtApp.hook('app:error', hideRouteVeil)
})