• 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: v35
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/minimal, /tailwind, /jsonresumeShow basics.image as avatar; absent = no photo
?photo_pos=x:y/minimal, /tailwindCrop anchor: both values 0–100, default 50:20 (center-top heuristic for portraits). Either part optional: ?photo_pos=:30 sets only Y.
?theme=<name>/jsonresumejsonresume theme package name (default: even). Any npm:jsonresume-theme-<name> is accepted.
?preprocess=html/jsonresumePre-render Markdown text fields to HTML before passing to the theme. Use for Mustache/Handlebars themes (kendall, flat, etc.) that output unescaped HTML via {{{triple}}}. Do not use with even — it runs its own Markdown renderer.
?force=1allBypass cache read; always fetch fresh data from upstream (result still saved to cache)

Known-working themes for /jsonresume

ThemeMarkdown supportNotes
even (default)✅ nativeRuns micromark on every text field internally
kendallvia ?preprocess=htmlMustache + {{{triple}}} — accepts raw HTML
flatvia ?preprocess=htmlMustache + {{{triple}}} — accepts raw HTML
stackoverflowvia ?preprocess=htmlHandlebars, unescaped output
papervia ?preprocess=htmlHandlebars, unescaped output
elegantvia ?preprocess=htmlHandlebars, unescaped output

Themes with exotic runtime dependencies (native addons, missing peer packages like @resume/core) may fail — the error page shows the exact message.

The unofficial jsonresume-theme-* npm ecosystem contains hundreds of community themes. The input field in the planned theme selector will accept arbitrary names.


Response Headers

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

Environment Variables

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

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


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 entry exists (any age), stale data is served silently

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:

  • Markdown-compatible paragraph/bullet formatting
  • photo extraction from RICH_MEDIA log
  • JSON Resume schema v1.0.0 compliance

linkedin-resume-ui applies presentation-only transformations:

  • md() — renders Markdown to HTML via npm:marked (GFM); used in /minimal and /tailwind
  • preprocessMarkdown() — same, applied to all text fields before passing to /jsonresume theme when ?preprocess=html
  • flatSkills() — expands { name: "Skills", keywords: [...] } into chips
  • dedupDegree() — removes "X, X" duplicated degree strings
  • dateRange() — formats ISO dates as YYYY-MM – YYYY-MM or YYYY-MM – Present
  • label normalization — \n-separated role titles → · separator

Photo Rendering

Photos are rendered as circular avatars (96×96 px) using a container div + inner <img>:

  • Proportional scaling: the <img> fills its container via object-fit: cover. Source aspect ratio is always preserved — no distortion.
  • Crop anchor: object-position is controlled by ?photo_pos=x:y. Default is 50% 20% (center horizontally, slightly above center vertically — a heuristic that works well for LinkedIn-style portrait photos where the face tends to be in the upper third).
  • /jsonresume: basics.image is stripped from the payload when ?photo is absent, because themes render the photo unconditionally if the field is set.

Error Handling

SituationBehaviour
LinkedIn API 429/5xx + stale cacheServe stale cache silently
LinkedIn API 429/5xx + no cachePass through response as-is
Network error + stale cacheServe stale cache silently
Network error + no cache502 + error page with DATA_API_URL hint
API returns non-JSON502 + error page
basics.name missing502 + error page
Unknown route404 + error page listing valid routes
Theme has no render() export500 + error page with list of working alternatives
Theme import/render throws500 + error page with exact error message and Deno hint

Current Version: v11

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
v6API error passthrough, basics.name validation, object-fit: cover
v7–v12npm: import fix (Deno module cache, __dirname support), theme compatibility
v13–v14Blob cache (24h TTL)
v15–v18Stale-while-revalidate, X-Cache-Age header, ?force=1
v19–v20?photo (presence-only), ?photo_pos=x:y, strip basics.image from /jsonresume payload
v11prose() → marked (full GFM Markdown), proportional photo with container div + object-fit, ?preprocess=html for Handlebars themes, contextual error hints

TODO

Active / next

  • 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: npmjs.com search: jsonresume-theme

  • Banner support — linkedin-mdp-api exposes a banner/background image URL in the raw data. JSON Resume schema does not have a standard banner field. Investigate: (a) whether any jsonresume themes support it via a non-standard field; (b) adding a banner strip to /minimal//tailwind.

  • basics.image proxy — LinkedIn photo URLs may expire or require auth; consider proxying through the val to avoid broken images.

PDF / DOCX export

Approaches evaluated (priority: minimum code + fast):

ApproachEffortSpeedQualityNotes
print.css polish for /minimalMinimalInstant (browser Ctrl+P)Good/minimal already has @media print skeleton; needs page-break rules and margin tuning. Recommended first step.
Puppeteer /pdf route~10 lines~1sExcellentRequires npm:puppeteer or Val Town @browserless; cold-start latency
npm:marked + Pandoc WASM client-side DOCX~20 lines JS5–20s (WASM load)GoodClient-side, no server cost; Markdown is the source format

Rejected approaches:

  • RenderCV — Python/Pydantic/LaTeX CLI only; no WASM, no serverless path.
  • Pandoc server-side — Haskell binary; cannot run in Deno/Val Town.
  • OpenResume — React SPA with proprietary JSON schema; no public render API.
  • HackMyResume/FluentCV — abandoned (last commit 2018).

Lower priority

  • <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

ToolFormatRenderStatus
JSON ResumeJSON30+ npm themes✅ In use
RenderCVYAMLLaTeX/Typst → PDF CLICLI only, no serverless
OpenResumeProprietary JSONReact SPA → PDFNo public API
Manfred MACMAC JSONgetmanfred.com SaaSNo open-source renderer
HackMyResumeJSON Resume / FRESHHTML/PDF/DOCX CLIAbandoned since 2018
PandocMarkdownDOCX/HTML/PDFUseful as Markdown→DOCX (WASM, client-side)

Known Issues

429 Too Many Requests from LinkedIn

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

jsonresume theme compatibility

Themes imported via npm:jsonresume-theme-<name> work if they only depend on pure-JS packages. Themes with native addons or missing peer packages (e.g. @resume/core) will fail — the error page shows the exact message.

DEFAULT env var warning in logs

Val Town emits WARNING: "DEFAULT" is not set on every request. Cosmetic only — set the env var to silence it.

LinkedIn API data is a snapshot

Profile edits may take hours or up to a day to appear. LinkedIn DMA API limitation.


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. If the file grows beyond ~1000 lines, consider splitting into:

  • themes/minimal.ts, themes/tailwind.ts, themes/jsonresume.ts
  • utils.ts (md, esc, dateRange, flatSkills, dedupDegree, photoCSS)
  • main.ts (router + cache + 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 by temporarily hardcoding a fixture 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.