diff --git a/src/config.ts b/src/config.ts index 933cc6de0..0b48e0f40 100644 --- a/src/config.ts +++ b/src/config.ts @@ -19,12 +19,14 @@ export interface TableOfContents { export interface Config { title?: string; + base?: string; pages?: (Page | Section)[]; // TODO rename to sidebar? toc?: TableOfContents; } export async function readConfig(root: string): Promise { for (const ext of [".js", ".ts"]) { + let config; try { const configPath = join(process.cwd(), root, ".observablehq", "config" + ext); const configStat = await stat(configPath); @@ -32,9 +34,12 @@ export async function readConfig(root: string): Promise { // any changes to the config on reload. TODO It would be better to either // restart the preview server when the config changes, or for the preview // server to watch the config file and hot-reload it automatically. - return (await import(`${configPath}?${configStat.mtimeMs}`)).default; + config = (await import(`${configPath}?${configStat.mtimeMs}`)).default; } catch { continue; } + if (typeof config?.base === "string" && !config.base.match(/^[/]\w+[/]$/)) + throw new Error(`invalid base: ${config.base}`); + return config; } } diff --git a/src/preview.ts b/src/preview.ts index 514b539f1..0c1afc594 100644 --- a/src/preview.ts +++ b/src/preview.ts @@ -51,6 +51,17 @@ class Server { try { const url = new URL(req.url!, "http://localhost"); let {pathname} = url; + const config = await readConfig(this.root); + const {base = "/"} = config ?? {}; + if (!base || !pathname.startsWith(base)) { + if (pathname === "/") { + res.writeHead(302, {Location: base}); + res.end(); + return; + } + throw new HttpError("Not found", 404); + } + pathname = pathname.slice(base.length - 1); if (pathname === "/_observablehq/runtime.js") { send(req, "/@observablehq/runtime/dist/runtime.js", {root: "./node_modules"}).pipe(res); } else if (pathname.startsWith("/_observablehq/")) {