A spreadsheet-authored, static-first portfolio with typed content validation, a layered solid-surface 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 an upfront Turnstile gate, 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 accepts one complete anonymous XLSX snapshot, using at most two bounded download attempts.
- 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 only when the active production content hash and commit SHA both match the candidate.
- 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, resources, local graphical abstracts, and optional self-hosted video. |
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 solid semantic surface tiers as restrained hierarchy. Text sits on quiet backgrounds, Light layers off-white, light-gray, and blue-gray surfaces, Dark layers charcoal and slate, and nested cards reduce visual weight with borders and neutral elevation instead of blur, gradients, or glow.
- Light, My mode, and Dark themes use separate warm, signature-navy, and graphite palettes through shared semantic color and interaction tokens. With no saved override, System follows the visitor's device light or dark preference and updates when that preference changes.
- 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. Native hard clipping exposes scrollable overflow without a visual fade, 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 surface primitives | Share semantic themes, geometry, solid surface tiers, 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, recommendation overlays, 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 | Gate form access, validate mail routing, enforce a keyed rolling quota, and deliver two sequential messages. |
- Git
- Node.js 22.13 or newer.
.nvmrcselects Node 22,package.jsondeclares the minimum version, and the setup helper enforces it. - npm, which is installed with Node.js.
package.json declares the application and development dependencies, while package-lock.json pins the exact dependency tree used by npm ci and CI. These files are this project's equivalent of a dependency requirements file; do not add a separate requirements.txt.
Windows:
git clone https://github.com/nicolasgioanni/Smart-Porfolio.git
cd Smart-Porfolio
npm run setup:localCross-platform:
git clone https://github.com/nicolasgioanni/Smart-Porfolio.git
cd Smart-Porfolio
npm run setup:local:nodesetup: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 local content when needed. To deliberately regenerate from the selected local source after setup, run npm run setup:local:node -- --force-generate.
For static UI work, use the smart server:
npm run dev:smartnpm run dev:smart:nodeIt starts at port 3000 or the first available port through 3010 and prints the selected URL. The standard npm path is also available:
npm 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.
Continue with Local development for the complete environment setup, UI versus Pages testing, command flags, verification, and safe cleanup. Contributors should then follow the Agent workflow; deployment operators should use Deployment.
- 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:priority.
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 |
Regenerate Next route types, then run 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 page entrance, the mobile bottom dock, and the 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:e2e:recommendations |
Chromium coverage for stable desktop card overlays, responsive natural flow, dismissal, and reduced motion. |
npm run test:priority |
Focused documentation, trust-boundary, content, shared-interface, routing, and architecture contracts selected for pull requests. |
npm run test:e2e:priority |
Chromium skeleton alignment, navigation, footer, and mocked contact-flow regression coverage. |
npm run test |
Complete Vitest suite, including components, content, Functions, scripts, and automation contracts. |
npm run test:e2e:full |
Every Playwright specification, including the full three-part skeleton regression suite. |
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:priority |
Documentation check, lint, typecheck, priority unit and browser suites, and normal build. |
npm run verify:full |
Documentation check, lint, typecheck, complete unit and browser suites, and normal build. Run on Ubuntu 24.04 before deployment. |
npm run verify |
Compatibility local gate: documentation check, lint, typecheck, full Vitest suite, and normal build. |
Use verify:priority for routine pull-request work after installing Chromium. Use verify:full for a release candidate on Ubuntu 24.04; it includes the complete Playwright suite and Linux-only zero-difference skeleton visual comparison. The compatibility verify command does not install or launch a browser.
Pull requests generate from checked-in templates without deployment credentials, then run the priority verification path and static build. Deployable branch candidates use one strict remote snapshot and the full verification path. 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 content hash and commit SHA with production; verify and deploy when either differs. |
| Manual dispatch | Target current main; forced mode bypasses only the exact-candidate no-op 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 a visible Turnstile gate before any contact fields are shown.
/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.- Successful verification shows a brief success state, enables Continue, and opens the form automatically after 500 milliseconds unless the visitor continues sooner. Form timing begins when the three-step name, contact-details, and review wizard opens.
- The review step requires two acknowledgements, and the message is limited to 500 characters in both browser and server validation.
- The final Send action locks the reviewed payload.
/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 and binds same-ID retries to an opaque keyed payload fingerprint, 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, opaque keyed full-payload fingerprint, and reservation timestamps. Request bodies and personal fields must not be logged.
Every new logical message requires a fresh single-use Turnstile token before its fields open. A valid 30-minute ticket can support only the same locked delivery and its retries; 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. Repository-local implementation guidance is in Agent workflow.
| 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.