fix: address SEO audit findings (prerender all pages, canonical, JSON-LD, llms.txt)#104
Conversation
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>
|
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 |
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.
|
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. |
|
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):
Let me know if you'd like me to tackle any of these next. |
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 defaulttry_files $uri $uri/serve the prerendered HTML instead of falling back to/200.html. Build output before / after:Move
/addonsfrom 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">andog:urlfrom the current pathname, emitshreflanghints (en,nl,x-default), adds twitter-card title/description/image, and accepts an optionaljsonLdprop.JSON-LD structured data.
SoftwareApplication+Organizationgraph.Articleschema with author, publisher, andmainEntityOfPage./llms.txt— site summary for AI crawlers per thellms.txtconvention, 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. Bloglastmodpulls from the post'sdatefrontmatter.svelte.config.js— add/addonsto 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:
X-Frame-OptionsandReferrer-Policyindocker/default.conf.templateare being shadowed by the innerlocation /block'sadd_header Cache-Controldirective (classic nginx gotcha — anyadd_headerin a child location overrides all parentadd_headers)./blog→http://postguard.eu/blog/downgrade (nginx not honoringX-Forwarded-Protobehind the TLS-terminating proxy). With trailing-slash routing this redirect still exists, but it now goes to a page that actually serves content./en/+/nl/URL variants yet. Worth addressing once i18n is URL-based.Reviewer quickstart
Then check:
curl -s http://localhost:3000/about/ | grep canonical→ canonical + hreflang presentcurl -s http://localhost:3000/llms.txt | head→ site summary servedcurl -s http://localhost:3000/.well-known/security.txt→ contact servedcurl -s http://localhost:3000/sitemap.xml | head -20→ lastmod + priority per URLcurl -s http://localhost:3000/blog/introducing-postguard/ | grep ld+json→ Article schema presentTest plan
npm run check— 0 errors, 0 warningsnpm run build— clean, all expected/<route>/index.htmlfiles emittednpm run dev) — each route returns populated HTML with the correct canonical URLsitemap.xmloutput verified manuallyllms.txtand/.well-known/security.txtreachableCloses #88
🤖 Generated with Claude Code