Skip to content

feat: add public profile Open Graph previews#400

Open
amritbej wants to merge 2 commits into
Dev-Card:mainfrom
amritbej:feature/og-meta-tags
Open

feat: add public profile Open Graph previews#400
amritbej wants to merge 2 commits into
Dev-Card:mainfrom
amritbej:feature/og-meta-tags

Conversation

@amritbej

@amritbej amritbej commented May 29, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Added a cached backend OG image endpoint for public profiles.
  • Generated a server-side PNG preview with avatar/initials, display name, bio, and platform badges.
  • Added canonical, Open Graph, and Twitter Card metadata to the public profile page.
  • Added focused backend tests for OG image generation and Redis cache behavior.

Closes #33

Validation

  • pnpm --filter @devcard/backend exec eslint src/routes/public.ts src/utils/og-image.ts src/__tests__/public.test.ts
  • pnpm --filter @devcard/backend exec vitest run src/__tests__/public.test.ts
  • pnpm --filter @devcard/web check

Note

pnpm --filter @devcard/backend build is currently blocked by existing unrelated TypeScript issues in backend test files.

@amritbej

Copy link
Copy Markdown
Contributor Author

@Harxhit ,
please check the pr i have linked

@Harxhit Harxhit added the gssoc:approved Required label for every approved PR. Gives the base +50 points and enables contribution tracking. label May 30, 2026
@amritbej

amritbej commented Jun 2, 2026

Copy link
Copy Markdown
Contributor Author

@Harxhit , please check the pr i have linked

@ShantKhatri please check it .

@amritbej amritbej force-pushed the feature/og-meta-tags branch from ccae586 to ece9eea Compare June 5, 2026 12:52
@vercel

vercel Bot commented Jun 5, 2026

Copy link
Copy Markdown

Someone is attempting to deploy a commit to the Prashantkumar Khatri's projects Team on Vercel.

A member of the Team first needs to authorize it.

@github-actions

github-actions Bot commented Jun 5, 2026

Copy link
Copy Markdown

CI — Checks Failed

Backend — FAIL

Check Result
Lint FAIL
Test FAIL
Typecheck FAIL

Mobile — SKIP

Check Result
Lint -
Test -

Web — FAIL

Check Result
Check FAIL
Build PASS

Last updated: Fri, 12 Jun 2026 06:25:06 GMT

@amritbej

amritbej commented Jun 5, 2026

Copy link
Copy Markdown
Contributor Author

@ShantKhatri , please review it .

@ShantKhatri

Copy link
Copy Markdown
Collaborator

@ShantKhatri , please review it .

Hi, checks are failing.

@Harxhit

Harxhit commented Jun 5, 2026

Copy link
Copy Markdown
Collaborator

@ShantKhatri , please review it .

Hi, checks are failing.

Please allow me some time.

@amritbej

amritbej commented Jun 9, 2026

Copy link
Copy Markdown
Contributor Author

@Harxhit ,
please check it

Signed-off-by: amritbej.sh <amritbej750@gmail.com>
@amritbej

Copy link
Copy Markdown
Contributor Author

@Harxhit ,
please review it it's been 2 weeks i have opened the pr and tell if anything wrong ..

@Harxhit Harxhit left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: this branch does not build

The headline issue is that the merge of main into this branch (5852175) was resolved by concatenating both sides of the conflict in apps/backend/src/routes/public.ts instead of merging them. The result does not parse, let alone type-check, so none of the validation listed in the PR description can be passing on the current head.

Reproduced locally on 5852175:

# typecheck
src/routes/public.ts(81,7): error TS1005: 'try' expected.
src/routes/public.ts(136,7): error TS1005: 'try' expected.
src/routes/public.ts(182,7): error TS1005: 'try' expected.
src/routes/public.ts(231,4): error TS1128: Declaration or statement expected.

# lint
src/routes/public.ts 81:6 error Parsing error: 'try' expected

# vitest
The symbol "CACHE_CONTROL_HEADER" has already been declared (public.ts:34:6)
Unexpected "catch" (public.ts:81:6)
  -> public.test.ts: 0 tests collected, suite fails to load

The PR note says the build is "blocked by existing unrelated TypeScript issues" — that is not accurate. The failures above are caused by this branch's merge, not by pre-existing problems, and they are syntax errors, not type nits.

Must fix (blocking)

  1. public.ts: redo the merge by hand. Every handler now contains the new code, a duplicated copy of the old code that is unreachable after a return, and a second catch block — that is what produces the 'try' expected errors at lines 81, 136, 182. Duplicated import * as publicService (lines 3 & 9) and duplicated const CACHE_CONTROL_HEADER (lines 21 & 34) also need to collapse to one each.
  2. Get tsc --noEmit, eslint, and vitest green and re-run the commands in the PR description before re-requesting review.

Should fix

  1. The client-side OG meta tags will not be seen by any social scraper. Slack/Twitter/Facebook/Discord/WhatsApp crawlers do not execute JavaScript; they read the initial HTML response. Injecting og:* / twitter:* via a React useEffect (ProfilePage.tsx) means the rich preview this PR is built for never renders for those bots. The backend image endpoint is fine, but the tags pointing at it need to be server-rendered (SSR / prerender / a meta-injecting middleware on the profile route). As written, the feature does not achieve its stated goal.
  2. fetchAvatarBase64 SSRF guard is incomplete and unbounded. Requiring https:// does not prevent SSRF — https://169.254.169.254/..., https://localhost, and https://10.0.0.x are all still fetched server-side. There is also no response-size cap, so a malicious avatar URL can stream an arbitrarily large body into memory via arrayBuffer(). See inline comment.

Nits

  1. The OG route queries app.prisma.user.findUnique directly, while every other handler in this file goes through publicService. Inconsistent; consider moving it behind the service for testability and parity.
  2. buildMetaDescription counts platforms via profile.links.length, but the image counts _count.platformLinks. These can disagree (links shown vs total connected).

The OG image generation itself (og-image.ts SVG template, XML escaping, hex sanitisation, initials fallback, fire-and-forget cache write, the test coverage) is solid work. The blocker is purely the broken merge — fix that and most of this is mergeable.

@@ -1,3 +1,11 @@
import { PLATFORMS } from '@devcard/shared';

import * as publicService from '../services/publicService.js';

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicate import: publicService is imported here from '../services/publicService.js' and again on line 9 from '../services/publicService'. Same for generateQRBuffer/generateQRSvg (lines 6 & 10). This is a redeclaration error — collapse to a single import each (keep the .js extension to match the rest of the file).

const MIN_QR_SIZE = 1;
const MAX_QR_SIZE = 2048;

const CACHE_CONTROL_HEADER = 'public, max-age=300, stale-while-revalidate=60';

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CACHE_CONTROL_HEADER is declared here and again at line 34 with the identical value. esbuild rejects this (The symbol "CACHE_CONTROL_HEADER" has already been declared), which is why the test suite fails to even load. Delete one.

return result.data
} catch (err: unknown) {
app.log.error({ err }, 'Failed to fetch public profile')
return reply.status(500).send({ error: 'Internal server error' })

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Everything from the next line down to the second } catch is leftover from the pre-merge version: it is unreachable (it follows return) and it introduces a second catch on a try that already has one, which is the error TS1005: 'try' expected at line 81. The same broken pattern repeats in the /card/:cardId, /:username/card/:cardId, and /qr-session handlers. Delete the duplicated tail of each try/catch so there is exactly one body and one catch.

async function fetchAvatarBase64(
url: string,
): Promise<{ data: string; mimeType: string } | null> {
if (!url.startsWith('https://')) {return null;}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment calls this an SSRF guard, but startsWith('https://') does not prevent SSRF — https://169.254.169.254/latest/meta-data/, https://localhost, and https://10.0.0.x all pass and get fetched by the server. If avatarUrl is fully validated/normalised at write time this is lower risk, but the comment overstates the protection. Two concrete asks: (a) validate the host against private/link-local ranges (or restrict to an allowlist of avatar CDNs), and (b) cap the download size — there is no Content-Length/byte limit before arrayBuffer(), so a hostile URL can stream an unbounded body into memory.

upsertMetaTag('property', 'og:url', canonicalUrl);
upsertMetaTag('property', 'og:title', title);
upsertMetaTag('property', 'og:description', description);
upsertMetaTag('property', 'og:image', ogImageUrl);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These og:* / twitter:* tags are injected at runtime in a useEffect. Social crawlers (Slack, Twitter, Facebook, Discord, WhatsApp) do not run JS — they parse the initial server HTML — so none of these tags, including this og:image, will be present when a link is unfurled. The feature needs the meta tags server-rendered (SSR/prerender/meta middleware) for the preview to actually appear. As-is, the backend image endpoint is reachable but nothing tells the scrapers about it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backend gssoc:approved Required label for every approved PR. Gives the base +50 points and enables contribution tracking. web

Projects

None yet

Development

Successfully merging this pull request may close these issues.

backend + web: implement permanent shareable public profile page with OG meta tags

3 participants