Portfolio Page — Design Document
Context and scope
This document describes the visual and structural design system used by the Portfolio pages (/ko/portfolio, /en/portfolio, rendered via _layouts/portfolio.html). Portfolio reuses several of the same includes as Resume (pro/resume/work.html, pro/resume/projects.html, pro/resume/education.html, pro/resume/skills.html, and others), driven by its own data files, _data/portfolio-{en,ko}.yml. This document focuses on what differs from Resume — page composition, data conventions for Work, and the two-column print mode — and defers to docs/resume/DESIGN.md for the shared card hierarchy, spacing, typography, and content rules (web/print line limits, single-item collapse, icon set, etc.), which apply identically here.
Goals
- Present the same four-level card hierarchy as Resume (
Work,Projects,Education), but with the fuller itemized detail (roles.items/results.items) that a portfolio reader is expected to want, rather than a resume’s quick-scan summary. - Preserve Resume’s line limits — up to two lines on the web and one line in single-column print — with Portfolio checked by the same
resume-lintrun, not a separate tool. - Support a denser, optionally two-column print layout for the larger number of entries Portfolio typically holds, without introducing a second card design.
Non-goals
- A distinct visual language from Resume. Portfolio should look like “Resume’s cards, at full detail,” not a different product.
- Independent print pagination logic. Two-column mode is a CSS column split (
body.print-2col), not a JS-driven layout engine. - A separate data shape per section. Portfolio’s
Educationreuses Resume’s exact flat field shape; onlyWorkdiffers (see below), and only because it predates the newer convention.
Relationship to the Resume card system
Portfolio’s page (en/portfolio.md / ko/portfolio.md front matter) renders these sections, in order, through the same shared includes Resume uses: work, projects, skills, education, awards, publications, certifications, interests, languages — all in a single content column (see Page layout).
Projects and Education are rendered by the identical Liquid includes used by Resume, with Portfolio’s own, more detailed data:
- Same four-level hierarchy (Section → Card header →
.resume-meta-row→ level-4 list). - Same
.resume-meta-rowclasses, icons (icon-user-tiefor Role,icon-checkmarkfor Results,icon-book/icon-keyfor Education’s Field/Keywords), and spacing rules (resume-role-row/resume-detail-row--firstfirst-row margin,resume-results-heading/resume-detail-row--tighttight-stack margin). - Same single-item collapse rule: a
results.itemslist with exactly one entry renders as inline text likeRole, instead of a heading + one-item list. (Today no Portfolio project hits this case — every Portfolio project has 2+results.items— but the rule applies automatically if that ever changes, since it lives in the shared template.) - Same level-4 list (
.resume-detail-list.resume-results-list) forresults.itemswhen there are 2 or more. Educationuses the exact same flat fields as Resume (area,studyType,gpa) — no divergence there.
Resume and Portfolio both use roles: { description, items } for current Work entries. Portfolio generally includes a description and fuller item list, while Resume keeps the same shape concise for quick scanning. _includes/pro/resume/work.html also retains the legacy flat role/results/growth fields for backward compatibility, so one template serves both documents without a Portfolio-specific include. See Data contract for the exact shape.
What Portfolio actually exercises more than Resume:
roles.itemsonProjects: every Portfolio project uses 2–4 role bullet items (rendered as a plain<ul class="work">), whereas Resume’sProjectsentries only useroles.description(plain text, no items). This is the one part of the shared component not yet promoted to the level-3/level-4.resume-meta-row+.resume-detail-listcontract — see Known limitations.skills.items: Portfolio projects list a tag stack of technologies per project (.skill-tags/.skill-tag), used far more consistently than in Resume.
Page layout
Portfolio uses a single content column (.column.column-3-0) rather than Resume’s two-column split (.column-3-5 / .column-2-5 for Projects/Work/Education vs Languages/Skills/Interests). page.sections in en/portfolio.md/ko/portfolio.md lists work, projects, skills, education, awards, publications, certifications, interests, languages, so the page is one long feed of cards under the shared header/contact block (pro/resume/header.html, pro/resume/buttons.html, pro/resume/basics.html, pro/resume/profiles.html).
Print and web parity
Portfolio uses the same local print-preview mechanism as Resume (?print=1, _sass/components/_print-preview.scss, no automatic window.print()), with one addition:
?cols=2adds aprint-2colclass to<body>. In_sass/components/_print-styles.scss, this makes.layout-portfolio .column-3-0a two-column CSS layout (column-count: 2), with each project’s section/heading kept from splitting across the column break. This is used when a longer Portfolio would otherwise print as many single- column A4 pages — the same card component still renders per project, just flowed into two columns instead of one.- PDF generation (
scripts/generate-pdf.js,pdf-config.yml) targets/en/portfolioand/ko/portfoliodirectly, independent of the?cols=2screen preview toggle — the actual PDF’s column behavior is whatever the compiled@media printrules resolve to for that route.
Content rules
All Resume content rules apply unchanged (see docs/resume/DESIGN.md). In addition, for Portfolio’s denser detail:
- Prefer 2–4 concise
roles.items/results.itemsentries per project; if a category would only ever have one entry, use the plaindescriptionfield instead of a one-itemitemslist (mirrors the single-item collapse rule already enforced forResults). skills.itemsper project should list concrete technologies (tools, frameworks, protocols), not restate the role/result text — they exist to be visually scannable tags, not prose.
Data contract
Portfolio uses its own data files, _data/portfolio-{en,ko}.yml. Education matches Resume’s shape exactly (see docs/resume/DESIGN.md). Work and Projects differ as follows:
labels:
role: Role # must be defined per language file; there is no cross-file fallback
results: Results
growth: Growth
area: Field
degree: Degree
gpa: GPA
work:
- company: string
headerIcon: emoji # optional; web only, omitted in print/PDF
position: string
website: url
startDate / endDate
roles:
description: string # single Role-style level-3 row
items: [{ header, content }] # rendered as a level-4 bullet list under that row
projects:
- headerIcon: emoji # optional; web only, omitted in print/PDF
position: string
company: string
website: url
startDate / endDate
roles:
description: string # short one-line summary (Role row)
items: [{ header, content }] # 2–4 bullets typical; plain <li>, not level-4 styled
results:
description: string # optional multi-clause summary (Results row)
items: [{ header, content }] # 2–4 bullets typical; level-4 styled list
skills:
description: string
items: [string]
labels.* is read as resume.labels.* by the shared includes, where resume resolves to whichever data_file the page declares (portfolio-en/portfolio-ko for Portfolio, resume-en/resume-ko for Resume). Each data file must define its own labels block in its own language — Portfolio does not inherit Resume’s labels, since they are two independent top-level YAML documents. Omitting it does not error; Liquid’s default:"Role" filter silently falls back to the English default regardless of page language, which is why this must be verified per data file rather than assumed from Resume’s file alone.
Validation: resume-lint
The same resume-lint run checks Portfolio alongside Resume — /ko/portfolio and /en/portfolio, in web, ?print=1, and ?print=1&cols=2 modes:
node .agents/skills/resume-lint/scripts/lint-resume-layout.js
It verifies the same three invariants as Resume (no wrapped rows, level-3 rows aligned to Project’s reference .resume-role-row, level-4 rows indented deeper than level-3), across every .resume-meta-row on the page — including Portfolio’s Work section, which is why the work.html regression described in the changelog below was caught. It does not check label language correctness (e.g. an English word appearing on the Korean page reads as “no wrapping issue” to the lint, since layout is unaffected) — that must still be checked visually or with a dedicated content assertion. In two-column print mode it verifies a successful, non-empty card render but permits natural wrapping because each column is intentionally narrower; one-line and absolute-x checks remain scoped to web and single-column print.
Known limitations / alternatives considered
resume-lintdoes not assert label language correctness (see previous section); it only measures layout. A missinglabels:block silently renders English defaults on the Korean page without failing the lint.Projects → roles.items(used heavily in Portfolio) is still an unstyled<ul class="work">list, not promoted to the level-3/level-4.resume-meta-row+.resume-detail-listcontract thatWork’s legacy shape now uses. Because Portfolio relies on it more than Resume, promoting it would benefit Portfolio the most if undertaken.- We considered giving Portfolio a denser, portfolio-specific card variant (e.g. smaller type scale to fit more per page) instead of reusing Resume’s card exactly. Rejected to keep a single source of truth for the card design and a single
resume-lintcontract instead of two parallel visual systems to keep in sync.
Changelog
- Portfolio project cards synchronized to the same four-level hierarchy fix applied to Resume’s
Work/Education(level-3.resume-meta-rowalignment with Project’s reference row). - Discovered and fixed a regression: restructuring
work.htmlto the flatrole/results/growthconvention had silently dropped support for Portfolio’s legacyroles.description/roles.itemsshape, blanking Portfolio’s entireWorksection content (title and dates still rendered; the role description and bullet list did not). Fixed by makingwork.htmlsupport both shapes, with the flat fields taking precedence. - Discovered and fixed a localization bug:
_data/portfolio-{en,ko}.ymlhad nolabels:block, soRole/Results/etc. rendered in English on the Korean Portfolio page (Liquid’sdefault:"Role"filter silently masked the missing key). Added alabels:block to both files matching Resume’s values per language. - Portfolio content audited and shortened wherever
resume-lintreported a wrapped row, preserving factual meaning (seeresume-lint’s procedure for the review discipline used). resume-lintscope extended to include/ko/portfolioand/en/portfolioin both web and print views, alongside Resume.