1175 lines
52 KiB
Markdown
1175 lines
52 KiB
Markdown
# 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`:
|
||
|
||
```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:
|
||
|
||
```ts
|
||
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:
|
||
|
||
```ts
|
||
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:
|
||
|
||
```bash
|
||
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`:
|
||
|
||
```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:
|
||
|
||
```ts
|
||
// 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:
|
||
|
||
```ts
|
||
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:
|
||
|
||
```json
|
||
{
|
||
"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:
|
||
|
||
```bash
|
||
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**
|
||
|
||
```bash
|
||
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`, 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:
|
||
|
||
```ts
|
||
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.
|
||
|
||
```ts
|
||
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:
|
||
|
||
```bash
|
||
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:
|
||
|
||
```vue
|
||
<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:
|
||
|
||
```vue
|
||
rel="me noopener noreferrer"
|
||
```
|
||
|
||
Project links remain `noopener noreferrer` without `me`.
|
||
|
||
- [ ] **Step 6: Add the semantic featured-operation component**
|
||
|
||
Create `OperationsProof.vue`:
|
||
|
||
```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:
|
||
|
||
```vue
|
||
<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:
|
||
|
||
```bash
|
||
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**
|
||
|
||
```bash
|
||
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:
|
||
|
||
```ts
|
||
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:
|
||
|
||
```bash
|
||
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`:
|
||
|
||
```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:
|
||
|
||
```ts
|
||
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:
|
||
|
||
```bash
|
||
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**
|
||
|
||
```bash
|
||
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:
|
||
|
||
```ts
|
||
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:
|
||
|
||
```bash
|
||
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:
|
||
|
||
```js
|
||
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:
|
||
|
||
```bash
|
||
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**
|
||
|
||
```bash
|
||
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:
|
||
|
||
```bash
|
||
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:
|
||
|
||
```bash
|
||
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:
|
||
|
||
```bash
|
||
--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:
|
||
|
||
```bash
|
||
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:
|
||
|
||
```bash
|
||
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:
|
||
|
||
```ts
|
||
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:
|
||
|
||
```bash
|
||
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:
|
||
|
||
```json
|
||
{
|
||
"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:
|
||
|
||
```json
|
||
{
|
||
"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:
|
||
|
||
```json
|
||
"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:
|
||
|
||
```bash
|
||
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:
|
||
|
||
```bash
|
||
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**
|
||
|
||
```bash
|
||
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`:
|
||
|
||
```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:
|
||
|
||
```bash
|
||
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:
|
||
|
||
```ts
|
||
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`:
|
||
|
||
```ts
|
||
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:
|
||
|
||
```bash
|
||
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**
|
||
|
||
```bash
|
||
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:
|
||
|
||
```bash
|
||
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:
|
||
|
||
```bash
|
||
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:
|
||
|
||
```bash
|
||
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:
|
||
|
||
```bash
|
||
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**
|
||
|
||
```bash
|
||
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.
|