24 KiB
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, andbuild:preview. -
Step 1: Initialize local source control without configuring a remote
Run:
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:
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:
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:
{
"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
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
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(), andassetUrl(). -
Step 1: Write failing schema tests for the public boundary
Cover exact acceptance and rejection behavior:
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:
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
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()andassetUrl()
Use explicit paths, never directory discovery for subject matter:
export interface ProductionInputLocations {
contentJson: URL;
assetManifestJson: URL;
preparedAssetRoot: URL;
siteConfigJson: URL;
}
export async function loadProductionInputs(
locations: ProductionInputLocations
): Promise<Readonly<ProductionInputs>>;
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
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:
{
"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
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
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
ProductionInputsinjection. -
Produces:
createApp(inputs),render(url, inputs), and semantic components with typed content props. -
Step 1: Write failing SSR/component tests
it('renders one H1 and all canonical section headings', async () => {
const html = await render('/', validInputs);
expect(count(html, '<h1')).toBe(1);
for (const label of ['PROFILE', 'EXPERIENCE', 'INFRASTRUCTURE', 'SELECTED BUILDS', 'ADDITIONAL WORK', 'TOOLKIT', 'CONTACT']) {
expect(html).toContain(label);
}
});
it('renders native links and a visible email without JavaScript', async () => {
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
export function createApp(inputs: Readonly<ProductionInputs>) {
const app = createSSRApp(App, { inputs });
app.provide(productionInputsKey, inputs);
return app;
}
export async function render(url: string, inputs: Readonly<ProductionInputs>) {
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 <header>, <nav aria-label="Portfolio sections">, <main>, seven named content sections plus Start, and <footer>. PortfolioSection accepts id, index, eyebrow, and heading; it renders a real <section aria-labelledby> and no subject-matter default strings. The infrastructure diagram is inline semantic SVG with role="img", a visible caption, and a text list equivalent.
- Step 4: Implement accessible project and Toolkit structures
Primary builds use <article> and real heading levels. Private projects never render link containers. Toolkit icons are aria-hidden="true"; each visible label is announced once. Contact renders the visible email plus mailto: and verified social links with visible labels and safe rel attributes.
- Step 5: Run unit tests and type checking
Run:
npm run typecheck
npm run test:unit -- tests/unit/AppShell.test.ts tests/unit/components.test.ts
Expected: PASS with no hydration-only branch in server code.
- Step 6: Commit the semantic application
git add src/app.ts src/entry-client.ts src/entry-server.ts src/App.vue src/components tests/unit/AppShell.test.ts tests/unit/components.test.ts
git commit -m "feat: render semantic portfolio document"
Task 5: Add the restrained responsive information layer
Files:
- Create:
src/styles/tokens.css - Create:
src/styles/base.css - Create:
src/styles/layout.css - Create:
src/styles/components.css - Create:
src/styles/print.css - Modify:
src/App.vue - Test:
tests/browser/responsive-layout.spec.ts
Interfaces:
-
Consumes: semantic class names and section IDs from Task 4.
-
Produces: information-layer CSS that remains legible over a static background at 320–2560 CSS pixels.
-
Step 1: Write failing geometric browser assertions
for (const viewport of [{ width: 320, height: 800 }, { width: 768, height: 1024 }, { width: 1440, height: 900 }]) {
test(`no horizontal overflow at ${viewport.width}`, async ({ page }) => {
await page.setViewportSize(viewport);
await page.goto('/');
expect(await page.evaluate(() => document.documentElement.scrollWidth <= document.documentElement.clientWidth + 1)).toBe(true);
});
}
- Step 2: Define design tokens and base typography
Use CSS custom properties for deep navy, near-black panels, off-white text, cyan technical accents, amber actions, focus ring, spacing, and type scale. Body copy uses a system/locally hosted neutral sans fallback. Route labels use a restrained monospace stack. Do not add body-text glow, glass blur, or scanlines to content.
- Step 3: Implement responsive section layouts
At 1280px and wider, panels occupy 40–45% and alternate sides while leaving the road center unobstructed. At 768–1279px, panels use a stable two-column/centered rhythm. Below 768px, content becomes one column, decorative HUD density drops, and controls stay at least 44×44 CSS pixels.
- Step 4: Add print and no-motion presentation
The print stylesheet removes decorative world layers and interactive controls, expands content, preserves URLs where useful, and labels the output as a portfolio—not a résumé. prefers-reduced-motion removes non-essential transitions from the information layer.
- Step 5: Run layout tests and inspect screenshots
Run npm run test:browser -- tests/browser/responsive-layout.spec.ts. Expected: all viewports pass and Playwright screenshots show readable panels without horizontal overflow.
- Step 6: Commit the information layer
git add src/styles src/App.vue tests/browser/responsive-layout.spec.ts
git commit -m "feat: add responsive professional information layer"
Task 6: Implement exact prerendering, metadata, and crawlable artifacts
Files:
- Create:
src/security/serialize-json-ld.ts - Create:
src/content/metadata.ts - Create:
scripts/prerender.mjs - Create:
scripts/verify-artifacts.mjs - Modify:
index.html - Modify:
vite.config.ts - Test:
tests/unit/metadata.test.ts - Test:
tests/integration/prerender.test.ts
Interfaces:
-
Consumes: server render entry, Vite client manifest, validated production inputs.
-
Produces:
dist/index.html,dist/sitemap.xml,dist/robots.txt,dist/_headers.example, anddist/_redirects.example. -
Step 1: Write failing metadata and raw-HTML tests
Assert exact H1/role/title identity equality, one canonical URL, escaped JSON-LD, optional sameAs, production versus preview robots behavior, and presence of all section content before <script type="module">.
expect(html).toContain('<h1');
expect(html.indexOf('Luca Pîslaru')).toBeLessThan(html.indexOf('<script'));
expect(jsonLd.name).toBe('Luca Pîslaru');
expect(jsonLd.jobTitle).toBe('System Administrator & Full-Stack Developer');
- Step 2: Implement safe metadata generation
serializeJsonLd(value) uses JSON.stringify() then replaces < with \u003c, > with \u003e, & with \u0026, U+2028, and U+2029. Metadata derives name/role/description/social values from PortfolioContent and URLs from SiteConfig; it never reads environment variables for copy.
- Step 3: Implement the Node ESM prerender script
The script accepts exactly --config <path>, loads the four explicit input locations, imports dist-ssr/entry-server.js, renders /, reads dist/client/.vite/manifest.json, injects markup/preloads/metadata into the template, computes the JSON-LD CSP hash, and writes final output. Unknown CLI arguments or missing files fail with a non-zero exit.
- Step 4: Implement artifact verification
verify-artifacts.mjs parses final HTML and fails if identity, any canonical section ID, visible email, infrastructure evidence, selected builds, Toolkit labels, canonical link, JSON-LD, sitemap, or robots file is absent; it also fails on source-map files and prohibited private-name fixtures.
- Step 5: Run the complete preview build
Run npm run build:preview. Expected: PASS and a complete static dist/index.html usable when client scripts are blocked.
- Step 6: Commit the prerender contract
git add src/security src/content/metadata.ts scripts index.html vite.config.ts tests/unit/metadata.test.ts tests/integration/prerender.test.ts
git commit -m "feat: prerender crawlable portfolio artifacts"
Task 7: Prove semantic, no-JavaScript, and keyboard usability
Files:
- Create:
tests/browser/semantic.spec.ts - Create:
tests/browser/no-javascript.spec.ts - Create:
tests/browser/keyboard.spec.ts - Create:
tests/browser/hydration.spec.ts - Create:
tests/helpers/serve-dist.ts - Modify:
playwright.config.ts
Interfaces:
-
Consumes: Phase 1 preview build.
-
Produces: a release-blocking semantic/browser baseline for later phases.
-
Step 1: Write raw semantic assertions
Verify one H1, logical headings, main/nav/footer landmarks, native anchor destinations, no private-project anchors, visible email, and complete text with JavaScript disabled.
- Step 2: Write keyboard traversal assertions
Tab from the skip link through navigation and contact. Each focus indicator must be visible and unobscured. Activating a section link focuses its heading only after explicit navigation; manual page scrolling does not move focus.
- Step 3: Write hydration integrity assertions
Capture console errors and DOM text before/after hydration. Fail on hydration mismatch, unhandled rejection, duplicate landmark, or subject-matter text mutation.
- Step 4: Run all Phase 1 gates
npm run typecheck
npm run test:unit
npm run build:preview
npm run test:browser -- tests/browser/semantic.spec.ts tests/browser/no-javascript.spec.ts tests/browser/keyboard.spec.ts tests/browser/hydration.spec.ts
Expected: every command passes.
- Step 5: Commit the browser baseline
git add tests/browser tests/helpers playwright.config.ts
git commit -m "test: lock semantic and no-script portfolio behavior"
Phase 1 completion checkpoint
Open dist/index.html with scripts disabled and confirm the full hiring-manager reading path, balanced systems/software evidence, and contact action. Do not begin Phase 2 until Luca approves this text-first build and its content contains no unapproved personal detail.