Skip to content

Commit

Permalink
feat: link crawling support
Browse files Browse the repository at this point in the history
  • Loading branch information
pi0 committed Mar 30, 2022
1 parent b9db0e9 commit 462398f
Show file tree
Hide file tree
Showing 9 changed files with 108 additions and 31 deletions.
9 changes: 6 additions & 3 deletions playground/public/index.html → playground/app.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
<!DOCTYPE html>
<html lang="en">
import { defineEventHandler } from 'h3'

export default defineEventHandler(() => {
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<link rel="icon" type="image/x-icon" href="/favicon.ico">
Expand All @@ -19,4 +21,5 @@ <h1>Welcome to Nitro playground!</h1>
</ul>
</body>
</html>
</html>`
})
6 changes: 2 additions & 4 deletions playground/nitro.config.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import { defineNitroConfig } from '../src'

export default defineNitroConfig({
renderer: './app',
prerender: {
routes: [
'/api/test',
'/api/cache'
]
crawlLinks: true
}
})
23 changes: 0 additions & 23 deletions src/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import { getRollupConfig } from './rollup/config'
import { prettyPath, writeFile, isDirectory, replaceAll, serializeTemplate } from './utils'
import { GLOB_SCAN_PATTERN, scanHandlers } from './scan'
import type { Nitro } from './types'
import { createNitro } from './nitro'

export async function prepare (nitro: Nitro) {
await cleanupDir(nitro.options.output.dir)
Expand Down Expand Up @@ -61,28 +60,6 @@ export async function build (nitro: Nitro) {
return nitro.options.dev ? _watch(nitro) : _build(nitro)
}

export async function prerender (nitro: Nitro) {
// Skip if no prerender routes specified
const routes = nitro.options.prerender.routes
if (!routes.length) {
return
}
// Build with prerender preset
nitro = await createNitro({
rootDir: nitro.options.rootDir,
preset: 'prerender'
})
await build(nitro)
// Import entry
const app = await import(resolve(nitro.options.output.serverDir, 'index.mjs'))
for (const route of routes) {
const res = await app.localFetch(route)
const isJSON = (res.headers.get('content-type') || '').includes('json')
const contents = await res.text()
await writeFile(join(nitro.options.output.publicDir, route + (isJSON ? '.json' : '.html')), contents, true)
}
}

export async function writeTypes (nitro: Nitro) {
const routeTypes: Record<string, string[]> = {}

Expand Down
3 changes: 2 additions & 1 deletion src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
import mri from 'mri'
import { resolve } from 'pathe'
import { createNitro } from './nitro'
import { build, prerender, prepare, copyPublicAssets } from './build'
import { build, prepare, copyPublicAssets } from './build'
import { prerender } from './prerender'
import { createDevServer } from './server/dev'

async function main () {
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export * from './scan'
export * from './server/dev'
export * from './options'
export * from './types'
export * from './prerender'
1 change: 1 addition & 0 deletions src/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const NitroDefaults: NitroConfig = {
},
routes: {},
prerender: {
crawlLinks: false,
routes: []
},
publicDir: 'public',
Expand Down
94 changes: 94 additions & 0 deletions src/prerender.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { resolve, join } from 'pathe'
import { createNitro } from './nitro'
import { build } from './build'
import type { Nitro } from './types'
import { writeFile } from './utils'

const CRAWL_EXTENSIONS = new Set(['', '.html', '.json'])

export async function prerender (nitro: Nitro) {
// Skip if no prerender routes specified
const routes = new Set(nitro.options.prerender.routes)
if (nitro.options.prerender.crawlLinks && !routes.size) {
routes.add('/')
}
if (!routes.size) {
return
}
// Build with prerender preset
nitro = await createNitro({
rootDir: nitro.options.rootDir,
preset: 'prerender'
})
await build(nitro)

// Import renderer entry
const app = await import(resolve(nitro.options.output.serverDir, 'index.mjs'))

// Start prerendering
const generatedRoutes = new Set()
const generateRoute = async (route: string) => {
const res = await app.localFetch(route)
const contents = await res.text()

const additionalExtension = getExtension(route) ? '' : guessExt(res.headers.get('content-type'))
const routeWithIndex = route.endsWith('/') ? route + 'index' : route
const fileName = routeWithIndex + additionalExtension
await writeFile(join(nitro.options.output.publicDir, fileName), contents, true)

// Crawl Links
if (nitro.options.prerender.crawlLinks && fileName.endsWith('.html')) {
const extractedLinks = extractLinks(contents, route)
.filter(link =>
!generatedRoutes.has(link) &&
link.match(/^\//g) &&
CRAWL_EXTENSIONS.has(getExtension(link))
)
for (const route of extractedLinks) { routes.add(route) }
}
}

const generateAll = async () => {
const results = await Promise.all(Array.from(routes).map(async (route) => {
if (generatedRoutes.has(route)) { return false }
generatedRoutes.add(route)
await generateRoute(route).catch(console.error)
return true
}))
return results.filter(Boolean).length // At least one route generated
}

for (let i = 0; i < 100; i++) {
const routesGenerated = await generateAll()
if (!routesGenerated) {
break
}
}
}

const LINK_REGEX = /href=['"]?([^'" >]+)/g
function extractLinks (html: string, _url: string) {
const links: string[] = []
for (const match of html.matchAll(LINK_REGEX)) {
links.push(match[1])
}
return links
}

const EXT_REGEX = /\.[a-z0-9]+$/
function getExtension (path: string): string {
return (path.match(EXT_REGEX) || [])[0] || ''
}

const ContentTypeToExt = {
json: '.json',
html: '.html'
}
function guessExt (contentType: string = ''): string {
for (const pattern in ContentTypeToExt) {
if (contentType.includes(pattern)) {
return ContentTypeToExt[pattern]
}
}
return ''
}
1 change: 1 addition & 0 deletions src/preset.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@

import type { NitroPreset } from './types'

export function defineNitroPreset (preset: NitroPreset) {
Expand Down
1 change: 1 addition & 0 deletions src/types/nitro.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export interface NitroOptions {
},

prerender: {
crawlLinks: boolean
routes: string[]
},

Expand Down

0 comments on commit 462398f

Please sign in to comment.