52 KiB
Portfolio Content, SEO, and Project Media 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: Publish an evidence-led, privacy-safe portfolio that positions Luca Pîslaru equally as a System Administrator and Full-Stack Developer and integrates approved project media without changing the eight-section journey.
Architecture: Keep the existing pre-rendered Vue/Three.js single-page architecture and canonical section contract. Extend the typed content boundary with availability, a featured infrastructure operation, and project media layouts; continue routing every binary through the existing private-source, OCR, hash, approval, manifest, and exact-closure pipeline. Metadata remains derived from the same public content and site configuration used to render the page.
Tech Stack: Vue 3, TypeScript, Zod, Vite SSR/prerendering, Vitest, Playwright, Sharp, existing asset privacy/publication scripts
Spec: docs/superpowers/specs/2026-09-04-portfolio-content-seo-media-design.md
Global Constraints
- Keep the canonical sections and exact order:
start,profile,experience,infrastructure,selected-builds,additional-work,toolkit,contact. - Do not alter the Three.js world, route-sign lifecycle, soundtrack, or section-to-audio mood mapping.
- Public identity is exactly
Luca Pîslaru — System Administrator & Full-Stack Developer. - Public current-role organization is
Trupa lui Răzuș; never publish the legal PFA name. - Public current-role title is
Full-Stack Developer & Hosting Administrator (Collaborator); do not use DevOps. - Public server OS terminology is
Linux; never publish its distribution. - Never publish a portrait, phone number, street address, downloadable CV, private domain, private product name, private repository link, server configuration, or real/private health information.
- The medication tracker remains
Private self-hosted medication tracker, has no URL, and is not described as production-ready or fully secure. - Footy Tracker is a two-person university collaboration. Credit Luca's lead data/ML contribution and the collaborator's initial pipeline contribution accurately.
- Preserve measured Footy results as
mAP@0.5values; do not convert them into ordinary percentages. - Do not claim a numerical performance improvement for Trupa lui Răzuș without reproducible evidence.
- Project media is optional at runtime but required for this feature's final acceptance. Unsafe or unapproved media is omitted rather than replaced with a broken placeholder.
- Supplied private screenshots are never copied unchanged into the public asset set.
- Keep semantic no-JavaScript content, keyboard access, reduced motion, forced colors, intrinsic media dimensions, and contextual text alternatives.
- Preserve the current soundtrack binaries, hashes, manifest records, transfer budgets, and playback contract.
- Run one broad end-of-phase review and at most one medium follow-up if high-impact issues remain.
File Structure
Content and contracts
content/public/portfolio-content.json: exact approved public copy and public asset references.content/public/asset-recipes.json: generic recipes for only the project/social assets selected for publication.content/public/asset-manifest.json: exact union of existing soundtrack records and approved project/social records.docs/content-approval-checklist.md: human-readable approval projection matching the public JSON.src/content/types.ts: typed availability, location, featured operation, contact introduction, and project media layout.src/content/schemas.ts: strict validation, generic-private-title restrictions, fixed section order, and exact asset closure.
Presentation
src/components/HeroJourney.vue: visible availability and email-first action.src/components/ContactDestination.vue: content-driven opportunity statement andrel=meidentity links.src/components/InfrastructureDiagram.vue: existing trust-flow diagram plus featured migration evidence.src/components/OperationsProof.vue: semantic featured-operation block inside Infrastructure.src/components/ProjectMediaGallery.vue: layout-only wrapper for single, portrait-strip, and chart-pair presentations.src/components/ProjectCaseStudy.vue: primary/supporting project copy plus gallery.src/components/ProjectCard.vue: concise supporting work plus optional single media item.src/styles/components.css,src/styles/layout.css,src/styles/print.css: responsive integration matching the existing night-drive language.
Metadata
src/content/metadata.ts: Bucharest city-level structured data and Twitter-card metadata.scripts/prerender.mjs: render Twitter tags and approved Open Graph image.scripts/verify-artifacts.mjs: verify the expanded metadata and social identity contract.
Media preparation
scripts/generate-social-preview.mjs: deterministic 1200×630 typographic night-drive social image generator.scripts/prepare-assets.mjs: retain and verify existing soundtrack assets while publishing approved project candidates.scripts/lib/merge-assets.mjs: pure collision-safe merge of retained and newly approved asset records.inputs/prepared-assets/: tracked, hash-bound public derivatives only.content/private/: ignored mapping, sanitized sources, reports, and exact approval register.
Verification
tests/fixtures/portfolio-content.valid.json: complete synthetic content fixture.tests/unit/content-schemas.test.ts: content and asset boundary tests.tests/integration/public-content.test.ts: exact public facts and prohibited-content checks.tests/unit/AppShell.test.ts: pre-rendered section, hero, contact, priority, and infrastructure evidence.tests/unit/ProjectMedia.test.ts: gallery semantics and media layout behavior.tests/unit/metadata.test.ts: JSON-LD, Open Graph, Twitter, and CSP hash.tests/integration/prepare-assets.test.ts: additive asset publication retaining soundtrack records/files.tests/integration/verify-artifacts.test.ts: compiled metadata and semantic content.tests/browser/semantic.spec.ts: live semantic links, attribution, contact, and fixed section contract.tests/browser/media-policy.spec.ts: approved still media and no automatic video behavior.tests/browser/media-failure.spec.ts: media failure degrades locally without removing text.tests/browser/responsive-layout.spec.ts,tests/browser/accessibility-reflow.spec.ts: portrait-strip and wide-media reflow.tests/browser/visual-regression.spec.ts: approved desktop/mobile/static visual baselines.
Task 1: Lock the expanded public content contract and exact copy
Files:
- Modify:
src/content/types.ts - Modify:
src/content/schemas.ts - Modify:
tests/fixtures/portfolio-content.valid.json - Modify:
tests/unit/content-schemas.test.ts - Modify:
content/public/portfolio-content.json - Modify:
tests/integration/public-content.test.ts - Modify:
docs/content-approval-checklist.md
Interfaces:
-
Consumes: the fixed
SectionIdunion, existing strict Zod boundary, and approved specification copy. -
Produces:
PortfolioContentwith typedidentity.location,identity.availability,infrastructure.featuredOperation,contact.introduction, and optionalPublicProject.mediaLayout. -
Step 1: Add failing schema tests for the new required evidence fields
Add explicit tests to tests/unit/content-schemas.test.ts:
it('requires public availability, city-level location, featured operations evidence, and contact copy', () => {
const content = cloneContent() as any;
expect(content.identity.location).toEqual({ city: 'Bucharest', country: 'Romania', countryCode: 'RO' });
expect(content.identity.availability).toMatch(/Bucharest.*remote/iu);
expect(content.infrastructure.featuredOperation.actions.length).toBeGreaterThanOrEqual(3);
expect(content.contact.introduction).toMatch(/System Administrator.*Full-Stack Developer/iu);
delete content.identity.availability;
expect(() => parsePortfolioContent(content)).toThrow();
});
it('accepts only the approved project media layouts', () => {
const content = cloneContent() as any;
content.projects.selected[0].mediaLayout = 'portrait-strip';
expect(() => parsePortfolioContent(content)).not.toThrow();
content.projects.selected[0].mediaLayout = 'carousel';
expect(() => parsePortfolioContent(content)).toThrow(/mediaLayout|unrecognized/iu);
});
Extend MutableContentFixture so the test remains typed instead of relying on any in the final version.
- Step 2: Add failing public-evidence assertions
Replace the generic assertions in tests/integration/public-content.test.ts with exact public facts:
expect(content.identity).toMatchObject({
name: 'Luca Pîslaru',
role: 'System Administrator & Full-Stack Developer',
location: { city: 'Bucharest', country: 'Romania', countryCode: 'RO' },
});
expect(content.experience[0]).toMatchObject({
organization: 'Trupa lui Răzuș',
role: 'Full-Stack Developer & Hosting Administrator (Collaborator)',
});
expect(content.infrastructure.featuredOperation.outcome).toMatch(/without data loss.*roughly a year/iu);
expect(content.projects.selected.map(({ id }) => id)).toEqual([
'private-medication-tracker',
'footy-tracker',
'opinion-dynamics-simulation',
]);
expect(content.projects.selected[1]?.contribution).toMatch(/collaborator/iu);
expect(content.projects.selected[1]?.outcome).toMatch(/0\.942 mAP@0\.5.*0\.198/iu);
Expand the prohibited-surface assertion without adding the real private tracker name to a tracked file:
expect(JSON.stringify(content)).not.toMatch(
/Arch Linux|Mihai Razus PFA|DevOps collaborator|CV download|heatmap|fully secure|production-ready|expert|percent/iu,
);
- Step 3: Run the focused tests and confirm the contract is red
Run:
npm run test:unit -- tests/unit/content-schemas.test.ts tests/integration/public-content.test.ts
Expected: failures identify the missing new fields and stale generic copy.
- Step 4: Extend the TypeScript content types
Add these exact shapes to src/content/types.ts:
export type ProjectMediaLayout = 'single' | 'portrait-strip' | 'chart-pair';
export interface FeaturedOperation {
title: string;
context: string;
actions: readonly string[];
outcome: string;
}
export interface InfrastructureCaseStudy {
id: string;
title: string;
summary: string;
bands: readonly InfrastructureEvidenceBand[];
featuredOperation: FeaturedOperation;
}
Extend the existing types exactly:
// PublicProject
mediaLayout?: ProjectMediaLayout;
// PortfolioContent.identity
location: { city: string; country: string; countryCode: string };
availability: string;
// ContactContent
introduction: string;
- Step 5: Extend the strict Zod schema
In src/content/schemas.ts, add a strict location object, required availability and contact introduction, required featured operation with actions: z.array(visibleString).min(3).max(6), and mediaLayout: z.enum(['single', 'portrait-strip', 'chart-pair']).optional().
Add a refinement that rejects mediaLayout when a project has no media, while continuing to allow the complete text-first public projection to omit both fields:
if (project.mediaLayout !== undefined && (project.media === undefined || project.media.length === 0)) {
context.addIssue({
code: 'custom',
message: 'Project mediaLayout requires at least one media presentation.',
path: ['mediaLayout'],
});
}
Keep selected projects at length three and keep the exact-one-experimental refinement.
- Step 6: Replace the public copy with the exact specification projection
Update content/public/portfolio-content.json from sections 4.1 through 4.11 of the specification. Use ASCII hyphens in rendered date ranges and sentences so the existing visible-copy punctuation contract remains stable. Preserve the four soundtrack regions byte-for-byte.
During this task, keep every project assetIds empty, omit every media and mediaLayout, and omit metadata.socialPreviewAssetId. This produces a fully testable text-first checkpoint before any binary approval.
The metadata fields are exactly:
{
"title": "Luca Pîslaru — System Administrator & Full-Stack Developer",
"description": "Portfolio of Luca Pîslaru, a Bucharest-based System Administrator and Full-Stack Developer working across Linux infrastructure and end-to-end software delivery."
}
- Step 7: Update the synthetic fixture and approval checklist
Add the required new fields to tests/fixtures/portfolio-content.valid.json using synthetic content and existing synthetic asset IDs. Update docs/content-approval-checklist.md so it records:
- Trupa lui Răzuș and the corrected role title;
- Bucharest and remote availability;
- the Nextcloud migration outcome;
- medication-tracker private/active/not-production-ready wording;
- Footy collaboration and measured results;
- project/social media as pending approval at this checkpoint.
Do not place private source paths, names, or values in the checklist.
- Step 8: Run the contract tests and type checker
Run:
npm run test:unit -- tests/unit/content-schemas.test.ts tests/integration/public-content.test.ts
npm run typecheck
git diff --check
Expected: all commands pass; the public JSON validates with soundtrack-only exact asset closure.
- Step 9: Commit the typed content projection
git add src/content/types.ts src/content/schemas.ts content/public/portfolio-content.json docs/content-approval-checklist.md tests/fixtures/portfolio-content.valid.json tests/unit/content-schemas.test.ts tests/integration/public-content.test.ts
git commit -m "content: publish evidence-led portfolio copy"
Expected: one content-contract commit with no project binaries, private files, Three.js changes, or audio changes.
Task 2: Render availability and the featured operations case study
Files:
- Modify:
src/components/HeroJourney.vue - Modify:
src/components/ContactDestination.vue - Create:
src/components/OperationsProof.vue - Modify:
src/components/InfrastructureDiagram.vue - Modify:
src/styles/components.css - Modify:
src/styles/layout.css - Modify:
src/styles/print.css - Modify:
tests/unit/AppShell.test.ts - Modify:
tests/browser/semantic.spec.ts
Interfaces:
-
Consumes:
identity.availability,identity.location,contact.introduction, andinfrastructure.featuredOperationfrom Task 1. -
Produces: complete server-rendered hero/contact/operations evidence while preserving all eight section IDs and layouts.
-
Step 1: Write the failing server-rendered hierarchy assertions
Update tests/unit/AppShell.test.ts so the hero and contact expectations become:
expect([...hero.children].map((element) => element.textContent?.trim())).toEqual([
'Luca Pîslaru',
'System Administrator & Full-Stack Developer',
validInputs.content.identity.valueProposition,
validInputs.content.identity.availability,
'Email Luca',
'Explore work',
]);
expect(hero.querySelector('a[href="mailto:hello@example.test"]')?.textContent).toBe('Email Luca');
expect(document.querySelector('[data-featured-operation]')?.textContent).toContain('Migrating a stateful Nextcloud service');
expect(document.querySelector('#contact')?.textContent).toContain('System Administrator');
Keep the existing assertions for one H1, seven H2 elements, eight canonical sections, and primary/supporting project priority.
- Step 2: Write the failing live semantic-link assertions
In tests/browser/semantic.spec.ts, require one hero mailto: link and one contact mailto: link with the same address, and require all social identity links to contain me, noopener, and noreferrer in their rel token set.
await expect(page.getByRole('link', { name: 'Email Luca' })).toHaveCount(2);
await expect(page.locator('[data-featured-operation]')).toContainText('without data loss');
- Step 3: Run the focused tests and confirm they fail
Run:
npm run test:unit -- tests/unit/AppShell.test.ts
npx playwright test tests/browser/semantic.spec.ts --project=desktop-chromium
Expected: hero, contact, operation, and rel=me assertions fail against the old components.
- Step 4: Render the email-first hero
In HeroJourney.vue, keep the H1 and role as the first two visible elements, then render:
<p class="hero-journey__value">{{ identity.valueProposition }}</p>
<p class="hero-journey__availability">{{ identity.availability }}</p>
<div class="hero-journey__actions">
<a class="hero-journey__action hero-journey__action--primary" href="mailto:luca.pislaru@gmail.com">Email Luca</a>
<a class="hero-journey__action" href="#selected-builds" @click="emit('explore', $event)">Explore work</a>
</div>
Do not hard-code the address in the final component. Add an email prop sourced from content.contact.email in AppShell.vue, and form the mailto: URL from that prop.
- Step 5: Make contact copy content-driven and mark identity links
Replace the hard-coded contact sentence with contact.introduction, change the primary link label to Email Luca, and set social links to:
rel="me noopener noreferrer"
Project links remain noopener noreferrer without me.
- Step 6: Add the semantic featured-operation component
Create OperationsProof.vue:
<script setup lang="ts">
import type { FeaturedOperation } from '@/content/types';
defineProps<{ id: string; operation: Readonly<FeaturedOperation> }>();
</script>
<template>
<section class="operations-proof" data-featured-operation :aria-labelledby="`${id}-heading`">
<h4 :id="`${id}-heading`">{{ operation.title }}</h4>
<p>{{ operation.context }}</p>
<ol>
<li v-for="action in operation.actions" :key="action">{{ action }}</li>
</ol>
<p class="operations-proof__outcome"><strong>Outcome:</strong> {{ operation.outcome }}</p>
</section>
</template>
Render it after the infrastructure text equivalent:
<OperationsProof id="nextcloud-migration" :operation="infrastructure.featuredOperation" />
The stable caller-provided ID keeps the accessible relationship independent of punctuation or future copy changes.
- Step 7: Style the new evidence without changing the journey geometry contract
Add restrained styles using existing variables only:
-
availability at
var(--step--1)with a readable maximum line length; -
two hero actions in a wrapping row with the email action using the current filled cyan treatment;
-
operation block separated by one strong rule, a two-column context/actions layout at wide widths, and one column below 48rem;
-
no new fixed heights, absolute positioning, animation, or viewport-dependent content hiding;
-
print layout shows the operation in document order.
-
Step 8: Run semantic, keyboard, reflow, and static checks
Run:
npm run test:unit -- tests/unit/AppShell.test.ts tests/unit/components.test.ts
npx playwright test tests/browser/semantic.spec.ts tests/browser/keyboard.spec.ts tests/browser/accessibility-reflow.spec.ts tests/browser/no-javascript.spec.ts --project=desktop-chromium
npm run typecheck
git diff --check
Expected: all checks pass and the eight-section sequence is unchanged.
- Step 9: Commit the new semantic evidence blocks
git add src/components/HeroJourney.vue src/components/ContactDestination.vue src/components/OperationsProof.vue src/components/InfrastructureDiagram.vue src/components/AppShell.vue src/styles/components.css src/styles/layout.css src/styles/print.css tests/unit/AppShell.test.ts tests/browser/semantic.spec.ts
git commit -m "feat: surface availability and operations evidence"
Task 3: Add restrained project-media layouts
Files:
- Create:
src/components/ProjectMediaGallery.vue - Modify:
src/components/ProjectCaseStudy.vue - Modify:
src/components/ProjectCard.vue - Modify:
src/styles/components.css - Modify:
src/styles/layout.css - Modify:
src/styles/print.css - Modify:
tests/unit/ProjectMedia.test.ts - Modify:
tests/browser/responsive-layout.spec.ts - Modify:
tests/browser/accessibility-reflow.spec.ts
Interfaces:
-
Consumes:
PublicProject.media,PublicProject.mediaLayout, existingProjectMedia, and existingallowAutomaticVideopolicy. -
Produces: one semantic gallery wrapper whose layout changes do not change media loading, accessibility, or asset resolution.
-
Step 1: Write failing gallery tests
Add tests to tests/unit/ProjectMedia.test.ts that mount the gallery with three image presentations and assert:
expect(wrapper.attributes('data-media-layout')).toBe('portrait-strip');
expect(wrapper.findAll('figure')).toHaveLength(3);
expect(wrapper.findAll('img').map((image) => image.attributes('loading'))).toEqual(['lazy', 'lazy', 'lazy']);
expect(wrapper.text()).toContain('Fictional demo profile');
Add one case proving single and chart-pair render the same ProjectMedia semantics and a case proving an empty list renders no gallery container.
- Step 2: Run the unit test and confirm it fails
Run:
npm run test:unit -- tests/unit/ProjectMedia.test.ts
Expected: failure because ProjectMediaGallery.vue does not exist.
- Step 3: Implement the layout-only gallery
Create ProjectMediaGallery.vue:
<script setup lang="ts">
import ProjectMedia from './ProjectMedia.vue';
import type { MediaPresentation, ProjectMediaLayout } from '@/content/types';
withDefaults(defineProps<{
media: readonly MediaPresentation[];
layout?: ProjectMediaLayout;
allowAutomaticVideo?: boolean;
}>(), { layout: 'single', allowAutomaticVideo: false });
</script>
<template>
<div v-if="media.length > 0" class="project-media-gallery" :data-media-layout="layout">
<ProjectMedia
v-for="presentation in media"
:key="`${presentation.kind}-${presentation.assetId}`"
:presentation="presentation"
:allow-automatic-video="allowAutomaticVideo"
/>
</div>
</template>
Replace the direct ProjectMedia loops in ProjectCaseStudy.vue and ProjectCard.vue with this wrapper. Add :data-media-layout="project.mediaLayout ?? 'none'" to the containing article for responsive styling.
- Step 4: Implement the responsive visual treatment
Use these layout rules in src/styles/components.css:
single: one full-width figure with the existing border and background;portrait-strip: three equal portrait columns, a small negative inline overlap no greater than0.75rem, alternating vertical offsets no greater than1rem, and no rotation;chart-pair: two equal columns with aligned captions and one column below 48rem;- every gallery has
isolation: isolate, visible focus/controls, and no hidden overflow that clips captions; - tracker primary card may expand to
min(72rem, 88%)on wide screens; Footy remains right-aligned and may expand tomin(68rem, 82%); the supporting research card remains visually quieter; - below 48rem every card is 100% wide and every gallery becomes one column or a keyboard/touch-scrollable portrait strip with scroll snapping disabled under reduced motion;
- no parallax, autoplay, hover-only information, fake device chrome, or CSS transforms that reduce text/image legibility.
Update print.css so images fit the page, captions follow their figures, and no media overlaps.
- Step 5: Add browser reflow assertions
In both responsive/reflow specs, use a synthetic manifest/content fixture or the final approved media after Task 5. At 320 CSS pixels require:
const gallery = page.locator('[data-media-layout="portrait-strip"]');
await expect(gallery).toHaveCount(1);
expect(await gallery.evaluate((node) => node.scrollWidth <= node.clientWidth || getComputedStyle(node).overflowX === 'auto')).toBe(true);
Also assert no document-level horizontal overflow and that all figures remain in DOM order.
- Step 6: Run focused layout verification
Run:
npm run test:unit -- tests/unit/ProjectMedia.test.ts
npx playwright test tests/browser/responsive-layout.spec.ts tests/browser/accessibility-reflow.spec.ts --project=desktop-chromium --project=mobile-chromium
npm run typecheck
git diff --check
Expected: all checks pass without requiring any production project binary.
- Step 7: Commit the media-layout component
git add src/components/ProjectMediaGallery.vue src/components/ProjectCaseStudy.vue src/components/ProjectCard.vue src/styles/components.css src/styles/layout.css src/styles/print.css tests/unit/ProjectMedia.test.ts tests/browser/responsive-layout.spec.ts tests/browser/accessibility-reflow.spec.ts
git commit -m "feat: add evidence-led project media layouts"
Task 4: Make approved project publication additive to the soundtrack closure
Files:
- Create:
scripts/lib/merge-assets.mjs - Modify:
scripts/prepare-assets.mjs - Modify:
tests/integration/prepare-assets.test.ts - Modify:
tests/integration/materialize-empty-asset-set.test.mjs
Interfaces:
-
Consumes: existing public manifest and
inputs/prepared-assets, generated project candidates, recipe IDs, and exact private approval records. -
Produces: a collision-free manifest and prepared-asset directory containing the unchanged soundtrack records/files plus newly approved project assets.
-
Step 1: Write failing pure merge tests
Create cases in tests/integration/prepare-assets.test.ts for:
const retained = [{ id: 'soundtrack-main-opus', fileName: 'nightdrive-main.opus', sha256: 'a'.repeat(64) }];
const generated = [{ id: 'medication-mobile-logs', fileName: 'project-medication-logs.webp', sha256: 'b'.repeat(64) }];
expect(mergeAssetRecords(retained, generated, new Set(['medication-mobile-logs']))).toEqual([
generated[0],
retained[0],
].sort((a, b) => a.fileName.localeCompare(b.fileName, 'en-US')));
Add rejection cases for duplicate IDs, case-folded filename collisions, a generated record that tries to replace an unmanaged soundtrack record, and an old managed recipe record that should be replaced by its newly generated record.
- Step 2: Add a failing publication test that starts with a soundtrack file
Prepare a scratch publish root with one manifest-bound soundtrack file, run the approved project publication path, and assert the soundtrack bytes/hash and project bytes/hash both remain present. Also assert an unmanifested file causes a fail-closed error before publication.
- Step 3: Run the focused tests and confirm they fail
Run:
npm run test:unit -- tests/integration/prepare-assets.test.ts tests/integration/materialize-empty-asset-set.test.mjs
Expected: failures identify the missing merge helper and destructive replacement behavior.
- Step 4: Implement collision-safe record merging
Create scripts/lib/merge-assets.mjs with one exported pure function:
export const mergeAssetRecords = (baseAssets, generatedAssets, managedIds) => {
const retained = baseAssets.filter(({ id }) => !managedIds.has(id));
const combined = [...retained, ...generatedAssets];
const ids = new Set();
const names = new Set();
for (const asset of combined) {
const folded = asset.fileName.toLocaleLowerCase('en-US');
if (ids.has(asset.id)) throw new Error('ASSET-MERGE-ID-001');
if (names.has(folded)) throw new Error('ASSET-MERGE-NAME-001');
ids.add(asset.id);
names.add(folded);
}
return combined.sort((left, right) => left.fileName.localeCompare(right.fileName, 'en-US'));
};
The final implementation must validate the same safe ID/filename/hash fields as the manifest schema before merging.
- Step 5: Extend approved publication without clearing retained assets
In scripts/prepare-assets.mjs, when approvalRegisterPath is supplied:
- Read and validate the current manifest.
- Treat current recipe IDs as the managed set.
- Verify every retained manifest file exists as a regular, non-linked file and matches its current SHA-256 hash.
- Reject unmanifested files in the publish root.
- Merge records with
mergeAssetRecords. - Copy approved candidate files using generated safe basenames.
- Remove only obsolete prior managed-recipe files whose exact old filenames came from the validated prior manifest.
- Write the merged manifest only after file verification succeeds.
- Leave every unmanaged soundtrack file and record unchanged.
Do not accept a caller-provided list of retained files and do not use a glob to select deletion targets.
- Step 6: Run asset-pipeline verification
Run:
npm run test:unit -- tests/integration/prepare-assets.test.ts tests/integration/materialize-empty-asset-set.test.mjs
npm run verify:approved-assets
git diff --check
Expected: all tests pass and the real repository's existing eight soundtrack assets still verify exactly.
- Step 7: Commit the additive publication path
git add scripts/lib/merge-assets.mjs scripts/prepare-assets.mjs tests/integration/prepare-assets.test.ts tests/integration/materialize-empty-asset-set.test.mjs
git commit -m "fix: preserve approved assets when adding project media"
Task 5: Prepare, inspect, and approve the project-media set
Files:
- Modify:
content/public/asset-recipes.json - Create:
scripts/generate-social-preview.mjs - Create privately/ignored:
content/private/sanitized-sources/** - Create privately/ignored:
content/private/asset-source-map.json - Create privately/ignored:
content/private/asset-approvals.json - Create in temporary review storage:
/tmp/portfolio-media-staging/** - Create in temporary review storage:
/tmp/portfolio-media-report/** - Modify after approval:
inputs/prepared-assets/** - Modify after approval:
content/public/asset-manifest.json - Modify:
tests/integration/prepare-assets.test.ts
Interfaces:
-
Consumes: source screenshots in the private handoff, fresh fictional medication demo captures, Footy rights decision, public recipes, denylist, and Task 4's additive publisher.
-
Produces: exact approved WebP project/social assets and manifest records without changing any soundtrack byte or record.
-
Step 1: Reduce recipes to the intended evidence set
Keep these recipe IDs:
medication-mobile-logsmedication-mobile-data-settingsmedication-mobile-notification-settingsopinion-polarization-chartopinion-final-distribution-chartcurrency-converter-interfacesocial-preview-nightdrive
Keep footy-detection-frame only if Luca confirms display permission for the underlying broadcast frame. If permission is not confirmed, remove the recipe and use the visible measured-results content as the fallback. Remove private-game and unused Opinion recipes from this publication set.
Add the social recipe with output project-social-preview.webp, width 1200, rights holder Luca Pîslaru, and All rights reserved.
- Step 2: Generate a deterministic social-preview source
Create scripts/generate-social-preview.mjs using Sharp with an SVG source. It accepts --output, rejects paths outside content/private/ or /tmp, and renders exactly 1200×630:
- dark navy sky and horizon using the existing site palette;
- one restrained perspective road shape and cyan/amber lane accents;
Luca Pîslaru;System Administrator & Full-Stack Developer;- no portrait, project screenshot, logo, private hostname, or third-party media.
Add a unit/integration test that verifies exact dimensions, opaque output, and absence of any private denylist term in OCR. Generate the private source:
node scripts/generate-social-preview.mjs --output content/private/sanitized-sources/social-preview.png
- Step 3: Create genuine fictional tracker captures
Use the private app only to produce three new capture states matching the approved activity/history, data/synchronization settings, and notification/reminder settings views. Every person, medication, quantity, schedule, account, server, and status value must be unmistakably fictional. The public generic title must replace or crop out the unreleased application branding.
Do not modify the private application's tracked source solely for capture. Use its existing demo/test injection path, an ephemeral local database, or a temporary untracked capture harness. If none can produce truthful screenshots, stop this task and ask Luca for fresh fictional-data screenshots; do not edit real health values into superficially plausible replacements.
Save the three final private sources under content/private/sanitized-sources/ with generic filenames. Inspect them at original resolution before continuing.
- Step 4: Copy the safe public-candidate sources into the private preparation root
Copy only the selected Opinion charts and Currency Converter screenshot from the handoff into content/private/sanitized-sources/. These copies remain ignored. Remove irrelevant desktop/window chrome from the Currency image only through a reproducible crop recorded in the private preparation notes, then inspect the final pixels and metadata.
If the Footy frame is rights-cleared, copy it into the same private source root; otherwise do not include it in the mapping.
- Step 5: Create the exact private mapping and denylist
Create content/private/asset-source-map.json with schema portfolio-private-source-map/v1 and exactly one relative source for every recipe ID. Create or extend the private denylist with:
- the unreleased medication-tracker name and branding variants;
- Luca's phone number and street-address fragments;
- private domains/subdomains and repository names;
- every real person or medication value visible in the original sources;
- unreleased healthcare and game names.
Never commit the mapping, denylist, sanitized sources, reports, or approval register.
- Step 6: Generate candidates without publishing
Run:
npm run prepare:assets -- --source-root content/private/sanitized-sources --mapping content/private/asset-source-map.json --denylist content/private/asset-denylist.txt --recipes content/public/asset-recipes.json --staging-root /tmp/portfolio-media-staging --report-dir /tmp/portfolio-media-report
Expected: OCR/binary/metadata checks pass, a contact sheet is produced, and approvalStatus remains pending. The public manifest and inputs/prepared-assets remain unchanged.
- Step 7: Perform the human media checkpoint
Open the contact sheet and every full-resolution candidate. Review:
- tracker data is unmistakably fictional and the private name is absent;
- no phone, address, domain, account, QR/barcode, notification shade, or browser identity is visible;
- Footy rights are documented if the frame is present;
- Opinion axes and labels are readable;
- Currency content is readable and surrounding desktop chrome is absent;
- social-preview text is accurate and not cropped;
- the asset set fits the night-drive visual language without appearing generated or ornamental.
Ask Luca to approve or reject the exact candidate set. Copy /tmp/portfolio-media-report/candidate-approval.json to content/private/asset-approvals.json only after explicit approval. Any changed byte requires regeneration and a new approval.
- Step 8: Publish the approved candidates additively
Run the same command with:
--approvals content/private/asset-approvals.json --publish-root inputs/prepared-assets --manifest content/public/asset-manifest.json
Expected: the manifest contains the unchanged eight soundtrack entries plus approved project/social entries; every prepared file matches its hash; no old soundtrack file is removed.
- Step 9: Verify and commit only public derivatives and recipes
Run:
npm run verify:approved-assets
npm run test:unit -- tests/integration/prepare-assets.test.ts tests/integration/materialize-empty-asset-set.test.mjs
git status --short
Expected: private files do not appear in Git status. Stage only the generator, recipes, manifest, prepared public derivatives, and tests:
git add scripts/generate-social-preview.mjs content/public/asset-recipes.json content/public/asset-manifest.json inputs/prepared-assets tests/integration/prepare-assets.test.ts
git commit -m "assets: add approved portfolio evidence media"
Task 6: Attach approved media and finalize project presentation
Files:
- Modify:
content/public/portfolio-content.json - Modify:
tests/integration/public-content.test.ts - Modify:
tests/unit/ProjectMedia.test.ts - Modify:
tests/browser/media-policy.spec.ts - Modify:
tests/browser/media-failure.spec.ts - Modify:
tests/browser/visual-regression.spec.ts - Modify:
docs/content-approval-checklist.md
Interfaces:
-
Consumes: exact manifest IDs from Task 5 and project-media layouts from Task 3.
-
Produces: hash-bound public media presentations with contextual alt text/captions and exact asset closure.
-
Step 1: Write failing public media assertions
In tests/integration/public-content.test.ts, require:
const medication = content.projects.selected[0]!;
expect(medication.mediaLayout).toBe('portrait-strip');
expect(medication.media).toHaveLength(3);
expect(medication.media?.every(({ alt }) => /fictional|demo/iu.test(alt))).toBe(true);
const opinion = content.projects.selected[2]!;
expect(opinion.mediaLayout).toBe('chart-pair');
expect(opinion.media).toHaveLength(2);
expect(opinion.media?.every(({ kind }) => kind === 'chart')).toBe(true);
expect(content.metadata.socialPreviewAssetId).toBe('social-preview-nightdrive');
If the Footy frame was not approved, assert footy.assetIds is empty and footy.media is omitted. If approved, assert one single image presentation and the rights-cleared manifest record.
- Step 2: Run the focused test and confirm it fails
Run:
npm run test:unit -- tests/integration/public-content.test.ts
Expected: failures identify absent public media references.
- Step 3: Add the medication portrait strip
Add three image presentations in this order:
{
"mediaLayout": "portrait-strip",
"assetIds": [
"medication-mobile-logs",
"medication-mobile-data-settings",
"medication-mobile-notification-settings"
],
"media": [
{
"kind": "image",
"assetId": "medication-mobile-logs",
"alt": "Medication activity screen using an unmistakably fictional demo profile.",
"caption": "Daily activity and history in a fictional demo state.",
"longDescription": "The mobile view groups scheduled entries and their recorded status without exposing real medication information."
},
{
"kind": "image",
"assetId": "medication-mobile-data-settings",
"alt": "Data and synchronization settings for a fictional medication-tracker profile.",
"caption": "Data controls supporting the offline-first workflow.",
"longDescription": "The settings view exposes synchronization and local-data controls used by the private self-hosted system."
},
{
"kind": "image",
"assetId": "medication-mobile-notification-settings",
"alt": "Reminder settings for fictional medication entries.",
"caption": "Reminder controls refined through months of private use.",
"longDescription": "The settings view groups notification behavior and the Android alarm capability used for scheduled reminders."
}
]
}
Keep the project private and URL-free.
- Step 4: Add the Opinion chart pair
Use opinion-polarization-chart and opinion-final-distribution-chart. Each record is kind: "chart" and contains visible variables, trend, conclusion, and a value table only where an exact source value is available.
For polarization use:
{
"caption": "Polarization rose quickly and then increased more gradually in this modeled run.",
"chart": {
"variables": "Horizontal axis: simulation step. Vertical axis: polarization index.",
"trend": "The index rises sharply during the first two steps, then continues upward more slowly.",
"conclusion": "This modeled scenario ended more polarized than it began.",
"table": {
"caption": "Selected modeled polarization values",
"columns": ["Step", "Polarization index"],
"rows": [["0", "0.765"], ["14", "0.932"]]
}
}
}
Do not infer exact final-distribution values from pixel positions. Omit its table unless they are recovered from the source data.
- Step 5: Add Currency and optional Footy media
Add the Currency image as a single presentation on its additional-work card with alt text describing the visible conversion interface, not its colors.
If rights-cleared, add the Footy frame with this caption:
Player boxes remain consistent across a compressed broadcast frame; the same image also shows uncertain jersey labels and role-classification limitations.
If not rights-cleared, do not add a Footy media object or asset ID.
- Step 6: Attach the social preview and maintain exact closure
Set:
"socialPreviewAssetId": "social-preview-nightdrive"
Update docs/content-approval-checklist.md to mark each exact media ID approved and to record Footy as included or deliberately omitted. Do not record private source paths.
- Step 7: Update browser media-policy tests
Replace the old zero-media expectation with the approved still count derived from the public JSON. Continue asserting:
- no project video source is attached automatically;
- all images use manifest URLs, intrinsic dimensions, and lazy loading;
- text remains present if an image request fails;
- Static and reduced-motion modes do not add video behavior;
- no private project has a public URL.
In media-failure.spec.ts, scope F8 to video, video source rather than requiring all [data-project-media] elements to be absent.
- Step 8: Run content, media, and closure checks
Run:
npm run verify:approved-assets
npm run test:unit -- tests/integration/public-content.test.ts tests/unit/ProjectMedia.test.ts tests/unit/content-schemas.test.ts tests/integration/materialize-empty-asset-set.test.mjs
npm run typecheck
git diff --check
Expected: exact content/manifest/prepared-file closure and all focused checks pass.
- Step 9: Build and inspect the media layout before accepting baselines
Run:
npm run build:preview
npx playwright test tests/browser/media-policy.spec.ts tests/browser/responsive-layout.spec.ts tests/browser/accessibility-reflow.spec.ts --project=desktop-chromium --project=mobile-chromium
Inspect the tracker strip, optional Footy frame, Opinion pair, Currency image, and text-only fallbacks at desktop, narrow mobile, 200% zoom, Static mode, and reduced motion. Fix high-impact hierarchy, crop, caption, contrast, or overflow issues before updating visual baselines.
- Step 10: Commit the approved media references and presentation
git add content/public/portfolio-content.json docs/content-approval-checklist.md tests/integration/public-content.test.ts tests/unit/ProjectMedia.test.ts tests/browser/media-policy.spec.ts tests/browser/media-failure.spec.ts tests/browser/visual-regression.spec.ts
git commit -m "content: integrate approved project evidence"
Task 7: Complete identity SEO and social metadata
Files:
- Modify:
src/content/metadata.ts - Modify:
scripts/prerender.mjs - Modify:
scripts/verify-artifacts.mjs - Modify:
tests/unit/metadata.test.ts - Modify:
tests/integration/verify-artifacts.test.ts - Modify:
tests/deployment/seo.spec.ts
Interfaces:
-
Consumes: public identity/location/social links, approved social-preview asset, canonical site configuration, and safe JSON-LD serializer.
-
Produces: consistent ProfilePage/Person JSON-LD, Open Graph, Twitter large-card metadata, CSP hash, and compiled artifact verification.
-
Step 1: Write failing metadata tests
Extend tests/unit/metadata.test.ts:
expect(metadata.jsonLd.mainEntity.homeLocation).toEqual({
'@type': 'Place',
address: {
'@type': 'PostalAddress',
addressLocality: 'Bucharest',
addressCountry: 'RO',
},
});
expect(metadata.twitter).toEqual({
card: 'summary_large_image',
title: metadata.title,
description: metadata.description,
image: 'https://portfolio.example.test/assets/project-social-preview.webp',
});
expect(JSON.stringify(metadata.jsonLd)).not.toMatch(/streetAddress|postalCode|telephone/iu);
Add a no-image case proving Twitter image is omitted when socialPreviewAssetId is absent while the card remains summary.
- Step 2: Add failing compiled-tag assertions
In tests/integration/verify-artifacts.test.ts, require exactly one of each:
og:title,og:description,og:type,og:url,og:imagewhen approved;twitter:card,twitter:title,twitter:description,twitter:imagewhen approved;- canonical link;
- safe serialized JSON-LD containing only city/country location.
Add duplicate and mismatched-image mutation cases that must fail.
- Step 3: Run focused metadata tests and confirm they fail
Run:
npm run test:unit -- tests/unit/metadata.test.ts tests/integration/verify-artifacts.test.ts
Expected: missing home location and Twitter metadata failures.
- Step 4: Extend metadata types and derivation
In src/content/metadata.ts, add:
interface PlaceJsonLd {
'@type': 'Place';
address: {
'@type': 'PostalAddress';
addressLocality: string;
addressCountry: string;
};
}
Add homeLocation: PlaceJsonLd to PersonJsonLd and derive it only from content.identity.location.city and .countryCode. Do not include email-derived, street, postal, or private domain fields.
Add a twitter object to PageMetadata:
twitter: {
card: 'summary' | 'summary_large_image';
title: string;
description: string;
image?: string;
};
Use summary_large_image only when the manifest-resolved social image exists; otherwise use summary.
- Step 5: Render and verify the Twitter tags
In scripts/prerender.mjs, escape and render Twitter tags next to Open Graph tags. In scripts/verify-artifacts.mjs, verify exact values against createPageMetadata; reject duplicates, an image not present in the manifest, or preview/production canonical drift.
Keep the JSON-LD CSP hash derived from the final serialized bytes after homeLocation is added.
- Step 6: Run SEO and security-focused verification
Run:
npm run test:unit -- tests/unit/metadata.test.ts tests/integration/verify-artifacts.test.ts
npm run test:deployment -- tests/deployment/seo.spec.ts
npm run typecheck
npm run build:preview
git diff --check
Expected: preview remains noindex/sitemap-free; production fixtures remain canonical/indexable; Open Graph, Twitter, and JSON-LD agree; CSP hash tests pass.
- Step 7: Commit the metadata refinement
git add src/content/metadata.ts scripts/prerender.mjs scripts/verify-artifacts.mjs tests/unit/metadata.test.ts tests/integration/verify-artifacts.test.ts tests/deployment/seo.spec.ts
git commit -m "seo: align public identity and social metadata"
Task 8: Run the broad acceptance pass and one bounded review
Files:
- Modify if evidence changes:
docs/reviews/audience-validation.md - Modify if evidence changes:
docs/reviews/visual-qa.md - Modify if evidence changes:
docs/accessibility/wcag-2.2-aa-checklist.md - Modify if evidence changes:
docs/content-approval-checklist.md - Modify only for demonstrated defects: files owned by Tasks 1-7
Interfaces:
-
Consumes: complete approved content, media, metadata, styles, and existing quality gates.
-
Produces: one verified compiled preview and a concise broad-review record with no unbounded review loop.
-
Step 1: Run static checks and the complete unit/integration suite
Run:
git diff --check
npm run typecheck
npm run test:unit
npm run verify:approved-assets
Expected: all commands pass. Do not continue to visual acceptance with a failing content, schema, privacy, or exact-asset boundary.
- Step 2: Build the compiled preview and verify artifacts
Run:
npm run build:preview
npm run check:budgets:transfer
Expected: client/SSR builds, prerendering, artifact verification, and transfer budgets pass. The pre-rendered document contains all eight sections, all project evidence, captions, contact methods, and metadata without JavaScript.
- Step 3: Run the focused browser acceptance matrix
Run:
npx playwright test tests/browser/semantic.spec.ts tests/browser/keyboard.spec.ts tests/browser/no-javascript.spec.ts tests/browser/reduced-motion.spec.ts tests/browser/accessibility-forced-colors.spec.ts tests/browser/accessibility-reflow.spec.ts tests/browser/responsive-layout.spec.ts tests/browser/media-policy.spec.ts tests/browser/media-failure.spec.ts tests/browser/visual-regression.spec.ts --project=desktop-chromium --project=mobile-chromium
Expected: all selected tests pass with no page errors, unhandled rejections, inaccessible media, overflow, focus loss, or changed canonical sections.
- Step 4: Run the privacy and accuracy audit
Inspect public content, pre-rendered HTML, public manifest, and final project images. Confirm:
- exact name and dual role are consistent;
- Trupa lui Răzuș and the corrected role are used everywhere;
- medication tracker has only its generic title and fictional media;
- Footy collaboration and metrics remain accurate;
- server OS is only Linux;
- no portrait, phone, street address, CV file, private URL, private brand, health data, QR/barcode, or server configuration is present;
- no claim says fully secure, flawless, guaranteed ranking, senior, expert, or production-ready;
- every public binary is manifest-bound, approved, and metadata-stripped.
Use the private denylist and existing privacy scanner rather than placing private strings into shell output or tracked test files.
- Step 5: Perform one broad human review
Review the compiled preview at desktop, mobile, 200% zoom, Static mode, reduced motion, keyboard-only navigation, and no JavaScript. Evaluate only:
- five-second identity and role comprehension;
- equal system-administration/full-stack weight;
- evidence hierarchy and scan length;
- tracker media as the main developer-side visual;
- Nextcloud migration as the main operations proof;
- captions and images reinforcing rather than repeating copy;
- night-drive integration without reduced readability;
- email remaining the dominant contact action.
Record findings once in the existing review documents. Fix all high-impact defects. Run one medium follow-up only if a meaningful high-impact defect remains after the fixes.
- Step 6: Re-run only gates affected by review fixes, then the final preview command
After fixes, run their focused tests and then:
npm run build:preview
Expected: the complete preview pipeline passes. Do not repeat broad subjective review if only minor preference-level alternatives remain.
- Step 7: Commit the final review evidence
git add docs/reviews/audience-validation.md docs/reviews/visual-qa.md docs/accessibility/wcag-2.2-aa-checklist.md docs/content-approval-checklist.md
git diff --cached --quiet || git commit -m "docs: record content and media acceptance"
Expected: documentation changes are committed only when evidence changed. The working tree retains pre-existing user-owned .playwright-mcp/ and private-handoff/ entries untouched.
Completion Criteria
- The eight canonical sections still exist exactly once and in the original order.
- The hero presents the exact public name and balanced dual role, Bucharest availability, email action, and selected-work action.
- Trupa lui Răzuș has factual, specific development and hosting evidence with no DevOps label or legal PFA name.
- Infrastructure includes the long-running Nextcloud migration outcome and recovery practices without exposing configuration.
- The medication tracker is visibly the strongest developer case study, uses only fictional/sanitized media, and remains private and candidly non-production-ready.
- Footy Tracker credits both collaborators, distinguishes Luca's lead contribution, and reports the exact measured results and limitations.
- Opinion Dynamics is visibly supporting experimental research.
- At least the approved tracker strip, Opinion charts, Currency image, and social preview are published; Footy media follows its separate rights gate.
- All social identities use
rel=me; canonical, Open Graph, Twitter, JSON-LD, robots, and sitemap behavior agree with the environment. - Full content remains substantive and navigable without JavaScript or motion.
- All prepared assets, manifest records, and content references have exact duplicate-free closure, and existing soundtrack bytes are unchanged.
- Typecheck, full unit/integration suite, compiled preview, selected browser matrix, asset verification, and transfer budgets pass.