Very Popular
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152import { merge, mimes } from "../core/utils.ts";import { posix } from "../deps/path.ts";
import type { FileResponse, Page, Site } from "../core.ts";
export interface Options { /** A function to return the page file associated with the provided url */ router?: Router;}
export type Router = (url: URL) => Promise<string | undefined>;
// Default optionsexport const defaults: Options = {};
/** A plugin to generate pages on demand in the server side */export default function (userOptions?: Partial<Options>) { const options = merge(defaults, userOptions);
return (site: Site) => { if (!options.router) { const router = new JsonRouter(site, site.src("_routes.json")); options.router = router.match.bind(router); }
site.options.server.router ||= (url) => serve(url, site, options.router!); };}
/** Build and serve a page on demand */async function serve( url: URL, site: Site, router: Router,): Promise<FileResponse | undefined> { const file = await router(url);
if (!file) { return; }
const page = await site.renderPage(file);
if (!page) { return undefined; }
// Redirect /example to /example/ const pageUrl = page.data.url as string; if (!url.pathname.endsWith("/") && pageUrl.endsWith("/")) { return [ null, { status: 301, headers: { "location": posix.join(url.pathname, "/"), }, }, ]; }
const body = page.content as string | Uint8Array; const response = { status: 200, headers: { "content-type": mimes.get(page.dest.ext) || mimes.get(".html")!, }, };
return [body, response];}
/** Class to load and manage static routes in a JSON file * Used by default if no router is provided */export class JsonRouter { /** Filename to save the {url: path} of on-demand pages */ #routesFile: string;
/** Pages that must be generated on demand */ #routes?: Map<string, string>;
constructor(site: Site, routesFile: string) { // Events to collect and save the routes automatically site.addEventListener( "afterRender", () => this.#collectRoutes(site.pages), ); site.addEventListener("afterBuild", () => this.#saveRoutes());
// Ignore the routes file by the watcher site.options.watcher.ignore.push(routesFile); this.#routesFile = routesFile; }
async match(url: URL): Promise<string | undefined> { if (!this.#routes) { await this.#loadRoutes(); }
const { pathname } = url; const path = this.#routes?.get(pathname);
// Handle urls like /example as /example/ if (!path && !pathname.endsWith("/")) { return this.#routes?.get(pathname + "/"); }
return path; }
/** Collect the routes of all pages with data.ondemand = true */ #collectRoutes(pages: Page[]): void { const routes = new Map<string, string>();
pages.forEach((page) => { if (page.data.ondemand) { routes.set(page.data.url as string, page.src.path + page.src.ext); } });
this.#routes = routes; }
/** Load the routes from the routesFile */ async #loadRoutes(): Promise<void> { try { const pages = JSON.parse( await Deno.readTextFile(this.#routesFile), ) as Record<string, string>; this.#routes = new Map(Object.entries(pages)); } catch { this.#routes = new Map(); } }
/** Save the routes into the routesFile */ async #saveRoutes(): Promise<void> { if (!this.#routes?.size) { return; }
const data: Record<string, string> = {}; this.#routes.forEach((path, url) => data[url] = path);
await Deno.writeTextFile( this.#routesFile, JSON.stringify(data, null, 2) + "\n", ); }}