Skip to content

Add-to-LinkedIn: share QtMesh Creator status & project quality as a LinkedIn certification #924

Description

@fernandotonon

Parent: gamification epic #796 (fits E-P5 "try this next" discovery / status surface) · Companion to public profile (#87/#88)

Add-to-LinkedIn: share QtMesh Creator status & project quality as a LinkedIn certification

What & why

Let users add their QtMesh standing to their LinkedIn profile via LinkedIn's "Add to profile" certification link — the same open mechanism Duolingo, freeCodeCamp, and Credly issuers use. Every shared credential links back to qtmesh.dev, so it doubles as an organic growth loop for the public profile (#87) and the gamification engagement goals (#796).

Scope boundary (important): this is the snapshot certification link, NOT the deep LinkedIn↔Duolingo live-score partnership (that needs a negotiated LinkedIn data-partner deal — out of reach for us). The link pre-fills LinkedIn's "Add licenses & certifications" form; the user clicks Save; a static entry appears on their profile pointing back to their QtMesh page as the verification URL. No LinkedIn OAuth, no API approval, no backend service, no data flows back to us — it is a pure client-side URL. This is deliberately not a "connected app" in the OAuth/MCP sense.

The mechanism (LinkedIn add-to-profile URL)

https://www.linkedin.com/profile/add?startTask=CERTIFICATION_NAME
  &name=<credential name>
  &organizationName=QtMesh Cloud
  &issueYear=<YYYY>&issueMonth=<M>
  &certId=<stable id>
  &certUrl=<public verification URL back to qtmesh.dev>

All params URL-encoded. startTask=CERTIFICATION_NAME opens the certifications form. certUrl is the verification link (must resolve to a public page that visibly shows the credential belongs to that user). No expirationYear (QtMesh status doesn't expire as a credential).

Two surfaces

Both server-rendered in src/index.ts (the Worker serves these pages — no frontend build change):

1. Public profile page — "QtMesh Creator" credential

  • Where: renderProfilePage() (src/index.ts:7859), served at /u/:userSlug (:7471). Add the button in the profile header (.who block, near the /u/<slug> handle) or under "Shareable badges".
  • Credential fields:
    • name = QtMesh Creator — Level <stats.level> (matches the page's own "QtMesh Creator" eyebrow + level badge). If level is 0/absent, fall back to QtMesh Creator.
    • organizationName = QtMesh Cloud
    • certId = the user slug (stable, unique, already public)
    • certUrl = <origin>/u/<slug>
    • issueYear/issueMonth = must be passed in, not computed — Date is fine in the Worker runtime here (unlike the workflow sandbox), but prefer the most recent achievement's earnedAt, else the account's first-seen date, so the "issued" date is meaningful and stable across renders rather than "now" every time. If neither is available, omit issueYear/Month (LinkedIn allows it).
  • Available data in renderProfilePage: user.slug, user.name, stats.level, stats.best_score, achievements[] (each has earnedAt, title, tier), origin. (Verified against the current renderer.)

2. Public project report page — "QtMesh Verified" credential (only when gate passes)

  • Where: the public report route /:userSlug/:projectSlug (src/index.ts:7483), which already gates on public_report_enabled === 1.
  • Only render the button when the latest scan's gate status is pass — a failing gate is not a credential. Pull gate status from the same project/scan data the page already loads.
  • Credential fields:
    • name = QtMesh Verified — <projectDisplay> (or include the score: QtMesh Verified — <projectDisplay> (Score <n>))
    • certId = <ownerSlug>/<projectSlug>
    • certUrl = the public report URL (fullPageUrl, already computed at :7501)
  • If the gate is not passing, render nothing (or a disabled/greyed hint) — never a "verified" cert for a failing project.

Implementation notes

  • New helper linkedInAddCertUrl(opts: { name; organizationName; certId; certUrl; issueYear?; issueMonth? }): string — builds the URL with encodeURIComponent on every value (helpers already used throughout, e.g. :976). Pure function, unit-testable.
  • New helper renderLinkedInButton(url: string): string — returns the anchor markup. Style it to match the existing profile CSS (reuse .stat/badge visual language; LinkedIn brand blue #0A66C2 for the button is acceptable and expected). target="_blank" rel="noopener". Include the LinkedIn "in" glyph inline (SVG, no external asset — the page already inlines all assets/CSS).
  • No new routes, no DB change, no auth. Everything derives from data already loaded for these two pages.
  • Privacy: only render on pages that are already public (profile is public only when profilePublic pref is on — the route already 404s otherwise; project button only when public_report_enabled). The button exposes nothing not already on the public page.
  • Analytics (optional): a lightweight ?utm_source=linkedin_cert on the certUrl so inbound clicks from LinkedIn are attributable, and/or a Sentry/gamification breadcrumb when the button is present. Keep it optional — don't block the feature on analytics.

Tests

  • scripts/test-gamification.mjs (pure logic) or a small new test: linkedInAddCertUrl produces a correctly-encoded URL with all params; special chars in name/slug are escaped; omitting issueYear/Month is valid.
  • scripts/test-gamification-api.mjs (already renders the profile HTML at :98): assert the rendered profile page contains a linkedin.com/profile/add link with the right name/certUrl for a public profile, and that a private profile (404) never emits one.
  • Project report: assert the button appears only when gate=pass; absent when gate=fail or report not public.

Acceptance criteria

  • Public profile page shows an "Add to LinkedIn" button; clicking it opens LinkedIn's add-certification form pre-filled with QtMesh Creator — Level N, org QtMesh Cloud, and certUrl = the profile URL.
  • Passing public project report shows an "Add to LinkedIn" button pre-filled with QtMesh Verified — <project> and the report URL; failing/non-public reports show none.
  • All params correctly URL-encoded (verify with a name containing spaces + a slug with a hyphen).
  • No new route/DB/auth; button only on already-public pages.
  • Tests above pass; npm run test:gamification + typecheck green.
  • Docs: a short "Add your QtMesh status to LinkedIn" note in the profile help / README.

Non-goals

  • The live auto-updating LinkedIn↔Duolingo-style score sync (needs a LinkedIn partnership).
  • Any LinkedIn OAuth / "sign in with LinkedIn" / reading LinkedIn data.
  • Sharing to other networks (X/Facebook "share" buttons) — separate follow-up if wanted.

References

  • src/index.ts: renderProfilePage (:7859), profile route (:7471), public report route (:7483, gate + fullPageUrl at :7500-7501), escapeXml (:1654), URL-encode precedent (:976), badge routes (:7229+).
  • scripts/test-gamification-api.mjs (:88-104 profile render + privacy tests).
  • LinkedIn add-to-profile: https://addtoprofile.linkedin.com/ (developer doc for the profile/add?startTask=CERTIFICATION_NAME URL params).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions