Skip to content

Blog Feature and Meta Image Updates - #17937

Merged
jeffmerrick merged 20 commits into
masterfrom
jeffmerrick/blog-feature-images
Mar 18, 2026
Merged

Blog Feature and Meta Image Updates#17937
jeffmerrick merged 20 commits into
masterfrom
jeffmerrick/blog-feature-images

Conversation

@jeffmerrick

Copy link
Copy Markdown
Contributor

Proposed changes

To make the blog images consistent but unique moving forward I'm proposing we default to custom designed feature images with an option to use template fallbacks. (Designing the custom images will be on me.)

  • Adds feature_image in addition to the meta_image, feature images are displayed in the blog listing and on the single blog posts, meta images now are only used as meta images. (This sets us up to move to a grid layout for the blog listing.)
  • Updates the /blog-meta-image skill to generate feature and meta images from either the new templates or a provided custom image.
  • Removes old templates and provides some new basic generic ones. New templates can be added as we have more artwork designed. Leaving out Pulumipus illustrations for now as they may be revisited.
  • Adds custom feature images for the 4 newest blog posts - if we move forward with this approach I can create more going back further. (Maybe the first two pages of the blog.)
  • Hides the image completely if there is no feature image in the name of consistency, but leaves old meta images untouched.

Future changes

  • More and more specialized templates for specific types of blog posts (announcements, releases, features, etc.)
  • Possibly review older high traffic/evergreen content as candidates for a custom designed image.
  • Grid layout for blog listings.

Resolves MA-542

@claude

claude Bot commented Mar 12, 2026

Copy link
Copy Markdown
Contributor

Docs Review

Scope: All 63 changed files in PR 17937 -- blog feature/meta image system overhaul, Hugo layout templates, blog-meta-image skill updates, blog post frontmatter additions, documentation updates, and Stencil component type changes.

Overall this is a well-structured PR that cleanly separates feature images (hero/listing) from meta images (OpenGraph/social). The two-pass rendering approach in the compose script is solid.

Issues

  1. poster.html -- fallback removed for posts without feature_image (layouts/partials/blog/poster.html line 2): The old template fell back to meta_image when rendering the blog listing thumbnail. The new version only checks feature_image, meaning all existing blog posts without a feature_image field will lose their listing image entirely. The PR description says this is intentional ("Hides the image completely if there is no feature image in the name of consistency"), but this will affect hundreds of existing posts. Please confirm this is the desired behavior -- if so, a comment in the template noting the intentional omission would help future maintainers.

  2. components.d.ts appears to be an unrelated auto-generated change (theme/stencil/src/components.d.ts lines 1112-1344): This file has ~170 lines of additions that add Attributes interfaces and refactor IntrinsicElements to use complex mapped types. This looks like a Stencil version bump artifact unrelated to the blog image work. Consider splitting this into a separate commit or PR for clarity, or at least noting it in the PR description.

  3. BLOGGING.md -- table aspect ratio nit (BLOGGING.md lines 166-173): The new table is clear. One small nit: the meta_image aspect ratio shows ~2:1 -- consider using the precise ratio ~1.91:1 (1200/628) since this is the OpenGraph standard and may help authors who look up the spec.

  4. compose_meta_image.py -- hex_to_rgba is called before it is defined (compose_meta_image.py lines 203-280): The tint_image function (line 203) calls hex_to_rgba (line 277). Python allows this at runtime since both are module-level functions and tint_image is called after both are defined, but the ordering is unusual and could confuse readers. Consider moving hex_to_rgba above tint_image for clarity.

  5. compose_meta_image.py -- argparse description still says "meta image" (compose_meta_image.py line 380): The description string passed to ArgumentParser still reads "Compose a meta image from a template PNG" -- should be updated to reflect both feature and meta modes.

  6. full-poster.html -- sizes="100vw" is overly broad (layouts/partials/blog/full-poster.html line 12): The blog content area is constrained by the lg:w-3/4 xxl:w-9/12 container (roughly 75 percent of the layout). Using sizes="100vw" will cause the browser to download images larger than needed. Consider matching the actual content width, e.g. sizes="(min-width: 1024px) 75vw, 100vw".

Suggestions (non-blocking)

  • Blog post meta images were also replaced (e.g., expanded-version-control-support/meta.png, pulumi-cloud-now-supports-google-sign-in/meta.png). The binary diffs cannot be reviewed here -- please verify that the new meta images look correct and include proper branding.
  • Feature image file sizes: With 8 new feature PNGs at 1884x1256, these could be sizeable. Consider verifying they are optimized (e.g., through pngquant or similar) to avoid bloating the repo.
  • The new logo_tint_mode parameter in catalog.yaml and compose_meta_image.py is a nice addition. The "colorize" mode luminance preservation approach is well-implemented.

Summary

The core architecture change (feature image + meta image separation) is clean and well-documented across the skill files, BLOGGING.md, CONTRIBUTING.md, and the blog archetype. The main concern is the intentional removal of listing images for posts without feature_image (item 1) and the unrelated components.d.ts changes (item 2).


Mention me (@claude) if you would like additional reviews or want me to help fix any of the above.

@pulumi-bot

Copy link
Copy Markdown
Collaborator

@kimberleyamackenzie kimberleyamackenzie left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Love this - if we decide to implement, it sounds like maybe we can cut Figma out of the process for day-to-day blog authors? If so will you loop me in? Because we pay for a bunch of seats in Figma for folks who only use it for blog meta images.

@jeffmerrick

Copy link
Copy Markdown
Contributor Author

@kimberleyamackenzie Yep! Would just be me making the feature images in Figma if we do it.

Comment thread theme/stencil/src/components.d.ts Outdated
@pulumi-bot

Copy link
Copy Markdown
Collaborator

@jeffmerrick

Copy link
Copy Markdown
Contributor Author

@claude Re-review

@claude

claude Bot commented Mar 13, 2026

Copy link
Copy Markdown
Contributor

Claude finished @jeffmerrick's task in 2m 14s —— View job


Re-review

  • Read key changed files
  • Review Hugo templates
  • Review compose script changes
  • Review skill/documentation changes
  • Post final review

Summary

All 6 issues from the previous review have been addressed. The components.d.ts noise is also gone (merged with master). Here's the status of each prior item:

# Prior issue Status
1 poster.html — no comment explaining intentional image omission ✅ Fixed — clear comment at top of file
2 components.d.ts unrelated Stencil changes ✅ Gone from diff (resolved via #17956 + rebase)
3 BLOGGING.md meta_image aspect ratio shown as ~2:1 ✅ Fixed — now shows ~1.91:1
4 hex_to_rgba defined after tint_image ✅ Fixed — hex_to_rgba (line 206) now precedes tint_image (line 212)
5 argparse description still said "meta image" only ✅ Fixed — now reads "Compose a feature image or meta image from a template PNG"
6 full-poster.html sizes="100vw" too broad ✅ Fixed — now correctly uses sizes="(min-width: 1024px) 75vw, 100vw"

One New Minor Finding

Archetype is missing feature.png placeholder (archetypes/blog-post/index.md): The archetype now defaults to feature_image: feature.png, but the archetypes/blog-post/ directory only contains meta.png — there is no feature.png placeholder alongside it. If a developer runs hugo new directly (bypassing the /new-blog-post skill), they'll get a frontmatter reference with no backing file, which will silently render nothing.

The /new-blog-post skill handles this correctly (it explicitly copies .claude/commands/_common/images/blog-post-feature-placeholder.pngfeature.png), so the typical workflow is fine. But adding feature.png to the archetype directory would make the fallback path (hugo new) consistent too.

This is non-blocking — the existing placeholder image is already in .claude/commands/_common/images/blog-post-feature-placeholder.png and just needs to be copied into archetypes/blog-post/feature.png. Fix this →

Everything Else Looks Good

  • The two-mode compose() pipeline (feature vs. meta) is clean and well-structured.
  • catalog.yaml is well-organized with clear template categories and logo inventory.
  • SKILL.md instructions are clear, with good progressive-question flow and a useful fast-path for pre-existing feature.png files.
  • The poster.html sizing ((min-width: 768px) 320px, 100vw) and full-poster.html sizing ((min-width: 1024px) 75vw, 100vw) are both appropriate for their contexts.
  • New blog post archetype comment for feature_image is clear and helpful.

This is ready to merge once you've confirmed the archetype note above.
| Branch

@pulumi-bot

Copy link
Copy Markdown
Collaborator

@pulumi-bot

Copy link
Copy Markdown
Collaborator

@jeffmerrick

Copy link
Copy Markdown
Contributor Author

Thanks @CamSoper!

Just a note that I'm going to spend some time creating images for the first two pages or so before merging.

@pulumi-bot

Copy link
Copy Markdown
Collaborator

@pulumi-bot

Copy link
Copy Markdown
Collaborator

@pulumi-bot

Copy link
Copy Markdown
Collaborator

@cnunciato cnunciato left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Looks great! ✨ Noticed a few little things we may want to fix up before we merge, but leaving an approval here to keep you unblocked.

@cnunciato cnunciato Mar 17, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I almost hesistate bring it up because it's so minor, but it's poking at my OCD, so I will 😄 To my eyes at least, the top circle looks ever so slightly off center.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thank you for bringing it up! That would've bugged me when I noticed it later. Fixed!


```
header: "Logo Tint Style"
question: "How should the tint color be applied to the logos?"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is likely the fault of the logos we're using, but I tried both options with a faked-up post involving AWS, TypeScript, and Docker, and neither one came out quite right:

Overlay:

Image

Colorize:

Image

As a follow-up (or before merge, if it wouldn't be too big a deal), we may want to run through these logos and check 'em all against our new darker backgrounds to make sure they look right. (Just wanted to call it out now to make sure it didn't get lost.)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@cnunciato Thanks for catching this, the colorize wasn't working as expected.

I've made some updates so the overlay method should work with all the logos we currently have, since the skill can also download logos, it may not work with all future logos. I've also adjusted the colorize method to look slightly better, but it really depends on the logo (it's basically a backup in case overlay doesn't work). So results may not be always great on the 2x and 3x versions since different logos might work better with different methods.

Hopefully we will mostly be designing custom images so it won't come up too much.

Overlay:
test-overlay

Colorize:
test-color

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Awesome. Looks great!

Comment thread layouts/blog/single.html Outdated
Comment thread layouts/blog/single.html Outdated
Co-authored-by: Christian Nunciato <chris@nunciato.org>
@pulumi-bot

Copy link
Copy Markdown
Collaborator

@pulumi-bot

Copy link
Copy Markdown
Collaborator

@pulumi-bot

Copy link
Copy Markdown
Collaborator

Dark-filled logos (e.g., AWS) were nearly invisible in color tint mode
because the original pixel lightness was preserved as-is. This remaps
lightness into [0.45, 1.0] so dark pixels become visible while
preserving relative contrast.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@cnunciato cnunciato left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Looks great! I added a bit to the script to set a baseline lightness value to keep dark logos (e.g., the text in the AWS logo) from getting lost in the dark background.

Click the merge button when you're ready! 🙌🏻


```
header: "Logo Tint Style"
question: "How should the tint color be applied to the logos?"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Awesome. Looks great!

@pulumi-bot

Copy link
Copy Markdown
Collaborator

@jeffmerrick
jeffmerrick merged commit 306c185 into master Mar 18, 2026
13 checks passed
@jeffmerrick
jeffmerrick deleted the jeffmerrick/blog-feature-images branch March 18, 2026 17:09
cnunciato added a commit that referenced this pull request Jul 2, 2026
… feature image (#20060)

* Add build-time blog social card (blog.mjs + generator wiring)

New Satori "blog" template renders the 1200x628 OpenGraph card for a blog
post at build time — the post's feature image bled off the right edge, faded
into the dark violet field by a gradient scrim, with a "Blog" badge, the
fitted title, and the Pulumi wordmark in the cleared left column. It
reproduces the retired Python meta-image composite; posts with no
feature_image fall back to a bundled generic art plate (blog-generic.png).

Wires it into scripts/generate-meta-images.mjs as a new recursive "blog"
SECTIONS entry (skips drafts; feature bytes fold into the cache key via
featureHash). No OG_TEMPLATE_VERSION bump — new manifest keys only. The
sampleGroupBy call site now also receives p.fields so blog can sample one
card per kind (feature vs generic).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Blog layouts: route meta image through the shared partial; archived panel

RSS <description> image and the BlogPosting JSON-LD image now resolve via
partials/meta-image-url.html instead of reading .Params.meta_image directly,
so a post with no meta_image picks up its build-time generated card (and both
stay in sync with the OpenGraph tags). blog-entity.html keeps the
blog-specific og-default fallback.

single.html gains a collapsed "Archived feature image" panel keyed on the
bundle's meta-legacy.* resource, so migrated off-brand legacy images stay
viewable at the bottom of the post.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Rework blog-meta-image skill into blog-feature-image (feature-only)

The skill no longer composites a meta.png; the OpenGraph card is now generated
at build time (blog.mjs). Renamed .claude/commands/blog-meta-image ->
blog-feature-image (invoked as /blog-feature-image; SKILL.md keeps a "formerly
/blog-meta-image" note).

- SKILL.md: drop the meta compositing step; frontmatter update sets only
  feature_image.
- compose_meta_image.py: delete meta mode (feature/overlay/logo/text compose,
  draw_text, word-wrap, code-span extraction); keep the template + logo
  placement + tint pipeline.
- catalog.yaml / asset-catalog.md: remove meta_canvas + text sections; delete
  the meta-overlay.png and meta-logo.png template assets.
- archetypes/blog-post: drop meta.png and the meta_image field; feature_image
  comment now notes it drives the build-time social card.
- new-blog-post.md: stop copying a meta placeholder; drop meta_image from the
  generated frontmatter; reword guidance to /blog-feature-image.
- delete the now-unused blog-post-meta-placeholder.png.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Migrate blog Group A (feature_image posts) to build-time cards

The 66 posts that already carry a feature_image drop their committed meta.png
and the meta_image frontmatter, so partials/meta-image-url.html serves the new
build-time blog card. 21 of them had their original committed meta image
overwritten by #17937 — those are restored from the parent commit
(306c185^) as meta-legacy.png first (special case: iam-for-postgres.png),
surfaced in the post's collapsed "Archived feature image" panel. No Group A
post referenced its meta.png in its body, so no post body changes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Migrate blog Group B (legacy meta images) to build-time cards

The remaining 700 posts drop meta_image so partials/meta-image-url.html serves
the build-time card. 645 off-brand legacy meta images are renamed to
meta-legacy.<ext> and surfaced in the post's collapsed "Archived feature
image" panel; the 55 whose meta image is still referenced elsewhere keep the
file untouched (14 referenced by data/blog_series.yml, 41 shown inline in the
post body). No post body content is changed — every body-shown image is kept
(the drift.png / set-up-drift.png filename collision is handled by exact
delimiter matching). blog_series.yml needs no edit; all 15 entries still
resolve.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Update references/tooling for the build-time blog card

- schedule-posts.py: social media image prefers feature.png, falls back to
  legacy meta.png/meta.jpg.
- AGENTS.md: add blog + events to the generator's meta-image list; drop the
  "blog is the exception" note; document feature_image-driven cards and the
  meta-legacy.png archive.
- BLOGGING.md: front-matter example uses feature_image; images section and
  checklist drop meta_image (build-time card from title + feature image); add
  an archived-images note; point authors at /blog-feature-image.
- docs-review/references/blog.md: the retired-logo / placeholder meta_image
  checks are now custom-override-only (the placeholder file is gone and posts
  ship no meta_image); meta-legacy.png is never flagged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Blog card: vertically center the badge + title group

Give the left column a fixed height spanning the band between the top padding
and the Pulumi wordmark, with justifyContent center, so the "Blog" badge + title
sit centered as a group rather than top-anchored — balanced for short titles.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Blog card: narrow the title box to 640 so wide titles clear the artwork

At 700px the title column reached x=790 and could overlap a wide feature
image's artwork (e.g. the two-logo KubeCon card). 640px pulls the longest
lines left of the artwork while barely affecting normal titles.

* Add proper feature images to 10 blog series

Each of these series has a new on-brand hero image (1884x1256). Every post in a
series gets the same feature.png + feature_image front-matter, so the build-time
generator now renders a branded feature card (instead of the generic plate) for
all 38 posts. Their series entries in data/blog_series.yml are repointed at the
new feature.png so the /blog/series/ listing matches. Series without a new image
(neo-things, platform-engineering-pillars, pulumi-copilot, organizational-patterns,
kubernetes-getting-started) are untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Add feature images to the remaining 5 blog series

The final 5 series (neo-things, platform-engineering-pillars,
organizational-patterns, kubernetes-getting-started, pulumi-copilot) get their
new on-brand hero image applied to every member post, matching the first batch.
Membership is tag-based here (these series are collected via /blog/tag/<slug>/,
and some have no `series:` front-matter key). 24 posts get feature.png +
feature_image, and their blog_series.yml entries repoint at the new image. All
15 series now have proper feature images and branded build-time cards.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Fix neo-things series: add the missing series: key to both posts

A blog series needs two things wired up: the series slug as a tag (so the post
appears on /blog/tag/<slug>/, the series landing page linked from
/blog/series/) AND a matching series: front-matter key (so single.html shows
the "In This Series" sidebar, which finds siblings via where .Params.series).
The two neo-things posts were only tagged, so they listed on the series page
but showed no series-nav sidebar. Adding series: neo-things brings them in line
with every other series. Audited all 15 series: this was the only mismatch, and
there are no orphan series keys.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Lint: enforce blog series tag/key consistency

Add a front-matter check to scripts/lint/lint-markdown.js that fails when a
blog post is tagged with a defined series slug but missing the matching
series: key (or vice versa) — the drift that left the neo-things posts off the
series sidebar. A series member needs both signals: the series: key (in-post
"In This Series" sidebar) and the tag (the /blog/tag/<slug>/ landing page). The
check reads the slug set from data/blog_series.yml and applies only to blog
posts; all current content passes. BLOGGING.md documents the both-required rule.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* blog-feature-image: auto-locate native libcairo (macOS/Homebrew)

compose_meta_image.py rasterizes logo SVGs via cairosvg, which dlopen()s the
system libcairo by leaf name. Homebrew installs it outside the default dylib
search path, so a bare `uv run` failed with a cryptic
'no library called "cairo" was found'. Add a startup shim that, when the loader
can't find cairo, sets DYLD_FALLBACK_LIBRARY_PATH to the Homebrew lib dir and
re-execs once (loop-guarded) so dyld picks it up — no manual env var needed.
SKILL.md documents the native dependency and the brew/apt fallback.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Retire stale placeholder-meta_image references; discourage custom overrides

Sweep the review-pipeline docs that still described the deleted
blog-post-meta-placeholder.png flow (output-format blocker list,
pre-computation image-validate candidate, seo-analyze social-meta check),
and add author guidance in BLOGGING.md / AGENTS.md / the blog archetype:
custom meta_image overrides are discouraged (the build-time card covers
virtually every post), and a `needs-design` PR label gets a
designer-made custom feature image.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Blog index: replace the committed meta image with a build-time title card

Drop meta_image from blog/_index.md and delete the old meta.png; the
generator now routes the /blog/ index to the plain "Blog" title card
(same treatment as the case-studies and events indexes) and keeps it
out of OG_SAMPLE so samples still exercise the blog post renderer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Remove the last three post meta_image overrides; designer card for /blog/

Drop the remaining custom meta_image overrides (sam-cogan-testing-best-
practices, getting-started-aks-pulumi-csharp, secure-cloud-kubernetes-
workloads-intro) and delete their meta.png files — the AKS post's was a
byte-for-byte duplicate of 00-overview-what-were-building.png, which now
appears in the post body where the workshop plan is described. Every
blog post now uses the build-time card.

The /blog/ index gets a committed designer-made dark "Blog" card
(meta_image: meta.png) instead of the generated title card; the
generator goes back to skipping it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Social: attach the titled build-time card to LinkedIn posts

_meta_image_path previously returned the post's feature.png (or a legacy
meta.png) — the untitled hero art — as the LinkedIn media upload. That
gave feature-ful posts a title-less image on LinkedIn, while X/Bluesky
show the titled build-time card via og:image on link unfurl.

Point LinkedIn at that same titled card instead, so the image carries
the post title everywhere. The card is a gitignored build artifact not
present in the schedule-social job's checkout, but it's live at deploy
time (the job runs after Build and deploy), and social_core uploads
media as a multipart file rather than a URL — so fetch the deployed card
to a temp file. The card is keyed by the post's directory path (matching
meta-image-key.html), and a short retry absorbs post-deploy CDN lag. If
the card can't be fetched, LinkedIn falls back to unfurling the link,
which still renders the same titled card.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Christian Nunciato <chris@nunciato.org>
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.

5 participants