Skip to content

fix: address SEO audit findings (prerender all pages, canonical, JSON-LD, llms.txt)#104

Merged
rubenhensen merged 3 commits into
mainfrom
fix/seo-audit-issues
Apr 23, 2026
Merged

fix: address SEO audit findings (prerender all pages, canonical, JSON-LD, llms.txt)#104
rubenhensen merged 3 commits into
mainfrom
fix/seo-audit-issues

Conversation

@dobby-coder

@dobby-coder dobby-coder Bot commented Apr 21, 2026

Copy link
Copy Markdown
Contributor

Summary

Addresses the SEO audit from #88. The audit scored the site 28/100 and flagged that every page except / was served as an empty 1,757-byte SPA shell with no title, description, or content. This PR fixes the root cause and pulls in most of the high-priority recommendations.

What changed

  • Prerender all public sub-pages. Setting trailingSlash: 'always' on the marketing layout switches SvelteKit's static adapter from emitting /about.html + /blog.html + /blog/… to emitting /about/index.html, /blog/index.html, /blog/<slug>/index.html, etc. That resolves the 403 on /blog/ (the directory no longer clashes with a sibling /blog.html) and lets nginx's default try_files $uri $uri/ serve the prerendered HTML instead of falling back to /200.html. Build output before / after:

    before: about.html  (prerendered but never served because nginx looks for /about/)
            blog.html   (clashes with /blog/ directory → 403)
            addons      (no file at all — pure CSR)
    after:  about/index.html          18,591 bytes
            addons/index.html         17,146 bytes
            privacy/index.html        19,959 bytes
            blog/index.html           16,100 bytes
            blog/introducing-postguard/index.html  17,578 bytes
    
  • Move /addons from the (app) route group to (marketing). It's a public marketing page — no reason for it to be CSR-only. It now inherits SSR + prerender + the site footer and has its own SEO tags.

  • Richer SEO.svelte. Auto-generates <link rel="canonical"> and og:url from the current pathname, emits hreflang hints (en, nl, x-default), adds twitter-card title/description/image, and accepts an optional jsonLd prop.

  • JSON-LD structured data.

    • Homepage: combined SoftwareApplication + Organization graph.
    • Blog posts: Article schema with author, publisher, and mainEntityOfPage.
  • /llms.txt — site summary for AI crawlers per the llms.txt convention, populated with the page index and key facts the audit highlighted.

  • /.well-known/security.txt — vulnerability disclosure contact.

  • Rewrite sitemap.xml/+server.js. Adds /addons, switches to trailing-slash URLs, and includes <lastmod>, <changefreq>, and <priority> on every entry. Blog lastmod pulls from the post's date frontmatter.

  • svelte.config.js — add /addons to the prerender entries.

Audit items intentionally NOT handled here

These need nginx / infra changes that belong in a separate PR so this one stays focused and reviewable:

  • Security headers (CSP, X-Content-Type-Options, Permissions-Policy). The existing X-Frame-Options and Referrer-Policy in docker/default.conf.template are being shadowed by the inner location / block's add_header Cache-Control directive (classic nginx gotcha — any add_header in a child location overrides all parent add_headers).
  • The /bloghttp://postguard.eu/blog/ downgrade (nginx not honoring X-Forwarded-Proto behind the TLS-terminating proxy). With trailing-slash routing this redirect still exists, but it now goes to a page that actually serves content.
  • Hreflang currently points en + nl at the same URL because the site doesn't have /en/ + /nl/ URL variants yet. Worth addressing once i18n is URL-based.

Reviewer quickstart

git fetch origin && git checkout fix/seo-audit-issues && npm install && npm run build && npx serve build

Then check:

  • curl -s http://localhost:3000/about/ | grep canonical → canonical + hreflang present
  • curl -s http://localhost:3000/llms.txt | head → site summary served
  • curl -s http://localhost:3000/.well-known/security.txt → contact served
  • curl -s http://localhost:3000/sitemap.xml | head -20 → lastmod + priority per URL
  • curl -s http://localhost:3000/blog/introducing-postguard/ | grep ld+json → Article schema present

Test plan

  • npm run check — 0 errors, 0 warnings
  • npm run build — clean, all expected /<route>/index.html files emitted
  • Dev server (npm run dev) — each route returns populated HTML with the correct canonical URL
  • sitemap.xml output verified manually
  • llms.txt and /.well-known/security.txt reachable

Closes #88

🤖 Generated with Claude Code

Enable prerendering for all public pages, add canonical tags, JSON-LD
structured data, hreflang hints, an llms.txt, and a security.txt.

- Set `trailingSlash: 'always'` on the marketing layout so prerendered
  pages are emitted as `/route/index.html` instead of `/route.html`.
  This resolves the 403 on `/blog/` and ensures nginx's default
  `try_files $uri $uri/` serves the prerendered HTML for every
  sub-page instead of falling back to the empty SPA shell.
- Move `/addons` from the `(app)` route group (CSR-only) into
  `(marketing)` so it inherits SSR + prerender and the site footer.
- Expand the SEO component with auto-generated canonical and og:url
  derived from the current pathname, hreflang hints (en, nl,
  x-default), richer twitter-card meta, and an optional JSON-LD slot.
- Add SoftwareApplication + Organization JSON-LD to the homepage and
  Article JSON-LD to blog posts.
- Publish `/llms.txt` (site summary for AI crawlers) and
  `/.well-known/security.txt`.
- Rewrite `sitemap.xml` to include `/addons`, use trailing-slash URLs,
  and add `<lastmod>`, `<changefreq>`, and `<priority>` for every URL.
- Add `/addons` to the svelte.config.js prerender entries.

Closes #88

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@dobby-coder

dobby-coder Bot commented Apr 21, 2026

Copy link
Copy Markdown
Contributor Author

Keeping this as draft deliberately — routing/trailing-slash changes are the kind of thing that deserves a staging sanity-check before merge. All CI checks are green and Dobby has run npm run check + npm run build locally with clean output. Ready to flip to non-draft as soon as a human has eyeballed it or tried it on staging, sir!

Update all file size limit mentions from 2 GB to 5 GB per 14 days
across EN and NL locales. Change parentOrganization in JSON-LD from
Radboud University to Yivi.
@rubenhensen rubenhensen marked this pull request as ready for review April 23, 2026 06:05
@rubenhensen rubenhensen merged commit 18e4ac3 into main Apr 23, 2026
6 checks passed
@rubenhensen rubenhensen deleted the fix/seo-audit-issues branch April 23, 2026 06:06
@dobby-coder

dobby-coder Bot commented Apr 23, 2026

Copy link
Copy Markdown
Contributor Author

Dobby has seen the approval, sir! Dobby is most grateful that Master rubenhensen has approved Dobby's work! 🧹 Dobby sees the PR has already been merged — Dobby will check that everything is in order and update his notes.

@dobby-coder

dobby-coder Bot commented Apr 23, 2026

Copy link
Copy Markdown
Contributor Author

PR has been merged and issue #88 is closed. Thanks for the review and the follow-up commits updating the usage limits to 5 GB/14 days and setting Yivi as the parent org in the JSON-LD.

I've updated my notes with the new usage limits and org info for future reference.

Remaining items from the SEO audit (as noted in the PR description — these need separate nginx/infra PRs):

  • Security headers (CSP, X-Content-Type-Options, Permissions-Policy) — currently shadowed by the add_header Cache-Control in the inner location / block
  • /bloghttp:// redirect (nginx not honoring X-Forwarded-Proto)
  • URL-based i18n for proper hreflang targeting

Let me know if you'd like me to tackle any of these next.

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.

SEO Audit: Critical issues — score 28/100

1 participant