Skip to content

fix(seo): resolve the in-repo Ahrefs September 2026 site-audit errors - #3716

Merged
jannikmaierhoefer merged 2 commits into
mainfrom
seo/ahrefs-2026-09-images
Sep 4, 2026
Merged

jannikmaierhoefer merged 2 commits into
mainfrom
seo/ahrefs-2026-09-images

Conversation

@jannikmaierhoefer

@jannikmaierhoefer jannikmaierhoefer commented Sep 4, 2026 •

Copy link
Copy Markdown
Member

Monthly Ahrefs Site Audit triage for langfuse.com (project "Langfuse (Subdomains)", crawl 2026-09-03, health score 98, 86 URLs with errors).

Fixes the two Error-importance issues whose root cause is in this repo: Image file size too large (6 URLs) and Hreflang and HTML lang mismatch (15 URLs). The remaining 71 error URLs sit on subdomains built from other repos — see "Intentionally out of scope".

1. Oversized images

Flagged URL Root cause Fix
/images/customers/slite/slite-agent-enterprise-search.png (2.22 MB) CustomerScreenshotCarousel passed unoptimized to next/image, so the raw PNG went to the browser Removed the prop
/images/customers/slite/slite-agent-triage.png (2.38 MB) same same
/images/customers/ravenna/ravenna-skill-builder.png (1.41 MB) same same
/images/customers/ravenna/ravenna-ticket-list.png (1.59 MB) same same
/images/docs/score-analytics-full-dashboard.png (3.78 MB) 5284px-wide screenshot with an unused alpha channel, served raw via a plain <img> in content/docs/evaluation/overview.mdx Resampled to 2048px, alpha dropped → 0.69 MB
_next/image?url=/images/find-us.png&w=3840 (2.44 MB) 6.85 MB satellite photo stored as PNG, so even the optimizer's PNG output stayed huge Converted to a 1920px progressive JPEG (find-us.jpg, 0.58 MB) and repointed content/marketing/find-us.mdx

unoptimized has been on the carousel since it was introduced in #3378 and nothing depends on it. next.config.mjs already sets images.unoptimized: true for STATIC_EXPORT=true, so the static export is unaffected. This is a real page-weight win on /users/slite and /users/ravenna, not only an audit checkbox.

2. Hreflang and HTML lang mismatch

/japan and the 14 /academy/japan/* pages serve Japanese content and declare a self-referencing ja-JP hreflang, but the document reported lang="en". Beyond the audit error, that misreports the content language to screen readers and browser translation.

Why not a nested <html lang="ja">: Next.js only lets the root layout render <html>, and its attributes are serialized before any child renders. Verified empirically against the dev server — a nested layout rendering <html lang="ja"> emits a second <html> tag rather than setting the attribute:

$ curl -s http://127.0.0.1:3333/japan | grep -o '<html[^>]*>'
<html lang="en" dir="ltr" class="…">
<html lang="ja">
$ curl -s http://127.0.0.1:3333/japan | grep -c "<html"
2

Why not per-language root layouts: that is what #3687 did, and it moves every route in app/ into a route group. It was closed unmerged, so this PR does not re-propose it.

What this does instead: components/DocumentLanguage.tsx, mounted by the two existing Japanese layouts (app/japan/layout.tsx, app/academy/japan/layout.tsx). It sets document.documentElement.lang from an inline script that runs while the browser is still parsing the document — before first paint — and an effect restores "en" when the reader navigates back out through client-side routing. <html> already carries suppressHydrationWarning, so mutating the attribute does not trip React. The script-src CSP already allows 'unsafe-inline'. No route or folder moves.

The Japanese paths also serve a Content-Language: ja response header, added in next.config.mjs.

Known limitation, stated plainly: the HTML source still ships lang="en"; only the live DOM carries lang="ja". Google renders JavaScript so it sees the corrected value, but whether the Ahrefs check clears depends on whether its crawler reads the rendered DOM or the raw response. If it stays flagged after the next crawl, the only remaining fix is the per-language root layouts from #3687.

Verification

Dev server (pnpm dev, port 3333) and a production build.

Build — npx next build (prebuild skipped; it needs a GitHub token): ✓ Compiled successfully in 67s, ✓ Generating static pages (962/962), no errors. Reverted the next-env.d.ts the build dirtied.

Document language:

Path Content-Language rendered document.documentElement.lang
/japan ja ja
/academy/japan/tracing ja ja
/docs absent en

Client-side navigation both directions, driven in a real browser:

/japan          → click /academy         → lang "en"  ✅ restored
/academy        → click /academy/japan/… → lang "ja"  ✅ applied

No console errors on /academy/japan/tracing; the page renders correctly.

Images — measured against the running dev server:

slite-agent-triage w=1920 Accept: image/webp → 103,168 bytes  image/webp
slite-agent-triage w=1920 Accept: */*        → 358,467 bytes  image/png
slite-agent-triage w=3840 Accept: image/webp → 251,592 bytes  image/webp
slite-agent-triage w=3840 Accept: */*        → 752,531 bytes  image/png
find-us          w=3840 Accept: */*          → 418,129 bytes  image/jpeg

Worst case — no WebP support, largest candidate — is 752 KB, under the 1 MB threshold. All four carousel images confirmed loading through /_next/image in the browser (complete: true), and /find-us confirmed serving find-us.jpg through the optimizer.

$ python3 -c "…"    # source re-encode, before → after
public/images/docs/score-analytics-full-dashboard.png: 3.78MB -> 0.69MB, (2048, 1234)
public/images/find-us.jpg: 6.85MB (png) -> 0.58MB (jpg), (1920, 1197)

$ grep -rn "find-us.png" content components components-mdx md-override app lib
(no matches — the only hit is the gitignored, build-regenerated public/md-src/find-us.md)

$ prettier --config .prettierrc.json --write <touched files>
all unchanged

$ node scripts/check-h1-headings.js
clean for tracked files

Both re-encoded images were rendered and compared against the originals at display size; no visible quality loss. No md-override/ file covers /find-us, /docs/evaluation/overview, /japan, or /academy/japan/*.

Intentionally out of scope

Other repos. Companion PR langfuse/langfuse-js#937 fixes the js.reference.langfuse.com broken links ([MIT](LICENSE) resolving to a file that exists only at the repo root — also 404 on GitHub and npm) and adds a canonical URL to the TypeDoc site. The five /modules/packages/* 404s turned out to be already fixed on langfuse-js@main; the deployed reference site is just built from an older commit.

Companion PR langfuse/langfuse-python#1858 fixes the python.reference.langfuse.com errors: pdoc's index.html is a bare meta-refresh stub with no title, links or canonical; the host returns that stub with HTTP 200 for every unmatched path (which is how two nonexistent …/concepts/models.md paths entered the crawl); and /langfuse was 2.7 MB, over the crawl limit. That PR needs the Cloudflare Pages build command repointed at its new build script to take effect — docs/ is gitignored and no workflow in that repo builds it.

cloud.langfuse.com/auth/* is a separate repo and separate hosting.

The 9 broken /cdn-cgi/l/email-protection links are Cloudflare's Email Address Obfuscation rewriting pnpm store paths in TypeDoc's "Defined in" lines (typescript@5.9.2 reads as an email address to it). Fix is a Cloudflare dashboard setting, not code.

Deferred, in this repo: the remaining 22 "Image file size too large" URLs are all legacy .gif assets in old blog and changelog posts, about half on static.langfuse.com. AGENTS.md says the site uses .mp4, never .gif, so the fix is re-encoding and re-uploading — a separate change, and several of the posts may simply want the asset removed.

No change needed: the 4 orphan pages (/cn, /kr, /wrapped, /oss-friends) are deliberately unlinked campaign and locale landing pages.

🤖 Generated with Claude Code

Three langfuse.com images exceeded the 1 MB image-size threshold in the
Ahrefs Site Audit, and in each case the full-resolution file was reaching
the browser.

- The customer-story carousel passed `unoptimized` to next/image, so
  visitors downloaded the raw 1.4-2.4 MB PNGs. Dropping the prop lets
  Next serve WebP/AVIF at the width `sizes` already declares.
  (`images.unoptimized` is still set globally for STATIC_EXPORT builds.)
- score-analytics-full-dashboard.png was a 5284px-wide screenshot with an
  unused alpha channel; resampled to 2048px, 3.78 MB -> 0.69 MB.
- find-us.png was a satellite photo stored as PNG; converted to a 1920px
  progressive JPEG, 6.85 MB -> 0.58 MB.

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

vercel Bot commented Sep 4, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated
langfuse-docs Ready Ready Preview Sep 4, 2026 8:02am UTC

Request Review

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown

@claude review

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Claude Code Review

No review was started: this request came from a bot account. Manual reviews can only be requested by someone with write access to this repository. Ask a maintainer to comment @claude review, or have your automation post the comment from a user account with write access.

Tip: disable this comment in your organization's Code Review settings.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Looks good, straightforward asset/config fix.

What was reviewed: the unoptimized prop removal in CustomerScreenshotCarousel.tsx (confirmed next.config.mjs still forces unoptimized: true for STATIC_EXPORT builds, so static export is unaffected); the find-us.mdx image path update to the new .jpg; and visually rendered both replaced binaries (find-us.jpg, score-analytics-full-dashboard.png) to confirm no quality/content regression. No md-override/ file exists for /find-us, so no override sync was needed.

Extended reasoning...

Overview

The diff touches five files: a one-line prop removal in components/customers/CustomerScreenshotCarousel.tsx, a one-line image path swap in content/marketing/find-us.mdx, and three binary asset changes (delete find-us.png, add find-us.jpg, replace score-analytics-full-dashboard.png with a smaller re-encode). This is purely an image-optimization/SEO fix with no logic or prose changes.

Security risks

None. No auth, crypto, permissions, or user input handling is touched. The only behavioral change is that carousel images now go through Next.js's built-in image optimizer instead of being served raw, which is a well-established, safe code path.

Level of scrutiny

Low. This is a small, mechanical, self-contained change (drop a prop, swap a filename, replace two binary assets). I verified the specific claim that removing unoptimized doesn't break the static-export build by checking next.config.mjs, which independently forces unoptimized: true when STATIC_EXPORT=true — so the PR's reasoning holds. I also visually rendered both new/changed images to confirm they display correctly and match their intended content (aerial map, dashboard screenshot) with no visible corruption or quality loss.

Other factors

No md-override/ file exists for /find-us, so the "keep overrides synchronized" rule doesn't apply here. The find-us.png reference is fully removed from source (grep confirms no remaining references outside the gitignored public/md-src/ build artifact). This is the kind of change that doesn't need a human to look closely before merging.

…ages

`/japan` and the `/academy/japan/*` tree serve Japanese content and carry a
self-referencing `ja-JP` hreflang, but the document reported `lang="en"`.
Besides the Ahrefs "Hreflang and HTML lang mismatch" error on 15 URLs, that
misreports the content language to screen readers and browser translation.

Next.js only lets the root layout render `<html>`, and its attributes are
serialized before any child renders, so one root layout cannot vary `lang` by
route -- a nested layout that renders `<html lang="ja">` emits a second `<html>`
tag rather than setting the attribute. Splitting the app into per-language root
layouts would work but restructures every route, so instead the two Japanese
layouts mount `DocumentLanguage`, which sets the attribute from an inline script
while the document is still parsing and restores the site default when the
reader navigates back out through client-side routing.

The Japanese paths also serve `Content-Language: ja`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@jannikmaierhoefer jannikmaierhoefer changed the title fix(seo): shrink the oversized images flagged by the Ahrefs site audit fix(seo): resolve the in-repo Ahrefs September 2026 site-audit errors Sep 4, 2026
@jannikmaierhoefer
jannikmaierhoefer added this pull request to the merge queue Sep 4, 2026
Merged via the queue into main with commit 4cc409f Sep 4, 2026
17 checks passed
@jannikmaierhoefer
jannikmaierhoefer deleted the seo/ahrefs-2026-09-images branch September 4, 2026 08:45

This branch was successfully deployed

1 active deployment
Preview — 2123a52b Deployed Sep 4, 2026 by vercel[bot]
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