feat(theme): render the not-found page through the layout

The default theme exports `NotFound.vue` as its `NotFound` component and
renders the not-found page like any page: an authored `404.md` as a doc
page without sidebar, aside or edit link unless frontmatter says otherwise,
the synthesized one bare. `useLayout()` gains `layout`, the resolved layout
name. Pages outside the sidebar no longer get the first sidebar entry as
their next link. The home link of the not-found page has no `aria-label`
anymore: it differed from the visible text, which fails WCAG 2.5.3.

BREAKING CHANGE: the `not-found` layout slot is removed, write a `404.md`
instead; `themeConfig.notFound.linkLabel` is removed, the visible
`linkText` is the accessible name.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
feat/not-found
Divyansh Singh 1 week ago
parent cb4ae79c40
commit 10e1780dcc

@ -62,7 +62,6 @@ export default defineAdditionalConfig({
title: 'PÁGINA NO ENCONTRADA', title: 'PÁGINA NO ENCONTRADA',
quote: quote:
'Pero si no cambias de dirección y sigues buscando, podrías terminar donde te diriges.', 'Pero si no cambias de dirección y sigues buscando, podrías terminar donde te diriges.',
linkLabel: 'ir a inicio',
linkText: 'Llévame a inicio' linkText: 'Llévame a inicio'
}, },

@ -70,7 +70,6 @@ export default defineAdditionalConfig({
title: 'صفحه پیدا نشد', title: 'صفحه پیدا نشد',
quote: quote:
'اما اگر جهت خود را تغییر ندهید و همچنان به جستجو ادامه دهید، ممکن است در نهایت به جایی برسید که در حال رفتن به آن هستید.', 'اما اگر جهت خود را تغییر ندهید و همچنان به جستجو ادامه دهید، ممکن است در نهایت به جایی برسید که در حال رفتن به آن هستید.',
linkLabel: 'برو به خانه',
linkText: 'من را به خانه ببر' linkText: 'من را به خانه ببر'
}, },

@ -62,7 +62,6 @@ export default defineAdditionalConfig({
title: '페이지를 찾을 수 없습니다', title: '페이지를 찾을 수 없습니다',
quote: quote:
'방향을 바꾸지 않고 계속 찾다 보면 결국 당신이 가고 있는 곳에 도달할 수도 있습니다.', '방향을 바꾸지 않고 계속 찾다 보면 결국 당신이 가고 있는 곳에 도달할 수도 있습니다.',
linkLabel: '홈으로 가기',
linkText: '집으로 데려가줘' linkText: '집으로 데려가줘'
}, },

@ -62,7 +62,6 @@ export default defineAdditionalConfig({
title: 'PÁGINA NÃO ENCONTRADA', title: 'PÁGINA NÃO ENCONTRADA',
quote: quote:
'Mas se você não mudar de direção e continuar procurando, pode acabar onde está indo.', 'Mas se você não mudar de direção e continuar procurando, pode acabar onde está indo.',
linkLabel: 'ir para a página inicial',
linkText: 'Me leve para casa' linkText: 'Me leve para casa'
}, },

@ -60,7 +60,6 @@ export default defineAdditionalConfig({
title: 'СТРАНИЦА НЕ НАЙДЕНА', title: 'СТРАНИЦА НЕ НАЙДЕНА',
quote: quote:
'Но если ты не изменишь направление и продолжишь искать, ты можешь оказаться там, куда направляешься.', 'Но если ты не изменишь направление и продолжишь искать, ты можешь оказаться там, куда направляешься.',
linkLabel: 'перейти на главную',
linkText: 'Отведи меня домой' linkText: 'Отведи меня домой'
}, },

@ -62,7 +62,6 @@ export default defineAdditionalConfig({
title: '页面未找到', title: '页面未找到',
quote: quote:
'但如果你不改变方向,并且继续寻找,你可能最终会到达你所前往的地方。', '但如果你不改变方向,并且继续寻找,你可能最终会到达你所前往的地方。',
linkLabel: '前往首页',
linkText: '带我回首页' linkText: '带我回首页'
}, },

@ -64,7 +64,6 @@ provide(layoutInfoInjectionKey, { heroImageSlotExists })
<template #page-top><slot name="page-top" /></template> <template #page-top><slot name="page-top" /></template>
<template #page-bottom><slot name="page-bottom" /></template> <template #page-bottom><slot name="page-bottom" /></template>
<template #not-found><slot name="not-found" /></template>
<template #home-hero-before><slot name="home-hero-before" /></template> <template #home-hero-before><slot name="home-hero-before" /></template>
<template #home-hero-info-before><slot name="home-hero-info-before" /></template> <template #home-hero-info-before><slot name="home-hero-info-before" /></template>
<template #home-hero-info><slot name="home-hero-info" /></template> <template #home-hero-info><slot name="home-hero-info" /></template>

@ -21,11 +21,7 @@ const { currentLang } = useLangs()
</blockquote> </blockquote>
<div class="action"> <div class="action">
<a <a class="link" :href="withBase(theme.notFound?.link ?? currentLang.link)">
class="link"
:href="withBase(theme.notFound?.link ?? currentLang.link)"
:aria-label="theme.notFound?.linkLabel ?? 'go to home'"
>
{{ theme.notFound?.linkText ?? 'Take me home' }} {{ theme.notFound?.linkText ?? 'Take me home' }}
</a> </a>
</div> </div>

@ -1,15 +1,12 @@
<script setup lang="ts"> <script setup lang="ts">
import { resolveDynamicComponent } from 'vue' import { resolveDynamicComponent } from 'vue'
import { useData } from '../composables/data'
import { useLayout } from '../composables/layout' import { useLayout } from '../composables/layout'
import NotFound from '../NotFound.vue'
import VPDoc from './VPDoc.vue' import VPDoc from './VPDoc.vue'
import VPHome from './VPHome.vue' import VPHome from './VPHome.vue'
import VPPage from './VPPage.vue' import VPPage from './VPPage.vue'
const { page, frontmatter } = useData() const { isHome, hasSidebar, layout } = useLayout()
const { isHome, hasSidebar } = useLayout()
function isRegistered(component: string): boolean { function isRegistered(component: string): boolean {
return typeof resolveDynamicComponent(component) !== 'string' return typeof resolveDynamicComponent(component) !== 'string'
@ -22,14 +19,12 @@ function isRegistered(component: string): boolean {
id="VPContent" id="VPContent"
:class="{ 'has-sidebar': hasSidebar, 'is-home': isHome }" :class="{ 'has-sidebar': hasSidebar, 'is-home': isHome }"
> >
<slot name="not-found" v-if="page.isNotFound"><NotFound /></slot> <VPPage v-if="layout === 'page' && !isRegistered('page')">
<VPPage v-else-if="frontmatter.layout === 'page' && !isRegistered('page')">
<template #page-top><slot name="page-top" /></template> <template #page-top><slot name="page-top" /></template>
<template #page-bottom><slot name="page-bottom" /></template> <template #page-bottom><slot name="page-bottom" /></template>
</VPPage> </VPPage>
<VPHome v-else-if="frontmatter.layout === 'home' && !isRegistered('home')"> <VPHome v-else-if="layout === 'home' && !isRegistered('home')">
<template #home-hero-before><slot name="home-hero-before" /></template> <template #home-hero-before><slot name="home-hero-before" /></template>
<template #home-hero-info-before><slot name="home-hero-info-before" /></template> <template #home-hero-info-before><slot name="home-hero-info-before" /></template>
<template #home-hero-info><slot name="home-hero-info" /></template> <template #home-hero-info><slot name="home-hero-info" /></template>
@ -42,7 +37,7 @@ function isRegistered(component: string): boolean {
<template #home-features-after><slot name="home-features-after" /></template> <template #home-features-after><slot name="home-features-after" /></template>
</VPHome> </VPHome>
<VPDoc v-else-if="(!frontmatter.layout || frontmatter.layout === 'doc') && !isRegistered('doc')"> <VPDoc v-else-if="layout === 'doc' && !isRegistered('doc')">
<template #doc-top><slot name="doc-top" /></template> <template #doc-top><slot name="doc-top" /></template>
<template #doc-bottom><slot name="doc-bottom" /></template> <template #doc-bottom><slot name="doc-bottom" /></template>
@ -58,7 +53,7 @@ function isRegistered(component: string): boolean {
<template #aside-bottom><slot name="aside-bottom" /></template> <template #aside-bottom><slot name="aside-bottom" /></template>
</VPDoc> </VPDoc>
<component v-else :is="frontmatter.layout || 'doc'" /> <component v-else :is="layout" />
</div> </div>
</template> </template>

@ -13,7 +13,9 @@ const editLink = useEditLink()
const control = usePrevNext() const control = usePrevNext()
const hasEditLink = computed( const hasEditLink = computed(
() => theme.value.editLink && frontmatter.value.editLink !== false () =>
theme.value.editLink &&
(frontmatter.value.editLink ?? !page.value.isNotFound) !== false
) )
const hasLastUpdated = computed(() => page.value.lastUpdated) const hasLastUpdated = computed(() => page.value.lastUpdated)
const showFooter = computed( const showFooter = computed(

@ -21,15 +21,26 @@ const sidebar = shallowRef<DefaultTheme.SidebarItem[]>([])
const isDesktop = useMediaQuery('(min-width: 60rem)') const isDesktop = useMediaQuery('(min-width: 60rem)')
export function useLayout(): DefaultTheme.Layout { export function useLayout(): DefaultTheme.Layout {
const { frontmatter, theme } = useData() const { frontmatter, page, theme } = useData()
// a not-found page reads like a doc page without the doc chrome; the one
// synthesized from the theme's `NotFound` component has no prose to style
const isNotFound = computed(() => !!page.value.isNotFound)
const layout = computed<string>(() => {
return (
frontmatter.value.layout ||
(isNotFound.value && !page.value.filePath ? 'page' : 'doc')
)
})
const isHome = computed(() => { const isHome = computed(() => {
return !!(frontmatter.value.isHome ?? frontmatter.value.layout === 'home') return !!(frontmatter.value.isHome ?? layout.value === 'home')
}) })
const hasSidebar = computed(() => { const hasSidebar = computed(() => {
return ( return (
frontmatter.value.sidebar !== false && (frontmatter.value.sidebar ?? !isNotFound.value) !== false &&
sidebar.value.length > 0 && sidebar.value.length > 0 &&
!isHome.value !isHome.value
) )
@ -43,7 +54,8 @@ export function useLayout(): DefaultTheme.Layout {
const hasAside = computed(() => { const hasAside = computed(() => {
if (isHome.value) return false if (isHome.value) return false
if (frontmatter.value.aside != null) return !!frontmatter.value.aside const aside = frontmatter.value.aside ?? (isNotFound.value ? false : null)
if (aside != null) return !!aside
return theme.value.aside !== false return theme.value.aside !== false
}) })
@ -59,6 +71,7 @@ export function useLayout(): DefaultTheme.Layout {
}) })
return { return {
layout,
isHome, isHome,
sidebar: shallowReadonly(sidebar), sidebar: shallowReadonly(sidebar),
sidebarGroups, sidebarGroups,

@ -22,6 +22,11 @@ export function usePrevNext() {
return isActive(page.value.relativePath, '', link.link, false, true) return isActive(page.value.relativePath, '', link.link, false, true)
}) })
// a page outside the sidebar (the not-found page, for one) has no
// neighbours; `candidates[-1 + 1]` would otherwise elect the first entry
const prevCandidate = index === -1 ? undefined : candidates[index - 1]
const nextCandidate = index === -1 ? undefined : candidates[index + 1]
const hidePrev = const hidePrev =
(theme.value.docFooter?.prev === false && !frontmatter.value.prev) || (theme.value.docFooter?.prev === false && !frontmatter.value.prev) ||
frontmatter.value.prev === false frontmatter.value.prev === false
@ -40,20 +45,20 @@ export function usePrevNext() {
: typeof frontmatter.value.prev === 'object' : typeof frontmatter.value.prev === 'object'
? frontmatter.value.prev.text ? frontmatter.value.prev.text
: undefined) ?? : undefined) ??
candidates[index - 1]?.docFooterText ?? prevCandidate?.docFooterText ??
candidates[index - 1]?.text, prevCandidate?.text,
link: link:
(typeof frontmatter.value.prev === 'object' (typeof frontmatter.value.prev === 'object'
? frontmatter.value.prev.link ? frontmatter.value.prev.link
: undefined) ?? candidates[index - 1]?.link, : undefined) ?? prevCandidate?.link,
target: target:
(typeof frontmatter.value.prev === 'object' (typeof frontmatter.value.prev === 'object'
? frontmatter.value.prev.target ? frontmatter.value.prev.target
: undefined) ?? candidates[index - 1]?.target, : undefined) ?? prevCandidate?.target,
rel: rel:
(typeof frontmatter.value.prev === 'object' (typeof frontmatter.value.prev === 'object'
? frontmatter.value.prev.rel ? frontmatter.value.prev.rel
: undefined) ?? candidates[index - 1]?.rel : undefined) ?? prevCandidate?.rel
}, },
next: hideNext next: hideNext
? undefined ? undefined
@ -64,20 +69,20 @@ export function usePrevNext() {
: typeof frontmatter.value.next === 'object' : typeof frontmatter.value.next === 'object'
? frontmatter.value.next.text ? frontmatter.value.next.text
: undefined) ?? : undefined) ??
candidates[index + 1]?.docFooterText ?? nextCandidate?.docFooterText ??
candidates[index + 1]?.text, nextCandidate?.text,
link: link:
(typeof frontmatter.value.next === 'object' (typeof frontmatter.value.next === 'object'
? frontmatter.value.next.link ? frontmatter.value.next.link
: undefined) ?? candidates[index + 1]?.link, : undefined) ?? nextCandidate?.link,
target: target:
(typeof frontmatter.value.next === 'object' (typeof frontmatter.value.next === 'object'
? frontmatter.value.next.target ? frontmatter.value.next.target
: undefined) ?? candidates[index + 1]?.target, : undefined) ?? nextCandidate?.target,
rel: rel:
(typeof frontmatter.value.next === 'object' (typeof frontmatter.value.next === 'object'
? frontmatter.value.next.rel ? frontmatter.value.next.rel
: undefined) ?? candidates[index + 1]?.rel : undefined) ?? nextCandidate?.rel
} }
} }
}) })

@ -12,6 +12,7 @@ import type { Theme } from 'vitepress'
import VPBadge from './components/VPBadge.vue' import VPBadge from './components/VPBadge.vue'
import Layout from './Layout.vue' import Layout from './Layout.vue'
import NotFound from './NotFound.vue'
export { default as VPBadge } from './components/VPBadge.vue' export { default as VPBadge } from './components/VPBadge.vue'
export { default as VPButton } from './components/VPButton.vue' export { default as VPButton } from './components/VPButton.vue'
@ -37,6 +38,7 @@ export { useLayout } from './composables/layout'
const theme: Theme = { const theme: Theme = {
Layout, Layout,
NotFound,
enhanceApp: ({ app }) => { enhanceApp: ({ app }) => {
app.component('Badge', VPBadge) app.component('Badge', VPBadge)
} }

@ -10,6 +10,12 @@ export namespace DefaultTheme {
* The layout state returned by `useLayout` from `vitepress/theme`. * The layout state returned by `useLayout` from `vitepress/theme`.
*/ */
export interface Layout { export interface Layout {
/**
* The layout the current page renders with: its `layout` frontmatter,
* or the default (`doc`, and `page` for the not-found page synthesized
* from the theme).
*/
layout: ComputedRef<string>
isHome: ComputedRef<boolean> isHome: ComputedRef<boolean>
sidebar: Readonly<ShallowRef<SidebarItem[]>> sidebar: Readonly<ShallowRef<SidebarItem[]>>
@ -505,13 +511,6 @@ export namespace DefaultTheme {
*/ */
link?: string link?: string
/**
* Set aria label for home link.
*
* @default 'go to home'
*/
linkLabel?: string
/** /**
* Set custom home link text. * Set custom home link text.
* *

Loading…
Cancel
Save