From 7110ba3879cb2b57a596ac6c2196ff8883928887 Mon Sep 17 00:00:00 2001 From: Ricardo Date: Tue, 24 Feb 2026 18:33:46 +0100 Subject: [PATCH] fix: compute ogSlug from permalink in template to avoid Eleventy 3.x race condition Both page.url AND page.inputPath are unreliable in eleventyComputed due to Eleventy 3.x parallel rendering (issue #3183). They return values from OTHER pages being processed concurrently, causing og:image meta tags to reference wrong OG images. Fix: compute ogSlug directly in base.njk from the permalink data value using existing Nunjucks filters (ogSlug, hasOgImage). permalink comes from frontmatter (per-file data) and is immune to cross-page contamination. --- _data/eleventyComputed.js | 62 +++++++------------------------------- _includes/layouts/base.njk | 16 ++++++---- 2 files changed, 21 insertions(+), 57 deletions(-) diff --git a/_data/eleventyComputed.js b/_data/eleventyComputed.js index 8eed4ea..fa56636 100644 --- a/_data/eleventyComputed.js +++ b/_data/eleventyComputed.js @@ -1,23 +1,20 @@ /** * Computed data resolved during the data cascade. * - * Eleventy 3.x parallel rendering causes `page.url` and `page.fileSlug` - * to return values from OTHER pages being processed concurrently. - * This affects both templates and eleventyComputed functions. + * Eleventy 3.x parallel rendering causes `page.url`, `page.fileSlug`, + * and `page.inputPath` to return values from OTHER pages being processed + * concurrently. This affects both templates and eleventyComputed functions. * - * Fix: ALL computed values derive from `page.inputPath` (the physical file - * path on disk), which is always correct regardless of parallel rendering. - * NEVER use `page.url` or `page.fileSlug` here. + * IMPORTANT: Only `permalink` is computed here, because it reads from the + * file's own frontmatter data (per-file, immune to race conditions). + * OG image lookups are done in templates using the `permalink` data value + * and Nunjucks filters (see base.njk). + * + * NEVER use `page.url`, `page.fileSlug`, or `page.inputPath` here. * * See: https://github.com/11ty/eleventy/issues/3183 */ -import { existsSync } from "node:fs"; -import { resolve, dirname } from "node:path"; -import { fileURLToPath } from "node:url"; - -const __dirname = dirname(fileURLToPath(import.meta.url)); - export default { eleventyComputed: { // Compute permalink from file path for posts without explicit frontmatter permalink. @@ -37,6 +34,8 @@ export default { } // No frontmatter permalink — compute from file path + // NOTE: data.page.inputPath may be wrong due to parallel rendering, + // but posts without frontmatter permalink are rare (only pre-beta.37 edge cases) const inputPath = data.page?.inputPath || ""; const match = inputPath.match( /content\/([^/]+)\/(\d{4})-(\d{2})-(\d{2})-(.+)\.md$/ @@ -49,44 +48,5 @@ export default { // For non-matching files (pages, root files), let Eleventy decide return data.permalink; }, - - // OG image slug — derive from inputPath (physical file), NOT page.url. - // page.url suffers from Eleventy 3.x parallel rendering race conditions - // where it can return the URL of a DIFFERENT page being processed concurrently. - // inputPath is the physical file path, which is always correct. - // OG images are generated as {yyyy}-{MM}-{dd}-{slug}.png by lib/og.js. - ogSlug: (data) => { - const inputPath = data.page?.inputPath || ""; - const match = inputPath.match( - /content\/([^/]+)\/(\d{4})-(\d{2})-(\d{2})-(.+)\.md$/, - ); - if (match) { - const [, , year, month, day, slug] = match; - return `${year}-${month}-${day}-${slug}`; - } - // Fallback for pages/root files: use last path segment - const segments = inputPath.split("/").filter(Boolean); - const last = segments[segments.length - 1] || ""; - return last.replace(/\.md$/, ""); - }, - - hasOgImage: (data) => { - const inputPath = data.page?.inputPath || ""; - const match = inputPath.match( - /content\/([^/]+)\/(\d{4})-(\d{2})-(\d{2})-(.+)\.md$/, - ); - let slug; - if (match) { - const [, , year, month, day, s] = match; - slug = `${year}-${month}-${day}-${s}`; - } else { - const segments = inputPath.split("/").filter(Boolean); - const last = segments[segments.length - 1] || ""; - slug = last.replace(/\.md$/, ""); - } - if (!slug) return false; - const ogPath = resolve(__dirname, "..", ".cache", "og", `${slug}.png`); - return existsSync(ogPath); - }, }, }; diff --git a/_includes/layouts/base.njk b/_includes/layouts/base.njk index b1f3e59..23d5f5e 100644 --- a/_includes/layouts/base.njk +++ b/_includes/layouts/base.njk @@ -23,15 +23,19 @@ - {# ogSlug and hasOgImage are pre-computed via eleventyComputed (race-condition safe) #} + {# Compute OG slug from permalink (frontmatter), NOT page.url or eleventyComputed. + permalink is per-file data immune to Eleventy 3.x parallel rendering race conditions. + page.url, page.inputPath, and eleventyComputed values can return data from OTHER pages. #} + {% set _ogSlug = permalink | ogSlug %} + {% set _hasOg = _ogSlug | hasOgImage %} {% if ogPhoto and ogPhoto != "" and (ogPhoto | length) > 10 %} {% elif image and image != "" and (image | length) > 10 %} {% elif contentImage and contentImage != "" and (contentImage | length) > 10 %} - {% elif hasOgImage %} - + {% elif _hasOg %} + {% else %} {% endif %} @@ -40,7 +44,7 @@ {# Twitter Card meta tags #} - {% set hasImage = hasOgImage or (ogPhoto and ogPhoto != "" and (ogPhoto | length) > 10) or (image and image != "" and (image | length) > 10) or (contentImage and contentImage != "" and (contentImage | length) > 10) %} + {% set hasImage = _hasOg or (ogPhoto and ogPhoto != "" and (ogPhoto | length) > 10) or (image and image != "" and (image | length) > 10) or (contentImage and contentImage != "" and (contentImage | length) > 10) %} @@ -50,8 +54,8 @@ {% elif contentImage and contentImage != "" and (contentImage | length) > 10 %} - {% elif hasOgImage %} - + {% elif _hasOg %} + {% endif %} {# Favicon #}