Tell search engines and link previews what each page is
- /sitemap.xml, from the API's /slugs plus the landing and listings: every address in every language, with hreflang and x-default. A 503 when the API is down, rather than a sitemap missing half the site. - /robots.txt, naming the sitemap on whatever domain this is served on. - Slugs renamed in the panel now redirect. The API has always kept the old address the moment one changed; middleware/redirects.ts answers it with a 301, from a list kept for a minute and through outages. - og:url, og:image (the record's own picture, the logo otherwise) and schema.org JSON-LD: Organization on the landing; WebPage, CreativeWork, Person or Service elsewhere. Shared through core/head. - The proxy-derived origin moves to core/origin.ts, used by the page shell, the landing and both server routes. In development only /api/* reaches Nitro, so the two files 404 under `npm start`; they were checked against a production build. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,7 @@ import {
|
||||
|
||||
import { copyFor, type Copy } from '../core/copy';
|
||||
import { listingAlternates, listingPath } from '../core/routes';
|
||||
import { memberLinks } from './member-card';
|
||||
import type { Locale } from '../core/api';
|
||||
import { RadarPage } from './radar-page';
|
||||
import { ServiceDetail } from './service-detail';
|
||||
@@ -121,6 +122,13 @@ export function seoFor(
|
||||
path: page.path,
|
||||
alternates: page.alternates ?? {},
|
||||
type: 'article',
|
||||
image: page.featured_image?.url,
|
||||
jsonLd: {
|
||||
'@type': 'WebPage',
|
||||
name: page.title,
|
||||
description: page.excerpt,
|
||||
inLanguage: page.locale,
|
||||
},
|
||||
};
|
||||
}
|
||||
case 'project': {
|
||||
@@ -132,6 +140,26 @@ export function seoFor(
|
||||
path: project.path,
|
||||
alternates: project.alternates ?? {},
|
||||
type: 'article',
|
||||
image: project.cover_image?.url,
|
||||
jsonLd: {
|
||||
'@type': 'CreativeWork',
|
||||
name: project.title,
|
||||
description: project.tagline,
|
||||
inLanguage: project.locale,
|
||||
dateCreated: project.started_at,
|
||||
dateModified: project.updated_at,
|
||||
keywords:
|
||||
(project.technologies ?? [])
|
||||
.map((technology) => technology.name)
|
||||
.join(', ') || undefined,
|
||||
creator: (project.members ?? []).map((member) => ({
|
||||
'@type': 'Person',
|
||||
name: member.name,
|
||||
})),
|
||||
...(project.client
|
||||
? { funder: { '@type': 'Organization', name: project.client.name } }
|
||||
: {}),
|
||||
},
|
||||
};
|
||||
}
|
||||
case 'member': {
|
||||
@@ -143,6 +171,16 @@ export function seoFor(
|
||||
path: member.path,
|
||||
alternates: member.alternates ?? {},
|
||||
type: 'profile',
|
||||
image: member.avatar?.url,
|
||||
jsonLd: {
|
||||
'@type': 'Person',
|
||||
name: member.name,
|
||||
jobTitle: member.headline,
|
||||
description: member.bio,
|
||||
image: member.avatar?.url,
|
||||
sameAs: memberLinks(member).map((link) => link.url),
|
||||
memberOf: { '@type': 'Organization', name: 'Pireon' },
|
||||
},
|
||||
};
|
||||
}
|
||||
case 'service': {
|
||||
@@ -154,6 +192,12 @@ export function seoFor(
|
||||
path: service.path,
|
||||
alternates: service.alternates ?? {},
|
||||
type: 'article',
|
||||
jsonLd: {
|
||||
'@type': 'Service',
|
||||
name: service.title,
|
||||
description: service.tagline,
|
||||
provider: { '@type': 'Organization', name: 'Pireon' },
|
||||
},
|
||||
};
|
||||
}
|
||||
case 'contribute':
|
||||
@@ -163,6 +207,11 @@ export function seoFor(
|
||||
path: listingPath('contribute', locale),
|
||||
alternates: listingAlternates('contribute'),
|
||||
type: 'website',
|
||||
jsonLd: {
|
||||
'@type': 'WebPage',
|
||||
name: copy.contribute.title,
|
||||
description: copy.contribute.lead,
|
||||
},
|
||||
};
|
||||
case 'radar':
|
||||
return {
|
||||
@@ -171,6 +220,11 @@ export function seoFor(
|
||||
path: listingPath('radar', locale),
|
||||
alternates: listingAlternates('radar'),
|
||||
type: 'website',
|
||||
jsonLd: {
|
||||
'@type': 'WebPage',
|
||||
name: copy.sections.radar.title,
|
||||
description: copy.radar.lead,
|
||||
},
|
||||
};
|
||||
case 'call': {
|
||||
const call = found.record;
|
||||
@@ -182,6 +236,12 @@ export function seoFor(
|
||||
path: call.path,
|
||||
alternates: call.alternates ?? {},
|
||||
type: 'article',
|
||||
image: call.project?.cover_image?.url,
|
||||
jsonLd: {
|
||||
'@type': 'WebPage',
|
||||
name: call.title,
|
||||
description: call.description,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user