feat(seo): render social card metadata for public resumes

Public resume pages only produced their OpenGraph and Twitter tags client side,
so a shared link had no card at all. The server now injects them into the shell
and swaps in the resume's own title and description.

The lookup is scoped to public, password-free resumes and deliberately avoids
resumeService.getBySlug: that counts a view and would expose a protected
resume's summary to an unauthenticated crawler. User-authored values are escaped
before they reach the HTML, and any lookup failure falls back to the plain shell.

getResumeSocialMeta is shared with the client route head so the two cannot drift.
This commit is contained in:
Amruth Pillai
2026-08-17 22:19:52 +02:00
parent d0fa9ae8da
commit 9d0dc36706
8 changed files with 277 additions and 12 deletions
+10 -10
View File
@@ -2,6 +2,7 @@ import type { ResumeData } from "@reactive-resume/schema/resume/data";
import type { RouterOutput } from "@/libs/orpc/client";
import { ORPCError } from "@orpc/client";
import { createFileRoute, lazyRouteComponent, notFound, redirect } from "@tanstack/react-router";
import { getResumeSocialMeta } from "@reactive-resume/resume/social-meta";
import { orpc } from "@/libs/orpc/client";
import { createNoindexFollowMeta, createResumeSocialMeta, getCanonicalRootUrl } from "@/libs/seo";
@@ -25,23 +26,22 @@ export const Route = createFileRoute("/$username/$slug")({
return { meta: [{ title: `${name} - Reactive Resume` }, createNoindexFollowMeta()] };
}
const { basics, summary, metadata } = resume.data;
const socialTitle = basics.headline ? `${name}${basics.headline}` : name;
const summaryText = summary.content
.replace(/<[^>]+>/g, " ")
.replace(/\s+/g, " ")
.trim();
const description = summaryText || basics.headline || name;
const social = getResumeSocialMeta(resume.data, resume.name || "Resume");
const base = getCanonicalRootUrl(typeof window === "undefined" ? undefined : window.location.origin);
const canonicalUrl = `${base}${params.username}/${params.slug}`;
const imageUrl = `${base}templates/jpg/${metadata.template}.jpg`;
const imageUrl = `${base}templates/jpg/${social.template}.jpg`;
return {
meta: [
{ title: `${name} - Reactive Resume` },
{ title: `${social.name} - Reactive Resume` },
createNoindexFollowMeta(),
...createResumeSocialMeta({ canonicalUrl, title: socialTitle, description, imageUrl }),
...createResumeSocialMeta({
canonicalUrl,
title: social.title,
description: social.description,
imageUrl,
}),
],
links: [{ rel: "canonical", href: canonicalUrl }],
};