Skip to content

fix(landing): revert unoptimized removal, compress source images instead - #5528

Merged
waleedlatif1 merged 1 commit into
stagingfrom
hotfix-blog-images
Jul 9, 2026
Merged

fix(landing): revert unoptimized removal, compress source images instead#5528
waleedlatif1 merged 1 commit into
stagingfrom
hotfix-blog-images

Conversation

@waleedlatif1

Copy link
Copy Markdown
Collaborator

Summary

  • Fixes the broken blog images on staging. PR fix(landing): fix Core Web Vitals regressions across public marketing pages #5522 removed unoptimized from local blog/library/integration cover images, expecting next/image's runtime optimizer to serve resized AVIF/WebP. That optimizer is failing on staging (root cause still under investigation — /_next/image requests don't go through the app's structured logger, so nothing showed up in application logs). Reverted unoptimized on all 7 spots immediately.
  • Gets the same perf win a different way: compressed the actual source image files (mozjpeg quality 82 for JPEGs, palette PNG for PNGs) at their exact existing pixel dimensions — verified programmatically (dimension mismatch aborts the write) and spot-checked visually. 13 blog/library covers + 3 author avatars + 1 brand logo, ~1.72MB → ~1.02MB combined (~41% smaller), zero resolution change.

Type of Change

  • Bug fix (production/staging regression)

Testing

  • tsc --noEmit clean, biome check clean
  • Verified every recompressed image's width/height matches the original exactly (script aborts on any mismatch)
  • Visually spot-checked several images (a photo avatar, a line-art PNG) for artifacts — none visible

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

…nstead

PR #5522 removed `unoptimized` from local blog/library/integration cover
images, expecting next/image's runtime optimizer to serve resized
AVIF/WebP. On staging, images broke entirely on /blog - the runtime
optimizer is failing there (root cause still under investigation:
_next/image requests aren't captured by the app's structured logger, so
the underlying error wasn't visible in application logs). Reverting
`unoptimized` immediately restores working images.

In its place, this compresses the actual source files (mozjpeg quality 82
for JPEGs, palette PNG for PNGs) at their EXACT existing pixel dimensions -
verified programmatically per-file (dimension mismatch aborts the write)
and spot-checked visually. This gets the same bandwidth/LCP win the
runtime optimizer was meant to provide, without depending on it:

- 13 blog/library cover images + 3 author avatars + 1 brand logo
- ~1.72MB -> ~1.02MB combined (~41% smaller), zero resolution change
@vercel

vercel Bot commented Jul 9, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Jul 9, 2026 1:23am

Request Review

@cursor

cursor Bot commented Jul 9, 2026

Copy link
Copy Markdown

PR Summary

Low Risk
Landing-only display change with no auth, data, or API impact; slightly larger direct image payloads versus a working optimizer.

Overview
Restores unoptimized on seven next/image usages for local landing assets (blog/library author and post covers, integration CTA logo) so images are served directly instead of through /_next/image.

This reverses the prior removal that relied on the Next.js image optimizer on staging, where those optimized requests were failing and cover images broke. Static assets are expected to be kept lean at source (per the PR description’s recompression work) rather than depending on runtime optimization for these paths.

Reviewed by Cursor Bugbot for commit 401d66b. Configure here.

@waleedlatif1 waleedlatif1 changed the title hotfix(landing): revert unoptimized removal, compress source images instead fix(landing): revert unoptimized removal, compress source images instead Jul 9, 2026
@greptile-apps

greptile-apps Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR restores direct serving for landing-page images and recompresses the source assets. The main changes are:

  • Added unoptimized back to blog, library, author, and integration images.
  • Replaced public cover, avatar, and logo assets with smaller files at the same paths.
  • Keeps image layout props and public URLs unchanged.

Confidence Score: 5/5

This looks safe to merge.

  • No blocking issues found in the changed code.
  • The only follow-up is a small asset-metadata check for recompressed JPEG avatars.

apps/sim/public/authors/sid.jpg

Important Files Changed

Filename Overview
apps/sim/app/(landing)/components/content-author-page/content-author-page.tsx Restores direct serving for local post-card cover images on author pages.
apps/sim/app/(landing)/components/content-index-page/content-index-page.tsx Restores direct serving for featured and compact content-card images.
apps/sim/app/(landing)/components/content-post-page/content-post-page.tsx Restores direct serving for post hero and related-content images.
apps/sim/app/(landing)/integrations/(shell)/[slug]/page.tsx Restores direct serving for the local brand logo in the integrations CTA.
apps/sim/public/** Recompresses public image assets in place while keeping their paths unchanged.

Comments Outside Diff (1)

  1. apps/sim/public/authors/sid.jpg, line 1 (link)

    P2 Orientation Metadata May Be Dropped

    This avatar was recompressed in place and now bypasses Next's image optimizer because the callers use unoptimized. If the original JPEG depended on EXIF orientation, the browser will render the raw pixels from this file and the author photo can appear rotated or mirrored on author/post pages.

Reviews (1): Last reviewed commit: "hotfix(landing): revert unoptimized remo..." | Re-trigger Greptile

@waleedlatif1

Copy link
Copy Markdown
Collaborator Author

Re: Greptile's EXIF orientation concern on apps/sim/public/authors/sid.jpg — checked all 13 original JPEGs touched in this PR (including sid.jpg) with sharp().metadata() before compressing: every single one reports orientation: undefined, i.e. none carried an EXIF orientation tag to begin with. So there's nothing for the recompression to have dropped/mis-rendered here. Valid category of risk for sharp re-encodes in general, just not applicable to these specific files — verified, not assumed.

@waleedlatif1
waleedlatif1 merged commit 2b09c08 into staging Jul 9, 2026
19 checks passed
@waleedlatif1
waleedlatif1 deleted the hotfix-blog-images branch July 9, 2026 01:28
waleedlatif1 added a commit that referenced this pull request Jul 12, 2026
* fix(landing): repair Lighthouse-flagged CWV audits on production

Empirically verified against a live full Lighthouse run of www.sim.ai
(production, pre-fix) plus a local build of the exact deployed commit with
source maps temporarily enabled for root-causing. Distinguished genuinely
failing audits from passing ones already misread as broken.

- fetchPriority missing on every LCP hero image: `priority` generates a
  preload <link> but Next does not auto-add fetchpriority=high to it -
  confirmed via raw deployed HTML diff. Added explicit fetchPriority='high'
  to all 5 priority Image usages (hero, enterprise, blog/library post +
  index cards).
- valid-source-maps failing: production ships no source maps at all
  (productionBrowserSourceMaps defaults false). Enabled it - safe here since
  this repo's frontend is already fully open source, so no incremental
  exposure versus Next's default.
- image-delivery-insight (55.8KB wasted): feature-integrate-ui.png's `sizes`
  hint was a flat 1050px regardless of viewport, so mobile fetched the
  1920w variant for a ~423px real render. Replaced with a responsive sizes
  expression derived from the sibling backdrop image's own (already
  correct) hint, scaled by the callout's documented 125% overhang.
- cache-insight (best-fixable portion): _next/static/* filenames are
  content-hashed and immutable per deploy, but shared one cache rule with
  unhashed /public assets, capping both at 1-day max-age. Split into two
  rules - hashed assets now get 1-year immutable, unhashed assets keep the
  shorter revalidating TTL. Verified via a real build + server that both
  paths now return the correct distinct header.

Investigated and NOT changed (documented, not assumed):
- legacy-javascript-insight (14KB): traced via sourcemap to
  next/dist/build/polyfills/polyfill-module.js - Next's own built-in
  polyfill bundle, not our code or a dependency, and not exposed via any
  next.config.ts option. No browserslist misconfiguration on our end (none
  exists; Next already defaults to its modern target).
- forced-reflow-insight: even with source maps present locally, the
  dominant cost (335-417ms) stayed [unattributed] by Chrome's own profiler,
  and the small attributed slice was non-deterministic between our own
  chunk and a third-party script (HubSpot analytics) across runs - not a
  confident single root cause worth a targeted fix.
- render-blocking-insight / network-dependency-tree / bf-cache: bf-cache's
  actual failure reason is Cache-Control: no-store on the main document -
  the exact root cause already fixed on staging (PR #5522/#5528, the
  PublicEnvScript/unstable_noStore fix) but not yet promoted to main/prod.
  Resolves once that ships, not additional work here.

* fix(landing): convert mothership cover from PNG to JPEG (/blog LCP 6.6s -> 2.8s)

Ran a full Lighthouse sweep across every public page as requested. /blog
scored 73 (LCP 6.6s) while every other page scored 95+ - reproduced
consistently across 3 runs, not noise. Traced via lcp-breakdown-insight:
the LCP image (mothership/cover.png, 241KB even after the earlier palette
compression pass) took 6+ seconds to download on simulated mobile
throttling, well beyond what its size should cost.

PNG is a poor fit for this illustration's subtle gradients versus JPEG's
lossy compression. Verified empirically before converting: same 1920x1080
resolution, visually identical (spot-checked), 241KB -> 65KB (73% smaller).
No other cover in the content set uses PNG and benefits the same way
(checked copilot/cover.png, the only other PNG cover - already optimal at
64KB, converting it yielded no improvement, left unchanged).

Verified fix: /blog score 73->93, LCP 6.6s->2.8s, reproduced across 3 runs.

* fix(landing): correct mobile sizes tier, drop non-functional cache rule

- integrations-callout: account for FeatureCard's max-lg:grid-cols-1 mobile
  stack in the sizes hint, verified against Lighthouse's measured mobile
  render width.
- next.config: remove a custom _next/static cache-control rule that never
  actually fired (confirmed via header-marker test) - Next's own built-in
  default already applies the correct immutable 1yr cache to that path.

* fix(landing): correct sizes underestimate + fix dead .map header rule

- integrations-callout: derive sizes from the section's actual grid math
  (fixed 386px copy column, 40px gap, section gutters) instead of an
  approximated vw fraction. Verified against a static reproduction of the
  layout rendered at each Tailwind breakpoint - the old 110vw mobile tier
  underestimated real render width by ~3% right at the 1023px stack
  boundary, which could cause the browser to pick a too-small srcset
  candidate and upscale.
- next.config: the .map header rule's trailing `$` was read as a literal
  character by Next's path-to-regexp source matcher, not a regex anchor,
  so the rule never matched a real .map URL (confirmed via routes-manifest
  regex + a live header check). Removed the dead anchor and added a
  bounded Cache-Control so a future decision to stop shipping source maps
  isn't undermined by a 1yr immutable cache on already-fetched maps.

* fix(llms): serve well-formed llms.txt, remove Mothership + dead static files

Both the marketing site and docs site's llms.txt validator errors ("does
not appear to contain any links") traced to the same root cause: a static
public/llms.txt shadowed a better-written, already-existing dynamic
app/llms.txt route, and every "link" in the static files (and in the
docs app's auto-generated route) was bare `label: url` text, not Markdown
link syntax - so a strict Markdown-link parser found zero matches even
though URLs were visibly present.

- apps/sim: delete public/llms.txt (dead code, shadowing the properly
  Markdown-linked app/llms.txt route.ts, confirmed via production headers
  showing the static file was what actually served). Fix llms-full.txt's
  Links/Support/Legal sections to use [label](url) syntax, correct a
  stale "Next.js 15" reference, and replace "Mothership" with "Chat" per
  the constitution's language rules.
- apps/docs: same shadowing issue - delete the orphaned public/llms.txt
  (also still said "Mothership"). Fix the auto-generated per-page link
  list in app/llms.txt/route.ts to emit [title](url) instead of
  "title: url" for every documentation page.

* fix(llms): actually include the route.ts fixes from the prior commit

The prior commit (3b2d35c) only staged the two deleted public/llms.txt
files - these two modified route.ts files (the Mothership/link-format
fixes they were meant to accompany) were left unstaged. No new changes,
just completing that commit's intent.
waleedlatif1 added a commit that referenced this pull request Jul 27, 2026
Three posts shipped an `ogImage` pointing at a file that was never
committed, so the library index rendered broken images and their
`og:image`, JSON-LD, and sitemap entries all 404'd. Several others were
authored without the brand font loaded or with the title clipping off the
bottom edge.

Covers were hand-made per post with no generator, which is why they
drifted. Adds `bun run library:covers`, rendering each cover from the
post's frontmatter title using the reference template already encoded in
the docs OG route, and regenerates all 20 so the grid is uniform.

Line widths come from the font's real advance metrics rather than an
average-glyph-width estimate: the template joins words with non-breaking
spaces to dodge a Satori space-measurement bug, which leaves hyphens as
the only fallback break points, so an under-measured line breaks
mid-compound.

Also drops six orphaned `cover.png` sources left over from the JPEG
compression pass in #5528.
waleedlatif1 added a commit that referenced this pull request Jul 27, 2026
Three posts shipped an `ogImage` pointing at a file that was never
committed, so the library index rendered broken images and their
`og:image`, JSON-LD, and sitemap entries all 404'd. Several others were
authored without the brand font loaded or with the title clipping off the
bottom edge.

Covers were hand-made per post with no generator, which is why they
drifted. Adds `bun run library:covers`, rendering each cover from the
post's frontmatter title using the reference template already encoded in
the docs OG route, and regenerates all 20 so the grid is uniform.

Line widths come from the font's real advance metrics rather than an
average-glyph-width estimate: the template joins words with non-breaking
spaces to dodge a Satori space-measurement bug, which leaves hyphens as
the only fallback break points, so an under-measured line breaks
mid-compound.

Also drops six orphaned `cover.png` sources left over from the JPEG
compression pass in #5528.
waleedlatif1 added a commit that referenced this pull request Jul 27, 2026
* fix(library): generate every post cover from one template

Three posts shipped an `ogImage` pointing at a file that was never
committed, so the library index rendered broken images and their
`og:image`, JSON-LD, and sitemap entries all 404'd. Several others were
authored without the brand font loaded or with the title clipping off the
bottom edge.

Covers were hand-made per post with no generator, which is why they
drifted. Adds `bun run library:covers`, rendering each cover from the
post's frontmatter title using the reference template already encoded in
the docs OG route, and regenerates all 20 so the grid is uniform.

Line widths come from the font's real advance metrics rather than an
average-glyph-width estimate: the template joins words with non-breaking
spaces to dodge a Satori space-measurement bug, which leaves hyphens as
the only fallback break points, so an under-measured line breaks
mid-compound.

Also drops six orphaned `cover.png` sources left over from the JPEG
compression pass in #5528.

* fix(library): re-render covers every run and add a sync check

Covers are derived artifacts, so skipping outputs that already exist left
an image showing the old title after a post's frontmatter `title` changed.
Every run now re-renders from scratch; rendering is deterministic, so an
unchanged title re-encodes to identical bytes and a full run stays a no-op
in git.

Replaces `--force` (now the default) with `--check`, which renders in
memory and compares against the committed bytes without writing, so CI can
catch both a stale cover and the missing-cover case that caused the
original breakage.

* fix(library): compare decoded pixels in the cover sync check

Byte-equality on the mozjpeg output assumed portable encoder bytes.
libvips/mozjpeg does not guarantee that across OS and CPU, so identical
input can encode differently on a contributor's machine or a Linux CI
runner and fail the check for no real reason — exactly where the check was
meant to run.

Decodes both images to greyscale and compares mean absolute difference
instead, which discards encoder variance while still testing what the
check is about. Measured on this cover set: re-encoding an identical
render with a deliberately different encoder moves it ~0.26, a one-word
title change moves it ~12; the threshold of 2 sits between them with ~8x
margin.

* fix(library): count redrawn pixels in the cover sync check

Averaging the difference diluted a local edit across all 810,000 pixels.
Changing a title's "2026" to "2027" moved the mean by 0.42 — under the
tolerance that absorbed encoder noise — so the check passed a cover still
showing the old year.

Counts pixels that moved more than 48 greyscale levels instead. Measured on
this cover set, that one-character edit redraws 2,559 pixels while three
deliberately different encodes of an identical render (quality 60/70 without
mozjpeg, quality 95 with) redraw none, so the count separates real drift
from encoder variance in both directions.

* fix(library): parse frontmatter with gray-matter and split oversized tokens

Two issues in the cover generator, neither reachable from a current title.

The hand-rolled frontmatter regex could disagree with `gray-matter`, which
is what renders the page and its `og:title`. On a double-quoted escape or a
block scalar the cover would have rendered a title the page never shows,
with `--check` calling it in sync. Uses `gray-matter` directly so there is
one parser.

`wrapTitleLines` only breaks between space-separated words, so a token wider
than the title box on its own stayed on an overflowing line, and the
non-breaking spaces left Satori no recourse but to break it at a hyphen —
the mid-compound break this layout exists to prevent. Oversized tokens now
split here, at hyphens first and per-character only for something like a
URL, and a font size is accepted only if every line measures within the box.

All 20 covers re-render byte-identically, so neither change alters current
output.
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.

1 participant