From b1388861f23772bdadac4e24a49fd57196de352c Mon Sep 17 00:00:00 2001 From: Bret Comnes Date: Sat, 18 Apr 2026 11:43:15 -0700 Subject: [PATCH 1/2] Inject pageUrl into page vars automatically Every page now gets a pageUrl derived from pageInfo.path so layouts can build canonical URLs without manually computing the path-to-URL conversion. User-supplied pageUrl in pageVars or globalVars still takes precedence. Closes #238 Fix pageUrl for loose markdown pages and Windows path separators Loose markdown files (not page.md/README.md) have outputName set to their slug.html rather than index.html, so the previous formula produced an incorrect directory-style URL for them. The URL is now derived from outputName: index pages get a trailing slash, non-index pages include the filename. fsPathToUrlPath is used to normalize any OS-specific path separators. Extract computePageUrl helper and add unit tests for URL derivation rules Co-Authored-By: Claude Sonnet 4.6 Remove unnecessary #computePageUrl wrapper method Fix pretty types Improve typecast Document pageUrl variable injection in vars --- README.md | 9 +++++---- lib/build-pages/compute-page-url.js | 17 +++++++++++++++++ lib/build-pages/page-data.js | 6 ++---- lib/build-pages/page-data.test.js | 19 +++++++++++++++++++ lib/identify-pages.js | 4 ++++ package.json | 1 + 6 files changed, 48 insertions(+), 8 deletions(-) create mode 100644 lib/build-pages/compute-page-url.js diff --git a/README.md b/README.md index a90490b..9495137 100644 --- a/README.md +++ b/README.md @@ -301,7 +301,7 @@ const blogIndex: PageFunction = async ({ return html`

I love ${favoriteCake}!!

` } @@ -930,8 +930,8 @@ const feedsTemplate: TemplateAsyncIterator = async function * ({ return { date_published: page.vars['publishDate'], title: page.vars['title'], - url: `${homePageUrl}/${page.pageInfo.path}/`, - id: `${homePageUrl}/${page.pageInfo.path}/#${page.vars['publishDate']}`, + url: `${homePageUrl}${page.pageInfo.url}`, + id: `${homePageUrl}${page.pageInfo.url}#${page.vars['publishDate']}`, content_html: await page.renderInnerPage({ pages }) } }, { concurrency: 4 }) @@ -1022,7 +1022,7 @@ const buildGlobalData: AsyncGlobalDataFunction = async ({ pages }) =
    ${blogPosts.map(p => html`
  • - + ${p.vars?.title}
  • @@ -1164,6 +1164,7 @@ Pages and Layouts receive an object with the following parameters: - `page`: An object of the page being rendered with the following parameters: - `type`: The type of page (`md`, `html`, or `js`) - `path`: The directory path for the page. + - `url`: The canonical URL path for the page (e.g. `/blog/my-post/` for index pages, `/blog/loose-page.html` for loose non-index pages). Combine with a `siteUrl` from `global.vars.ts` to build full URLs: `` `${vars.siteUrl}${page.url}` ``. - `outputName`: The output name of the final file. - `outputRelname`: The relative output name/path of the output file. - `pageFile`: Raw `src` path details of the page file diff --git a/lib/build-pages/compute-page-url.js b/lib/build-pages/compute-page-url.js new file mode 100644 index 0000000..94f3f01 --- /dev/null +++ b/lib/build-pages/compute-page-url.js @@ -0,0 +1,17 @@ +import { fsPathToUrlPath } from './page-builders/fs-path-to-url.js' + +/** + * Derive the canonical URL path for a page from its filesystem path and output name. + * Index pages get a trailing-slash URL; other outputs include the filename. + * + * @param {object} params + * @param {string} params.path - The page's directory path relative to src root + * @param {string} params.outputName - The output filename (e.g. 'index.html' or 'loose-md.html') + * @returns {string} + */ +export function computePageUrl ({ path, outputName }) { + if (outputName === 'index.html') { + return path ? fsPathToUrlPath(path) + '/' : '/' + } + return path ? fsPathToUrlPath(path) + '/' + outputName : '/' + outputName +} diff --git a/lib/build-pages/page-data.js b/lib/build-pages/page-data.js index e04bc84..a64542f 100644 --- a/lib/build-pages/page-data.js +++ b/lib/build-pages/page-data.js @@ -5,7 +5,6 @@ import { resolveVars, resolvePostVars } from './resolve-vars.js' import { pageBuilders } from './page-builders/index.js' -// @ts-expect-error import pretty from 'pretty' /** @@ -152,13 +151,12 @@ export class PageData { if (!this.#initialized) throw new Error(`Initialize PageData before accessing vars for page "${this.pageInfo?.path ?? ''}"`) try { const { globalVars, globalDataVars, pageVars, builderVars } = this - // @ts-ignore - return { + return /** @type {T} */ (/** @type {unknown} */ ({ ...globalVars, ...globalDataVars, ...pageVars, ...builderVars, - } + })) } catch (err) { throw new Error( `Failed to resolve vars for page "${this.pageInfo?.path ?? ''}": ${err instanceof Error ? err.message : String(err)}`, diff --git a/lib/build-pages/page-data.test.js b/lib/build-pages/page-data.test.js index 82123f7..13acbb8 100644 --- a/lib/build-pages/page-data.test.js +++ b/lib/build-pages/page-data.test.js @@ -1,6 +1,7 @@ import { test } from 'node:test' import assert from 'node:assert' import { PageData } from './page-data.js' +import { computePageUrl } from './compute-page-url.js' test.describe('PageData.vars', () => { test('throws with page path before initialization', () => { @@ -51,3 +52,21 @@ test.describe('PageData.vars', () => { ) }) }) + +test.describe('computePageUrl', () => { + test('root index.html maps to /', () => { + assert.strictEqual(computePageUrl({ path: '', outputName: 'index.html' }), '/') + }) + + test('nested index.html gets a trailing-slash URL', () => { + assert.strictEqual(computePageUrl({ path: 'blog/post', outputName: 'index.html' }), '/blog/post/') + }) + + test('non-index output includes filename in URL', () => { + assert.strictEqual(computePageUrl({ path: 'md-page', outputName: 'loose-md.html' }), '/md-page/loose-md.html') + }) + + test('non-index file at root includes filename only', () => { + assert.strictEqual(computePageUrl({ path: '', outputName: 'robots.txt' }), '/robots.txt') + }) +}) diff --git a/lib/identify-pages.js b/lib/identify-pages.js index 5b0ccd0..cec555a 100644 --- a/lib/identify-pages.js +++ b/lib/identify-pages.js @@ -8,6 +8,7 @@ import { resolve, relative, join, basename } from 'path' import { pageBuilders } from './build-pages/index.js' import { DomStackDuplicatePageError } from './helpers/domstack-error.js' import { nodeHasTS } from './helpers/has-ts.js' +import { computePageUrl } from './build-pages/compute-page-url.js' const __dirname = import.meta.dirname @@ -157,6 +158,7 @@ const shaper = ({ * @property {PageFileAsset | undefined} [pageVars] - The variables associated with the page. (Replace 'any' with the appropriate type if known.) * @property {Object | undefined} [workers] - Web worker files associated with this page. * @property {string} path - The directory path for the page. + * @property {string} url - The canonical URL path for the page (e.g. `/blog/my-post/` or `/blog/loose-page.html`). * @property {string} outputName - The name of the output file. * @property {string} outputRelname - The relative name/path for the output file. * @property {boolean} draft - If the page is marked as a draft or not. Draft pages are only included when buildDrafts is passed. @@ -317,6 +319,7 @@ export async function identifyPages (src, opts = {}) { pageVars, workers: Object.keys(workerFiles).length > 0 ? { ...workerFiles } : undefined, path: dir, + url: computePageUrl({ path: dir, outputName: 'index.html' }), outputName: 'index.html', outputRelname: join(dir, 'index.html'), draft: opts?.buildDrafts ? /\.draft\.(html|md|js)$/.test(page.basename) : false, @@ -344,6 +347,7 @@ export async function identifyPages (src, opts = {}) { pageFile: fileInfo, type: 'md', path: dir, + url: computePageUrl({ path: dir, outputName }), outputName, outputRelname: join(dir, outputName), draft: opts.buildDrafts ? isDraftFile : false, diff --git a/package.json b/package.json index dadc038..7e5e10b 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ "@types/markdown-it": "^14.1.1", "@types/markdown-it-footnote": "^3.0.4", "@types/node": "^25.3.0", + "@types/pretty": "^2.0.3", "@voxpelli/tsconfig": "^16.0.0", "auto-changelog": "^2.4.0", "cheerio": "^1.0.0-rc.10", From c7ecef48106c90612c6067b04119d357dd43abfc Mon Sep 17 00:00:00 2001 From: Bret Comnes Date: Sun, 24 May 2026 15:17:58 -0700 Subject: [PATCH 2/2] Fix type error --- lib/build-pages/page-data.js | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/lib/build-pages/page-data.js b/lib/build-pages/page-data.js index a64542f..a7a44fc 100644 --- a/lib/build-pages/page-data.js +++ b/lib/build-pages/page-data.js @@ -266,17 +266,15 @@ export class PageData { if (!layout) throw new Error('A layout is required to render') const innerPage = await this.renderInnerPage({ pages }) - return pretty( - await layout.render({ - vars, - styles, - scripts, - page: pageInfo, - pages, - // @ts-expect-error - innerPage type varies by page builder but layout handles it - children: innerPage, - workers: this.workers - }) - ) + const rendered = await layout.render({ + vars, + styles, + scripts, + page: pageInfo, + pages, + children: /** @type {U} */ (/** @type {unknown} */ (innerPage)), + workers: this.workers + }) + return pretty(String(rendered)) } }