Skip to content

feat(pages): resolve crawler meta URLs against the request - #821

Merged
cpojer merged 2 commits into
voidzero-dev:mainfrom
0froq:fix/pages-absolute-crawler-head
Nov 13, 2026
Merged

cpojer merged 2 commits into
voidzero-dev:mainfrom
0froq:fix/pages-absolute-crawler-head

Conversation

@0froq

@0froq 0froq commented Nov 13, 2026

Copy link
Copy Markdown
Collaborator

Fixes #807

Description

Social crawlers only read an absolute og:image, and void.config.ts cannot know the site's origin. After the head layers merge, a relative content is now resolved against the request URL for og:image, og:video, og:audio and their :url / :secure_url variants, og:url, and twitter:image. Absolute, protocol-relative, and every other meta tag are left as written. Config, middleware, and page values resolve the same way, and page data carries the same absolute value so client navigation matches.

Prerendering keeps the values relative. It renders on a placeholder origin, so baking that origin into the built HTML would break every real deployment.

Test

With og:image: '/og-image.jpg' in void.config.ts and a built playground/vue-pages under vp preview:

  • before: <meta property="og:image" content="/og-image.jpg">
  • after: <meta property="og:image" content="http://localhost:4174/og-image.jpg">, and description: '/not-a-url' unchanged

Social crawlers and link previews only read an absolute URL, and a page
cannot know its own origin. Resolve a relative `content` against the
request URL for og:image, og:video, og:audio and their variants, og:url
and twitter:image, after the head layers merge.

Prerendered pages keep the value relative: the build runs on a
placeholder origin, and baking that origin into the HTML would break
every real deployment.
@0froq

0froq commented Nov 13, 2026

Copy link
Copy Markdown
Collaborator Author

👁️‍🗨️ Review this pull request with grouped, summarized diffs at:
👉 https://pulls.review/gh/voidzero-dev/void/821?from=0froq

raw result

💭 analyzed by openai-compatible/opencode-go/deepseek-v4.1-flash
🕰️ 2026-11-13 22:10 UTC
🔗 head 88ea008
🤖 automated by pulls.review

{
  "headSha": "88ea008f099f83b4bc510b7c348719f9094c3b76",
  "result": {
    "overallSummary": "Social crawlers and link previews only read absolute URLs, but `void.config.ts` cannot know the deployment origin. This PR resolves relative values for the crawler-only meta tags (`og:image`/`video`/`audio` plus `:url`/`:secure_url`, `og:url`, `twitter:image`) against the request URL after the head layers merge, leaving absolute, protocol-relative and all other values untouched. Prerenders deliberately keep relative values because they render on a placeholder origin.",
    "groups": [
      {
        "key": "crawler-url-absolutize",
        "label": "Absolutize crawler meta URLs",
        "summary": "Adds `absolutizeHead` in `head.ts`, applied after `resolveHead` at every page render entry point, so a relative `og:image` and friends become absolute without the app knowing its origin. Only the listed keys are touched, values already absolute or malformed are kept verbatim, and prerenders pass `undefined` to stay relative.",
        "category": "core",
        "filePaths": [
          "packages/void/src/pages/head.ts",
          "packages/void/src/pages/protocol.ts"
        ],
        "critical": true,
        "fileNotes": [
          {
            "path": "packages/void/src/pages/head.ts",
            "text": "The base is the request URL, so a bare relative value like `og-image.jpg` resolves against the page's directory, not the site root; only root-relative `/...` values always point at the origin. A new array copy is only made on the first resolved entry, otherwise the input is returned unchanged."
          },
          {
            "path": "packages/void/src/pages/protocol.ts",
            "text": "The error-boundary path (`renderPageFailure`) plus `runPageGet` and `runPageMutation` all funnel through the new `resolveRequestHead`, so server-rendered HTML and the page data sent to the client always carry the same absolute values."
          }
        ]
      },
      {
        "key": "tests",
        "label": "Unit tests",
        "summary": "Covers the new `absolutizeHead`: each required key, `twitter:image` matched by `name`, pass-through for absolute/protocol-relative/unrelated values, the `undefined` head and prerender cases, and malformed input.",
        "category": "tests",
        "filePaths": [
          "packages/void/test/unit/pages-head.test.ts"
        ]
      },
      {
        "key": "docs",
        "label": "Docs",
        "summary": "Documents which meta tags get resolved, shows a config example, and warns that prerendered pages keep relative values.",
        "category": "docs",
        "filePaths": [
          "docs/guide/pages-routing/head.md"
        ]
      }
    ],
    "schemaVersion": 1,
    "source": "llm",
    "generatedAt": "2026-11-13T22:10:08.661Z",
    "model": "openai-compatible/opencode-go/deepseek-v4.1-flash",
    "locale": "en"
  }
}

@cpojer cpojer left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice fix!

@cpojer
cpojer merged commit 5085fcf into voidzero-dev:main Nov 13, 2026
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Resolve a relative og:image against the request URL

2 participants