Files
Portfolio/docs/superpowers/plans/2026-09-04-portfolio-content-seo-media-implementation.md

52 KiB
Raw Permalink Blame History

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.5 values; 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 and rel=me identity 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 SectionId union, existing strict Zod boundary, and approved specification copy.

  • Produces: PortfolioContent with typed identity.location, identity.availability, infrastructure.featuredOperation, contact.introduction, and optional PublicProject.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.


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, and infrastructure.featuredOperation from 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, existing ProjectMedia, and existing allowAutomaticVideo policy.

  • 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 than 0.75rem, alternating vertical offsets no greater than 1rem, 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 to min(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:

  1. Read and validate the current manifest.
  2. Treat current recipe IDs as the managed set.
  3. Verify every retained manifest file exists as a regular, non-linked file and matches its current SHA-256 hash.
  4. Reject unmanifested files in the publish root.
  5. Merge records with mergeAssetRecords.
  6. Copy approved candidate files using generated safe basenames.
  7. Remove only obsolete prior managed-recipe files whose exact old filenames came from the validated prior manifest.
  8. Write the merged manifest only after file verification succeeds.
  9. 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-logs
  • medication-mobile-data-settings
  • medication-mobile-notification-settings
  • opinion-polarization-chart
  • opinion-final-distribution-chart
  • currency-converter-interface
  • social-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:image when approved;
  • twitter:card, twitter:title, twitter:description, twitter:image when 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.