• Blog
  • Docs
  • Pricing
  • We’re hiring!
Log inSign up
foxdark410

foxdark410

linkedin-resume-ui

Presentation layer for JSON Resume data from linkedin-mdp-api
Public
Like
linkedin-resume-ui
Home
Code
2
README.md
H
main.ts
Environment variables
1
Branches
1
Pull requests
Remixes
History
Val Town is a collaborative website to build and scale JavaScript apps.
Deploy APIs, crons, & store data – all from the browser, and deployed in milliseconds.
Sign up now
Code
/
README.md
Code
/
README.md
Search
…
Viewing readonly version of main branch: v27
View latest version
README.md

linkedin-resume-ui

Presentation layer (HTTP val) that fetches JSON Resume data from linkedin-mdp-api and renders it as a styled HTML page in the browser.

Architecture: This val is a pure UI layer. It contains zero LinkedIn/OAuth logic — all data fetching and normalization lives in linkedin-mdp-api.


Live Endpoints

URLDescription
/302 redirect to the route configured in DEFAULT env var
/minimalSerif CSS-only theme (print-friendly)
/tailwindCard-based theme via Tailwind CDN
/jsonresumeServer-side render using a jsonresume.org theme

Query Parameters

ParamApplies toDescription
?photo=1/minimal, /tailwindShow basics.image as a circular avatar (96×96 px)
?theme=<name>/jsonresumejsonresume theme package name (default: even)
?force=1allBypass cache read; always fetch fresh data from upstream API (result is still saved to cache)

Known-working themes for /jsonresume: even, kendall, flat, stackoverflow, paper, elegant and most others from jsonresume.org/themes. Themes are loaded via npm: so Deno unpacks the real package and fs.readFileSync works correctly with __dirname.

Unofficial themes are published to npm under the jsonresume-theme-* prefix beyond the official list. You can use any such package by name: ?theme=macchiato, ?theme=onepage, etc. The input field in the theme selector (planned) will allow arbitrary names for this reason.


Response Headers

HeaderDescription
X-Cache-AgeSeconds since the last successful cache write. -1 means no cache entry exists yet. 0 means data was just fetched fresh.

Environment Variables

Set these in the val's Environment Variables sidebar.

KeyRequiredDefaultDescription
DATA_API_URL—hardcoded linkedin-mdp-api endpointBase URL of the data API (no trailing slash)
DEFAULT—minimalDefault route for / redirect. Can include query string, e.g. tailwind?photo=1

Note: DEFAULT warning in logs ("DEFAULT" is not set) is harmless — the val falls back to minimal automatically.


Caching

Resume data is cached in Val Town Blob Storage for 24 hours.

DetailValue
Blob keylinkedin_resume_cache
Format{ fetchedAt: number (ms), data: JsonResume }
TTL24 hours
?force=1Skips cache read only; result is still written to cache
Stale fallbackIf upstream returns 429 / 5xx / network error and a stale cache entry exists (any age), the stale data is served instead of an error page

Data Flow

Browser → /minimal (or /tailwind, /jsonresume)
           └─ Blob cache (24h TTL)
                └─ fetch DATA_API_URL/jsonresume   [on cache miss or ?force]
                     └─ linkedin-mdp-api
                          └─ LinkedIn DMA API (OAuth)

linkedin-mdp-api applies all data normalization:

  • double-space → \n paragraph trick
  • bullet character detection via BULLETS env var
  • photo extraction from RICH_MEDIA log
  • JSON Resume schema v1.0.0 compliance

linkedin-resume-ui applies presentation-only fixes on top:

  • flatSkills() — expands { name: "Skills", keywords: [...] } into chips
  • dedupDegree() — removes "X, X" duplicated degree strings from API
  • prose() — converts \n\n → <p>, \n → <br>, strips &nbsp;/\u00a0
  • dateRange() — formats ISO dates as YYYY-MM – YYYY-MM or YYYY-MM – Present
  • label normalization — \n-separated role titles → · separator

Error Handling

SituationBehaviour
LinkedIn API returns 429 / 401 / 5xx + stale cache existsServe stale cache silently
LinkedIn API returns 429 / 401 / 5xx + no cacheResponse body passed through as-is with original status code
Network error reaching DATA_API_URL + stale cache existsServe stale cache silently
Network error reaching DATA_API_URL + no cache502 + styled error page with URL shown
API returns non-JSON502 + styled error page
basics.name missing or empty502 + styled error page (guards against blank render)
Unknown route (e.g. /foo)404 + styled error page listing valid routes
jsonresume theme import fails500 + styled error page with hint

Current Version: v18

Changelog

VersionKey changes
v1–v3Initial themes, token passing, basic structure
v4Skills flatting, degree dedup, prose(), label normalization
v5DEFAULT redirect, ?photo=1, jsonresume server-side render via esm.sh, require → ESM fix
v6API error passthrough, basics.name validation, photo object-fit: cover, photo removed from /jsonresume
v7–v12npm: import fix (Deno module cache, __dirname support), theme compatibility
v13–v14Blob cache (24h TTL), stale-while-revalidate skeleton
v15–v18Stale-while-revalidate (serve stale on 429/network error), X-Cache-Age response header, ?force=1

TODO

Active / next

  • /jsonresume photo control — themes render basics.image unconditionally if set. Strip basics.image from payload when ?photo=0 (or when ?photo=1 is absent), mirroring /minimal//tailwind behaviour. Currently ?photo is ignored for /jsonresume.

  • Theme selector UI — a nav bar / dropdown listing available routes and themes. Input field should be editable (free text) to support unofficial jsonresume-theme-* npm packages beyond the official list. Official theme list source: registry.npmjs.org/-/v1/search?text=jsonresume-theme

  • Photo crop position — object-fit: cover always crops to geographic center, which is wrong for portraits. Options:

    • ?photo_x=<0–100>&?photo_y=<0–100> query params → object-position: X% Y%
    • Read EXIF/XMP SubjectArea / RegionArea face metadata from the image (requires a small fetch + parser, e.g. npm:exifr)
    • Default heuristic: object-position: center 20% (faces tend to be in top third of portrait photos)
  • Banner support — linkedin-mdp-api exposes a banner/background image URL in the raw data. Investigate whether any jsonresume themes support a banner field, and whether it's worth adding a banner strip to /minimal//tailwind.

  • Paragraph formatting in jsonresume themes — the JSON Resume spec defines summary as plain text; most npm themes output it as a single <p> block (wall of text). Options:

    • Pre-process summary into HTML before passing to theme render (risky: some themes HTML-escape the value again)
    • Post-process theme HTML output with a regex replacing \n\n inside <p>
    • Contribute a patch upstream or pick themes that handle it well

PDF / DOCX export

Comparison of approaches (priority: minimum code + fast):

ApproachCode effortSpeedQualityNotes
print.css in /minimalMinimal — already has @media print skeletonInstant (browser native)GoodZero server cost; user triggers Ctrl+P. No new route needed. Recommended first step.
Puppeteer /pdf route~10 linesFast (~1s)Excellent (Chrome PDF)Requires npm:puppeteer or Val Town @browserless; cold-start latency
npm:marked /markdown route~5 linesInstantGood (GitHub-style)Fetches /markdown from API, renders to HTML; no new data type needed
Pandoc WASM client-side DOCX~20 lines JSSlow (5–20s WASM load)Good (Pandoc)Client-side only, no server cost; markdown is the source format

Rejected approaches:

  • RenderCV — Python/Pydantic/LaTeX CLI only; no WASM, no serverless path.
  • Pandoc server-side — Pandoc is a Haskell binary; cannot run in Deno/Val Town.
  • OpenResume — React SPA with its own proprietary JSON schema; no public render API. Embedding would require mapping our JSON Resume → their schema (~50 lines) plus iframe/postMessage plumbing, for the same result Puppeteer gives in 10 lines.
  • HackMyResume/FluentCV — abandoned (last commit 2018), broken deps.

Lower priority

  • basics.image proxy — LinkedIn photo URLs may expire or require auth; consider proxying through the val to avoid broken images.
  • <meta og:…> tags for social sharing preview.
  • Dark mode via @media (prefers-color-scheme: dark).
  • ?lang= param to filter multi-language data (dependent on API support).

Alternative Tools & Ecosystem

Tools we evaluated that solve similar problems. Kept here for reference.

ToolFormatRenderWhy not used
JSON Resumejsonresume JSON30+ npm themes, registry renderer✅ In use — best open-source theme ecosystem
RenderCVYAML (Pydantic)LaTeX/Typst → PDF CLICLI only, no WASM, no serverless
OpenResumeProprietary JSONReact SPA → PDFNo public API; own schema incompatible with JSON Resume
Manfred MACMAC JSONgetmanfred.com SaaSNo open-source renderer; format is niche
HackMyResumeJSON Resume / FRESHHTML/PDF/DOCX CLIAbandoned since 2018
EuropassXMLOfficial EU rendererXML-heavy, EU-specific, no npm library
PandocMarkdown (source)DOCX/HTML/PDFNo resume semantics; useful only as Markdown→DOCX converter (WASM, client-side)

Known Issues

429 Too Many Requests from LinkedIn

LinkedIn DMA API has a per-day rate limit per application. Once exhausted, all endpoints return 429 until midnight UTC. The stale cache fallback means the UI continues to serve the last known data silently.

jsonresume theme compatibility

Themes are imported via npm:jsonresume-theme-<name>. Deno unpacks the full package into its module cache, so __dirname and fs.readFileSync work correctly. Most themes from jsonresume.org/themes should work. If a theme has exotic runtime dependencies it may still fail — the error page shows the exact message.

DEFAULT env var warning in logs

Val Town's runtime emits WARNING: "DEFAULT" is not set on every request. This is cosmetic — the val defaults to minimal. Set the env var to silence it.

LinkedIn API data is a snapshot (not real-time)

Profile edits on LinkedIn may take hours or up to a day to appear in API output. This is a LinkedIn DMA API limitation, not a bug in this val.


Related Vals

  • foxdark410/linkedin-mdp-api — data layer: fetches LinkedIn DMA snapshot, exports JSON Resume, MAC, RenderCV, Markdown, plain text.

Development Notes

File structure

linkedin-resume-ui/
└── main.ts   # Single HTTP handler — all themes, routing, helpers in one file

All logic intentionally lives in one file to keep the val self-contained and easy to fork. If the file grows beyond ~1000 lines, consider splitting into:

  • themes/minimal.ts
  • themes/tailwind.ts
  • themes/jsonresume.ts
  • utils.ts (esc, prose, dateRange, flatSkills, dedupDegree)
  • main.ts (router + error handler)

Deno import conventions (Val Town)

  • npm packages: import X from "npm:package-name"
  • Val Town std lib: import { blob } from "https://esm.town/v/std/blob"

Testing without LinkedIn token

When the LinkedIn API limit is hit (429), test locally by temporarily hardcoding a fixture JSON Resume object in main.ts and bypassing the fetch call:

// Temporary fixture for local dev / rate-limit debugging const json = { basics: { name: "Test User", label: "Engineer" }, work: [] };
FeaturesVersion controlCode intelligenceCLIMCP
Use cases
TeamsAI agentsSlackGTM
DocsShowcaseTemplatesNewestTrendingAPI examplesNPM packages
AboutAlternativesPricingBlogNewsletterCareers
We’re hiring!
Brandhi@val.townStatus
X (Twitter)
Discord community
GitHub discussions
YouTube channel
Bluesky
Open Source Pledge
Terms of usePrivacy policyAbuse contact
© 2026 Val Town, Inc.