A spreadsheet-authored, static-first portfolio with typed content validation, a restrained glass interface, and integrity-checked Cloudflare deployment.
Smart Portfolio is Nicolas Gioanni's personal software engineering, research, and cybersecurity portfolio. Its public content is maintained independently from the interface through one workbook, then validated and converted to typed JSON before Next.js builds the site.
The core routes are static. A visitor's browser receives generated pages and never requests spreadsheet content. GitHub Actions deploys only candidates that pass content validation, documentation checks, lint, type checking, tests, a static build, and artifact-integrity checks. Runtime request handling is isolated to the contact verification and delivery Functions.
The workbook is a lightweight content-authoring surface rather than a general-purpose content management system. It holds only reviewed public content and has no control over application code, secrets, or deployment.
| Capability | Purpose |
|---|---|
| Workbook-authored content | Lets the portfolio owner update public content without editing React components. |
| Strict XLSX contract | Rejects missing, unexpected, hidden, duplicate-normalized, malformed, or invalid worksheets before build. |
| Typed generated content | Gives selectors and components one validated JSON shape instead of spreadsheet rows. |
| Semantic content hashing | Compares a canonical normalized content subset so workbook metadata and harmless formatting do not trigger deployments. |
| Static-first delivery | Keeps portfolio data out of request-time APIs and serves the core experience from Cloudflare's edge. |
| Exact tested artifact | Deploys the same export that passed the quality gate, with a SHA-256 manifest checked before upload. |
| Focused interaction | Adds theme, navigation, skills, recommendation, and motion behavior without moving content rendering into the browser. |
| Isolated contact flow | Uses final-submit Turnstile verification, a short-lived signed ticket, strict validation, a pseudonymous rolling quota, and sequential Resend delivery. |
| Preview and production isolation | Keeps develop deployments, browser keys, origins, hostnames, and production aliases separate. |
flowchart TB
Owner[Portfolio owner] --> Workbook[Public-safe XLSX workbook]
Workbook --> Actions[GitHub Actions]
Actions --> Validate[Validate and normalize]
Validate --> Json[Typed JSON and content hash]
Json --> Build[Next.js static export]
Build --> Artifact[Integrity-checked artifact]
Artifact --> Pages[Cloudflare Pages]
Pages --> Browser[Visitor browser]
Browser --> Verify[/api/contact/verify]
Verify --> Turnstile[Cloudflare Turnstile]
Browser --> Submit[/api/contact]
Submit --> DNS[Mail-domain DNS]
Submit --> D1[Cloudflare D1 quota]
Submit --> Resend[Resend]
The workbook participates only at build time. Cloudflare Pages serves the static export, while the two contact paths form one isolated runtime boundary. The verification Function validates one fresh Turnstile token operation and issues a signed ticket; the delivery Function validates that ticket and the contact payload before calling Resend.
Deep dives:
- The portfolio owner edits the reviewed public workbook.
- GitHub Actions downloads one anonymous XLSX snapshot.
- The generator verifies the exact visible worksheet set, headers, rows, and file constraints.
- Source rows are normalized and validated against the typed content contract.
- A deterministic SHA-256 hash is calculated from a canonical normalized content subset.
- Eligible scheduled and non-forced manual runs become successful no-ops when the active production hash matches.
- Changed or forced candidates run the complete quality gate.
- Next.js builds from the already-tested generated snapshot without downloading again.
- The exact static artifact is hashed, transferred, verified, uploaded, and smoke-tested.
The remote workbook has exactly nine visible sheets:
| Sheet | Purpose |
|---|---|
profile |
Identity, biography, role, image, and profile references. |
links |
Header, Home, social, email, and repository destinations. |
research |
Research summaries, evidence, skills, impact, and resources. |
projects |
Project summaries, problem and solution context, skills, and links. |
experience |
Work, research, teaching, and leadership history. |
recommendations |
Recommendation text, attribution, source, and display order. |
education |
Institution, program, dates, supporting facts, and logo. |
skills |
Category, icon, proficiency, summary, and evidence of use. |
site_settings |
Public UI settings, limits, legal metadata, and feature switches. |
Worksheet titles are matched by trimmed lowercase text, so capitalization and physical order do not matter. Internal spaces, hyphens, spelling changes, extra sheets, duplicate-normalized sheets, and hidden sheets are invalid.
The public /resume route provides private-request instructions only. It does not authorize a resume workbook sheet, and any such extra sheet is rejected.
The workflow performs an ordinary anonymous HTTPS download. It uses no Google API, Drive connector, OAuth grant, service account, or Google account access. Local development can use checked-in CSV templates; production candidates enable strict remote mode and cannot fall back to template content.
See Content pipeline, sheet schema, and local content editing.
The interface uses glass-inspired surfaces as restrained hierarchy, not as a full-screen effect. Text sits on quiet backgrounds, large panels use bounded blur, and nested cards reduce visual weight.
- Navy, Light, and Dark themes share semantic color and interaction tokens.
- A floating desktop header provides profile preview, navigation, social links, and theme selection. At
980pxand below, a safe-area-aware bottom dock replaces it with one swipeable rail containing the routes, GitHub, LinkedIn, Email, and theme controls. - Home combines a profile overview with experience, education, research, projects, skills, and recommendations.
- The role line can rotate through spreadsheet-configured titles while exposing one stable accessible label.
- Skills with complete evidence open keyboard-managed dialogs; incomplete legacy rows remain static badges.
- Experience offers one page-wide plain-language or technical view and expandable, role-specific evidence chapters.
- Long recommendations expand in place with measured overflow and reduced-motion support.
- The footer is an explicit progressive disclosure that remains in normal document flow.
- Route skeletons mirror final layouts without hiding available static content.
- Focus-visible treatments, ARIA state, keyboard behavior, semantic headings, and responsive reflow are component requirements.
See Design system, Accessibility, Animation guidelines, and Skeleton loading guidelines.
Page routes are statically exported. This table covers the visitor flows documented in this guide; hydration is limited to the interactions noted below.
| Route | Purpose | Rendering and interaction |
|---|---|---|
/ |
Portfolio overview | Static content with role, skills, recommendation, motion, and shared-shell hydration. |
/experience |
Detailed experience showcase | Logo-led role cards with a page-wide audience switch and inline evidence disclosures. |
/research |
Detailed research work | Static evidence cards and verified external resources. |
/projects |
Detailed engineering projects | Static evidence cards with project-skill dialogs where configured. |
/recommendations |
Complete recommendation collection | Static cards with expandable long quotes; navigation is content-dependent. |
/resume |
Private resume request instructions | Static request page with contact and email actions; no resume file or workbook sheet is published. |
/contact |
Prioritized contact workflow | Static form shell that calls the two same-origin Pages Functions; marked noindex. |
/privacy |
Privacy notice | Static footer-only legal route. |
/terms |
Terms and accuracy notice | Static footer-only legal route. |
/security |
Security and disclosure notice | Static footer-only legal route. |
Primary navigation is assembled by navigationItems.ts from the central route registry. Resume is always present, Recommendations is included only when configured, and Contact plus legal routes are intentionally available through the footer. Mobile preserves the canonical route order, then continues through the configured social and theme controls in one native horizontal rail. Edge fades expose hidden overflow, and direct interaction pauses drift until five seconds of inactivity have elapsed.
| Area | Technology | Role in this repository |
|---|---|---|
| Application | Next.js App Router and React | Build route components into a static export and hydrate focused interactions. |
| Language | TypeScript | Define content, component, script, and test contracts under strict checking. |
| Typography | Space Grotesk through next/font |
Self-manage the primary font with system fallbacks. |
| Workbook parsing | ExcelJS | Parse the complete anonymous XLSX snapshot and inspect worksheet state and cells. |
| Local source parsing | csv-parse |
Read checked-in content templates through the same row model. |
| Content safety | Custom normalizers and validators | Enforce required data, exact references, URL rules, grouped fields, and UI invariants. |
| Interface | CSS custom properties and reusable glass primitives | Share semantic themes, geometry, surfaces, and interaction states without a UI framework. |
| Icons | simple-icons and local semantic icons |
Render configured technology and destination marks. |
| Browser behavior | IntersectionObserver and native browser APIs | Drive focused reveal, footer, responsive, and preference behavior. |
| Quality | ESLint, TypeScript, Vitest, Testing Library, jsdom, and Playwright Chromium | Verify code, types, content, components, Functions, scripts, CSS contracts, automation, responsive navigation, and footer lifecycle behavior in a real browser. |
| Automation | GitHub Actions | Own candidate selection, verification, artifact transfer, deployment, and scheduled checks. |
| Hosting | Cloudflare Pages and Wrangler | Serve the static export and compile the isolated Pages Functions through Direct Upload. |
| Contact | Pages Functions, Turnstile, D1, DNS, and Resend HTTPS API | Assess final-submit abuse risk, validate mail routing, enforce a keyed rolling quota, and deliver two sequential messages. |
- Git
- Node.js 22.13 or newer
- npm
git clone https://github.com/nicolasgioanni/Smart-Porfolio.git
cd Smart-Porfolio
npm run setup:local
npm run dev:smartsetup:local verifies the Node.js version, creates a local .env from the placeholder example when missing, uses npm ci only when dependencies are missing or stale, and regenerates content only when needed. dev:smart starts at port 3000 or the first available port through 3010 and prints the selected URL.
git clone https://github.com/nicolasgioanni/Smart-Porfolio.git
cd Smart-Porfolio
npm run setup:local:node
npm run dev:smart:nodenpm ci
npm run generate:content
npm run devWith no workbook URL configured, generation uses the local templates. The standard Next.js development server opens at http://localhost:3000 unless another port is provided.
To test the built static export and contact Functions together, configure development values in the ignored .env and run:
npm run dev:pagesThis command builds first, applies pending migrations to Wrangler's local D1 state, then starts Pages development with the address printed by Wrangler. It does not require production credentials or a remote database.
See Local development for command flags, setup-state behavior, Function configuration, and safe cleanup.
- Edit the matching CSV under
src/content/templates/. - Place approved public assets under
public/and use safe root-relative paths. - Run
npm run generate:content. - Run the development server and inspect Home plus the affected detail route.
- Run focused tests and
npm run verify.
Do not edit src/content/generated/portfolio.generated.json as the authoring source. Regeneration replaces it.
- Maintain one anonymously downloadable workbook with the nine exact sheets.
- Keep row-one headers aligned with the checked-in templates.
- Review all values and workbook metadata for anonymous public release.
- Configure the XLSX export locator in the build environment.
- Use strict mode for deployment candidates.
See Content sheet schema for fields and Content replacement checklist before publishing a broad content update.
Never copy real values into tracked documentation or .env.example.
| Variable | Placement | Purpose |
|---|---|---|
PORTFOLIO_WORKBOOK_URL |
Local .env; GitHub Actions secret for automatic log redaction |
Anonymous HTTPS XLSX locator. It is not a Google credential. |
PORTFOLIO_REQUIRE_REMOTE_CONTENT |
Local .env when needed; workflow sets it directly |
Fails generation instead of allowing template fallback. |
| Variable | Placement | Purpose |
|---|---|---|
NEXT_PUBLIC_TURNSTILE_SITE_KEY |
Local .env; GitHub repository variable for production builds |
Public Turnstile widget key included in the client bundle. |
NEXT_PUBLIC_TURNSTILE_PREVIEW_SITE_KEY |
GitHub repository variable | Separate public key for develop preview builds, with no production fallback. |
| Variable | Placement | Purpose |
|---|---|---|
TURNSTILE_SECRET_KEY |
Local .env; Cloudflare encrypted secret |
Server-side Siteverify credential and source for the derived ticket key. |
RESEND_API_KEY |
Local .env; Cloudflare encrypted secret |
Resend API authentication. |
CONTACT_RECIPIENT_EMAIL |
Local .env; Cloudflare encrypted secret |
Private owner destination. |
TURNSTILE_ALLOWED_HOSTNAMES |
Local .env; reviewed Wrangler variable |
Exact accepted Siteverify hostnames. |
CONTACT_ALLOWED_ORIGINS |
Local .env; reviewed Wrangler variable |
Exact same-origin request allowlist. |
CONTACT_FROM_EMAIL |
Local .env; reviewed Wrangler variable |
Verified Resend sender identity. |
CONTACT_REPLY_TO_EMAIL |
Local .env; reviewed Wrangler variable |
Fixed public reply-to for visitor confirmations. |
CONTACT_RATE_LIMIT_DB is a Wrangler D1 binding rather than an environment variable. Production and preview use distinct remote databases whose reviewed UUIDs are pinned in wrangler.jsonc; local Pages development uses the contact-rate-limit-local emulation identifier.
Deployment credentials and immutable Cloudflare target variables are documented in Deployment. Secret placement and logging rules are documented in Security.
| Command | Scope |
|---|---|
npm run docs:check |
Markdown structure, local links and images, path case, private URL patterns, placeholders, and excluded local-only references. |
npm run generate:content |
Source loading, normalization, validation, hashing, and generated snapshot. |
npm run db:migrate:local |
Apply tracked migrations to Wrangler's local D1 state. |
npm run lint |
Source, test, script, and configuration lint rules with zero warnings. |
npm run typecheck |
Strict TypeScript checking without output. |
npm run test:footer |
Focused footer behavior and style regressions. |
npm run test:navigation |
Focused header, route rail, responsive navigation, and style-contract regressions. |
npm run test:e2e:navigation |
Chromium coverage for the mobile bottom dock and unchanged desktop header. |
npm run test:e2e:footer |
Chromium coverage for compact first render, route transitions, restored scroll, and user-scroll expansion. |
npm run test |
Complete Vitest suite, including components, content, Functions, scripts, and automation contracts. |
npm run build |
Regenerate content, create the static export, and write deployment version metadata. |
npm run build:generated |
Build the existing generated snapshot without fetching content again. |
npm run verify |
Documentation check, lint, typecheck, full tests, and normal build in sequence. |
verify does not call the focused Vitest scripts separately because the full suite already includes those files. CI keeps named footer and navigation gates, then installs Chromium and runs both Playwright suites. Run npm run test:e2e:navigation and npm run test:e2e:footer separately during local verification because verify does not install or launch a browser.
Pull requests generate from checked-in templates without deployment credentials, then run the complete verification path and static build. Deployable branch candidates use one strict remote snapshot. Artifact checks and live smoke tests run as deployment-specific stages.
See Testing, Quality checklist, and Performance checklist.
The deployment design makes GitHub Actions the sole deployment owner. Operators must keep Cloudflare Pages Git integration disabled so provider-side builds cannot bypass repository verification.
| Event | Result |
|---|---|
Pull request to main or develop |
Verify a local-template snapshot; never deploy. |
Push to develop |
Verify one strict workbook snapshot and deploy only the develop preview. |
Push to main |
Verify one strict workbook snapshot and deploy production. |
| Daily schedule | Compare the canonical normalized content subset with production; verify and deploy only when changed. |
| Manual dispatch | Target current main; forced mode bypasses only the unchanged optimization. |
Configured production URLs are nicolasmgioanni.dev and Cloudflare's assigned smart-portfolio-bds.pages.dev domain. The configured stable preview alias is develop.smart-portfolio-bds.pages.dev.
The workflow checks that a production candidate still matches current main, verifies the downloaded artifact, validates the environment-specific D1 binding, applies pending migrations, runs pinned local Wrangler from repository root, and smoke-tests the deployed root, robots file, sitemap, content manifest, integrity manifest, and GET rejection from both contact Functions. Generated production content and deployment state are not committed after upload. The active /content-version.json records current deployed content and candidate metadata; it does not prove that a post-upload smoke test succeeded or that a remote D1 migration is active.
See Deployment for setup and Operations for event behavior, no-ops, retries, manifests, and rollback considerations.
The contact page is static, but its submission path crosses a narrow server trust boundary:
- The visitor completes the three-step name, contact-details, and review wizard while the browser prepares an interaction-only Turnstile widget with a public site key.
- The final Send action locks the reviewed payload and executes the widget, which stays hidden unless Cloudflare requires interaction.
/api/contact/verifyaccepts JSON POST from an exact allowed origin, verifies the fresh token's success, action, hostname, and submission-bound custom data, then sets a short-lived signed host-only ticket. A separate operation UUID scopes one bounded retry for transient Siteverify failures./api/contactvalidates method, media type, origin, request size, strict fields, timing, honeypot, acknowledgements, ticket binding, and the submitted address's mail-domain routing.- D1 reserves one of two slots for a keyed normalized address during a rolling 24-hour window, without storing raw contact fields.
- Resend accepts the visitor confirmation first and then the owner notification, each with a separate submission-scoped idempotency key.
The Functions return specific safe error codes with generic provider details, set their own no-store and security headers, and keep the recipient and provider credentials server-side. Full submissions are not stored in D1; it contains only the submission UUID, address HMAC, and reservation timestamps. Request bodies and personal fields must not be logged.
Every new logical message requires a fresh single-use Turnstile token. A valid 30-minute ticket can support only retries of the same locked delivery identity and payload; it cannot authorize another new message.
The D1 quota limits a supplied address but cannot authenticate its owner or collapse every alias. Cloudflare WAF rate limiting for both JSON paths remains an operator-managed defense in depth. Repository code and tests cannot prove the live rule or the response customization available on the active Cloudflare plan.
See Contact system, Security, and Security checklist.
.github/ GitHub Actions workflow
docs/ Guides, references, and checklists
functions/ Cloudflare contact verification and delivery
migrations/ Versioned Cloudflare D1 contact-rate schema
public/ Static assets, headers, and Function route allowlist
scripts/ Content, local automation, manifests, smoke checks, tests
tests/e2e/ Playwright browser regressions
src/app/ Static Next.js routes and loading files
src/components/ Layout, navigation, theme, glass, portfolio, and contact UI
src/content/ Types, local templates, and generated snapshot
src/lib/ Content, CSV, formatting, and theme helpers
src/styles/ Semantic tokens and focused CSS layers
See Project structure for route ownership, component groups, styling responsibilities, tests, and a change-location matrix.
The complete reader-oriented index is docs/README.md.
| Goal | Guides |
|---|---|
| Understand the system | Architecture and project structure |
| Edit content | Content pipeline, sheet schema, and local editing |
| Work locally and test | Local development and testing |
| Change the interface | Design system, accessibility, and animation |
| Deploy and operate | Deployment, operations, and troubleshooting |
| Review security | Security, contact system, and security checklist |
| Extend safely | Maintenance and engineering standards |
Smart Portfolio source code is available under the MIT License.
Copyright 2026 Nicolas Gioanni.