Blog
I wrote my resume with MDX and React

I wrote my resume with MDX and React

I write my resume in MDX, render it in PDF, and publish the PDF on my portfolio.

GGia Bảo•5 min read

My resume is an MDX file. When I add a project or rewrite a bullet, the site's build generates an updated PDF.

Since I already write my blog posts in MDX, it only makes sense to write my resume the same way. I can edit the text in the same editor and track the changes in Git.

My portfolio already uses Next.js, so pdfcn was a good fit for building the PDF layout with React. I can adjust the resume's font sizes and spacing through its theme. You can read the resulting PDF on my portfolio or download the same document.

Huge shout out to Aniket Pawar for creating such an amazing piece of software!

Why I embedded a PDF in my portfolio

When I submit my resume to a recruiter, I send a PDF. I wanted that document to be readable on my portfolio too.

The resume page lets you read the document before you download it. You can zoom in, follow its links, and download the PDF when you want a copy. The page keeps the document within the site's navigation and design.

That gives me one PDF layout to maintain. If I change the spacing or page breaks, the document on the page and the downloaded file change together.

The content lives in MDX

The source is apps/web/docs/resume/resume.mdx, alongside my blog content in the same repository. Its frontmatter holds my name, location, and contact links. The body holds the resume sections and bullet points.

Here is a shortened excerpt:

apps/web/docs/resume/resume.mdx
## Education
 
--
title="University of Information Technology, VNUHCM"
subtitle="Bachelor of Information Systems (Advanced Program)"
location="Ho Chi Minh City, Vietnam"
--
 
- **Language proficiency:** IELTS 7.0 (British Council)
- **Coursework:** Data Structures, Algorithms, Databases, Systems Design, Web Frameworks, Software Engineering

The -- blocks are a small syntax I added for resume entries. My parser reads attributes such as the entry's title, date, and location before MDX compilation. Those blocks are specific to this project.

I can rewrite a bullet or add a project in the same editor I use for blog posts. Git records the changes, so I can inspect a diff of the wording.

And if I'm feeling too lazy to rewrite a bullet myself, I can ask my coding agent to edit the Markdown. I can review that change in the diff and inspect the generated PDF afterward.

Making Markdown look like a resume

Resume PDF showcase

The Education block above puts the university name on one line, with the degree and location below. The full resume also includes a date, which sits to the right of the university name.

A React component defines the layout of one part of the document. I have one for section headings and another for the title, date, and details of an entry. Every education or project entry uses that layout, so a change to it applies throughout the resume.

I started with pdfcn's components and customized them for this document. The PDF uses a serif font, uppercase section headings, and a divider under each section. The entry's bullet points sit below its title and details.

The theme sets font sizes, page margins, and the gaps between sections. The entry component controls alignment. If I want dates on the left or more space below a project title, I can change the layout in React. The wording stays in MDX, so I don't need to edit each project to apply a style change.

Editing and publishing the resume

If I rewrite a project bullet, I save the MDX file and refresh the resume page in development. The local /resume.pdf route checks for source changes and generates a fresh document when needed. I can read the revised wording in its final layout before publishing it.

To export a copy directly, I run this from the repository root:

bun run render:resume

That writes apps/web/public/resume.pdf. The production build runs the same export before building the site, so the deployment includes the updated document. In production, /resume.pdf serves that file.

A resume update goes through the same Git commit and deployment as other content changes on my site. I don't have a separate manual export step to remember.

Getting the page breaks right

A project bullet that grows from one line to two can change where the next section starts. The A4 page stays the same size, so extra content has to fit in the remaining space or continue onto another page.

My renderer measures the header and each section before producing the PDF. The pagination code tries to keep a section together when it fits on a page. If there isn't room for a section heading and its opening content at the bottom, it moves the section to a new page.

For small overflows, it can tighten the line spacing within a limit. Sections longer than a page can split across pages. These rules help with page breaks, but I still inspect the result after editing a long bullet.

I check for crowded lines and large gaps between sections. A document that fits can still look cramped. Sometimes shortening a sentence is a better fix than reducing the spacing again.

Reading the finished document

The resume page displays the PDF with zoom controls and clickable links. Opening /resume.pdf directly lets the browser display the file in its own PDF viewer. The download button uses that URL too.

To explore the implementation, start with the MDX source for the content format, then the resume components and renderer for the layout and PDF generation.

Continue reading

All posts
  • Next.js had instant navigation before 16.3
    Published on
    Read more

    Next.js had instant navigation before 16.3

    Next.js already supported instant navigation. What changed in 16.3, and when should you enable its new features?

    Gia Bảo•13 min read
    • Next.js
    • Engineering
    • Performance
  • Welcome to giabao.dev
    Published on
    Read more

    Welcome to giabao.dev

    A minimal, shadcn/ui-inspired dev portfolio and blog.

    Gia Bảo•1 min read
    • Portfolio
    • Blog