Document Your Work: PDF + Portfolio
Build a private PDF doc pipeline and deploy a portfolio site to Vercel.
Introduction
30 Second Summary
You've spent months building something complex, but when it comes time to show your work, you're stuck. Either the details are locked behind an NDA, or they live scattered across your memory with no clean way to present them.
In this project, you will build a two-part documentation system. A private PDF captures your full technical knowledge, while a live portfolio website showcases your engineering skills without revealing protected details.
What You'll Build
A Python script that generates a professionally styled PDF from markdown files, paired with a static HTML/CSS portfolio site deployed to Vercel.
By the end of this project, you'll have:
- A Python generation script that converts markdown documentation into a professionally formatted PDF using WeasyPrint.
- A private PDF document containing full architecture, changelog, incident, and roadmap documentation for your project.
- A live portfolio website deployed to Vercel that presents your engineering work in an NDA-safe way for potential employers.
- Secret Mission: Add a dark mode toggle to your portfolio site using vanilla JavaScript and localStorage persistence.
Are there any prerequisites?
You'll need Python and Node.js installed on your machine, plus a free Vercel account for deployment. Step 1 walks you through verifying your installations and setting up everything you need.
Set Up Your Documentation Project
Your documentation project has two distinct audiences. The private side holds full technical detail for developer handoff and personal reference. The public side showcases your engineering skills to potential employers without revealing NDA-protected information.
In this step, you will create a folder structure that separates these two sides. You will also install the tools that power each deliverable.
In this step, get ready to:
- Create the folder structure for private docs and public portfolio.
- Install WeasyPrint and the markdown Python library.
- Install and log into the Vercel CLI.
Create the project structure
The folder structure keeps private documentation completely separate from the public portfolio. This way you never accidentally deploy confidential content.
- Open your terminal by pressing Ctrl+Alt+T on Ubuntu.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and all subdirectories by running this command:
mkdir -p fmc-docs/private/docs fmc-docs/private/styles fmc-docs/portfolio
What does this command do?
The -p flag tells mkdir to create parent directories as needed. One command builds the entire tree at once.
- fmc-docs/private/docs/ will hold your markdown documentation files.
- fmc-docs/private/styles/ will hold the CSS stylesheet for PDF formatting.
- fmc-docs/portfolio/ will hold your public HTML/CSS site.
- Move into the new project folder by running:
cd fmc-docs
- Confirm the directories exist by running:
ls -R
You should see portfolio and private listed, with docs and styles nested inside private.
Not seeing both directories?
Make sure you ran the mkdir command from ~/Desktop. If you were in a different directory, the folder was created there instead.
Run pwd to check your current location. You should see /home/<your-user>/Desktop/fmc-docs as the output.
help me find where my fmc-docs folder was created
Install WeasyPrint and the markdown library
WeasyPrint is an HTML/CSS-to-PDF engine. Combined with the Python markdown library, it lets you write documentation in markdown and render it as a professionally styled PDF.
- Install both packages by running this command:
pip install weasyprint markdown
What do these packages do?
weasyprint converts HTML and CSS into PDF documents. It supports @page rules for headers, footers, and page numbers.
markdown (Python-Markdown) converts your .md files into HTML. It supports extensions like fenced code blocks and tables.
- Verify the installation by running:
weasyprint --info
You should see system information printed to the terminal, including library versions and platform details.
Seeing an error about Pango?
WeasyPrint requires Python 3.10+ and the Pango library. On Ubuntu, Pango is typically pre-installed. If you see an error mentioning Pango, install it by running:
sudo apt install libpango-1.0-0 libpangoft2-1.0-0
Then run weasyprint --info again to confirm it works.
help me fix WeasyPrint installation errors on Ubuntu
Install the Vercel CLI
Vercel is a hosting platform for static sites and serverless functions. You will use its CLI to deploy your portfolio directly from the terminal.
- Install the Vercel CLI globally by running:
npm i -g vercel
- Verify the installation by running:
vercel --version
You should see a version number printed in the terminal.
Command not found?
The Vercel CLI requires Node.js 18+. Check your Node version by running node --version. If it is below 18, update Node.js before retrying the install.
If you see a permissions error, try running the install with sudo npm i -g vercel instead.
help me install the Vercel CLI on Ubuntu
- Log into Vercel by running:
vercel login
What happens during login?
The CLI will open your browser to authenticate. Select your preferred login method (GitHub, GitLab, email, or another option). Once you confirm in the browser, the terminal session is authenticated.
If you do not have a Vercel account yet, you can create one for free during this step.
Your project structure is ready and all three tools are installed. Next up, you will fill the private/docs/ folder with your FMC markdown documentation.
Write Your Private Markdown Documentation
Your project folders are ready. Now it's time to fill them with the actual technical documentation that will become your private PDF.
The PDF generation script you'll build in the next step reads markdown files from the private/docs/ folder. Your job in this step is to create five markdown files that capture the full technical story of the FMC project.
In this step, get ready to:
- Write the architecture documentation covering tech stack, patterns, and system components.
- Document version history, engineering incidents, and the V2 roadmap.
- Create a title page for the PDF cover.
Create the architecture document
The architecture document is the most important file in your private documentation. It captures the technical decisions that make the FMC app work.
- Create a new file called architecture.md inside the private/docs/ folder and paste the following content:
# Architecture
## Tech Stack
| Layer | Technology | Purpose |
|-------|------------|---------|
| Framework | Flutter | Cross-platform mobile development |
| State Management | Riverpod | Reactive state with dependency injection |
| Authentication | Firebase Auth | OTP-based phone authentication |
| Database | Cloud Firestore | Real-time document database |
| File Storage | Firebase Storage | Geotagged media uploads |
| Crash Reporting | Crashlytics | Production crash monitoring |
## Architecture Pattern
The project follows a **feature-first architecture** with the **repository pattern**.
Each feature module encapsulates its own UI, logic, and data layers. The repository pattern abstracts all data sources (Firebase, local cache) behind interfaces, so business logic never depends on a specific backend implementation.
## System Components
### Auth System
- OTP-based phone authentication via Firebase Auth
- Session persistence across app restarts
- Signup validation and error handling
### Media Handling
- Camera capture with geotagging
- Gallery selection with metadata extraction
- Upload pipeline with retry mechanisms
- Loading state synchronization
### Dashboard
- Real-time activity feed from Firestore
- Role-based content filtering
- Pull-to-refresh with optimistic updates
### Reliability Layer
- Crashlytics integration for production monitoring
- Retry/recovery systems for uploads
- Permission handling across Android versions
- Graceful degradation on low-end devices
## Folder Structure
```
lib/
├── features/
│ ├── auth/
│ │ ├── presentation/
│ │ ├── application/
│ │ ├── domain/
│ │ └── data/
│ ├── media/
│ │ ├── presentation/
│ │ ├── application/
│ │ ├── domain/
│ │ └── data/
│ ├── dashboard/
│ └── reporting/
├── shared/
│ ├── providers/
│ ├── widgets/
│ └── utils/
└── main.dart
```
Each feature folder contains four layers:
- **presentation**: UI widgets and screens
- **application**: State notifiers and controllers
- **domain**: Entities and repository interfaces
- **data**: Repository implementations and data sources
What does this document cover?
The architecture file captures four critical pieces of information. The tech stack table gives anyone a quick overview of the tools used. The architecture pattern section explains why features are isolated. The system components section maps out what the app actually does. The folder structure shows how code is organized on disk.
The Python-Markdown library will convert this to HTML using the fenced_code and tables extensions. That means your fenced code blocks and pipe-delimited tables will render properly in the final PDF.
- Save architecture.md.
- Confirm the file exists by running:
ls private/docs/
You should see architecture.md listed in the output.
Don't see the file?
Make sure you created the file inside fmc-docs/private/docs/ and not in a parent folder. Check that your terminal is in the fmc-docs directory when running ls.
help me find where my architecture.md file ended up
Create the changelog, incidents, and roadmap files
The remaining three documentation files capture the project's history, the hard problems you solved, and where the product is heading next.
- Create a new file called changelog.md inside private/docs/ and paste the following content:
# Changelog
## v1.6 Stable (Current Release)
### Additions
- Finalized production-ready authentication flow
- Completed upload reliability system with retry mechanisms
- Added Crashlytics integration for crash monitoring
- Implemented graceful error recovery across all features
### Fixes
- Resolved camera retake freeze during upload flows
- Fixed loading state synchronization issues
- Stabilized permission handling on Android 12+
### Engineering Notes
- Full stabilization pass before client handoff
- All critical crash paths covered by Crashlytics
- Performance validated on low-end Android devices
## v1.5 Beta
### Additions
- Upload state management with progress tracking
- Deferred permission strategy for camera and storage
- Dashboard pull-to-refresh with optimistic updates
### Fixes
- Eliminated OTP bypass vulnerability in auth flow
- Fixed session persistence across cold restarts
- Resolved duplicate upload triggers on unstable networks
## v1.4 Alpha
### Additions
- Media capture pipeline with geotagging
- Firebase Storage integration with upload queuing
- Permission request flows for camera and gallery
### Fixes
- Android permission persistence across device restarts
- Gallery picker memory issues on low-RAM devices
## v1.3 Prototype
### Additions
- Initial Flutter project scaffold with Riverpod
- Firebase Auth setup with OTP phone verification
- Cloud Firestore data model for field reports
- Basic dashboard with activity feed
### Engineering Notes
- Feature-first architecture established
- Repository pattern implemented for data layer abstraction
What does this document cover?
The changelog traces the evolution from prototype to production. Each version documents what was added, what was fixed, and any engineering decisions worth remembering. This is invaluable for developer handoff or when you return to the project months later.
- Save changelog.md.
- Create a new file called incidents.md inside private/docs/ and paste the following content:
# Engineering Incidents
## Android Permission Persistence
**Severity**: High
**Versions Affected**: v1.3 - v1.5
**Status**: Resolved in v1.6
### Problem
On certain Android devices (Samsung, Xiaomi), once a user denied a permission, the denial persisted across app restarts and even reinstalls. The standard `shouldShowRequestPermissionRationale` API returned false, making it impossible to re-prompt.
### Root Cause
Android's "Don't ask again" behavior is inconsistent across OEM skins. Some manufacturers cache permission state at the system level beyond the app sandbox.
### Solution
- Implemented deferred permission strategy (request only at point of use)
- Added fallback flows when permissions are permanently denied
- Created retry mechanisms that guide users to system settings
- Tested across 12 device models before release
### Impact
Zero user lockouts during onboarding after fix deployed.
---
## OTP Authentication Bypass
**Severity**: Critical
**Versions Affected**: v1.4
**Status**: Resolved in v1.5
### Problem
During a production audit, discovered that the signup validation flow could be bypassed by manipulating the client-side auth state before OTP verification completed.
### Root Cause
Auth state listener was updating UI before the server-side verification callback confirmed the OTP. A race condition allowed navigation to the authenticated screen.
### Solution
- Added server-side verification gate before auth state propagation
- Implemented strict validation ordering in signup flow
- Added integration tests covering the full auth lifecycle
### Impact
Production-ready authentication system with no bypass vectors.
---
## Camera Retake Freeze
**Severity**: Medium
**Versions Affected**: v1.5
**Status**: Resolved in v1.6
### Problem
When users captured a photo, dismissed it, and retook another photo in quick succession, the app froze. The camera preview became unresponsive and required a force-close.
### Root Cause
Camera lifecycle events were not properly synchronized with upload state management. Disposing the camera controller while an upload state update was in progress caused a deadlock.
### Solution
- Synchronized camera lifecycle events with upload state machine
- Added debouncing on retake actions
- Ensured camera controller disposal waits for state settlement
### Impact
Reliable media capture workflow with no freezes across all tested devices.
---
## Upload Reliability on Unstable Networks
**Severity**: High
**Versions Affected**: v1.3 - v1.5
**Status**: Resolved in v1.6
### Problem
Field workers operating in areas with intermittent connectivity experienced silent upload failures. Files appeared to send but never arrived on the server.
### Root Cause
Firebase Storage upload tasks were not properly monitored for network interruption. Failed uploads left orphaned state entries that blocked subsequent attempts.
### Solution
- Built retry system with exponential backoff
- Added upload state tracking (pending, uploading, failed, complete)
- Implemented orphan detection and cleanup on app resume
- Added user-visible upload progress indicators
### Impact
Consistent upload success across device tiers and network conditions.
Why document incidents separately?
Incident documentation captures the hard-won knowledge that changelogs miss. The problem, root cause, and solution pattern is a standard format used in engineering postmortems. It proves you can diagnose and fix complex production issues.
- Save incidents.md.
- Create a new file called roadmap.md inside private/docs/ and paste the following content:
# V2 Roadmap
## Overview
Version 2 expands the platform from field reporting into full organizational management. The core architecture remains (feature-first with repository pattern), but adds five new system modules.
## Planned Systems
### Admin Hierarchy
- Multi-level organizational structure (admin > supervisor > field worker)
- Role-based permissions at each level
- Delegation and approval workflows
- Audit trail for administrative actions
### Live Tracking
- Real-time GPS tracking of field workers during active shifts
- Geofenced check-in/check-out at job sites
- Route history and time-on-site calculations
- Battery-optimized background location updates
### Analytics Dashboard
- Completion rates by team, region, and time period
- Upload success metrics and failure categorization
- Response time tracking for field reports
- Exportable reports in CSV and PDF formats
### Geofence Alerts
- Configurable geofences around job sites
- Automatic notifications on entry/exit
- Missed-visit detection and escalation
- Integration with live tracking module
### Role-Based Access Control
- Granular permissions per feature module
- Custom role definitions beyond the three-tier hierarchy
- API-level access restrictions via Firestore Security Rules
- Audit logging for permission changes
## Technical Considerations
| Concern | Approach |
|---------|----------|
| Background location | WorkManager + Fused Location Provider |
| Real-time sync | Firestore listeners with offline persistence |
| Analytics aggregation | Cloud Functions scheduled jobs |
| Access control | Firestore Security Rules + custom claims |
| Push notifications | Firebase Cloud Messaging with topic routing |
What does this document cover?
The roadmap documents where the product is heading. It captures planning decisions and technical approaches before any code is written. This is especially useful for handoff scenarios where another developer needs to continue V2 work.
- Save roadmap.md.
- Confirm all three new files exist by running:
ls private/docs/
You should see architecture.md, changelog.md, incidents.md, and roadmap.md in the output.
Missing one of the files?
Double-check that each file was saved to the correct folder. All markdown files belong in fmc-docs/private/docs/. If you accidentally created one in private/ or fmc-docs/, move it into the docs/ subdirectory.
help me find and move my misplaced markdown files
Create the title page markdown file
The title page serves as the cover for your PDF. The generation script handles it specially by using an HTML template instead of converting the markdown directly. The file still needs to exist so the script knows to include the cover page.
- Create a new file called title.md inside private/docs/ and paste the following content:
# FMC - Field Management & Compliance Platform
**Document Type**: Technical Documentation
**Version**: 1.6 Stable
**Date**: (auto-generated by script)
**Classification**: CONFIDENTIAL - Subject to NDA
Why does the script need this file?
The generate_pdf.py script checks whether title.md exists in the document list before inserting the styled cover page. If the file is missing, the script skips the title page entirely. The actual content you write here is for your own reference. The script uses its own HTML template to render the cover page with proper styling and the current date.
- Save title.md.
- Verify all five files are in place by running:
ls private/docs/
You should see five files: architecture.md, changelog.md, incidents.md, roadmap.md, and title.md.
Only seeing some of the files?
Make sure you saved every file after pasting the content. Unsaved files in your editor won't appear on disk. Also confirm you're running ls from the fmc-docs directory, not from inside private/docs/ itself (where the path would just be ls with no prefix).
help me check which markdown files I'm missing
Your private documentation is written and ready. Next up, you'll build the Python script that converts these markdown files into a professionally styled PDF.
Build the PDF Generation Script
Your markdown documentation is ready. Now you need a way to turn those five files into a single, professionally formatted PDF that looks like a real technical document.
In this step, you will write a CSS stylesheet that controls how the PDF looks (page size, fonts, headers, footers) and a Python script that reads your markdown, converts it to HTML, and passes it through WeasyPrint to generate the final PDF.
In this step, get ready to:
- Create a print-oriented CSS stylesheet with page rules, typography, and code block formatting.
- Write a Python script that converts your markdown docs into a styled PDF.
- Run the script and verify the generated PDF output.
Create the CSS stylesheet for PDF formatting
WeasyPrint uses CSS to control the appearance of your PDF. Unlike browser CSS, print stylesheets support special @page rules that define page size, margins, headers, and footers.
- Open private/styles/pdf-style.css in your text editor and add the page setup and typography rules by pasting this code:
@page {
size: A4;
margin: 2.5cm 2cm;
@bottom-center {
content: "Page " counter(page) " of " counter(pages);
font-size: 9pt;
color: #666;
}
@top-center {
content: "FMC Technical Documentation - CONFIDENTIAL";
font-size: 8pt;
color: #999;
}
}
@page :first {
@top-center {
content: "";
}
@bottom-center {
content: "";
}
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
font-size: 11pt;
line-height: 1.6;
color: #1a1a1a;
}
h1 {
font-size: 22pt;
color: #0d47a1;
border-bottom: 2px solid #0d47a1;
padding-bottom: 8px;
margin-top: 40px;
page-break-after: avoid;
}
h2 {
font-size: 16pt;
color: #1565c0;
margin-top: 30px;
page-break-after: avoid;
}
h3 {
font-size: 13pt;
color: #1976d2;
margin-top: 20px;
page-break-after: avoid;
}
What does this code do?
The @page rule sets the PDF to A4 paper size with comfortable margins. It also places automatic page numbers at the bottom center and a confidential header at the top of every page.
The @page :first rule suppresses headers and footers on the title page so it stays clean. The heading styles use page-break-after: avoid to prevent headings from appearing alone at the bottom of a page.
- Below the heading styles, add the code block and table formatting:
code {
font-family: "JetBrains Mono", "Fira Code", "Courier New", monospace;
font-size: 9pt;
background-color: #f5f5f5;
padding: 2px 5px;
border-radius: 3px;
border: 1px solid #e0e0e0;
}
pre {
background-color: #f5f5f5;
border: 1px solid #e0e0e0;
border-radius: 5px;
padding: 15px;
font-size: 9pt;
line-height: 1.4;
overflow-wrap: break-word;
white-space: pre-wrap;
page-break-inside: avoid;
}
pre code {
background: none;
border: none;
padding: 0;
}
table {
width: 100%;
border-collapse: collapse;
margin: 15px 0;
font-size: 10pt;
page-break-inside: avoid;
}
th, td {
border: 1px solid #ddd;
padding: 8px 12px;
text-align: left;
}
th {
background-color: #e3f2fd;
font-weight: 600;
color: #0d47a1;
}
tr:nth-child(even) {
background-color: #fafafa;
}
What does this code do?
The pre block uses page-break-inside: avoid so code snippets in your documentation never get split across two pages. Tables get the same treatment, keeping them visually intact.
The pre code rule removes the background and border from code inside pre blocks, since the outer pre already provides the container styling.
- Finally, add the list styles, blockquote, and the title page class styles at the bottom of the file:
ul, ol {
padding-left: 25px;
}
li {
margin-bottom: 4px;
}
blockquote {
border-left: 4px solid #1976d2;
margin: 15px 0;
padding: 10px 20px;
background-color: #e3f2fd;
font-style: italic;
}
hr {
border: none;
border-top: 1px solid #e0e0e0;
margin: 30px 0;
}
.title-page {
text-align: center;
padding-top: 200px;
page-break-after: always;
}
.title-page h1 {
font-size: 32pt;
border: none;
color: #0d47a1;
}
.title-page p {
font-size: 14pt;
color: #555;
margin: 10px 0;
}
.confidential-notice {
margin-top: 80px;
padding: 15px;
border: 2px solid #c62828;
color: #c62828;
font-weight: bold;
font-size: 12pt;
}
What does this code do?
The .title-page class creates a centered cover page with page-break-after: always to force the content that follows onto a new page.
The .confidential-notice class adds a bold red border and text to make the NDA warning visually prominent on the title page.
- Save pdf-style.css.
Not sure where the file goes?
The file path is private/styles/pdf-style.css. Make sure you created the styles folder inside private/ in the previous step. If it does not exist, create it now.
help me find where to save my CSS file
✔️ Awesome, I've got everything!
Great. Double check you have saved your file.
ⓧ I'd like to double check the full code
@page {
size: A4;
margin: 2.5cm 2cm;
@bottom-center {
content: "Page " counter(page) " of " counter(pages);
font-size: 9pt;
color: #666;
}
@top-center {
content: "FMC Technical Documentation - CONFIDENTIAL";
font-size: 8pt;
color: #999;
}
}
@page :first {
@top-center {
content: "";
}
@bottom-center {
content: "";
}
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
font-size: 11pt;
line-height: 1.6;
color: #1a1a1a;
}
h1 {
font-size: 22pt;
color: #0d47a1;
border-bottom: 2px solid #0d47a1;
padding-bottom: 8px;
margin-top: 40px;
page-break-after: avoid;
}
h2 {
font-size: 16pt;
color: #1565c0;
margin-top: 30px;
page-break-after: avoid;
}
h3 {
font-size: 13pt;
color: #1976d2;
margin-top: 20px;
page-break-after: avoid;
}
code {
font-family: "JetBrains Mono", "Fira Code", "Courier New", monospace;
font-size: 9pt;
background-color: #f5f5f5;
padding: 2px 5px;
border-radius: 3px;
border: 1px solid #e0e0e0;
}
pre {
background-color: #f5f5f5;
border: 1px solid #e0e0e0;
border-radius: 5px;
padding: 15px;
font-size: 9pt;
line-height: 1.4;
overflow-wrap: break-word;
white-space: pre-wrap;
page-break-inside: avoid;
}
pre code {
background: none;
border: none;
padding: 0;
}
table {
width: 100%;
border-collapse: collapse;
margin: 15px 0;
font-size: 10pt;
page-break-inside: avoid;
}
th, td {
border: 1px solid #ddd;
padding: 8px 12px;
text-align: left;
}
th {
background-color: #e3f2fd;
font-weight: 600;
color: #0d47a1;
}
tr:nth-child(even) {
background-color: #fafafa;
}
ul, ol {
padding-left: 25px;
}
li {
margin-bottom: 4px;
}
blockquote {
border-left: 4px solid #1976d2;
margin: 15px 0;
padding: 10px 20px;
background-color: #e3f2fd;
font-style: italic;
}
hr {
border: none;
border-top: 1px solid #e0e0e0;
margin: 30px 0;
}
.title-page {
text-align: center;
padding-top: 200px;
page-break-after: always;
}
.title-page h1 {
font-size: 32pt;
border: none;
color: #0d47a1;
}
.title-page p {
font-size: 14pt;
color: #555;
margin: 10px 0;
}
.confidential-notice {
margin-top: 80px;
padding: 15px;
border: 2px solid #c62828;
color: #c62828;
font-weight: bold;
font-size: 12pt;
}
Write the Python generation script
The script reads your markdown files in a specific order, converts each to HTML, wraps everything in a template, and passes it to WeasyPrint with your CSS stylesheet.
- Create private/generate_pdf.py and add the imports and configuration constants:
import markdown
from weasyprint import HTML
from pathlib import Path
from datetime import date
DOCS_DIR = Path("docs")
STYLES_DIR = Path("styles")
OUTPUT_FILE = "fmc-technical-docs.pdf"
DOC_ORDER = [
"title.md",
"architecture.md",
"changelog.md",
"incidents.md",
"roadmap.md",
]
What does this code do?
The DOC_ORDER list defines the exact sequence your documents will appear in the PDF. The title page comes first, followed by architecture, changelog, incidents, and roadmap.
Using Path from pathlib makes file operations work consistently across operating systems.
- Below the constants, add the HTML template strings that wrap your converted markdown:
HTML_TEMPLATE = """<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>FMC Technical Documentation</title>
</head>
<body>
{content}
</body>
</html>
"""
TITLE_HTML = """<div class="title-page">
<h1>FMC</h1>
<p>Field Management & Compliance Platform</p>
<p><strong>Technical Documentation</strong></p>
<p>Version 1.6 Stable</p>
<p>{date}</p>
<div class="confidential-notice">
CONFIDENTIAL - Subject to NDA
</div>
</div>
"""
What does this code do?
The HTML_TEMPLATE is the outer HTML document that wraps all your converted content. The {content} placeholder gets replaced with the combined HTML from all your markdown files.
The TITLE_HTML generates the cover page directly as HTML instead of converting from markdown. This gives you precise control over the title page layout using the .title-page and .confidential-notice CSS classes you wrote earlier.
- Add the two functions that do the actual work, plus the main entry point at the bottom of the file:
def convert_markdown_to_html(filepath):
"""Read a markdown file and convert it to HTML."""
content = filepath.read_text(encoding="utf-8")
extensions = ["fenced_code", "tables", "toc", "nl2br"]
html = markdown.markdown(content, extensions=extensions)
return html
def generate_pdf():
"""Generate the full PDF from all markdown documents."""
sections = []
for filename in DOC_ORDER:
filepath = DOCS_DIR / filename
if not filepath.exists():
print(f"Warning: {filepath} not found, skipping.")
continue
if filename == "title.md":
sections.append(TITLE_HTML.format(date=date.today().strftime("%B %d, %Y")))
else:
html_content = convert_markdown_to_html(filepath)
sections.append(f'<section class="doc-section">{html_content}</section>')
combined_html = HTML_TEMPLATE.format(content="\n".join(sections))
css_path = STYLES_DIR / "pdf-style.css"
css_string = css_path.read_text(encoding="utf-8") if css_path.exists() else ""
html_doc = HTML(string=combined_html)
html_doc.write_pdf(OUTPUT_FILE, stylesheets=[css_path])
print(f"PDF generated: {OUTPUT_FILE}")
if __name__ == "__main__":
generate_pdf()
What does this code do?
The convert_markdown_to_html() function reads a single markdown file and converts it using extensions for fenced code blocks, tables, table of contents, and line breaks.
The generate_pdf() function loops through DOC_ORDER, builds up an HTML string from all documents, then passes it to WeasyPrint along with your CSS stylesheet. The write_pdf() call generates the final PDF file.
- Save generate_pdf.py.
Seeing a red underline on the imports?
If your editor highlights the import markdown or from weasyprint import HTML lines, it may not be using the same Python environment where you installed the packages. Make sure your editor is using the correct Python interpreter.
help me fix import errors in my generate_pdf.py script
✔️ Awesome, I've got everything!
Great. Double check you have saved your file.
ⓧ I'd like to double check the full code
import markdown
from weasyprint import HTML
from pathlib import Path
from datetime import date
DOCS_DIR = Path("docs")
STYLES_DIR = Path("styles")
OUTPUT_FILE = "fmc-technical-docs.pdf"
DOC_ORDER = [
"title.md",
"architecture.md",
"changelog.md",
"incidents.md",
"roadmap.md",
]
HTML_TEMPLATE = """<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>FMC Technical Documentation</title>
</head>
<body>
{content}
</body>
</html>
"""
TITLE_HTML = """<div class="title-page">
<h1>FMC</h1>
<p>Field Management & Compliance Platform</p>
<p><strong>Technical Documentation</strong></p>
<p>Version 1.6 Stable</p>
<p>{date}</p>
<div class="confidential-notice">
CONFIDENTIAL - Subject to NDA
</div>
</div>
"""
def convert_markdown_to_html(filepath):
"""Read a markdown file and convert it to HTML."""
content = filepath.read_text(encoding="utf-8")
extensions = ["fenced_code", "tables", "toc", "nl2br"]
html = markdown.markdown(content, extensions=extensions)
return html
def generate_pdf():
"""Generate the full PDF from all markdown documents."""
sections = []
for filename in DOC_ORDER:
filepath = DOCS_DIR / filename
if not filepath.exists():
print(f"Warning: {filepath} not found, skipping.")
continue
if filename == "title.md":
sections.append(TITLE_HTML.format(date=date.today().strftime("%B %d, %Y")))
else:
html_content = convert_markdown_to_html(filepath)
sections.append(f'<section class="doc-section">{html_content}</section>')
combined_html = HTML_TEMPLATE.format(content="\n".join(sections))
css_path = STYLES_DIR / "pdf-style.css"
css_string = css_path.read_text(encoding="utf-8") if css_path.exists() else ""
html_doc = HTML(string=combined_html)
html_doc.write_pdf(OUTPUT_FILE, stylesheets=[css_path])
print(f"PDF generated: {OUTPUT_FILE}")
if __name__ == "__main__":
generate_pdf()
Run the script and verify the PDF output
- Navigate into the private directory and run the script:
cd private
python generate_pdf.py
You should see the message PDF generated: fmc-technical-docs.pdf printed in your terminal.
- Confirm the PDF file was created by running:
ls fmc-technical-docs.pdf
You should see fmc-technical-docs.pdf listed in the output.
- Open fmc-technical-docs.pdf in your PDF viewer.
- Check that the title page displays "FMC" centered with the confidential notice in red.
- Scroll through and verify that headings are styled in blue, code blocks have a gray background, and page numbers appear at the bottom of each page.
Seeing an error about Pango or missing libraries?
WeasyPrint depends on Pango for text rendering. On Ubuntu, Pango is typically pre-installed. If you see an error mentioning Pango, install it by running:
sudo apt install libpango-1.0-0 libpangoft2-1.0-0
Then run python generate_pdf.py again.
help me fix WeasyPrint errors when generating my PDF
Your PDF pipeline is working. Next up, you will build the public portfolio site that showcases your engineering skills without revealing confidential details.
Build the Portfolio Website
You now have a private PDF that preserves every technical detail of your FMC project. The next piece is a public portfolio site that showcases your engineering skills to potential employers.
The key challenge here is presenting your work without revealing NDA-protected details. You will build a static HTML/CSS site that uses generic terminology ("Field Management Platform" instead of the client name) while still demonstrating real engineering depth.
In this step, get ready to:
- Build a single-page HTML portfolio presenting your work in an NDA-safe way.
- Style the portfolio with responsive CSS using cards, grids, and a timeline.
- Create a Vercel configuration file for clean URL deployment.
Create the HTML structure
Your portfolio is a single-page site with distinct sections: an overview, architecture decisions, engineering challenges, development methodology, skills, and key learnings. Each section presents your real accomplishments using generic, NDA-safe language.
- Create a new file called index.html inside the portfolio/ folder by running:
touch portfolio/index.html
- Open portfolio/index.html in your text editor and paste the following HTML:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Field Management Platform - Engineering Portfolio</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<header>
<div class="container">
<h1>Field Management Platform</h1>
<p class="subtitle">Mobile Engineering Portfolio</p>
<p class="role">Lead Mobile Engineer | Flutter + Firebase</p>
</div>
</header>
<main class="container">
<section class="overview">
<h2>Project Overview</h2>
<p>Designed and built a production-grade field management and reporting platform for organizations managing field employees, audits, inspections, and reporting workflows. The platform handles secure authentication, geotagged media uploads, and real-time activity tracking.</p>
<div class="tech-stack">
<span class="badge">Flutter</span>
<span class="badge">Firebase Auth</span>
<span class="badge">Cloud Firestore</span>
<span class="badge">Firebase Storage</span>
<span class="badge">Riverpod</span>
<span class="badge">Crashlytics</span>
</div>
</section>
<section class="architecture">
<h2>Architecture Decisions</h2>
<div class="card-grid">
<div class="card">
<h3>Feature-First Architecture</h3>
<p>Organized codebase by feature rather than layer, enabling independent development and testing of each module (auth, media, dashboard, reporting).</p>
</div>
<div class="card">
<h3>Repository Pattern</h3>
<p>Abstracted data sources behind repository interfaces, allowing seamless switching between Firebase and local caching without touching business logic.</p>
</div>
<div class="card">
<h3>Immutable Entities</h3>
<p>Enforced immutability across all data models, eliminating state mutation bugs and improving predictability in complex upload/retry flows.</p>
</div>
</div>
</section>
<section class="challenges">
<h2>Engineering Challenges Solved</h2>
<div class="card-grid">
<div class="card">
<h3>Cross-Device Permission Recovery</h3>
<p>Solved Android permission persistence issues where denied permissions became permanent across sessions on certain devices. Implemented retry flows, fallback systems, and deferred permission strategies.</p>
<span class="impact">Impact: Zero user lockouts during onboarding</span>
</div>
<div class="card">
<h3>Upload Reliability on Unstable Networks</h3>
<p>Built retry mechanisms and upload state synchronization to handle unreliable connections and low-end Android devices. Implemented loading state management and graceful failure recovery.</p>
<span class="impact">Impact: Consistent upload success across device tiers</span>
</div>
<div class="card">
<h3>Security Hardening</h3>
<p>Identified and removed authentication bypass vulnerability during production audit. Stabilized auth validation flow and improved signup validation handling.</p>
<span class="impact">Impact: Production-ready authentication system</span>
</div>
<div class="card">
<h3>Camera Lifecycle Stabilization</h3>
<p>Resolved application state freezes during camera retake flows by synchronizing camera lifecycle events with upload state management.</p>
<span class="impact">Impact: Reliable media capture workflow</span>
</div>
</div>
</section>
<section class="methodology">
<h2>Development Methodology</h2>
<div class="timeline">
<div class="timeline-item">
<span class="phase">Phase 1</span>
<p>MVP prototyping and core feature integration</p>
</div>
<div class="timeline-item">
<span class="phase">Phase 2</span>
<p>Reliability stabilization and device compatibility</p>
</div>
<div class="timeline-item">
<span class="phase">Phase 3</span>
<p>Production hardening, security improvements, and UX refinement</p>
</div>
<div class="timeline-item">
<span class="phase">Phase 4</span>
<p>Scalability planning for V2 (admin hierarchy, analytics, live tracking)</p>
</div>
</div>
</section>
<section class="skills">
<h2>Skills Demonstrated</h2>
<div class="skills-grid">
<div class="skill-category">
<h3>Mobile Engineering</h3>
<ul>
<li>Flutter cross-platform development</li>
<li>State management with Riverpod</li>
<li>Camera/gallery integration</li>
<li>Low-end device optimization</li>
</ul>
</div>
<div class="skill-category">
<h3>Backend Integration</h3>
<ul>
<li>Firebase Authentication (OTP)</li>
<li>Cloud Firestore data modeling</li>
<li>Firebase Storage upload pipelines</li>
<li>Crashlytics monitoring</li>
</ul>
</div>
<div class="skill-category">
<h3>Reliability Engineering</h3>
<ul>
<li>Retry/recovery systems</li>
<li>Permission handling across Android versions</li>
<li>Upload state synchronization</li>
<li>Production crash monitoring</li>
</ul>
</div>
</div>
</section>
<section class="key-learnings">
<h2>Key Learnings</h2>
<ul>
<li>Android permissions behave inconsistently across devices and OS versions. Deferred permission handling significantly improves onboarding reliability.</li>
<li>Retry systems are critical for field applications operating on unstable networks.</li>
<li>Crash monitoring must be integrated before production rollout, not after.</li>
<li>Stabilization phases are as important as feature development for production readiness.</li>
</ul>
</section>
</main>
<footer>
<div class="container">
<p>Built with Flutter + Firebase | Production Release v1.6 Stable</p>
</div>
</footer>
</body>
</html>
What does this HTML do?
This single-page portfolio is structured into distinct sections that map to what a hiring manager looks for:
- The header introduces you with a generic project name ("Field Management Platform") instead of the real client name.
- The overview section describes what the platform does with tech stack badges for quick scanning.
- The challenges section proves problem-solving ability by describing engineering problems and their measurable impact.
- The methodology timeline shows you understand phased delivery and production readiness.
Why plain HTML instead of React?
This portfolio is pure static content. React would add build complexity (bundler, JSX compilation, npm scripts) for zero benefit on a simple showcase page.
Plain HTML/CSS deploys instantly to Vercel with no build step needed.
- Save portfolio/index.html.
File not saving?
Make sure the portfolio/ folder exists inside your fmc-docs directory. If not, create it with mkdir portfolio from the fmc-docs folder.
help me create the portfolio folder structure
Style the portfolio
The stylesheet transforms the raw HTML into a professional, responsive layout. It uses a blue color scheme, card-based components, and CSS Grid for adaptive layouts across screen sizes.
- Create a new file called style.css inside the portfolio/ folder and paste the following CSS:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
line-height: 1.6;
color: #1a1a2e;
background-color: #fafbfc;
}
.container {
max-width: 900px;
margin: 0 auto;
padding: 0 24px;
}
header {
background: linear-gradient(135deg, #0d47a1, #1976d2);
color: white;
padding: 60px 0;
text-align: center;
}
header h1 {
font-size: 2.4rem;
font-weight: 700;
margin-bottom: 8px;
}
header .subtitle {
font-size: 1.1rem;
opacity: 0.9;
margin-bottom: 4px;
}
header .role {
font-size: 1rem;
opacity: 0.8;
}
main {
padding: 48px 0;
}
section {
margin-bottom: 48px;
}
h2 {
font-size: 1.6rem;
color: #0d47a1;
margin-bottom: 20px;
padding-bottom: 8px;
border-bottom: 2px solid #e3f2fd;
}
h3 {
font-size: 1.1rem;
color: #1565c0;
margin-bottom: 8px;
}
.tech-stack {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 16px;
}
.badge {
background-color: #e3f2fd;
color: #0d47a1;
padding: 6px 14px;
border-radius: 20px;
font-size: 0.85rem;
font-weight: 500;
}
.card-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 20px;
}
.card {
background: white;
border: 1px solid #e8eaed;
border-radius: 8px;
padding: 24px;
transition: box-shadow 0.2s ease;
}
.card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
.card p {
color: #555;
font-size: 0.95rem;
margin-bottom: 12px;
}
.impact {
display: inline-block;
background-color: #e8f5e9;
color: #2e7d32;
padding: 4px 10px;
border-radius: 4px;
font-size: 0.8rem;
font-weight: 500;
}
.timeline {
position: relative;
padding-left: 30px;
border-left: 3px solid #e3f2fd;
}
.timeline-item {
margin-bottom: 24px;
position: relative;
}
.timeline-item::before {
content: "";
position: absolute;
left: -37px;
top: 6px;
width: 12px;
height: 12px;
background-color: #1976d2;
border-radius: 50%;
border: 2px solid white;
}
.phase {
display: inline-block;
background-color: #0d47a1;
color: white;
padding: 2px 10px;
border-radius: 4px;
font-size: 0.8rem;
font-weight: 600;
margin-bottom: 4px;
}
.skills-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 24px;
}
.skill-category {
background: white;
border: 1px solid #e8eaed;
border-radius: 8px;
padding: 24px;
}
.skill-category ul {
list-style: none;
padding: 0;
}
.skill-category li {
padding: 6px 0;
padding-left: 20px;
position: relative;
color: #555;
font-size: 0.95rem;
}
.skill-category li::before {
content: "\2192";
position: absolute;
left: 0;
color: #1976d2;
}
.key-learnings ul {
list-style: none;
padding: 0;
}
.key-learnings li {
padding: 12px 16px;
margin-bottom: 8px;
background: white;
border-left: 3px solid #1976d2;
border-radius: 4px;
color: #444;
font-size: 0.95rem;
}
footer {
text-align: center;
padding: 32px 0;
background-color: #f5f5f5;
color: #666;
font-size: 0.9rem;
}
@media (max-width: 600px) {
header h1 {
font-size: 1.8rem;
}
.card-grid {
grid-template-columns: 1fr;
}
.skills-grid {
grid-template-columns: 1fr;
}
}
What does this stylesheet do?
The CSS creates a clean, professional look with several key techniques:
- A gradient header (#0d47a1 to #1976d2) gives the page a polished first impression.
- CSS Grid with auto-fit and minmax() makes cards responsive without media queries for each breakpoint.
- The .timeline uses a left border with absolutely positioned circles to create a visual progression.
- The @media query at 600px collapses grids to single columns for mobile devices.
- Save portfolio/style.css.
- Open portfolio/index.html in your browser by running:
xdg-open portfolio/index.html
You should see a blue gradient header with "Field Management Platform" in large white text, followed by card-based sections with tech badges and a timeline.
Page looks unstyled?
Check that style.css is in the same folder as index.html. The HTML references it with a relative path (href="style.css"), so both files must be inside portfolio/.
help me debug why my portfolio CSS isn't loading
Create the Vercel configuration
Vercel needs a small configuration file to enable clean URLs on your deployed site. This removes the .html extension from URLs so visitors see /about instead of /about.html.
- Create a new file called vercel.json inside the portfolio/ folder and paste the following:
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"cleanUrls": true
}
What does cleanUrls do?
When cleanUrls is set to true, Vercel removes the .html extension from all served pages. If someone visits a path ending in .html, Vercel redirects them to the clean version automatically.
- Save portfolio/vercel.json.
- Confirm all three portfolio files exist by running:
ls portfolio/
You should see index.html, style.css, and vercel.json listed.
Missing a file?
If any file is missing from the output, double check that you created it inside the portfolio/ folder (not in private/ or the root fmc-docs/ directory).
help me check my portfolio folder structure
✔️ Awesome, I've got everything!
Great. Double check you have saved all three files in your portfolio/ folder.
ⓧ I'd like to double check the full code
Here is the complete code for each file in your portfolio/ folder.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Field Management Platform - Engineering Portfolio</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<header>
<div class="container">
<h1>Field Management Platform</h1>
<p class="subtitle">Mobile Engineering Portfolio</p>
<p class="role">Lead Mobile Engineer | Flutter + Firebase</p>
</div>
</header>
<main class="container">
<section class="overview">
<h2>Project Overview</h2>
<p>Designed and built a production-grade field management and reporting platform for organizations managing field employees, audits, inspections, and reporting workflows. The platform handles secure authentication, geotagged media uploads, and real-time activity tracking.</p>
<div class="tech-stack">
<span class="badge">Flutter</span>
<span class="badge">Firebase Auth</span>
<span class="badge">Cloud Firestore</span>
<span class="badge">Firebase Storage</span>
<span class="badge">Riverpod</span>
<span class="badge">Crashlytics</span>
</div>
</section>
<section class="architecture">
<h2>Architecture Decisions</h2>
<div class="card-grid">
<div class="card">
<h3>Feature-First Architecture</h3>
<p>Organized codebase by feature rather than layer, enabling independent development and testing of each module (auth, media, dashboard, reporting).</p>
</div>
<div class="card">
<h3>Repository Pattern</h3>
<p>Abstracted data sources behind repository interfaces, allowing seamless switching between Firebase and local caching without touching business logic.</p>
</div>
<div class="card">
<h3>Immutable Entities</h3>
<p>Enforced immutability across all data models, eliminating state mutation bugs and improving predictability in complex upload/retry flows.</p>
</div>
</div>
</section>
<section class="challenges">
<h2>Engineering Challenges Solved</h2>
<div class="card-grid">
<div class="card">
<h3>Cross-Device Permission Recovery</h3>
<p>Solved Android permission persistence issues where denied permissions became permanent across sessions on certain devices. Implemented retry flows, fallback systems, and deferred permission strategies.</p>
<span class="impact">Impact: Zero user lockouts during onboarding</span>
</div>
<div class="card">
<h3>Upload Reliability on Unstable Networks</h3>
<p>Built retry mechanisms and upload state synchronization to handle unreliable connections and low-end Android devices. Implemented loading state management and graceful failure recovery.</p>
<span class="impact">Impact: Consistent upload success across device tiers</span>
</div>
<div class="card">
<h3>Security Hardening</h3>
<p>Identified and removed authentication bypass vulnerability during production audit. Stabilized auth validation flow and improved signup validation handling.</p>
<span class="impact">Impact: Production-ready authentication system</span>
</div>
<div class="card">
<h3>Camera Lifecycle Stabilization</h3>
<p>Resolved application state freezes during camera retake flows by synchronizing camera lifecycle events with upload state management.</p>
<span class="impact">Impact: Reliable media capture workflow</span>
</div>
</div>
</section>
<section class="methodology">
<h2>Development Methodology</h2>
<div class="timeline">
<div class="timeline-item">
<span class="phase">Phase 1</span>
<p>MVP prototyping and core feature integration</p>
</div>
<div class="timeline-item">
<span class="phase">Phase 2</span>
<p>Reliability stabilization and device compatibility</p>
</div>
<div class="timeline-item">
<span class="phase">Phase 3</span>
<p>Production hardening, security improvements, and UX refinement</p>
</div>
<div class="timeline-item">
<span class="phase">Phase 4</span>
<p>Scalability planning for V2 (admin hierarchy, analytics, live tracking)</p>
</div>
</div>
</section>
<section class="skills">
<h2>Skills Demonstrated</h2>
<div class="skills-grid">
<div class="skill-category">
<h3>Mobile Engineering</h3>
<ul>
<li>Flutter cross-platform development</li>
<li>State management with Riverpod</li>
<li>Camera/gallery integration</li>
<li>Low-end device optimization</li>
</ul>
</div>
<div class="skill-category">
<h3>Backend Integration</h3>
<ul>
<li>Firebase Authentication (OTP)</li>
<li>Cloud Firestore data modeling</li>
<li>Firebase Storage upload pipelines</li>
<li>Crashlytics monitoring</li>
</ul>
</div>
<div class="skill-category">
<h3>Reliability Engineering</h3>
<ul>
<li>Retry/recovery systems</li>
<li>Permission handling across Android versions</li>
<li>Upload state synchronization</li>
<li>Production crash monitoring</li>
</ul>
</div>
</div>
</section>
<section class="key-learnings">
<h2>Key Learnings</h2>
<ul>
<li>Android permissions behave inconsistently across devices and OS versions. Deferred permission handling significantly improves onboarding reliability.</li>
<li>Retry systems are critical for field applications operating on unstable networks.</li>
<li>Crash monitoring must be integrated before production rollout, not after.</li>
<li>Stabilization phases are as important as feature development for production readiness.</li>
</ul>
</section>
</main>
<footer>
<div class="container">
<p>Built with Flutter + Firebase | Production Release v1.6 Stable</p>
</div>
</footer>
</body>
</html>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
line-height: 1.6;
color: #1a1a2e;
background-color: #fafbfc;
}
.container {
max-width: 900px;
margin: 0 auto;
padding: 0 24px;
}
header {
background: linear-gradient(135deg, #0d47a1, #1976d2);
color: white;
padding: 60px 0;
text-align: center;
}
header h1 {
font-size: 2.4rem;
font-weight: 700;
margin-bottom: 8px;
}
header .subtitle {
font-size: 1.1rem;
opacity: 0.9;
margin-bottom: 4px;
}
header .role {
font-size: 1rem;
opacity: 0.8;
}
main {
padding: 48px 0;
}
section {
margin-bottom: 48px;
}
h2 {
font-size: 1.6rem;
color: #0d47a1;
margin-bottom: 20px;
padding-bottom: 8px;
border-bottom: 2px solid #e3f2fd;
}
h3 {
font-size: 1.1rem;
color: #1565c0;
margin-bottom: 8px;
}
.tech-stack {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 16px;
}
.badge {
background-color: #e3f2fd;
color: #0d47a1;
padding: 6px 14px;
border-radius: 20px;
font-size: 0.85rem;
font-weight: 500;
}
.card-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 20px;
}
.card {
background: white;
border: 1px solid #e8eaed;
border-radius: 8px;
padding: 24px;
transition: box-shadow 0.2s ease;
}
.card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
.card p {
color: #555;
font-size: 0.95rem;
margin-bottom: 12px;
}
.impact {
display: inline-block;
background-color: #e8f5e9;
color: #2e7d32;
padding: 4px 10px;
border-radius: 4px;
font-size: 0.8rem;
font-weight: 500;
}
.timeline {
position: relative;
padding-left: 30px;
border-left: 3px solid #e3f2fd;
}
.timeline-item {
margin-bottom: 24px;
position: relative;
}
.timeline-item::before {
content: "";
position: absolute;
left: -37px;
top: 6px;
width: 12px;
height: 12px;
background-color: #1976d2;
border-radius: 50%;
border: 2px solid white;
}
.phase {
display: inline-block;
background-color: #0d47a1;
color: white;
padding: 2px 10px;
border-radius: 4px;
font-size: 0.8rem;
font-weight: 600;
margin-bottom: 4px;
}
.skills-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 24px;
}
.skill-category {
background: white;
border: 1px solid #e8eaed;
border-radius: 8px;
padding: 24px;
}
.skill-category ul {
list-style: none;
padding: 0;
}
.skill-category li {
padding: 6px 0;
padding-left: 20px;
position: relative;
color: #555;
font-size: 0.95rem;
}
.skill-category li::before {
content: "\2192";
position: absolute;
left: 0;
color: #1976d2;
}
.key-learnings ul {
list-style: none;
padding: 0;
}
.key-learnings li {
padding: 12px 16px;
margin-bottom: 8px;
background: white;
border-left: 3px solid #1976d2;
border-radius: 4px;
color: #444;
font-size: 0.95rem;
}
footer {
text-align: center;
padding: 32px 0;
background-color: #f5f5f5;
color: #666;
font-size: 0.9rem;
}
@media (max-width: 600px) {
header h1 {
font-size: 1.8rem;
}
.card-grid {
grid-template-columns: 1fr;
}
.skills-grid {
grid-template-columns: 1fr;
}
}
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"cleanUrls": true
}
Your portfolio site is ready for deployment. Next up, you will push it live to the internet with Vercel so anyone with the URL can see your work.
Deploy to Vercel
Your portfolio site is built and ready for the world to see. In this step, you'll deploy it to Vercel so it's accessible via a live URL.
Vercel serves static HTML/CSS sites directly with zero build configuration. You'll use the Vercel CLI to create a project, deploy a preview, then promote it to production.
In this step, get ready to:
- Deploy your portfolio using the Vercel CLI.
- Promote the deployment to production.
- Verify your live site renders correctly and is NDA-safe.
Deploy using Vercel CLI
The Vercel CLI walks you through an interactive setup the first time you deploy a directory. It will ask several questions to configure your project.
- Navigate to your portfolio directory and start the deployment by running these commands:
cd ~/Desktop/fmc-docs/portfolio
vercel
What will the CLI ask me?
The Vercel CLI will present several interactive prompts. Here is how to respond to each one:
- Set up and deploy? Enter Y (yes).
- Which scope do you want to deploy to? Select your personal account.
- Link to existing project? Enter N (no, create a new one).
- What's your project's name? Type field-management-portfolio (or any name you prefer).
- In which directory is your code located? Press Enter to accept ./ (the current directory).
- Want to override the settings? Enter N (no).
Why does Vercel select Other as the framework?
Your portfolio is plain HTML and CSS with no JavaScript framework. Vercel auto-detects this and selects "Other", which means it serves your files directly as a static site with no build step required.
After answering the prompts, Vercel uploads your files and gives you a preview deployment URL.
Seeing a login error?
If the CLI says you're not logged in, run vercel login first and follow the authentication flow. Then re-run vercel in the same directory.
Help me troubleshoot my Vercel login
Deploy to production
The first vercel command created a preview deployment. To make your site live on a production URL, you need to promote it.
- Deploy to production by running this command:
vercel --prod
Vercel outputs your production URL in the terminal. It follows the pattern your-project-name.vercel.app.
What is the difference between preview and production?
A preview deployment is a temporary URL for testing before you go live. A production deployment is your permanent, shareable URL that you can put on your resume or LinkedIn profile.
Deployment failed?
Make sure your entry file is named exactly index.html (lowercase, case-sensitive). Vercel requires this exact filename to serve your static site.
Also confirm you're running the command from inside the portfolio/ directory, not the parent fmc-docs/ folder.
Help me fix my Vercel deployment
Verify your live site
Your portfolio is now live on the internet. Time to confirm everything looks right.
- Copy the production URL from your terminal output.
- Open it in your browser.
- Confirm all sections render correctly: the header, tech stack badges, architecture cards, challenges cards, timeline, skills grid, and footer.
- Test the responsive layout by pressing F12 to open DevTools, then click the device toggle icon (or press Ctrl+Shift+M on Windows / Cmd+Shift+M on macOS in Firefox).
- Verify no NDA-breaking details are visible. The site should say "Field Management Platform" and never mention the client name or app name "FMC".
What should I see?
Your live site displays a blue gradient header, card-based sections for architecture and challenges, a vertical timeline, and a skills grid. On mobile widths, cards stack into a single column.
Site showing a 404?
Double check that vercel.json and index.html are both in the same portfolio/ directory you deployed from. If the URL path shows /index or nothing loads, redeploy with vercel --prod from inside portfolio/.
Help me fix my 404 error on Vercel
Secret mission
Add Dark Mode Toggle
Your portfolio looks great in light mode, but what about visitors who prefer dark interfaces? Add an interactive dark mode toggle that remembers each visitor's preference and respects their system settings.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally and uses the Vercel free tier, so there are no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- Local fmc-docs/ folder (contains private documentation, PDF, portfolio site, and generation script).
- Vercel project deployment (your portfolio site hosted on Vercel free tier).
- Python packages: weasyprint and markdown (installed via pip).
- Vercel CLI (installed globally via npm).
Keep everything running
No action needed. Choose this if you plan to update your documentation or portfolio site in the future.
- Your Vercel deployment stays live at your production URL at no cost on the free tier.
- Your local fmc-docs/ folder remains on your Desktop for future edits.
- The weasyprint and markdown packages stay installed for regenerating your PDF.
- The Vercel CLI stays installed globally for future deployments.
Pause - I'll come back to this later
Free up local disk space while keeping your Vercel deployment live.
- Your Vercel site remains live at no cost. No action needed there.
- Keep your fmc-docs/ folder somewhere safe (external drive, cloud storage) so you can resume later.
- You can uninstall the Python packages to free up space. Reinstall them later when you return.
Uninstall the Python packages by running:
pip uninstall weasyprint markdown -y
You can reinstall them anytime with pip install weasyprint markdown.
Delete - I don't want to use this again
Remove all project resources and start fresh.
- Delete your local project folder by running:
cd ~/Desktop
rm -rf fmc-docs
- Delete your Vercel project. Open the Vercel dashboard, select your portfolio project, then navigate to Settings in the sidebar. Scroll to the bottom of the General page and click Delete in the Delete Project section. Confirm by entering the project name and clicking Continue.
- Uninstall the Python packages by running:
pip uninstall weasyprint markdown -y
- Uninstall the Vercel CLI by running:
npm uninstall -g vercel
Nice Work!
Nice Work!
You've built a complete documentation system for your engineering work. You now have a private, regeneratable PDF for technical handoff and a live portfolio URL you can put on your resume.
Here's what you learned:
- Built a Python + WeasyPrint pipeline that converts markdown documentation into a professionally styled PDF with page numbers, headers, and print-optimized formatting.
- Created an NDA-safe portfolio website using plain HTML and CSS that showcases your mobile engineering skills without revealing protected client details.
- Deployed a static site to Vercel using the Vercel CLI, giving you a live production URL accessible from anywhere.
- Added a dark mode toggle with localStorage persistence and system preference detection using vanilla JavaScript.
Ready to quiz yourself?