# Portfolio Phase 1: Foundation and Semantic SSR Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Produce a complete, responsive, pre-rendered portfolio whose content, navigation, metadata, and contact path work before any interactive scene code exists. **Architecture:** A strict public-input layer validates the four production inputs before Vue receives them. Vue renders one semantic document through the official server renderer; a Node prerender script emits final static HTML, metadata, sitemap, robots rules, and host examples, then the browser hydrates the identical immutable snapshot. **Tech Stack:** Node.js 22.22.1, npm 10.9.4, Vue 3, TypeScript, Vite, `vue/server-renderer`, Zod, Vitest, Vue Test Utils, Playwright **Spec:** `docs/superpowers/specs/2026-09-01-portfolio-journey-design.md` ## Global Constraints - The raw HTML contains the canonical H1, hybrid role, eight section headings, project and infrastructure evidence, Toolkit labels, and visible email link. - Vue components contain interface language only; all subject-matter and rendered accessibility copy comes from `PortfolioContent`. - Unknown input keys, extra asset files, invalid URLs, private links, and unmanifested asset IDs fail before rendering. - Preview configuration uses `https://portfolio.localhost`; production configuration is not created until Luca approves the real canonical host. - The private CV/evidence ledger and original media never enter the repository. - No scene, Web Audio, video autoplay, analytics, public source map, or third-party executable asset is introduced in this phase. --- ### Task 1: Initialize the private repository and testable Vue toolchain **Files:** - Create: `.gitignore` - Create: `.nvmrc` - Create: `package.json` - Create: `package-lock.json` - Create: `tsconfig.json` - Create: `vite.config.ts` - Create: `vitest.config.ts` - Create: `playwright.config.ts` - Create: `index.html` - Test: `tests/unit/toolchain.test.ts` **Interfaces:** - Consumes: empty project folder plus the approved spec. - Produces: npm scripts `dev`, `typecheck`, `test:unit`, `test:browser`, `build:client`, `build:ssr`, `prerender`, and `build:preview`. - [ ] **Step 1: Initialize local source control without configuring a remote** Run: ```bash git init -b main ``` Expected: `.git/` exists and `git remote -v` prints nothing. - [ ] **Step 2: Write the runtime pins and ignore rules** Create `.nvmrc` containing `22.22.1`. Create `.gitignore` with: ```gitignore node_modules/ dist/ dist-ssr/ coverage/ playwright-report/ test-results/ .env* config/site.production.json content/private/ inputs/original-assets/ *.map ``` - [ ] **Step 3: Install the exact toolchain into the lockfile** Run: ```bash npm init -y npm install vue three zod npm install --save-dev typescript vite @vitejs/plugin-vue vue-tsc @vue/server-renderer vitest @vue/test-utils happy-dom @playwright/test @axe-core/playwright eslint prettier ``` Expected: `package-lock.json` records exact resolved versions and `npm audit --omit=dev` completes without a Critical or High production finding. - [ ] **Step 4: Add strict compiler and test configuration** Use strict TypeScript with `noUncheckedIndexedAccess`, `exactOptionalPropertyTypes`, DOM and ES2023 libraries, `moduleResolution: "Bundler"`, and the `@/* → src/*` alias. Configure Vitest for `happy-dom`, global setup disabled, and coverage over `src/**/*.{ts,vue}`. Configure Playwright projects for desktop Chromium and mobile Chromium initially; Phase 3 expands the matrix. The `package.json` scripts must be: ```json { "scripts": { "dev": "vite --host 127.0.0.1", "typecheck": "vue-tsc --noEmit", "test:unit": "vitest run", "test:browser": "playwright test", "build:client": "vite build --outDir dist/client --manifest", "build:ssr": "vite build --ssr src/entry-server.ts --outDir dist-ssr", "prerender": "node scripts/prerender.mjs", "build:preview": "npm run typecheck && npm run test:unit && npm run build:client && npm run build:ssr && npm run prerender -- --config config/site.preview.json" } } ``` - [ ] **Step 5: Write and run the first failing toolchain test** ```ts import { describe, expect, it } from 'vitest'; describe('toolchain', () => { it('runs TypeScript tests', () => { expect(import.meta.env.MODE).toBe('test'); }); }); ``` Run `npm run test:unit`. Expected: PASS after the Vitest configuration is valid. - [ ] **Step 6: Commit the initialized foundation** ```bash git add .gitignore .nvmrc package.json package-lock.json tsconfig.json vite.config.ts vitest.config.ts playwright.config.ts index.html tests/unit/toolchain.test.ts git commit -m "chore: initialize portfolio toolchain" ``` --- ### Task 2: Define and enforce the four public production inputs **Files:** - Create: `src/content/types.ts` - Create: `src/content/schemas.ts` - Create: `src/content/load-production-inputs.ts` - Create: `src/content/asset-url.ts` - Create: `tests/fixtures/portfolio-content.valid.json` - Create: `tests/fixtures/asset-manifest.valid.json` - Create: `tests/fixtures/site-config.preview.json` - Test: `tests/unit/content-schemas.test.ts` - Test: `tests/unit/production-inputs.test.ts` **Interfaces:** - Consumes: four explicit URLs passed to `loadProductionInputs()`. - Produces: `PortfolioContent`, `PublicAssetManifest`, `SiteConfig`, `ProductionInputs`, `parsePortfolioContent()`, `parsePublicAssetManifest()`, `parseSiteConfig()`, `loadProductionInputs()`, and `assetUrl()`. - [ ] **Step 1: Write failing schema tests for the public boundary** Cover exact acceptance and rejection behavior: ```ts it('rejects a public link on a private project', () => { const input = structuredClone(validContent); input.projects.selected[0].visibility = 'private'; input.projects.selected[0].url = 'https://example.test/private'; expect(() => parsePortfolioContent(input)).toThrow(/private project.*url/i); }); it('rejects unknown site configuration keys', () => { expect(() => parseSiteConfig({ environment: 'preview', canonicalUrl: 'https://portfolio.localhost', buildId: 'preview-1', contactEmail: 'forbidden@example.test' })).toThrow(/unrecognized/i); }); it('rejects an asset reference absent from the manifest', () => { expect(() => validateProductionInputs(contentWithMissingAsset, manifest, site, rootUrl)) .toThrow(/unmanifested asset/i); }); ``` Run `npm run test:unit -- tests/unit/content-schemas.test.ts`. Expected: FAIL because the schemas do not exist. - [ ] **Step 2: Define stable public types** `src/content/types.ts` must define these roots and discriminants: ```ts export type SectionId = 'start' | 'profile' | 'experience' | 'infrastructure' | 'selected-builds' | 'additional-work' | 'toolkit' | 'contact'; export type ProjectVisibility = 'public' | 'private'; export type ProjectMaturity = 'learning' | 'experimental' | 'prototype' | 'work-in-progress' | 'active'; export interface PublicProject { id: string; title: string; visibility: ProjectVisibility; maturity: ProjectMaturity; summary: string; problem: string; contribution: string; technicalDecisions: readonly string[]; outcome: string; url?: string; assetIds: readonly string[]; } export interface PortfolioContent { identity: { name: 'Luca Pîslaru'; role: 'System Administrator & Full-Stack Developer'; valueProposition: string }; navigation: ReadonlyArray<{ id: SectionId; label: string }>; profile: { introduction: string; screeningFacts: ReadonlyArray<{ label: string; value: string }> }; experience: readonly ExperienceRecord[]; systemsOperations: SystemsCaseStudy; infrastructure: InfrastructureCaseStudy; projects: { selected: readonly PublicProject[]; additional: readonly PublicProject[] }; toolkit: readonly ToolkitGroup[]; contact: ContactContent; metadata: MetadataContent; sceneDescription: string; } export interface SiteConfig { environment: 'preview' | 'production'; canonicalUrl: string; buildId: string; } ``` Define the referenced `ExperienceRecord`, `SystemsCaseStudy`, `InfrastructureCaseStudy`, `ToolkitGroup`, `ContactContent`, and `MetadataContent` in the same file with only public fields described by the spec. - [ ] **Step 3: Implement strict Zod schemas and cross-record checks** All object schemas use `.strict()`. `parsePortfolioContent()` must assert the exact identity literals, exactly eight unique canonical navigation IDs, two primary selected builds plus the supporting research panel, generic private titles, HTTPS allowlisted social hosts, no URL on private projects, and no empty visible copy. `parseSiteConfig()` accepts only the three declared keys and validates preview or production URL rules. `parsePublicAssetManifest()` enforces unique IDs, generated safe filenames, lowercase SHA-256, media type, and dimensions/duration. Run the schema test. Expected: PASS. - [ ] **Step 4: Write failing closed-set loader tests** ```ts it('rejects an extra prepared file', async () => { await expect(loadProductionInputs(fixtureUrls.extraPreparedFile)).rejects.toThrow(/extra prepared asset/i); }); it('returns deeply frozen inputs', async () => { const inputs = await loadProductionInputs(fixtureUrls.valid); expect(Object.isFrozen(inputs.content.projects.selected)).toBe(true); expect(() => (inputs.site.buildId = 'mutated')).toThrow(); }); ``` - [ ] **Step 5: Implement `loadProductionInputs()` and `assetUrl()`** Use explicit paths, never directory discovery for subject matter: ```ts export interface ProductionInputLocations { contentJson: URL; assetManifestJson: URL; preparedAssetRoot: URL; siteConfigJson: URL; } export async function loadProductionInputs( locations: ProductionInputLocations ): Promise>; export function assetUrl( manifest: PublicAssetManifest, assetId: string, base = '/assets/' ): string; ``` The loader parses JSON with duplicate-key detection, validates every prepared file against the manifest, rejects links/special files/extra members, hashes bytes with SHA-256, and deep-freezes the result. Run both input test files; expected: PASS. - [ ] **Step 6: Commit the input contract** ```bash git add src/content tests/fixtures tests/unit/content-schemas.test.ts tests/unit/production-inputs.test.ts git commit -m "feat: define closed portfolio input contract" ``` --- ### Task 3: Create the approved public content projection **Files:** - Create: `content/public/portfolio-content.json` - Create: `content/public/asset-manifest.json` - Create: `config/site.preview.json` - Create: `scripts/materialize-empty-asset-set.mjs` - Create: `docs/content-approval-checklist.md` - Modify: `package.json` - Test: `tests/integration/public-content.test.ts` **Interfaces:** - Consumes: the supplied CV, project descriptions, public repository evidence, and Luca’s explicit approval of the public email/social URLs. - Produces: validated public JSON inputs with no evidence IDs, source paths, approval identities, original filenames, or private names. - [ ] **Step 1: Record the exact approval checklist before transcribing content** The checklist contains one row for identity, each screening fact, each experience entry, systems operations, each infrastructure evidence band, every project, every Toolkit item, contact field, metadata field, and asset record. Each row records `approved`, `omit`, or `needs Luca confirmation`; execution stops and asks Luca about any third state before public JSON is written. - [ ] **Step 2: Extract claims from the supplied CV and project evidence into a private scratch document** Use the PDF workflow to read `/home/luca/Documents/cv-en-septembrie-2026-np.pdf`. Keep the scratch file outside tracked project paths, compare every claim with the approved design, and do not copy phone, street address, birth information, or identifiers into public content. - [ ] **Step 3: Obtain explicit confirmation for the public contact fields** Confirm one public email and each optional GitHub, GitLab, and LinkedIn URL. Omit any unconfirmed social link from both visible content and metadata. Do not create `config/site.production.json` in this task. - [ ] **Step 4: Write the public content and empty-safe manifest** Use the exact generic private titles from the spec. Asset IDs may appear only when their Phase 3 sanitized derivatives exist; until then, use empty `assetIds` arrays and text-first case-study layouts. `config/site.preview.json` is exactly: ```json { "environment": "preview", "canonicalUrl": "https://portfolio.localhost", "buildId": "preview-semantic-v1" } ``` `materialize-empty-asset-set.mjs` validates that the current manifest contains zero assets, creates `inputs/prepared-assets/` when absent, and fails if that directory contains any member. Add `prepare:preview-assets` before the build commands in `build:preview`; Phase 3 replaces this command with the real asset preparation workflow. - [ ] **Step 5: Verify content rules in an integration test** ```ts it('contains approved identity and balanced evidence without prohibited surfaces', async () => { const { content } = await loadRepositoryPreviewInputs(); expect(content.identity).toEqual(expect.objectContaining({ name: 'Luca Pîslaru', role: 'System Administrator & Full-Stack Developer' })); expect(content.projects.selected.filter(p => p.visibility === 'private').every(p => !p.url)).toBe(true); expect(JSON.stringify(content)).not.toMatch(/Arch Linux|CV download|heatmap|expert|percent/i); }); ``` Run `npm run test:unit -- tests/integration/public-content.test.ts`. Expected: PASS. - [ ] **Step 6: Commit only the public projection and checklist** ```bash git add content/public config/site.preview.json scripts/materialize-empty-asset-set.mjs docs/content-approval-checklist.md tests/integration/public-content.test.ts package.json git commit -m "content: add approved public portfolio projection" ``` --- ### Task 4: Build the semantic Vue document and section components **Files:** - Create: `src/app.ts` - Create: `src/entry-client.ts` - Create: `src/entry-server.ts` - Create: `src/App.vue` - Create: `src/components/AppShell.vue` - Create: `src/components/SiteNavigation.vue` - Create: `src/components/PortfolioSection.vue` - Create: `src/components/ExperienceTimeline.vue` - Create: `src/components/InfrastructureDiagram.vue` - Create: `src/components/ProjectCaseStudy.vue` - Create: `src/components/ProjectCard.vue` - Create: `src/components/ToolkitGrid.vue` - Create: `src/components/ContactDestination.vue` - Test: `tests/unit/AppShell.test.ts` - Test: `tests/unit/components.test.ts` **Interfaces:** - Consumes: a readonly `ProductionInputs` injection. - Produces: `createApp(inputs)`, `render(url, inputs)`, and semantic components with typed content props. - [ ] **Step 1: Write failing SSR/component tests** ```ts it('renders one H1 and all canonical section headings', async () => { const html = await render('/', validInputs); expect(count(html, ' { const html = await render('/', validInputs); expect(html).toContain('href="#contact"'); expect(html).toContain('href="mailto:'); }); ``` Run the tests. Expected: FAIL because app modules do not exist. - [ ] **Step 2: Implement universal app creation and server rendering** ```ts export function createApp(inputs: Readonly) { const app = createSSRApp(App, { inputs }); app.provide(productionInputsKey, inputs); return app; } export async function render(url: string, inputs: Readonly) { return renderToString(createApp(inputs)); } ``` `entry-client.ts` loads the identical serialized input snapshot and calls `.mount('#app')` to hydrate. It must not fetch copy after load. - [ ] **Step 3: Implement semantic sections** Use `
`, `