Give projects and members their own pages

The API has always served a project's and a member's detail, with
`path` and `alternates` in three languages, and the site had nowhere to
show it. A project now has its page — tagline, status, technologies,
description, technical decisions, what was learned, gallery, client,
dates, lead, team, repositories and open calls — and so does a member:
headline, bio, links, technologies and projects. Cards on the landing
link to both.

Addresses follow the API's rule read backwards (core/routes.ts):
pages at the root, records under a section named in their language,
the default language unprefixed. One catch-all route serves them all.
Not a route per shape: Analog registers each loader as an internal
Nitro route, and Nitro's router cannot hold `/:prefix/:slug` beside
`/:prefix/:section/:slug` — the first became unreachable.

The frame around a record (header, footer, lights, 404/503, title,
description, canonical, hreflang) is one component now, PageShell, and
each type only brings its body, all sharing one layout partial.

Also fixes chips that stepped down one row after the first, on the
landing's cards too: @pi/ui's reset gives every `li + li` a top margin,
right for prose and wrong for a row spaced by `gap`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
pigui
2026-09-29 14:57:13 +02:00
parent 9af6b5aff9
commit 439f951f2e
33 changed files with 2001 additions and 745 deletions

View File

@@ -150,11 +150,13 @@ landing/src/
core/copy.ts the chrome's own words, in es / en / ca
core/icons.ts the icon set, inlined and registered as literals
core/pi-strings.ts the strings `@pi/ui` translates on its own
core/content-page.ts the load function both content-page routes share
core/routes.ts which record an address belongs to (resolveRoute)
core/routed-page.ts the load function for every address but the landing
pages/(home).server.ts one load function, eleven parallel calls
pages/(home).page.ts the landing itself
pages/[slug].page.ts a page the panel publishes, default language
pages/[locale]/[slug].page.ts the same, in any other language
pages/[...path].page.ts everything else: pages, projects, members, 404
ui/page-shell.ts header, footer, lights, 404/503 and SEO around a record
ui/routed-page.ts picks the body for the record's type
ui/ presentational components, one per payload type
(thin wrappers over `@pi/ui`), each with its `.scss`
server/
@@ -186,9 +188,13 @@ page component through `injectResponse()` (`@analogjs/router/tokens`), which is
the *render* request's response; the one a page `load` sees belongs to the
internal request Analog resolves loaders through and never reaches the browser.
That is also why the 404 status comes from middleware instead — except for
addresses shaped like a content page, where only the API knows whether the page
exists: the middleware lets those through and `ui/content-page.ts` sets the 404
itself from `ngOnInit`, once its input has arrived.
addresses shaped like a record, where only the API knows whether it exists: the
middleware lets those through and `ui/page-shell.ts` sets the 404 itself from
`ngOnInit`, once its input has arrived.
**`@pi/ui`'s reset spaces every `li + li`** with a top margin — right for a
list of prose, wrong for a row of chips laid out with `gap`, where it dropped
every chip but the first. Lists spaced by `gap` reset it on their own `> li`.
**Empty sections disappear**, along with their entry in the nav — a young site
shows a hero and a contact form rather than eight headings over nothing.
@@ -202,14 +208,29 @@ page cached with `s-maxage` that varied on `Accept-Language` would hand the
second visitor the wrong language anyway. The switcher in the header is three
plain links, and `<html lang>` follows.
**Content pages are the exception**, because the API decides their address: a
page's `path` and `alternates` put the default language unprefixed and every
other one under its code — `/sobre-nosotros`, `/en/about-us`, `/ca/qui-som` —
so those are the routes that answer. `core/api.ts#contentRoute` is the one rule
for that shape; `/es/…` is not one of them, so a page has one address rather
than two. The switcher on a page follows `alternates`, and a language the page
has no translation for leads to the landing in that language instead of a 404.
The same `alternates` become `hreflang` links, with a `canonical`.
**Records are the exception**, because the API decides their address: a
record's `path` and `alternates` put the default language unprefixed and every
other one under its code, pages at the root and everything else under a section
named in that language:
```
/sobre-nosotros /en/about-us /ca/qui-som
/proyectos/catalogo /en/projects/catalogo /ca/projectes/catalogo
/equipo/ada /en/team/ada /ca/equip/ada
```
`core/routes.ts#resolveRoute` is that rule read backwards, and its `SECTIONS`
mirror the API's `config/site.php` (`paths`) — a change there has to be made
here too. `/es/…` is not a shape the API produces, so a record has one address
rather than two. The switcher on a record follows `alternates`, and a language
it has no translation for leads to the landing in that language instead of a
404. The same `alternates` become `hreflang` links, with a `canonical`.
**One catch-all route** (`pages/[...path]`) serves every address but the
landing, and `resolveRoute` sorts the shapes out. Not one route per shape:
Analog registers each loader as an internal Nitro route, and Nitro's router
cannot hold two parameters of different names at the same depth —
`/:prefix/:slug` beside `/:prefix/:section/:slug` made the first unreachable.
Featured pages go in the header after the section anchors; every page is in the
footer.