Repository navigation
fix(seo): prefer page bundle images and emit og:image dimensions - #2337
Merged
Merged
Conversation
A page bundle image matching *feature*, then *cover* or *thumbnail*, now wins over schema.image.url as the preview image, and is filled to a 1280x640 png like a front-matter thumbnail. An SVG bundle image without a raster sibling keeps its own image. head/opengraph.html emits og:image:width, og:image:height and og:image:type only when they are known: for a processed image, for a bundle image's own media type, and for schema.image, whose type comes from its url's extension and whose dimensions are emitted only when it declares both. A front-matter thumbnail that image-dimension.html handles gets none, because that helper returns the dimensions it was given whether or not it processed the image. Closes #2330 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A build whose schema.image declares a height but no width, and one whose schema.image has dimensions but no url, show that neither emits a dimension. Each og:image case also reads back the JSON-LD image url, which must stay null on a page without any image. Refs #2330 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…mages A raster front-matter thumbnail that resolves to a resource, in the page bundle or under assets, now goes to the image adapter itself, after utilities/GetImage.html turns it upright by its EXIF orientation. It is filled to 1280x640 like the raster sibling of an SVG and gets og:image:width, og:image:height and og:image:type. A file under static and a remote URL keep the image-dimension.html path without dimensions. URL change for existing sites: a raster page resource given as a relative front-matter thumbnail, such as "feature.png" in a page bundle, rendered the site's base URL as its og:image. It now renders the processed image, for example .../feature-1280x640.png. An assets thumbnail keeps its URL: every og:image in the exampleSite build is the same before and after, img/flowers.jpg included (.../img/flowers-1280x640.png). The first of opengraph.images now gets a type from its extension, and no dimensions. Refs #2330 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A page bundle image now goes through utilities/GetImage.html before it is filled to 1280x640, as a front-matter thumbnail already did, so a JPEG with EXIF orientation 6 comes out upright instead of on its side. Only a png, jpeg, gif, tiff, bmp or webp bundle image is passed to it, the types GetImage.html supports. Any other keeps its own file, as it did before 81cb16b, with its own media type and no dimensions. That stops a HEIC bundle image from failing the build in Fill. A bundle AVIF image, which 81cb16b filled, now also keeps its own file, as a front-matter one does. assets/helpers/image-media-type.html and the remote thumbnail check in head/seo.html no longer parse the url, which failed the build on a literal "%". The check now tests for the "http" prefix, in any letter case, that makes utilities/GetResource.html fetch a url. A second strict-images build serves two remote thumbnails and fails when one is resolved as a resource. The feature.webp fixture of thumbnails/featured was a PNG with a .webp extension, whose metadata Hugo cannot read; GetImage.html asks for it. It is now a WebP file with the same pixels. Correction to 81cb16b and ef9fb88: their bodies say that a bundle image, and a raster front-matter thumbnail that resolves to a resource, are filled to 1280x640. With this change, both hold only for png, jpeg, gif, tiff, bmp and webp images. A front-matter resource of another type, such as AVIF or HEIC, keeps the image-dimension.html path. Refs #2330 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
scripts/test-strict-images.cjs decodes the processed EXIF fixtures to read four pixels. Hugo writes each of them as a non-interlaced 8-bit RGB PNG, so the decoder now reads that kind only, and the script reports any other as a failure instead of stopping with an exception. The gray and palette branches, which no fixture reached, are gone. Correction to 05677e0: its body says that a bundle AVIF image now keeps its own file "as a front-matter one does". A front-matter AVIF resource takes the image-dimension.html path instead; for one in a page bundle, that path renders the site's base URL as its og:image. Refs #2330 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A bundle image that Hugo cannot decode, such as an empty file, a truncated JPEG or a Git LFS pointer, failed the build in Fill, and one named "feature 50%.png" or "feature%zz.png" failed it when its URL was built. origin/main builds all of these whenever schema.image.url is set, as the theme's defaults set it, because it does not read the bundle image then. head/seo.html now processes the image inside try. A bundle image it cannot process keeps its own file and media type, without dimensions. When Hugo cannot build its permalink either, the preview image comes from schema.image.url, then opengraph.images. A front-matter thumbnail that names a page resource or asset it cannot process keeps the image-dimension.html path, as on origin/main. Fill leaves an empty file in the image cache for an image it cannot decode, and from the third build on it returns that file without an error. So the fill now runs in head/seo.html, which turns away a result with a width of 0, instead of in the image adapter, which returns only a url. The png keeps the name the adapter gave it. A front-matter thumbnail that names an AVIF or HEIC page resource no longer goes to utilities/GetImage.html, which logged "Unsupported image" for it. Both sources now share one list of the types GetImage.html supports. Known new warning: a page bundle image whose bytes do not match its extension, such as PNG bytes in a .webp file, now logs Hugo's "Unable to decode metadata from image" warning, both when it is matched by name and when the front matter names it as the thumbnail. origin/main logged no such warning for either. With Hugo v0.168.0 and v0.169.0, --panicOnWarning does not stop the build on this warning; a build that fails on any WARN line in its log does stop. Giving the file the extension of its contents clears the warning. Tests: bundle fixtures for each of these inputs run in every test:templates build, and the strict-images build fails on any warning they log. A new image-warnings build covers front-matter AVIF, HEIC and undecodable page resources and the two mislabeled images, and scripts/test-strict-images.cjs compares its warnings with the ones it expects. The script now also fails when the remote thumbnail build renders without its two remote cases. Correction to 81cb16b, ef9fb88 and 05677e0: their bodies say that a bundle image, and a raster front-matter thumbnail that resolves to a resource, are filled to 1280x640, by the image adapter in ef9fb88. With this change, an image that Fill cannot decode is not filled, a bundle image without a permalink does not win over schema.image.url, and head/seo.html fills the image itself. Refs #2330 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…afely Three defects in the preview image of 55cdf4f, found by a blind review. A bundle image named with a space, a non-ASCII letter, a "#", a "?" or a "%" got an og:image that named no published file, tagged 1280x640 image/png all the same. head/seo.html built the path of the processed png from the permalink of the image, in which Hugo escapes a space or a non-ASCII letter, so "feature 1.png" was published as "feature%201-1280x640.png", while its url, once decoded, names "feature 1-1280x640.png". Hugo cuts a permalink short at a "#" or "?"; path.Ext then returned "", replace put "-1280x640.png" between every character, and the build published a tree of directories under public/-1280x640.png/. The path now comes from the decoded permalink. For an image named with a "#", "?" or "%", the file name comes from the name of the image instead, and each of the three becomes a hyphen: "feature#1.png" gives "feature-1-1280x640.png". A front-matter thumbnail takes the same step. A bundle image whose name starts with "http", such as "http-feature.png", logged "Missing image ... Cannot find image resource", which origin/main does not: utilities/GetResource.html takes any url that starts with "http" for a remote one. head/seo.html now looks the image up as "./http-feature.png", which Resources.Get finds as the resource of that name on Hugo v0.153.1, v0.168.0 and v0.169.0. A schema.image width or height that is no integer, such as HUGO_PARAMS_SCHEMA_IMAGE_WIDTH=1280px, failed the build in int, where origin/main exits 0, and a negative one was emitted as it was. The new assets/helpers/positive-integer.html accepts an int, a float without a fraction, or a string of decimal digits without a leading zero, and returns 0 for anything else. The dimensions are emitted only when both are positive. Tests: bundle fixtures named with a space, a non-ASCII letter, a "#", a valid percent escape, "http-feature.png" and "https_cover.png", and a front-matter thumbnail named with a space, run in every test:templates build. The og-image content adapter adds a bundle image named with a "?" to the strict-images build except on Windows, since a Go module zip refuses that file name. scripts/test-strict-images.cjs now decodes each og:image on the host of the test site, checks that it names a file of the size its tags give, and fails on any directory named like an image. A new og-schema-units build sets a width of "1280px", and the helper is tested directly. Correction to 55cdf4f: its body says that with Hugo v0.168.0 and v0.169.0, --panicOnWarning does not stop the build on Hugo's "Unable to decode metadata from image" warning. For a bundle image that holds only because, under that flag, Image.Meta returns the warning as an error as well as logging it. utilities/GetImage.html reads .Meta, so the step fails inside try, and the bundle image keeps its own file and media type, unprocessed. Checked on v0.168.0 and v0.169.0. A front-matter thumbnail with such bytes falls back to the image-dimension.html path, and where that path logs a missing image, --panicOnWarning stops the build on that warning, as it does on origin/main. Refs #2330 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A front-matter thumbnail that names the asset "img/asset#1.png" must be processed to img/asset-1-1280x640.png, next to the asset. The name of a global resource carries its directory, "/img/asset#1.png", so a step that took the whole name rather than its base would publish the png under img/img/. No earlier fixture had a "#", "?" or "%" in a name with a directory, and that mutant of cb01268 passed every test. Refs #2330 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two paragraphs added in cb01268 were wrapped unevenly. The text is unchanged. Correction to cb01268: its body says that a bundle image named with a space, a non-ASCII letter, a "#", a "?" or a "%" got an og:image that named no published file. That holds for a space, a non-ASCII letter and a valid percent escape such as "feature%25.png". For a "#" or a "?", the og:image named a file in the tree of directories that the build published under public/-1280x640.png/, so a server answered it, but with a path built from the image's name one character at a time. A name with an invalid percent escape, such as "feature 50%.png", still takes the fallback. Its body also says that assets/helpers/positive-integer.html accepts an int; it accepts a positive int, and returns 0 for zero and for a negative one. Refs #2330 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…mage The header of the og:image assertions lists the overlays that set schema.image or opengraph.images, and says that every other build sets neither. cb01268 added og-schema-units, which sets both, without adding it to that list. The paragraph is rewrapped as well. Correction to e0fbae4: its body says that for a "#" or a "?", the og:image path was built from the name of the image one character at a time. It was built from the permalink of the image, which Hugo had cut short at that character, so it held the directory as well: "/og-image/hash-bundle/feature" for "feature#1.png". Refs #2330 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comments only. scripts/test-strict-images.cjs counted three pages for which origin/main logs a missing image although this build processes their thumbnail; it logs one for four: thumbnails/raster-bundle, og-image/exif-thumbnail, og-image/space-thumbnail and og-image/hash-asset. The comments in head/seo.html and in the og:image assertions said that the processed png is published next to the image. Its path is the decoded path of the permalink of the image, so a baseURL with a path, such as "https://example.org/sub/", puts the png under public/sub/, and "feature%2F.png" gives "feature/feature-2F-1280x640.png", one directory down. The comments that described Hugo's "Unable to decode metadata from image" warning as one for bytes that do not match the extension now describe the wider case below. Correction to 55cdf4f, "Known new warning": the accepted warning is not only for an image whose bytes do not match its extension. It covers any page bundle image, and any front-matter thumbnail that names a page resource, whose metadata Hugo cannot decode. utilities/GetImage.html reads .Meta for the EXIF orientation of the image, and Hugo logs the warning when it cannot decode that metadata. A JPEG with a corrupt EXIF block logs it too, such as one whose Orientation tag has an invalid TIFF type or whose first IFD claims more entries than the block holds. Renaming the file does not clear the warning for a corrupt EXIF block: such a JPEG logs it as .jpg and as .jpeg. In a normal build the image is still processed to its 1280x640 png. Under --panicOnWarning, .Meta returns the warning as an error: for a bundle image the build still exits 0 on it, and og:image is the image's own file, unprocessed, while a front-matter thumbnail takes the image-dimension.html path. Checked on Hugo v0.168.0 and v0.169.0. origin/main logs no such warning for these images. Correction to 55cdf4f: its body says that a front-matter thumbnail that names an AVIF or HEIC page resource no longer goes to utilities/GetImage.html. It still reaches GetImage.html, through image-dimension.html, image-set.html and the image adapter, and GetImage.html logs "Missing image" for it, as it does on origin/main. Only the "Unsupported image" warning, which 0bbe622 logged in addition, is gone. Refs #2330 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comments only. scripts/test-strict-images.cjs and the og:image assertions said that a JPEG with a corrupt EXIF block logs Hugo's "Unable to decode metadata from image" warning. Not every corrupt EXIF block does: a JPEG whose first IFD offset points past the end of the block, or whose EXIF block has no TIFF header, is processed without that warning on Hugo v0.168.0 and v0.169.0. Both comments now say that a JPEG whose EXIF block Hugo cannot decode, such as one whose Orientation tag has an invalid TIFF type, logs the same warning. Correction to 753c0fb: its body says "A JPEG with a corrupt EXIF block logs it too", and that renaming the file does not clear the warning "for a corrupt EXIF block". Both hold only for a JPEG whose EXIF block Hugo cannot decode, such as one whose Orientation tag has an invalid TIFF type or whose first IFD claims more entries than the block holds; such a JPEG logs the warning as .jpg and as .jpeg. A JPEG with another kind of corrupt EXIF block may be processed without any warning. Refs #2330 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
✅ Deploy Preview for gethinode-demo ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
markdumay
enabled auto-merge
November 6, 2026 12:08
On Windows CI (windows-latest, Node 22 and 24, Hugo v0.169.0) the image-warnings check of scripts/test-strict-images.cjs failed. utilities/LogMsg.html ends a "Missing image" warning with the path of the page file, which Hugo gives with the separator of the operating system, so the build logged og-image\avif-thumbnail\index.md where expectedWarnings has og-image/avif-thumbnail/index.md. normalizeWarning now turns the backslashes of that path into slashes and leaves the rest of the line as it is, so a different message, page or number of warnings still fails the comparison. Fed the image-warnings output in the log of CI job 127884185018, the script before this change reports the five "Missing image" lines as different, and with it all eight lines match. A fixed Windows line is also checked on every platform, so a run on Linux or macOS fails if the normalization goes. In the same job the first build of the script passed on Windows, including the check that decodes each og:image and finds its file, the check for directories named like an image and the size checks, which build their paths with path.join. They need no change. The remote thumbnail build failed there for a reason in mod-utils, which this commit leaves alone. Refs #2330 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On Windows CI the remote thumbnail build of scripts/test-strict-images.cjs stopped in utilities/GetResource.html, which passed a remote url it could not fetch to resources.GetMatch: "CreateFile ...\assets\https:\127.0.0.1\img: The filename, directory name, or volume label syntax is incorrect" (job 127884185018). From mod-utils v6.18.2, GetResource.html returns nil for a url that starts with http:// or https://, in any letter case, when it cannot fetch it, and passes any other url to GetMatch as before. GetResource.html is the only template that v6.18.2 changes. The bump is limited to mod-utils, in go.mod and go.sum and, as with the last mod-utils bump, in exampleSite/go.mod and exampleSite/go.sum. On macOS the three builds of scripts/test-strict-images.cjs log the same warnings and render the same assertion tables with v6.18.1 and v6.18.2, and the remote thumbnail build still passes both of its cases. Refs #2330 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
markdumay
disabled auto-merge
November 6, 2026 13:15
…ension assets/helpers/image-dimension.html skips its fileExists check under /static for a url, but tested for one with a case-sensitive hasPrefix "http://" or "https://". An src such as "HTTP://127.0.0.1:52260/img/remote.png" therefore reached fileExists "/static/HTTP:/127.0.0.1:52260/img/remote.png". That was already so on main, where b72c8bf added the check. On macOS and Linux fileExists returns false for that path; on Windows it fails the build: the remote thumbnail build of scripts/test-strict-images.cjs stopped with "error calling fileExists: CreateFile ...\static\HTTP:\127.0.0.1:52260\img\remote.png: The filename, directory name, or volume label syntax is incorrect" (job 127913073443). The check now compares on lower $src, and nothing else changes. A trace in image-dimension.html during the remote thumbnail build shows fileExists called for "HTTP://127.0.0.1:<port>/img/remote.png" before this change and not after; for the lower-case url it is called in neither. The output on macOS is the same either way, so only a Windows build can show the failure itself. Refs #2330 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
assets/helpers/image-dimension.html takes an src that starts with http:// or https://, in any letter case, for a url. The remote thumbnail build had an upper-case case only for http://, so a check that lowered the src for one prefix only would still pass it. A third page, whose thumbnail starts with HTTPS://, covers the other prefix, and scripts/test-strict-images.cjs now wants all three remote cases to pass. On macOS the three pass with or without that fix; only a Windows build, where fileExists fails on a path with "HTTPS:" in it, can tell. Refs #2330 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
markdumay
enabled auto-merge
November 6, 2026 13:56
Collaborator
Author
|
🎉 This PR is included in version 3.52.1 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
This was referenced Nov 6, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #2330
Summary
thumbnail, then page bundle*feature*, then{*cover*,*thumbnail*}, thenschema.image.url, thenopengraph.images[0]. Before, a site-levelschema.image.urlhid every bundle image.og:image:width,og:image:heightandog:image:typeare emitted when they are known: from a processed image, or fromschema.image.width/heightwhen both are positive integers. When unknown, none of the three is emitted.#,?or%produces a URL that resolves.Known new warning
Reading EXIF orientation calls
.Meta. When Hugo cannot decode an image's metadata, it now logsUnable to decode metadata from image. This happens for a file whose bytes don't match its extension, or a JPEG whose EXIF block Hugo can't parse.--panicOnWarningthe build still succeeds, and the image is left unprocessed.The commit bodies of 55cdf4f, 753c0fb and a379271 record the details.
Tests
tests/templates/layouts/_partials/tests/og-image.html, the og-schema, og-schema-partial, og-schema-height, og-schema-units, og-fallback and image-warnings environments, andscripts/test-strict-images.cjscover:mainmain's templates fail these tests.pnpm testpasses from a clean cache. On the exampleSite, og:image, twitter:image and JSON-LD are unchanged; pages only gain the dimension tags.The combined branch with #2329 passes the full suite.
🤖 Generated with Claude Code