Skip to content

fix(seo): correct event status, poi image and breadcrumb urls - #470

Merged
TheMeinerLP merged 12 commits into
mainfrom
fix/seo-structured-data
Oct 10, 2026
Merged

TheMeinerLP merged 12 commits into
mainfrom
fix/seo-structured-data

Conversation

@TheMeinerLP

@TheMeinerLP TheMeinerLP commented Oct 10, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Three corrections to the schema.org and canonical URLs emitted by the event detail, community POI and team pages. Each is a fix for output that was wrong on the live site; the additions from the same review are in feat/seo-structured-data.

Changes

Four commits, one per defect:

  1. Event status (f99f9dd): the event detail page emitted eventStatus: EventScheduled for every phase, so a past event still claimed to be upcoming. schema.org has no "completed" status, so a past event now carries no eventStatus. Announced, running and hidden (unlisted, not yet started) events keep EventScheduled. The mapping is layers/events/utils/eventSchemaStatus.ts and takes the phase the server already decided in useEventDetail, so no clock is read on the page.
  2. POI image (9990726): the POI CreativeWork took the raw frontmatter thumbnail (/images/...) as a relative URL. In production those images are served through img.onelitefeather.net, so the path on onelitefeather.net returns 404. usePageSeo now returns the socialImage it already computes for og:image, and the POI page uses that URL, so both tags share one resolution.
  3. Trailing slash in index URLs (5a32292): the POI breadcrumb pointed at /de/community-poi/, which 301s to the canonical /de/community-poi. The POI back link and the team fallback URLs (ItemList on the team index, Person on a member without a slug) had the same slash and are corrected too. tests/seo/breadcrumb-urls.spec.ts now checks every locale-prefixed template literal ending in /, not only the locale root.
  4. Event image (79225f4): the event detail Event.image had the same defect as the POI image (new URL(event.value.thumbnail, site.url) on the raw /images path). It now uses the socialImage from usePageSeo, the same URL as og:image, and is omitted when there is no thumbnail.

Tests

  • New specs: tests/events/event-schema-status.spec.ts, tests/seo/poi-jsonld-image.spec.ts, tests/seo/event-jsonld-image.spec.ts; extended tests/seo/breadcrumb-urls.spec.ts.
  • pnpm test: 156 files, 1236 tests passed on this branch.
  • pnpm quality: no regression (ESLint errors 186, warnings 17, TypeScript errors 3, same as baseline).
  • pnpm build: passes.

The rendered JSON-LD and the live redirects were not re-checked in this PR; the findings come from the review of the live site, and the changes are covered by the unit specs above.

https://claude.ai/code/session_019zws4ceJu2jctk3XT48fMX

The event detail page declared eventStatus EventScheduled for every phase,
so a past event still claimed to be upcoming. schema.org has no "completed"
status, so a past event now carries no eventStatus instead of a false one.
Announced, running and hidden (unlisted, not yet started) events keep
EventScheduled, which stays correct for them.

The mapping lives in layers/events/utils/eventSchemaStatus.ts and takes the
phase the server already decided in useEventDetail, so no clock is read here
(see tests/events/phase-on-server.spec.ts).

Test: pnpm exec vitest run tests/events/event-schema-status.spec.ts

Claude-Session: https://claude.ai/code/session_019zws4ceJu2jctk3XT48fMX
The community POI breadcrumb pointed its ancestor at /de/community-poi/,
which 301s to the canonical /de/community-poi. Breadcrumb items must name
the canonical URL, the same way the locale root was already fixed.

The same trailing slash was in the POI back link and in the team fallback
URLs (ItemList on the team index, Person on a member without a slug), so
they are corrected too. breadcrumb-urls.spec.ts now covers every
locale-prefixed template literal ending in a slash, not only the locale
root, so the next index page cannot drift back.

Test: pnpm exec vitest run tests/seo/breadcrumb-urls.spec.ts

Claude-Session: https://claude.ai/code/session_019zws4ceJu2jctk3XT48fMX
The POI CreativeWork took the raw frontmatter thumbnail (/images/...) as its
image and left it relative. In production those images resolve through
img.onelitefeather.net, so the path on onelitefeather.net returns 404 and the
structured data pointed at a missing file.

usePageSeo now returns the socialImage it already computes for og:image, and
the POI page uses that URL for the CreativeWork image. Both tags therefore
share one resolution path and one absolute, transformed URL.

Test: pnpm exec vitest run tests/seo/poi-jsonld-image.spec.ts

Claude-Session: https://claude.ai/code/session_019zws4ceJu2jctk3XT48fMX
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Oct 10, 2026 •

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
launchpad 1e1c6d1 Commit Preview URL

Branch Preview URL
Oct 10 2026, 09:55 PM

TheMeinerLP and others added 9 commits October 10, 2026 23:48
The emitter table, module versions and the sitemap version tell no
longer matched main, and violations fixed since were still listed as
live. The open zeroRuntime gap stays documented.

Claude-Session: https://claude.ai/code/session_019zws4ceJu2jctk3XT48fMX
The embedded BlueMap started loading at page load despite
loading="lazy": on the Yggdrasil page that was 284 requests and about
70 MB on mobile. A placeholder button now mounts the iframe only when
asked for.

Claude-Session: https://claude.ai/code/session_019zws4ceJu2jctk3XT48fMX
…468)

nuxt-posthog starts posthog-js on every page. Before this change that
loaded surveys.js (35 KB, ~85 % unused), dead-clicks-autocapture.js
(629 ms script evaluation on /en/projects/falco, TBT 1.6 s there) and
posthog-recorder.js (69 KB, ~37 KB unused) during startup.

Nothing in the repository uses surveys, dead-click events or session
replay (no references in layers/, pages/, docs/ or openspec/), so:

- disable_surveys: true
- capture_dead_clicks: false (client value overrides the server-side
  setting that enables it)
- disable_session_recording: true at init; the recorder is started from
  layers/base/plugins/posthog-recording.client.ts after idle time
  (requestIdleCallback, 4 s timeout, 2 s timer fallback)

Left unchanged on purpose:
- posthog.init stays synchronous. The consent spec requires capture from
  page start and the stored opt-out applied before any capture; deferring
  init would replace nuxt-posthog's client plugin and reorder that.
- Feature flags still bootstrap from the SSR state, and pageview capture
  is still nuxt-posthog's router.afterEach hook.
- startSessionRecording() still honours opt-out, sampling and linked flags.

Tests: tests/base/defer-until-idle.spec.ts and
tests/analytics/posthog-config.spec.ts (both red before the change).

Claude-Session: https://claude.ai/code/session_019zws4ceJu2jctk3XT48fMX
The image transforms for og:image and the Article image variants set
width and height but no fit, so Cloudflare scaled them down and served
630x630 where 1200x630 is declared. fit=cover crops to the size the
tags announce.

Claude-Session: https://claude.ai/code/session_019zws4ceJu2jctk3XT48fMX
* feat(seo): describe projects as richer SoftwareApplication

The project detail page emitted a SoftwareApplication without the fields
search engines use to relate a project to its code and runtime. Add them
from the existing frontmatter:

- codeRepository from links.source
- applicationCategory DeveloperApplication
- runtimePlatform from platforms. Paper, Folia and Minestom are server
  runtimes the plugin runs inside, not operating systems, so
  operatingSystem would be the wrong property.
- license as an spdx.org URL for known SPDX ids. schema.org expects a URL
  here; free text such as "Proprietary" is omitted instead of guessed at.

programmingLanguage is deliberately not emitted: the frontmatter does not
state it.

The node is built by layers/projects/utils/softwareApplication.ts so the
mapping is unit-tested without rendering the page.

Test: pnpm exec vitest run tests/projects tests/seo

Claude-Session: https://claude.ai/code/session_019zws4ceJu2jctk3XT48fMX

* feat(seo): link POI build to its Place node

When a POI has coordinates the page emits a Place, but the CreativeWork
describing the build did not reference it, so the Place was an unconnected
node in the graph. The CreativeWork now sets contentLocation to the Place's
@id, and only when that Place is emitted.

Test: pnpm exec vitest run tests/seo/poi-place-link.spec.ts

Claude-Session: https://claude.ai/code/session_019zws4ceJu2jctk3XT48fMX
The home page was titled "Home", project titles lacked the terms people
search for, and several blog titles and descriptions ran far past what
Google shows. Titles now name the German-speaking Minecraft server and
the Paper and Minestom projects, and descriptions fit a search snippet.

Claude-Session: https://claude.ai/code/session_019zws4ceJu2jctk3XT48fMX
Event.image resolved the raw /images path against the site origin,
which 404s; it now uses the same image URL as og:image.

Claude-Session: https://claude.ai/code/session_019zws4ceJu2jctk3XT48fMX
* fix(about): state 2019 as the founding year everywhere

The site contradicted itself. The FAQ (de and en) and public/llms.txt said
2019, while the About texts (de, en) and the Organization identity in
nuxt.config.ts said 2021. The Organization JSON-LD served on every page
carried foundingDate "2021", so search engines received the wrong year.

The owner confirmed 2019. Changed:
- nuxt.config.ts schemaOrg.identity.foundingDate: '2021' -> '2019-09-01'
  (the full date the identity used before 268ee96)
- content/about/de/home.json: "2021 als ... gegründet" -> "2019"
- content/about/en/home.json: "Founded in 2021" -> "Founded in 2019"

tests/about/founding-year.spec.ts asserts that every source names 2019.

Claude-Session: https://claude.ai/code/session_019zws4ceJu2jctk3XT48fMX

* fix(team): give team profiles a real meta description

The profile page used the member's slogan as meta description when the
member had no bio. blndr2 has only the slogan "Modrinth.", so the live page
ships <meta name="description" content="Modrinth."> on
https://onelitefeather.net/de/team/blndr2.

New rule, applied to the page description, the Person JSON-LD and the OG
card:
- the bio, when there is one
- otherwise the slogan, only if it is a sentence of at least 50 characters
  ending in . ! or ?
- otherwise a sentence built from the role (or the rank label when there is
  no role), via team.profile_description_in_area / _plain in the i18n files

The title appends the role when the member has one ("joltras – Entwicklung").

Claude-Session: https://claude.ai/code/session_019zws4ceJu2jctk3XT48fMX

* fix(team): word the generated profile description naturally

Claude-Session: https://claude.ai/code/session_019zws4ceJu2jctk3XT48fMX
Keep both the absolute POI image from this branch and the Place link
from #469.

Claude-Session: https://claude.ai/code/session_019zws4ceJu2jctk3XT48fMX
@TheMeinerLP
TheMeinerLP merged commit aab9f8f into main Oct 10, 2026
8 checks passed
@TheMeinerLP
TheMeinerLP deleted the fix/seo-structured-data branch October 10, 2026 21:56
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