Viae is a full-featured resume builder that combines a structured section-by-section editor with multiple layout templates, real-time ATS readability scoring, and high-fidelity PDF export — all in one cohesive application.
It is built with Next.js 16 (App Router, React 19), uses Supabase for authentication and persistence, renders PDFs client-side with @react-pdf/renderer, and ships four distinct templates.
Templates
Viae ships four templates, each with its own typography, layout strategy, and visual character. All templates share the same content model — switching between them preserves every field, bullet point, and date range.
| Template | Layout | Typeface | Character |
|---|
| Classic | Single column, gradient header bar | Helvetica / system | Familiar, clean, expected. A traditional timeline format with a blue-to-indigo header, section dividers, and a clear information hierarchy. |
| Modern | Two column, dark sidebar | Helvetica / system | Asymmetric layout with a dark left sidebar housing contact info and grouped skills, while the main column carries experience, education, and supporting sections. High contrast. |
| Compact | Dense single column, thin accent bar | Helvetica / system | Maximizes content per page. A slim top accent bar anchors the layout; contact info sits inline with the name header. Tight spacing, condensed typography, minimal decoration. |
| Elegant | Centered single column, serif | Times-Roman / Georgia | Refined and traditional. A decorative divider beneath the centered name header sets the tone. Descriptions and institution names are set in italics. Longer leading and wider margins give the page room to breathe. |
Each template exists as a self-contained plugin with three render surfaces — a live Preview component (React), a PDFDocument component (@react-pdf/renderer), and a Thumbnail for the template picker — registered in a central registry. This means adding a fifth template requires only dropping in a new folder and one line in the registry.
ATS Scoring Engine
The scoring engine evaluates resumes across seven dimensions, each weighted by its impact on automated application tracking system readability:
| Category | Points | What it measures |
|---|
| Contact Information | 15 | Presence and completeness of name, phone, email, LinkedIn, GitHub, and portfolio links |
| Section Completeness | 20 | Whether all eight content sections (Overview, Experience, Skills, Projects, Education, Activities, Achievements, References) are populated |
| Experience Quality | 20 | Number of entries, bullet points per entry, date range completeness, and measurable metrics (percentages, dollar amounts, numerical results) |
| Education Detail | 10 | Course name, institution, and date range presence |
| Skills Coverage | 10 | Skill groups populated; multiple groups scored higher for keyword density |
| Bullet Strength | 15 | Action verb detection ("achieved", "developed", "implemented", "led", "designed"), bullet length (50+ characters preferred), and quantified outcomes |
| Formatting & Consistency | 10 | Consistent date formatting and overall section count (6+ sections filled scores higher) |
Total: 100 points. A letter grade is assigned: A (90+), B (80+), C (70+), D (60+), F (below 60).
The score is computed in real-time via a pure function (computeResumeScore) that walks the full ResumeData object. It uses no external API calls — the analysis happens entirely client-side. Results are displayed in a ScorePanel with a circular donut gauge, per-category progress bars, and specific improvement tips per category (e.g. "Add quantified metrics to your experience bullet points" when bullet points lack numbers).
The score is persisted as a JSONB column on the resumes table and updated each time the resume is saved.
Editor
The editor is built as a single-page form divided into nine collapsible sections. Each section maps to a distinct interface in the ResumeData type and is rendered by its own component within components/resume-builder/sections/.
- Personal Section — Full name (required), designation, phone, email. GitHub, LinkedIn, and portfolio URL fields each have an associated display-text input (e.g. show a custom handle instead of the full URL). The resume title field is marked with a red asterisk to signal it's required for the save button to activate.
- Overview — A single textarea for a professional summary or objective paragraph.
- Experience — Repeatable cards. Each card has designation, company, employment status (badge), date range (plain text inputs with format hints like "e.g. June 2025"), and a dynamic list of bullet points. Bullet points have add/remove controls and can be reordered.
- Skills — Grouped skill lists. Each group has a title and a comma-separated skills textarea. Groups can be added or removed.
- Projects — Repeatable cards with title, status badge, bullet points, live URL, and a tools/technologies field.
- Education — Repeatable cards with course, institution, date range, and result/grade.
- Activities — Repeatable cards with title, organization, date range (using
startDate/endDate), and description.
- Achievements — Repeatable cards with a result heading and description body (rendered italic in PDF output).
- References — Repeatable cards with name, designation, company, phone, and email.
All sections use a shared set of primitive components (FormField, RepeatableCard, BulletPointList, SectionHeader, SectionDivider) that enforce consistent spacing, label styling, and field background colour across the entire form. Input fields use a soft grey background (--field-background: #f1f5f9) to visually distinguish editable areas from static labels.
Autosave does not exist. Saving is manual only — the "Save Draft" button in the action bar persists the current state to Supabase. This avoids unexpected writes and keeps the user in control.
Preview and PDF
The preview page uses a two-column layout on desktop: the rendered template on the left and the ATS ScorePanel on the right. On mobile, the ScorePanel stacks below the preview card.
Preview flow starts with a template selection step (even when changing templates from an existing resume), which persists the new template_id to the database before redirecting. The preview renders the selected template's Preview component with the current ResumeData — the same React component tree used for live editing previews.
PDF generation uses @react-pdf/renderer to produce a .pdf blob client-side. Each template has a corresponding PDFDocument component that mirrors the layout and typography of its preview counterpart. The PDF document is wrapped in a shared ResumePDFDocument layer that injects fonts, applies global styles, and dispatches to the correct template. If PDF rendering fails, the print fallback uses window.print().
Key PDF details:
- Classic PDF header has
paddingHorizontal: 40 to align with body text
- Section subtitle styles have
marginTop: 3, marginBottom: 3 for consistent vertical rhythm
- Body text light style uses
marginTop: 2, marginBottom: 2
- Skill rows use
marginBottom: 5
- Achievement descriptions use an italic
achievementDesc style
Dashboard and Workspace
The dashboard is the user's home base. It lists all saved resumes with:
- Resume title or fallback chain (
resume.name || resume.data.title || resume.data.fullName || 'Untitled Resume')
- Completion progress bar computed from the number of filled sections
- ATS grade badge (A–F) displayed alongside the progress percentage
- Last edited timestamp
- Preview button — enabled only when full name, designation, and at least one contact field (phone or email) are present; greyed out otherwise
- Delete with confirmation dialog
The sidebar shows the five most recently edited resumes, refreshed on resume-saved and resume-deleted custom events and on window focus.
Every dashboard page uses a sticky header (py-[18px], bg-sidebar/70, backdrop-blur-md) and shared DashboardBreadcrumbs with the Home lucide icon and / separator.
Architecture
app/
├── (auth)/ # Auth callback routes
├── auth/ # Login page
├── dashboard/
│ ├── page.tsx # Workspace — resume list, create button
│ ├── new/
│ │ ├── page.tsx # New resume editor
│ │ ├── select/page.tsx # Template picker
│ │ └── preview/page.tsx # Preview for unsaved resumes
│ ├── edit/[id]/page.tsx # Edit existing resume
│ ├── preview/[id]/page.tsx # Preview saved resume
│ ├── resumes/page.tsx # Full resume list view
│ └── account/page.tsx # Account settings
├── layout.tsx # Root layout — SupabaseProvider, ThemeProvider, Toaster
├── page.tsx # Landing / marketing page
└── globals.css # Design tokens, CSS variables, Tailwind layers
components/
├── resume-builder/
│ ├── ResumeBuilder.tsx # Editor orchestrator
│ ├── ResumeBuilderProvider.tsx # Context provider
│ ├── sections/ # PersonalSection, OverviewSection, ExperienceSection, …
│ └── common/ # FormField, RepeatableCard, BulletPointList, ActionBar, SectionHeader
├── resume-score/
│ ├── ScorePanel.tsx # Full score breakdown panel
│ └── ScoreGauge.tsx # Circular donut gauge
├── resume-pdf.tsx # PDF document wrapper (dispatches to template PDF)
├── frosted-header.tsx # Sticky glass-morphism header
├── dashboard-breadcrumbs.tsx # Breadcrumb navigation component
└── dashboard-sidebar.tsx # Sidebar with recents and grade badges
templates/
├── types.ts # Template interface (id, name, description, Preview, PDFDocument, Thumbnail)
├── registry.ts # Template registration and lookup
├── classic/ # Classic template
├── modern/ # Modern template
├── compact/ # Compact template
└── elegant/ # Elegant template
hooks/
├── use-resume-builder.ts # Core builder hook — state, field updates, save, import/export
├── use-resume-score.ts # ATS scoring pure function
├── use-resumes.ts # Resume list fetching and deletion
└── use-resume-progress.ts # Completion percentage calculation
types/
├── resume.ts # ResumeData and all section interfaces
├── helpers.ts # defaultData(), empty*(), migrateItem(), loadSaved()
└── score.ts # ResumeScore, ScoreCategory, gradeFromScore()
supabase/migrations/ # Database migrations
Tech Stack
| Category | Choice |
|---|
| Framework | Next.js 16 (App Router, React 19.2) |
| Authentication | Supabase SSR (@supabase/ssr) |
| Database | Supabase PostgreSQL with JSONB for resume data |
| Styling | Tailwind CSS v4 (PostCSS plugin), dark mode via class strategy |
| UI Primitives | HeroUI (@heroui/react) |
| PDF Generation | @react-pdf/renderer v4 |
| Icons | Lucide React |
| Notifications | Sonner (<Toaster position="bottom-right" richColors />) |
| Typography | Bricolage Grotesque (variable) + Inter (system) |
| Language | TypeScript throughout |
| Package Manager | npm |
Polishing Details
- Fallback title chain — The dashboard and breadcrumbs display
resume.name || resume.data.title || resume.data.fullName || 'Untitled Resume', so a resume always has a name even before the user types one.
- Context-aware back button — On preview pages,
?from=editor shows "Back to Editor"; without the param, it shows "Back to Dashboard".
- Clickable contact URLs — In preview and PDF output, phone numbers get
tel:, emails get mailto:, and URLs get https:// normalization via a toUrl() helper.
- Soft field backgrounds — Input backgrounds are
--field-background: #f1f5f9 in light mode, a subtle visual cue that distinguishes editable fields from static labels.
- Breadcrumb separator — Uses plain
/ text instead of a HeroUI Breadcrumbs component, avoiding cloneElement className override issues.
- Save notifications — Sonner toasts appear on successful saves.
- Template thumbnails — The template picker shows a visual
Thumbnail component for each template, not just text labels.
- Grade badges — The sidebar and dashboard cards show A–F grade badges derived from the persisted score.