Skip to content
This repository was archived by the owner on Jul 6, 2025. It is now read-only.

Commit 9bafed7

Browse files
author
Je
committed
refactor: improve 404 page
1 parent 31e5b07 commit 9bafed7

File tree

5 files changed

+118
-91
lines changed

5 files changed

+118
-91
lines changed

app.ts

Lines changed: 24 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React, { ComponentType, createContext, useCallback, useEffect, useState } from 'https://esm.sh/react'
22
import { hydrate, render } from 'https://esm.sh/react-dom'
33
import { DataContext } from './data.ts'
4-
import { E404Page, errAppEl, errPageEl } from './error.ts'
4+
import { E404Page, E501App, E501Page } from './error.ts'
55
import events from './events.ts'
66
import route from './route.ts'
77
import { RouterContext } from './router.ts'
@@ -19,21 +19,22 @@ function ALEPH({ config }: {
1919
config: {
2020
manifest: AppManifest
2121
data: Record<string, any>
22-
app?: { Component: ComponentType<any> }
23-
page: { Component: ComponentType<any> }
2422
pageModules: Record<string, { moduleId: string, hash: string }>
2523
url: RouterURL
24+
AppComponent?: ComponentType<any>
25+
E404Component?: ComponentType<any>
26+
PageComponent?: ComponentType<any>
2627
}
2728
}) {
2829
const [manifest, setManifest] = useState(() => config.manifest)
2930
const [data, setData] = useState(() => config.data)
3031
const [app, setApp] = useState(() => ({
31-
Component: config.app ? (util.isLikelyReactComponent(config.app.Component) ? config.app.Component : () => errAppEl) : null
32+
Component: config.AppComponent ? (util.isLikelyReactComponent(config.AppComponent) ? config.AppComponent : E501App) : null
3233
}))
3334
const [pageModules, setPageModules] = useState(() => config.pageModules)
3435
const [page, setPage] = useState(() => ({
3536
url: config.url,
36-
Component: util.isLikelyReactComponent(config.page.Component) ? config.page.Component : () => errPageEl
37+
Component: config.PageComponent ? (util.isLikelyReactComponent(config.PageComponent) ? config.PageComponent : E501Page) : (config.E404Component || E404Page)
3738
}))
3839
const onpopstate = useCallback(async () => {
3940
const { baseUrl, defaultLocale, locales } = manifest
@@ -42,23 +43,25 @@ function ALEPH({ config }: {
4243
Object.keys(pageModules),
4344
{
4445
defaultLocale,
45-
locales: Object.keys(locales),
46-
fallback: '/404'
46+
locales: Object.keys(locales)
4747
}
4848
)
49-
if (url.pagePath in pageModules) {
49+
if (url.pagePath && url.pagePath in pageModules) {
5050
const mod = pageModules[url.pagePath]!
5151
const { default: Component } = await import(getModuleImportUrl(baseUrl, mod))
5252
if (util.isLikelyReactComponent(Component)) {
5353
setPage({ url, Component })
5454
} else {
5555
setPage({
5656
url,
57-
Component: () => errPageEl
57+
Component: E501Page
5858
})
5959
}
6060
} else {
61-
setPage({ url })
61+
setPage({
62+
url,
63+
Component: config.E404Component || E404Page
64+
})
6265
}
6366
}, [manifest, pageModules])
6467

@@ -85,7 +88,7 @@ function ALEPH({ config }: {
8588
setApp({ Component })
8689
} else {
8790
setPage({
88-
Component: () => errAppEl
91+
Component: E501App
8992
})
9093
}
9194
} else if (moduleId === './data.js' || moduleId === './data/index.js') {
@@ -190,10 +193,12 @@ export async function bootstrap({
190193
locales,
191194
dataModule,
192195
appModule,
196+
e404Module,
193197
pageModules
194198
}: AppManifest & {
195199
dataModule: Module | null
196200
appModule: Module | null
201+
e404Module: Module | null
197202
pageModules: Record<string, Module>
198203
}) {
199204
const { document } = window as any
@@ -203,7 +208,7 @@ export async function bootstrap({
203208
let url: RouterURL
204209
if (dataEl) {
205210
const data = JSON.parse(dataEl.innerHTML)
206-
if (data.url && util.isNEString(data.url.pagePath) && data.url.pagePath in pageModules) {
211+
if (util.isPlainObject(data.url)) {
207212
url = data.url
208213
} else {
209214
throw new Error("invalid ssr-data")
@@ -214,8 +219,7 @@ export async function bootstrap({
214219
Object.keys(pageModules),
215220
{
216221
defaultLocale,
217-
locales: Object.keys(locales),
218-
fallback: '/404'
222+
locales: Object.keys(locales)
219223
}
220224
)
221225
}
@@ -224,24 +228,25 @@ export async function bootstrap({
224228
const [
225229
{ default: data },
226230
{ default: AppComponent },
231+
{ default: E404Component },
227232
{ default: PageComponent }
228233
] = await Promise.all([
229234
dataModule ? import(getModuleImportUrl(baseUrl, dataModule)) : Promise.resolve({ default: {} }),
230235
appModule ? import(getModuleImportUrl(baseUrl, appModule)) : Promise.resolve({}),
231-
pageModule ? import(getModuleImportUrl(baseUrl, pageModule)) : Promise.resolve({ default: E404Page }),
236+
e404Module ? import(getModuleImportUrl(baseUrl, e404Module)) : Promise.resolve({}),
237+
pageModule ? import(getModuleImportUrl(baseUrl, pageModule)) : Promise.resolve({}),
232238
])
233239
const el = React.createElement(
234240
ALEPH,
235241
{
236242
config: {
237243
manifest: { baseUrl, defaultLocale, locales },
238244
data,
239-
app: appModule ? { Component: AppComponent } : undefined,
240-
page: {
241-
Component: PageComponent
242-
},
243245
pageModules,
244246
url,
247+
AppComponent,
248+
E404Component,
249+
PageComponent,
245250
}
246251
}
247252
)

error.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,34 @@
11
import React from 'https://esm.sh/react'
22
import Head from './head.ts'
33

4-
export const errAppEl = React.createElement(
4+
const e501AppEl = React.createElement(
55
ErrorPage,
66
{
77
status: 501,
8-
text: 'app module should export default as a react component',
8+
text: 'app module should default as a react component',
99
refreshButton: true
1010
}
1111
)
12-
13-
export const errPageEl = React.createElement(
12+
const e501PageEl = React.createElement(
1413
ErrorPage,
1514
{
1615
status: 501,
17-
text: 'page module should export default as a react component',
16+
text: 'page module should default as a react component',
1817
refreshButton: true
1918
}
2019
)
20+
const e404PageEl = React.createElement(ErrorPage, { status: 404 })
21+
22+
export function E501App() {
23+
return e501AppEl
24+
}
25+
26+
export function E501Page() {
27+
return e501PageEl
28+
}
2129

2230
export function E404Page() {
23-
return React.createElement(ErrorPage, { status: 404 })
31+
return e404PageEl
2432
}
2533

2634
export function ErrorPage({ status, text = getStatusText(status), refreshButton }: { status: number, text?: string, refreshButton?: boolean }) {

project.ts

Lines changed: 65 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ export default class Project {
5454
baseUrl: '/',
5555
defaultLocale: 'en',
5656
ssr: {
57-
fallback: 'fallback.html'
57+
fallback: '404.html'
5858
},
5959
buildTarget: mode === 'development' ? 'es2018' : 'es2015',
6060
sourceMap: false,
@@ -89,10 +89,15 @@ export default class Project {
8989
}
9090

9191
isHMRable(moduleId: string) {
92-
if (reHttp.test(moduleId)) {
93-
return false
94-
}
95-
return moduleId === './app.js' || moduleId === './data.js' || (moduleId === './data/index.js' && !this.#modules.has('./data.js')) || moduleId.startsWith('./pages/') || moduleId.startsWith('./components/') || reStyleModuleExt.test(moduleId)
92+
return !reHttp.test(moduleId) && (
93+
moduleId === './404.js' ||
94+
moduleId === './app.js' ||
95+
moduleId === './data.js' ||
96+
(moduleId === './data/index.js' && !this.#modules.has('./data.js')) ||
97+
moduleId.startsWith('./pages/') ||
98+
moduleId.startsWith('./components/') ||
99+
reStyleModuleExt.test(moduleId)
100+
)
96101
}
97102

98103
getModule(id: string): Module | null {
@@ -168,10 +173,13 @@ export default class Project {
168173
Array.from(this.#pageModules.keys()),
169174
{
170175
location,
171-
defaultLocale,
172-
fallback: '/404'
176+
defaultLocale
173177
}
174178
)
179+
if (url.pagePath === '') {
180+
return [200, this.getSPAIndexHtml()]
181+
}
182+
175183
const mainModule = this.#modules.get('./main.js')!
176184
const { code, head, body } = await this._renderPage(url)
177185
const html = createHtml({
@@ -278,13 +286,13 @@ export default class Project {
278286

279287
const { ssr } = this.config
280288
if (ssr) {
281-
const fallback = ((util.isPlainObject(ssr) && ssr.fallback ? util.trimSuffix(ssr.fallback, '.html') : '') || 'fallback') + '.html'
282-
await writeTextFile(path.join(outputDir, fallback), this.getSPAIndexHtml())
283289
for (const pathname of this.#pageModules.keys()) {
284290
const [_, html] = await this.getPageHtml({ pathname })
285291
const htmlFile = path.join(outputDir, pathname, 'index.html')
286292
await writeTextFile(htmlFile, html)
287293
}
294+
const fallback = path.join(outputDir, util.isPlainObject(ssr) && ssr.fallback ? ssr.fallback : '404.html')
295+
await writeTextFile(fallback, this.getSPAIndexHtml())
288296
} else {
289297
await writeTextFile(path.join(outputDir, 'index.html'), this.getSPAIndexHtml())
290298
}
@@ -340,9 +348,10 @@ export default class Project {
340348
if (typeof ssr === 'boolean') {
341349
Object.assign(this.config, { ssr })
342350
} else if (util.isPlainObject(ssr)) {
351+
const fallback = util.isNEString(ssr.fallback) ? ssr.fallback : '404.html'
343352
const include = util.isArray(ssr.include) ? ssr.include : []
344353
const exclude = util.isArray(ssr.exclude) ? ssr.exclude : []
345-
Object.assign(this.config, { ssr: { include, exclude } })
354+
Object.assign(this.config, { ssr: { fallback, include, exclude } })
346355
}
347356
if (/^es(20\d{2}|next)$/i.test(buildTarget)) {
348357
Object.assign(this.config, { buildTarget: buildTarget.toLowerCase() })
@@ -394,6 +403,7 @@ export default class Project {
394403
switch (name.replace(reModuleExt, '')) {
395404
case 'app':
396405
case 'data':
406+
case '404':
397407
await this._compile('./' + name)
398408
break
399409
}
@@ -453,6 +463,7 @@ export default class Project {
453463
}
454464
const moduleId = './' + path.replace(reModuleExt, '.js')
455465
switch (moduleId) {
466+
case './404.js':
456467
case './app.js':
457468
case './data.js':
458469
case './data/index.js': {
@@ -558,6 +569,7 @@ export default class Project {
558569
locales: {},
559570
dataModule: null,
560571
appModule: null,
572+
e404Module: null,
561573
pageModules: {}
562574
}
563575
const module = this._parseUrl('./main.js')
@@ -585,6 +597,14 @@ export default class Project {
585597
}
586598
deps.push({ url, hash })
587599
}
600+
if (this.#modules.has('./404.js')) {
601+
const { url, hash } = this.#modules.get('./404.js')!
602+
config.e404Module = {
603+
moduleId: './404.js',
604+
hash
605+
}
606+
deps.push({ url, hash })
607+
}
588608
this.#pageModules.forEach(({ moduleId }, pagePath) => {
589609
const { url, hash } = this.#modules.get(moduleId)!
590610
const mod = { moduleId, hash }
@@ -791,7 +811,7 @@ export default class Project {
791811
const compileOptions = {
792812
target: this.config.buildTarget,
793813
mode: this.mode,
794-
reactRefresh: this.isDev && !mod.isRemote && (mod.id === './app.js' || mod.id.startsWith('./pages/') || mod.id.startsWith('./components/')),
814+
reactRefresh: this.isDev && !mod.isRemote && (mod.id === './404.js' || mod.id === './app.js' || mod.id.startsWith('./pages/') || mod.id.startsWith('./components/')),
795815
rewriteImportPath: (path: string) => this._rewriteImportPath(mod, path),
796816
}
797817
const { diagnostics, outputText, sourceMapText } = compile(mod.url, sourceContent, compileOptions)
@@ -977,46 +997,40 @@ export default class Project {
977997

978998
private async _renderPage(url: RouterURL) {
979999
const start = performance.now()
980-
const ret: RenderResult = { code: 200, head: [], body: '' }
981-
if (this.#pageModules.has(url.pagePath)) {
982-
const pm = this.#pageModules.get(url.pagePath)!
983-
if (pm.rendered.has(url.pathname)) {
984-
const cache = pm.rendered.get(url.pathname)!
985-
return { ...cache }
986-
}
987-
try {
988-
const appModule = this.#modules.get('./app.js')
989-
const pageModule = this.#modules.get(pm.moduleId)!
990-
const [
991-
{ renderPage, renderHead },
992-
app,
993-
page
994-
] = await Promise.all([
995-
import(this.#modules.get('//deno.land/x/aleph/renderer.js')!.jsFile),
996-
appModule ? this.importModuleAsComponent('./app.js') : Promise.resolve({}),
997-
this.importModuleAsComponent(pm.moduleId)
998-
])
999-
const data = await this.getData()
1000-
const html = renderPage(data, url, appModule ? app : undefined, page)
1001-
const head = renderHead([
1002-
pageModule.deps.map(({ url }) => url).filter(url => reStyleModuleExt.test(url)),
1003-
appModule?.deps.map(({ url }) => url).filter(url => reStyleModuleExt.test(url))
1004-
].filter(Boolean).flat())
1005-
ret.code = 200
1006-
ret.head = head
1007-
ret.body = `<main>${html}</main>`
1008-
pm.rendered.set(url.pathname, { ...ret })
1009-
log.debug(`render page '${url.pagePath}' in ${Math.round(performance.now() - start)}ms`)
1010-
} catch (err) {
1011-
ret.code = 500
1012-
ret.head = ['<title>500 Error - Aleph.js</title>']
1013-
ret.body = `<pre>${AnsiUp.ansi_to_html(err.stack)}</pre>`
1014-
log.error(err.stack)
1015-
}
1016-
} else {
1017-
ret.code = 404
1018-
ret.head = ['<title ssr>404 Error - Aleph.js</title>']
1019-
ret.body = '<main><p><strong><code>404</code></strong><small> - </small><span>page not found</span></p></main>'
1000+
const ret: RenderResult = { code: 200, head: [], body: '<main></main>' }
1001+
const pm = this.#pageModules.get(url.pagePath)!
1002+
if (pm.rendered.has(url.pathname)) {
1003+
const cache = pm.rendered.get(url.pathname)!
1004+
return { ...cache }
1005+
}
1006+
try {
1007+
const appModule = this.#modules.get('./app.js')
1008+
const pageModule = this.#modules.get(pm.moduleId)!
1009+
const [
1010+
{ renderPage, renderHead },
1011+
app,
1012+
page
1013+
] = await Promise.all([
1014+
import(this.#modules.get('//deno.land/x/aleph/renderer.js')!.jsFile),
1015+
appModule ? this.importModuleAsComponent('./app.js') : Promise.resolve({}),
1016+
this.importModuleAsComponent(pm.moduleId)
1017+
])
1018+
const data = await this.getData()
1019+
const html = renderPage(data, url, appModule ? app : undefined, page)
1020+
const head = renderHead([
1021+
pageModule.deps.map(({ url }) => url).filter(url => reStyleModuleExt.test(url)),
1022+
appModule?.deps.map(({ url }) => url).filter(url => reStyleModuleExt.test(url))
1023+
].filter(Boolean).flat())
1024+
ret.code = 200
1025+
ret.head = head
1026+
ret.body = `<main>${html}</main>`
1027+
pm.rendered.set(url.pathname, { ...ret })
1028+
log.debug(`render page '${url.pagePath}' in ${Math.round(performance.now() - start)}ms`)
1029+
} catch (err) {
1030+
ret.code = 500
1031+
ret.head = ['<title>500 Error - Aleph.js</title>']
1032+
ret.body = `<main><pre>${AnsiUp.ansi_to_html(err.stack)}</pre></main>`
1033+
log.error(err.stack)
10201034
}
10211035
return ret
10221036
}

0 commit comments

Comments
 (0)