Skip to content

About

Github hosted page

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

77 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Interactive Resume Template

Live site Pages No build step Runtime dependencies Last commit Repo size

A single-page, interactive resume / portfolio site you can host for free on GitHub Pages — see it live at aaronified.github.io as an example. No build step, no framework, no dependencies to install — just static HTML, a CDN copy of Tailwind CSS, Lucide icons, and a few plain JavaScript data files you edit to make the site your own.

It renders five sections — Overview, Career Trajectory, Projects, FAQ, and Recommendations — plus light/dark theming that remembers the visitor's choice and an Ask-me chat bubble that answers visitors' questions by quoting your own content back at them (no API key, no server, no AI).

The repo currently ships with one person's content as a worked example. To build your own, replace the content in the data/*.js files (and a few spots in index.html) as described below. You never need to touch the rendering logic.


Quick start

  1. Get the files. Fork this repo, or click Use this template / download it.

  2. Preview locally. No tooling required — either open index.html directly in your browser, or serve the folder (so relative image paths resolve exactly like production):

    # from the project root, pick one:
    python -m http.server 8000      # then visit http://localhost:8000
    npx serve .
  3. Edit your content in the data/*.js files (see Customizing your content).

  4. Set your title & SEO in data/personal.js (see Page title, SEO & favicon).

  5. Deploy to GitHub Pages (see Deploying to GitHub Pages).


How the repo is organized

.
├── index.html              # Markup + all rendering logic (edit metadata only)
├── data/                   # ← Your content lives here. Edit these files.
│   ├── personal.js         # Name, title, photo, contact links
│   ├── overview.js         # Bio, quick facts, global footprint, competencies
│   ├── trajectory.js       # Career/education timeline
│   ├── projects.js         # Projects gallery + section config
│   ├── faqs.js             # Frequently asked questions
│   ├── recommendations.js  # Testimonials + references
│   └── chat.js             # Ask-me chat: copy, suggestions, matching thresholds
├── assets/                 # Your photo, company/school logos, avatars
├── robots.txt              # Crawler rules + sitemap pointer (update the domain)
├── sitemap.xml             # Single-URL sitemap (update the domain)
└── README.md               # This guide

Each file in data/ defines one or more global consts (e.g. PERSONAL_DATA). index.html loads them with <script> tags and renders them into the page on load. Editing content = editing a data file. Nothing else is required.


Customizing your content

All content is plain JavaScript objects/arrays. Keep the quotes and commas intact — a stray comma or missing quote will stop the page from rendering. Fields marked (HTML allowed) accept inline HTML such as <br> or <strong> for light formatting.

Inline highlights (markers)

Inside most text fields you can wrap a phrase in a marker to highlight it. There are three types, each styled to match its surroundings — inside a timeline card a highlight picks up that card's brand colour; elsewhere it uses the site accent (indigo in light mode, amber in dark):

  • Significant numbers / achievements → wrap in asterisks — renders as coloured, bold text.
  • Skills & techniques → wrap in carets ^…^ — renders with a subtle highlighter background.
  • Software / tools → wrap in backticks — renders as a small pill badge.

Examples — the raw text you'd type into a data field:

saving *₹50 lakh/month*      →  coloured figure
using ^geofencing^           →  highlighted skill
Introduced `TIBCO Spotfire`  →  software pill

Markers work in the overview bio, competency descriptions, timeline descriptions & highlights, FAQ answers, and recommendation quotes. They don't nest, and any HTML you already use (e.g. <br>) is left untouched. The marker characters (*, ^, backtick) are reserved for this, so avoid them as literal punctuation in those fields. Keep it light — a few highlights per entry reads best; over- highlighting defeats the purpose.

Guidance for dense skill lists (e.g. competencies): pill only standalone software (languages, platforms, apps — Python, Power BI, AWS); put libraries/packages and a couple of signature skills in the highlight (^pandas^, ^NLP^) rather than pilling everything; and group each tool with its own packages/features — e.g. `Python` (^pandas^, ^NumPy^, ^scikit-learn^) rather than listing all tools first and all packages after.

data/personal.js — header & contacts

const PERSONAL_DATA = {
  name: "Your Name",
  title: "Your Headline / Current Role",
  profileImage: "assets/your-photo.jpg",
  contacts: [
    { type: "phone",    label: "+1 555 123 4567",        href: "tel:+15551234567",              icon: "phone" },
    { type: "email",    label: "you@example.com",         href: "mailto:you@example.com",         icon: "mail" },
    { type: "linkedin", label: "linkedin.com/in/you",     href: "https://linkedin.com/in/you",    icon: "contact-round" }
  ]
};
  • icon is any Lucide icon name.
  • type: "linkedin" (or any external link) opens in a new tab; other types open in the same tab.
  • Add or remove contact entries freely.

data/overview.js — bio, quick facts, footprint, competencies

const OVERVIEW_DATA = {
  bio: "A short paragraph about you.",            // (HTML allowed)
  // Free-form facts. "Current Position" and "Experience" are NOT listed here — they're computed
  // automatically (see below). Set emphasis:true to render a fact as a larger, highlighted row.
  quickFacts: [
    { label: "Core Expertise", value: "Analytics and Insights" },
    { label: "Function Setup", value: "Ground Up Analytics" },
    { label: "Role Base",      value: "Hyderabad, India" }
  ],
  competencies: [
    {
      title: "Skill Area",
      icon: "brain-circuit",                       // optional; any Lucide icon name
      description: "What you do in this area."
    }
  ]
};
  • competencies render as a responsive card grid; add as many as you like. icon is any Lucide icon name (omit it for no icon).

Auto-computed fields (derived from TRAJECTORY_DATA — you don't edit these):

  • Current Position — the role whose period ends in "Present" (else the most recent entry), shown as "<role>, <company>". Add your newest role and it updates itself.
  • Experience — the summed duration of all type: "work" roles ("Present" counts up to today). Shown in months up to 24 months, then in whole years. (This is independent of any "X years" you write in the bio, which stays exactly as you type it.)
  • Geographic footprint — countries (with flags) and a city count parsed from each entry's location ("City, Country"). The heading reads "Global Footprint" for more than one country and "Geographic Footprint" for a single country. If there's only one city, the whole footprint widget is hidden.

These three come first in the Quick Facts panel; the free-form quickFacts above follow them.

data/trajectory.js — the career timeline

TRAJECTORY_DATA is an array in reverse-chronological order — the first entry appears at the top of the timeline (most recent) and the last at the bottom.

Standard entry (single role):

{
  id: "unique-slug",                 // must be unique across all entries
  role: "Your Title",
  company: "Company / School",
  location: "City, Country",
  flag: "🇮🇳",                        // emoji flag for the location
  period: "Jan 2024 - Present",
  type: "work",                      // "work" | "education" | "internship" (sets the badge icon)
  colors: { light: "#C62828", dark: "#EF5350" },  // brand accent per theme
  logo: "assets/company-logo.jpg",   // optional; omit for no logo
  link: "https://www.linkedin.com/company/...",   // optional; makes the logo + name clickable
  description: "One or two sentences of context.",
  highlights: [                      // optional; bullet list. Use [] for none.
    "An achievement with a number.",
    "Another achievement."
  ],
  score: "8.18/10"                   // optional; typically for education entries
}

Merged entry (multiple roles at the same organization): set isMerged: true and provide a roles array instead of a top-level description/highlights:

{
  id: "acme",
  role: "Multiple",
  company: "Acme Corp",
  location: "City, Country",
  flag: "🇮🇳",
  period: "Sep 2020 - Dec 2023",
  type: "work",
  colors: { light: "#C20068", dark: "#FF6B9D" },
  logo: "assets/acme-logo.jpg",
  isMerged: true,
  roles: [
    {
      role: "Senior Role",
      period: "Jul 2022 - Dec 2023",
      description: "Context for this role.",
      highlights: ["…", "…"]
    },
    {
      role: "Earlier Role",
      period: "Sep 2020 - Jul 2022",
      description: "Context for this role.",
      highlights: ["…"]
    }
  ]
}
  • type controls the badge icon: work → briefcase, education → graduation cap, internship → award.
  • colors sets the card's accent in light and dark themes — use the org's brand color, or any hex you like.
  • link (optional) turns the logo and company name into a link that opens in a new tab. Convention: use the org's LinkedIn company page for employers and the official website for schools. Omit it and the logo/name simply render as plain, non-clickable text.

data/projects.js — projects gallery

Two exports. PROJECTS_DATA is the list of cards; PROJECTS_CONFIG holds the section-wide switches, so behaviour is a data edit rather than a code edit.

const PROJECTS_DATA = [
  {
    id: "my-tool",                    // unique slug — the PDF export keys its saved choices off this
    name: "My Tool",
    tagline: "One short line under the title",
    featured: true,                   // pins the card to the "hero" group up top
    status: "Active",                 // small badge: "Active" / "Live" / "Alpha" / …
    period: "2026 – Present",
    colors: { light: "#0d9488", dark: "#2dd4bf" },   // brand accent, like trajectory.js
    logo: "…/mark.svg",               // mark beside the title
    logoDark: "…/mark-dark.svg",      // optional dark-theme variant
    icon: "wrench",                   // Lucide icon used as the mark when there's no logo file
    repo: "you/my-tool",              // "owner/name" — source for auto-fetched README badges
    summary: "Paragraph. Markers work here.",
    highlights: ["Bullet. Markers work here too."],
    tech: ["Go", "SQLite"],           // stack pills
    links: [                          // first entry is the primary link (and the one used in the PDF)
      { label: "GitHub", href: "https://github.com/you/my-tool", icon: "folder-git-2" }
    ],
    gallery: "wide",                  // tile preset name from PROJECTS_CONFIG.gallery.tiles
    screenshots: [{ src: "…", alt: "Describe the screenshot" }]
  },
  {
    id: "next-thing", name: "Next Thing", wip: true,   // work-in-progress treatment: no dead links
    status: "Work in Progress", period: "In development",
    note: "Repository & demo coming soon",             // muted chip shown instead of links
    summary: "…", highlights: [], tech: [], links: [], screenshots: []
  }
];

Pulling images straight from GitHub. Any src is just a string, so besides a local assets/… path it can point at a file in one of your repos:

https://raw.githubusercontent.com/<owner>/<repo>/HEAD/<path-in-repo>

GitHub serves those with a real image content-type, Access-Control-Allow-Origin: * and a 5-minute cache, so updating the repo updates this page — nothing to copy or re-sync. HEAD follows the repo's default branch; pin a tag or branch instead if you'd rather the résumé not move when the repo does. A tile whose image fails to load drops itself rather than showing a broken frame, and a logo that fails falls back to the project's icon.

README badges. Give a project a repo and its badges (build status, released version, licence, …) are read from that repo's README at runtime and shown under the title — so they never go stale. Only images from an allow-listed host count, which keeps inline screenshots out and means a README can't inject arbitrary content. Results are cached in localStorage, so repeat visits paint the badges immediately with no layout shift; a repo whose README has no badges simply shows no badge row. Set badges: [{ alt, src, href }] on a project to declare them by hand instead (used as the offline fallback either way).

const PROJECTS_CONFIG = {
  featured: { enabled: true, label: "Hero project", icon: "star" },
  groups: {
    featured: { heading: "Hero projects", sub: "The two I put the most into" },
    rest:     { heading: "Other projects", sub: "" }        // "" or null hides a heading
  },
  badges: {
    enabled: true,
    source: "readme",      // "readme" = parse the repo README | "data" = only use per-project `badges`
    branch: "HEAD",
    headerOnly: true,      // scan only the README header, so inline screenshots stay out
    max: 6, cacheHours: 12, height: 20,
    allowHosts: ["img.shields.io", "github.com", …],        // safety allow-list
    exclude: []            // drop badges whose alt text contains any of these
  },
  gallery: {
    default: "wide",
    tiles: {               // values become CSS custom properties — any valid CSS works
      wide:   { min: "210px", max: "1fr",   aspect: "16 / 10", fit: "cover",   position: "top" },
      poster: { min: "120px", max: "180px", aspect: "2 / 3",   fit: "cover",   position: "center" },
      full:   { min: "210px", max: "1fr",   aspect: "16 / 10", fit: "contain", position: "center" }
    }
  }
};

Featured projects render first, in data order, under the groups.featured heading; everything else follows under groups.rest. If every project is featured — or none is — the split says nothing, so one unheaded list is rendered instead.

data/faqs.js — FAQ accordion

const FAQS_DATA = [
  { question: "A question about your work or philosophy?",
    answer: "Your answer. <br><br> Use <br> for paragraph breaks." }   // (HTML allowed)
];

Answers support the inline markers as well, so you can pick out a figure, a method or a named tool without writing any HTML.

data/recommendations.js — testimonials & references

Three exports:

// 1. Testimonials with full quote text
const RECOMMENDATIONS_DATA = [
  {
    author: "Person Name",
    title: "Their Title",
    relationship: "How they know you (e.g. Reported to me at …)",
    avatarImage: "assets/their-avatar.jpg",
    text: "The full recommendation quote.",
    linkedin: "https://linkedin.com/in/them"
  }
];

// 2. Heading + blurb shown above the References grid
const REFERENCES_INTRO = {
  heading: "Professional References",
  description: "Short intro line.<br/>Can include HTML."               // (HTML allowed)
};

// 3. Reference contacts (name + link, no quote)
const REFERENCES_DATA = [
  {
    name: "Person Name",
    title: "Their Title",
    relationship: "e.g. Direct Manager at …",
    avatarImage: "assets/their-avatar.jpg",
    linkedin: "https://linkedin.com/in/them"
  }
];

data/chat.js — the ask-me chat

A floating Ask about my work bubble sits in the bottom-right corner on every tab. A visitor types a question; it answers by quoting the matching entry from your own data files and links to the section it came from.

There is no AI here, and that is the point. No API key, no server, no network call — a key cannot live in a public static page, and a chatbot that invents your career is worse than none. It builds a small search index over data/*.js in the browser, ranks entries with BM25, and if nothing scores well enough it says "I don't have anything on that" and offers suggestions. It can only ever say things you have written on the page.

const CHAT_CONFIG = {
  enabled: true,                                  // false removes the widget entirely
  launcher: { label: "Ask about my work", icon: "message-circle" },
  title: "Ask about my work",
  greeting: "Ask me anything about my work…",     // first message in the transcript
  disclaimer: "Not an AI — I search this résumé and quote it back.",
  suggestions: ["What do you do now?", "Tell me about Tippani"],   // starter chips
  noMatch: "I don't have anything on that…",
  followups: {                       // the three "go deeper" chips under every answer
    enabled: true, count: 3, label: "Go deeper",
    padWithSuggestions: true,        // top up from `suggestions` when the résumé runs out of links
    templates: { more: "What else did you deliver at {company}?", … }
  },
  scoring: {
    minScore: 1.2,        // ↑ = answers less often, says "I don't know" more
    coverage: 0.45,       // cover this share of the question → answered straight…
    strongMatch: 0.65,    // …below that, this much matched information → answered under a hedge
    relCutoff: 0.45,      // how close a runner-up must score to be quoted too
    maxResults: 3,        // entries quoted per answer
    maxBullets: 3,        // highlight bullets quoted per entry
    k1: 1.2, b: 0.6       // BM25 internals — leave alone unless you know them
  },
  aliases: [["machine learning", "ml"]],   // [what visitors type, what your résumé calls it]
  stopwords: ["the", "and", "know", "best", …]   // words ignored when matching
};

What it answers without searching. A few questions are handled by explicit rules so they can never drift from your data: greetings, "who are you", years of experience (computed from TRAJECTORY_DATA, never hard-coded), current role, base location, contact links, and "list your projects". Off-résumé personal questions (salary, age, hobbies) get a straight decline.

Follow-up suggestions. Every answer ends with three chips that go deeper into what was just quoted — the rest of that entry's bullets, your other role at that employer, someone who worked with you there, the job either side of it, a skill, a sibling project, a related FAQ. They are built from your own data, so a visitor can walk the whole résumé without typing.

The rule that keeps them honest: every chip is checked against the index before it is offered, and dropped unless asking it really returns the entry it promises. A project with no highlights, a skill that appears nowhere else, a starter question pointing at content you deleted — all silently drop out. A chip can never lead to "I don't have anything on that".

The three chips come from three different generators where possible, so they point in three directions (deeper / sideways / across) rather than three shades of the same thing. "What else did you deliver at X?" is special: re-asking would return the same bullets, so that chip renders the ones you haven't seen yet, and retires when the entry is exhausted. Hedged answers and declines fall back to the starter suggestions instead — there is nothing to go deeper into.

Reword any of it in followups.templates; {company}, {role}, {skill}, {project} and {person} are filled from your data, and a template set to "" turns that generator off. Set followups.enabled: false to go back to plain starter chips everywhere.

Slurs and abuse. A public box that echoes what people type needs a guard. Anything matching the abuse patterns is refused before matching runs, and the message is replaced by moderation.hiddenLabel in the transcript rather than echoed — a slur typed into it never gets rendered on the page. Matching happens after case, accents, leetspeak (n1gg3r), censor characters (f*ck) and stretched letters are folded together, and word boundaries are kept so ordinary words are safe (the Scunthorpe problem). Add patterns for your own context with moderation.extraPatterns: ["..."]; set moderation.enabled: false to switch the guard off.

Three answers, not two. Every question lands in one of three places:

The quoted entries… Reply
cover coverage of the question answered straight — "From my time at Allcargo Gati:"
cover less, but carry strongMatch worth of matched content answered under partialLeadIn — "I don't have a direct answer for that. The closest thing on my résumé:"
match nothing worth showing noMatch + suggestion chips

The middle tier exists because no threshold can separate "tell me about a challenge you faced" from "is Tippani tasty?" — to any word-counting measure they are identical (one rare word matched, one unknown word left over). Declining loses the first; answering plainly overclaims the second; showing the entry under a hedge is honest for both. strongMatch is measured in "words your résumé never uses", so it keeps its meaning whatever you write.

Tuning it.

  • Hedges questions it should answer straight → lower coverage.
  • Declines things it should at least show → lower strongMatch.
  • Visitors use a word you don't (they type "machine learning", you wrote "ML") → add an aliases pair rather than rewording your content.
  • A word that is noise in questions but happens to appear in your content ("current", "explain", "favourite", "know", "best") belongs in stopwords — that is the single most effective knob.

Nothing is stored: no transcript in localStorage, no analytics, no requests. Closing the tab ends the conversation.

Images (assets/)

Put your photo, company/school logos, and recommender avatars in assets/ and reference them by relative path (e.g. "assets/my-photo.jpg"). Square images work best for the profile photo, logos, and avatars. Filenames are case-sensitive on GitHub Pages — match them exactly.


Page title, SEO & favicon

You don't need to touch index.html for any of this — it's all data-driven. applySeo() reads the data files on load and writes every search-related tag into the document.

What you configure

In data/personal.js:

seo: {
  title:       "Your Name — What You Do | Where",  // browser tab + Google's blue link
  description: "…",                                // the grey snippet under the link (~155 chars)
  keywords:    "Your Name, Your Name job, …",       // lead with the name variants people type
  siteUrl:     "https://yourname.github.io",        // canonical origin — no trailing slash
  ogImage:     "assets/your-photo.jpg"              // link-preview thumbnail
},
schema: {
  givenName: "Your", familyName: "Name",            // optional; falls back to splitting `name`
  addressLocality: "Your City", addressCountry: "IN"
}

Then update the two site-level files at the repo root, which can't be generated at runtime:

  • robots.txt — change the Sitemap: line to your domain.
  • sitemap.xml — change <loc> to your domain and refresh <lastmod> when you edit the resume.

What gets generated for you

Everything below is derived — you never write it twice:

Tag Source
<title>, description, keywords, author PERSONAL_DATA.seo / .name
<link rel="canonical"> seo.siteUrl (falls back to the serving origin)
Open Graph + Twitter card seo + profileImage — controls LinkedIn/Slack/WhatsApp link previews
schema.org Person JSON-LD job title & employer from the newest type: "work" entry in trajectory.js; alumniOf from the type: "education" entries; knowsAbout from the competency skills lists; sameAs from every https:// contact link
rel="me" on profile links any contact whose href is an external URL

The Person record is the part that matters most for a search on your name: it declares that this page is you, and sameAs + rel="me" fold your LinkedIn and GitHub profiles into the same identity so they reinforce each other instead of competing for the same query.

  • Favicon — auto-generated: a monogram of your initials (derived from PERSONAL_DATA.name) on a gradient tile. Nothing to configure.
  • Footer copyright name — filled automatically from PERSONAL_DATA.name.

One caveat worth understanding

The whole page — headings, timeline, and the SEO tags above — is rendered by JavaScript, because index.html is deliberately kept free of profile content. Crawlers that execute JavaScript (Googlebot, Bingbot) index it correctly; crawlers that don't will see an empty shell. In practice that covers the search engines people actually use for name lookups, but it does mean link previews on some chat apps and scrapes by simpler bots may come up blank. Making those work would require either hard-coding your details into index.html or adding a build step that pre-renders it — a deliberate trade against keeping the template profile-agnostic.

After you deploy

Two manual steps that meaningfully speed up a name search ranking:

  1. Add the site to Google Search Console and Bing Webmaster Tools, verify ownership, and submit sitemap.xml. Use URL Inspection → Request Indexing for the first crawl.
  2. Link to the site from your LinkedIn profile and GitHub profile. Those are high-authority pages that already rank for your name, and the inbound links are what tie the sameAs claims together from both directions.

Hosting on GitHub Pages

The site is fully static (no build step), so GitHub serves the files as-is and hosting is free.

Get a clean root URL with no folder — the username.github.io trick

GitHub Pages has two kinds of sites, and the repository name decides the URL:

Repo name Site type Published URL
your-username.github.io (exactly your username) User/organization site https://your-username.github.io/ — root, no folder
anything else, e.g. resume Project site https://your-username.github.io/resume/ — served from a /resume/ subfolder

So to get a bare https://your-username.github.io/ link with no subfolder, the repository must be named exactly your-username.github.io (all lowercase, matching your GitHub username). That is why this repo is named aaronified.github.io → it serves at https://aaronified.github.io/. Because the app derives its own live URL from window.location and uses relative asset paths (assets/…, data/…), it works at either kind of URL without edits — but the root URL is cleanest for a résumé.

Step-by-step

  1. Create the repo. On GitHub, create a new public repository named exactly your-username.github.io (replace your-username with your real username, lowercase).

  2. Push the files. Point this project at that repo and push to main (or upload the files through Add file → Upload files in the GitHub web UI):

    git remote add origin git@github.com:your-username/your-username.github.io.git
    git branch -M main
    git push -u origin main
  3. Enable Pages. In the repo: Settings → Pages → Build and deployment. Set Source to Deploy from a branch, choose branch main and folder / (root), then Save.

  4. Wait for the first deploy (~1 minute; a green check appears on the commit). Visit https://your-username.github.io/. HTTPS is on automatically.

  5. Update anytime by pushing to main (or editing files in the web UI) — Pages redeploys within a minute. A hard refresh (Ctrl/Cmd-Shift-R) clears any cached copy.

A few notes:

  • Only one user/organization site per account (the single username.github.io repo). Everything else is a project site under a /subfolder/.
  • Custom domain (optional): Settings → Pages → Custom domain, add e.g. resume.example.com, create the matching DNS record at your registrar, and tick Enforce HTTPS. GitHub writes a CNAME file to the repo.
  • No Jekyll processing is needed here; if you ever see build quirks, add an empty .nojekyll file at the repo root to disable Jekyll entirely.

Downloadable PDF Resume

The Save PDF button (under the profile photo, and in the sticky bar) opens the export screen: a section rail, the settings, and a live preview of the document itself. Three ways out:

Button What it does
Save PDF Builds the PDF in your browser and downloads it, correctly named. Text stays selectable, links stay clickable, fonts are embedded. About 132 KB for the three pages this résumé makes at the default selection, no dialog.
DOCX A real Word file, written without a library — about 85 KB, set in Calibri rather than the PDF's embedded Noto Sans, because a .docx that named a font the reader does not have would fall back unpredictably. Deliberately single-column — an ATS reads multi-column layouts in the wrong order, so this is the one to upload to a form.
Print… Your browser's print dialog, if you want paper or its own PDF writer.

All three render the same curated layout built from the same data, not the on-screen page:

  • Hero — photo left; name, tagline, contacts (incl. the live web URL from PERSONAL_DATA.website or the auto-detected host), and computed Experience · Base on the right.
  • Summary, then Core Competencies flattened to paragraphs.
  • Career Trajectory — an invisible-border table where each role is its own block (Company, City | Role | Period + a detail row with the description and achievement bullets), separated by rules. Making each role a block lets a multi-role employer flow across a page break instead of leaving a big gap.
  • Projects — name, summary, achievement bullets, tech line and the primary link, in the order you set on the selection screen.
  • Education — degrees, with any internships nested under the college attended at that time (matched automatically by date).
  • Links are real links. Contact lines, employer names and project URLs become clickable annotations in the PDF and hyperlink relationships in the DOCX. (If you use Print… instead, every browser's own Save as PDF keeps them, but Windows' Microsoft Print to PDF is a printer driver and flattens everything — pick the browser's option, not that one.)
  • The PDF is the preview, transcribed. The generator does not lay the document out a second time: the browser has already done that in the preview, so the exporter walks those sheets and writes each line where the browser put it. Two layout engines would drift; one cannot.
  • Fonts are embedded from assets/fonts/ — the complete Noto Sans, regular and bold, the same files the preview renders with. One global font, not a subset, so no character can change shape between the screen and the file; that is why ₹ and accented characters survive. Anything even Noto Sans cannot draw (CJK, emoji) is reported after the save rather than dropped in silence — see assets/fonts/README.md.
  • Colours: body text is dark grey, headings are black, and all inline highlights render as bold blue (no pills/backgrounds).
  • FAQ and Recommendations are omitted.

The export screen

Three columns. The rail on the left lists every section with its count and jumps you to it. The middle column holds the settings. The live preview on the right renders the actual document — not an impression of it: the print stylesheet lives in a shared .pr-doc scope used by both the preview and the printed page, and a test asserts the two compute identical styles and identical geometry — every selector the stylesheet defines, across both paper sizes and all three densities — so drift in the styling or the page box gets caught rather than shipped. Dashed guides mark where each page would end, and the header reads e.g. "3 pages (last ~40%) · A4 · normal" — the fill of the last page being what tells you whether one more trim saves a page. Page breaks are the browser's to make, so treat the guides as close but indicative. Below 1024px the preview hides and below 768px the rail does; the settings remain.

Everything below tailors the PDF only — the on-screen page never changes — and your choices are saved to localStorage (pdfExportConfig), so they persist across sessions rather than resetting per export. If you later edit data/*.js, saved choices are reconciled against the new data (added items appear, removed ones drop, out-of-range picks reset) and a short notice lists what changed.

Each section is laid out as two columns: a left column with the section name, a count, and its universal ("All items") toggles — which stays pinned as you scroll that section, then gives way to the next section — and a right column with the individual items. Everything is fully per-item customisable — the universal toggles are simply bulk switches that flip all of a section's child toggles at once (e.g. hide every logo, or every reference photo). Per-item toggles are packed into compact rows to keep scrolling short.

  • Page — paper size (A4 or US Letter) and density (Compact / Normal / Roomy). Density moves text size, leading, block spacing and page margins together, so it genuinely changes how much fits: on this résumé, compact is ~2.0 pages where normal is ~2.4 and roomy ~3.0.
  • Contact Details — universal: profile photo. Per line (reorderable via ▲/▼ arrows or drag): include phone, GitHub, email, LinkedIn, and the derived live web URL.
  • Summary — include toggle plus an editor to rewrite the opening paragraph for this export.
  • Core Competencies — reorderable pills (arrows or drag); include toggle; the pencil expands an editor to rewrite the text with the highlight markers (`software` → pill, ^skill^ → highlight, *number* → key figure). A one-line help strip above the field shows each marker's rendered result, and a live preview sits below it.
  • Work Experience and Education & Internships — universal: Logos, Descriptions (+ Scores for education). Per entry: include, compact Description / Logo / (Score) toggles, and an achievement count (− / +) with a Choose which to hide pick-list (default: drop from the last backward). Education descriptions are off by default (they duplicate the degree title) but can be switched on.
  • Projects — reorderable (▲/▼ arrows or drag) so they print in the order you want. Universal: Descriptions. Per project: include, Description toggle, and the same achievement count (− / +) with a Choose which to hide pick-list. Projects flagged wip: true are off by default.
  • References — off by default. Universal: Photos, Titles, Relationships, Contacts. Merges your recommenders and professional references (RECOMMENDATIONS_DATA + REFERENCES_DATA) into one combined list — the recommendation quote text is never included. Per person (reorderable): include, show/edit title, show/edit relationship, show photo, show contact info (LinkedIn + editable email + editable mobile).

Use Reset to defaults to clear all choices — it highlights whenever your current selection differs from the defaults, so you can always tell (and undo) at a glance. Quality depends on the browser's print engine, so preview before sharing.

Browser furniture: the running header/footer on each page (URL, date, page number) is the browser's own, toggled by the Headers and footers checkbox in the print dialog. We deliberately don't add a custom CSS footer (it would clash with the browser's). Automatic page numbers can't be generated by CSS in browser print-to-PDF — leave Headers and footers on if you want them, or use a print library such as paged.js.


Notes & tips

  • No build / no install. Tailwind and Lucide load from a CDN, so there's no npm install. The trade-off is a small runtime cost and a dependency on those CDNs being reachable.
  • Theming. Light/dark is toggled by the button in the nav and saved to localStorage; it defaults to the visitor's system preference. Per-entry accent colors come from each timeline entry's colors field.
  • Validate your JSON-ish data. After editing a data/*.js file, a quick node --check data/that-file.js catches syntax mistakes (missing comma/quote) before you deploy.
  • Encoding. Files are UTF-8. Keep emoji flags and symbols as real UTF-8 characters; avoid editors/tools that re-save as a different code page.
  • Accessibility. The template includes a skip-to-content link, focus rings, ARIA attributes on the accordions, and honors prefers-reduced-motion. Try to preserve these if you customize the markup.

About

Github hosted page

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages