Skip to content

Latest commit

 

History

History
1309 lines (979 loc) · 53.2 KB

File metadata and controls

1309 lines (979 loc) · 53.2 KB

Portfolio Modernization Guide — Vanilla HTML → Angular (GitHub Pages)

Branch: develop_2026
Author context: This guide was generated by an AI after a full audit of the current static site. Any AI agent (or developer) can pick up from any step and continue the migration.


Table of Contents

  1. Current Architecture Summary
  2. Why Migrate?
  3. Recommended Technology: Angular + Static Export
  4. Alternatives Evaluated
  5. Pre-Migration Checklist
  6. Step-by-Step Migration
  7. File Mapping Reference
  8. Known Gotchas
  9. Progress Tracking

1. Current Architecture Summary

The site is a 100 % vanilla static website (no build step for the main app) with the following structure:

Concern Current implementation
Templating Raw HTML files with fetch() injecting partials/header.html and partials/footer.html
Styling style.css (2 000 + lines), user_guides.css, api-styles.css
Interactivity Inline <script> blocks + darkmode.js + particles.js (Three.js)
Data blogs.json, manuals.json read at runtime by inline JavaScript
Routing File-system paths (no SPA router)
Testing Playwright E2E via npm run test
CI / CD GitHub Actions (.github/workflows/ci.yml) — auto-deploy to main if commit message contains the MAIN keyword
Deploy target GitHub Pages — served from the root of main

Key pages (23 HTML files total):

/index.html                          ← Home
/about/about.html                    ← About
/projects/blogs/blogs.html          ← Blogs list  (+ 3 detail pages)
/projects/user_guides/user_guides.html ← Guides list (+ 3 detail pages)
/projects/white_papers/white_papers.html
/projects/content_strategy/content_strategy.html
/projects/visuals/visuals.html
/api-documentation/index.html       ← API Docs (+ 5 sub-pages)

CDN dependencies: Google Fonts (Poppins), Font Awesome 6.5, Three.js r128, Prism.js 1.24.


2. Why Migrate?

Pain point today After migration
Every new page requires copy-pasting header/footer HTML Shared Angular components — change once, updates everywhere
Dark mode state lives in localStorage and must be manually wired to every page Angular DarkModeService — fully reactive, zero boilerplate per page
Blogs and guides added by editing JSON + running a Python script Typed BlogService / GuideService — add a TypeScript interface entry, the UI updates automatically
No type safety Full TypeScript — catch typos, bad data shapes, missing fields at build time
Testing is limited to Playwright smoke tests Add Jest/Karma unit tests for components + keep Playwright for E2E
Difficult to add animations or new UI features without CSS/JS soup Angular animations API + component isolation
CSS is one monolithic file — risky to change Scoped component styles — changes are isolated

3. Recommended Technology: Angular + Static Export

Angular with @angular/ssr (previously Angular Universal) in static prerender (SSG) mode is the best fit because:

  • The user explicitly asked about Angular.
  • Angular CLI's ng deploy plugin for GitHub Pages (angular-cli-ghpages) handles the entire publish workflow.
  • SSG pre-renders every route to plain HTML files — GitHub Pages serves them with zero server dependency.
  • TypeScript is first-class in Angular.
  • Standalone components (Angular 17+) reduce boilerplate dramatically.

Minimum tool versions required:

Tool Version
Node.js 20 LTS or later
npm 10 +
Angular CLI 17 + (supports standalone components + SSG)

4. Alternatives Evaluated

Framework GitHub Pages compatible TypeScript Bundle size Verdict
Angular 17+ (SSG) Yes (angular-cli-ghpages) Native Medium ✅ Recommended
Astro Yes (built-in static output) Yes Minimal Good alternative — best for content-heavy, low-JS sites
Next.js (static export) Yes (next export) Yes Medium Good, but React-centric — no Angular
Nuxt 3 (SSG) Yes Yes Medium Good if Vue is preferred
Eleventy Yes Via plugins Minimal Great for pure HTML/Markdown sites, less component model

If Angular feels heavyweight for a portfolio site, Astro is the best fallback — it supports Angular components via the @astrojs/angular integration and produces zero-JavaScript pages by default.


5. Pre-Migration Checklist

Complete these before writing any Angular code.

  • Node 20 LTS installed (node -v)
  • Angular CLI 17+ installed globally: npm install -g @angular/cli
  • Working on the develop_2026 branch (already done)
  • All current Playwright tests pass on main: npm run test
  • Inventory of all routes complete (see §7 — File Mapping Reference)
  • Audit images — confirm every image in /images/ is actually referenced somewhere
  • Decide on the repository deploy strategy:
    • Option A (recommended): new /angular-app/ subdirectory inside the same repo, build output goes to /docs/ or a gh-pages branch
    • Option B: migrate fully — the repo root becomes the Angular workspace, old HTML files removed after migration

This guide uses Option A to allow a side-by-side comparison while migrating.


6. Step-by-Step Migration

Step 1 — Bootstrap the Angular project

# From the repo root
cd "c:/Users/T0309801/Documents/Thales Docs/Github/osaron.github.io"

# Create the Angular workspace inside the repo
ng new portfolio-app \
  --directory=angular-app \
  --routing=true \
  --style=scss \
  --ssr=false \
  --standalone=true

cd angular-app

Why --ssr=false here? We first build a working SPA, then add SSG prerendering in Step 2 once routing is stable. Adding SSR from day one increases complexity.

Verify the scaffold works:

ng serve
# Visit http://localhost:4200 — Angular welcome page should appear

Step 2 — Configure for GitHub Pages

GitHub Pages serves the site from https://osaron.github.io/. Because the repository is a user/organization page (the repo is named osaron.github.io), the app is served at the root (/), so no --base-href adjustment is needed.

Install the deploy plugin:

npm install --save-dev angular-cli-ghpages

Configure angular.json — add a deploy target inside projects.portfolio-app.architect:

"deploy": {
  "builder": "angular-cli-ghpages:deploy",
  "options": {
    "dir": "dist/portfolio-app/browser",
    "branch": "gh-pages",
    "name": "osaron",
    "email": "oscarondo14@gmail.com"
  }
}

Note: if you keep deploying from the main branch root (current approach), set "branch": "main" and "dir": "dist/portfolio-app/browser" and update the CI to copy the build output to the repo root. Deploying to a dedicated gh-pages branch is cleaner.

Handle client-side routing on GitHub Pages (GitHub Pages returns 404 for unknown paths):

Create angular-app/src/404.html:

<!DOCTYPE html>
<html>
  <head>
    <script>
      // SPA redirect hack for GitHub Pages
      var l = window.location;
      l.replace(
        l.protocol + '//' + l.hostname + (l.port ? ':' + l.port : '') +
        l.pathname.split('/').slice(0, 1).join('/') +
        '/?p=/' + l.pathname.slice(1).replace(/&/g, '~and~') +
        (l.search ? '&q=' + l.search.slice(1).replace(/&/g, '~and~') : '') +
        l.hash
      );
    </script>
  </head>
</html>

Add to angular.json under assets:

{ "glob": "404.html", "input": "src", "output": "/" }

Add the redirect handler to index.html (before </head>):

<script>
  (function(l) {
    if (l.search[1] === '/') {
      var decoded = l.search.slice(1).split('&').map(function(s) {
        return s.replace(/~and~/g, '&')
      }).join('?');
      window.history.replaceState(null, null,
        l.pathname.slice(0, -1) + decoded + l.hash
      );
    }
  }(window.location))
</script>

Step 3 — Migrate global styles and assets

Copy assets:

# From repo root
cp -r images/ angular-app/src/assets/images/
cp -r projects/user_guides/connect-smart-cloud/connect_smart_ugu_cover.png angular-app/src/assets/images/covers/
# Copy all PDF files
cp -r projects/ angular-app/src/assets/projects/

Migrate CSS variables to SCSS:

Create angular-app/src/styles/_variables.scss:

// Light mode defaults
:root {
  --bg-color: #ffffff;
  --text-color: #1a1a2e;
  --card-bg: #f5f5f5;
  --accent: #6c63ff;
  --nav-bg: rgba(255, 255, 255, 0.95);
  --shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
}

// Dark mode overrides
body.dark-mode {
  --bg-color: #0d0d1a;
  --text-color: #e0e0f0;
  --card-bg: #1a1a2e;
  --accent: #9d8fff;
  --nav-bg: rgba(13, 13, 26, 0.95);
  --shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
}

Update angular-app/src/styles.scss:

@import 'styles/variables';
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap');
@import url('https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css');

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: 'Poppins', sans-serif;
  background-color: var(--bg-color);
  color: var(--text-color);
  transition: background-color 0.3s ease, color 0.3s ease;
}

Step 4 — Create shared components (Header, Footer)

cd angular-app
ng generate component shared/header --standalone
ng generate component shared/footer --standalone

header.component.ts — replace the fetch()-injected partial:

import { Component, inject } from '@angular/core';
import { RouterLink, RouterLinkActive } from '@angular/router';
import { DarkModeService } from '../../core/services/dark-mode.service';

@Component({
  selector: 'app-header',
  standalone: true,
  imports: [RouterLink, RouterLinkActive],
  templateUrl: './header.component.html',
  styleUrl: './header.component.scss'
})
export class HeaderComponent {
  darkMode = inject(DarkModeService);
}

Copy the nav HTML from partials/header.html into header.component.html, replacing:

  • href="..." links → [routerLink]="['/path']" Angular directives
  • Dark mode toggle → (click)="darkMode.toggle()" binding
  • Moon/sun icon → [class]="darkMode.icon$ | async"

footer.component.html — copy from partials/footer.html verbatim, update image paths to use assets/:

<!-- Before -->
<img src="/images/icons/Github Logo White.png">
<!-- After -->
<img src="assets/images/icons/Github Logo White.png">

Step 5 — Migrate the dark-mode service

ng generate service core/services/dark-mode

dark-mode.service.ts — typed TypeScript replacement for darkmode.js:

import { Injectable, signal, computed, effect } from '@angular/core';
import { DOCUMENT } from '@angular/common';
import { inject } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class DarkModeService {
  private doc = inject(DOCUMENT);
  private _dark = signal<boolean>(
    localStorage.getItem('darkMode') === 'true'
  );

  isDark = this._dark.asReadonly();
  icon = computed(() => this._dark() ? 'fa-sun' : 'fa-moon');

  constructor() {
    effect(() => {
      const body = this.doc.body;
      this._dark()
        ? body.classList.add('dark-mode')
        : body.classList.remove('dark-mode');
      localStorage.setItem('darkMode', String(this._dark()));
    });
  }

  toggle() {
    this._dark.update(v => !v);
  }
}

Step 6 — Set up routing

Edit angular-app/src/app/app.routes.ts:

import { Routes } from '@angular/router';

export const routes: Routes = [
  { path: '', loadComponent: () => import('./pages/home/home.component').then(m => m.HomeComponent) },
  { path: 'about', loadComponent: () => import('./pages/about/about.component').then(m => m.AboutComponent) },
  { path: 'projects/blogs', loadComponent: () => import('./pages/projects/blogs/blogs-list.component').then(m => m.BlogsListComponent) },
  { path: 'projects/blogs/:slug', loadComponent: () => import('./pages/projects/blogs/blog-detail.component').then(m => m.BlogDetailComponent) },
  { path: 'projects/user-guides', loadComponent: () => import('./pages/projects/user-guides/guides-list.component').then(m => m.GuidesListComponent) },
  { path: 'projects/user-guides/:slug', loadComponent: () => import('./pages/projects/user-guides/guide-detail.component').then(m => m.GuideDetailComponent) },
  { path: 'projects/white-papers', loadComponent: () => import('./pages/projects/white-papers/white-papers.component').then(m => m.WhitePapersComponent) },
  { path: 'projects/content-strategy', loadComponent: () => import('./pages/projects/content-strategy/content-strategy.component').then(m => m.ContentStrategyComponent) },
  { path: 'projects/visuals', loadComponent: () => import('./pages/projects/visuals/visuals.component').then(m => m.VisualsComponent) },
  { path: 'api-docs', loadComponent: () => import('./pages/api-docs/api-docs.component').then(m => m.ApiDocsComponent), children: [
    { path: '', redirectTo: 'getting-started', pathMatch: 'full' },
    { path: 'getting-started', loadComponent: () => import('./pages/api-docs/getting-started.component').then(m => m.GettingStartedComponent) },
    { path: 'authentication', loadComponent: () => import('./pages/api-docs/authentication.component').then(m => m.AuthenticationComponent) },
    { path: 'api-reference', loadComponent: () => import('./pages/api-docs/api-reference.component').then(m => m.ApiReferenceComponent) },
    { path: 'content-api', loadComponent: () => import('./pages/api-docs/content-api.component').then(m => m.ContentApiComponent) },
    { path: 'guides', loadComponent: () => import('./pages/api-docs/guides.component').then(m => m.GuidesComponent) },
    { path: 'resources', loadComponent: () => import('./pages/api-docs/resources.component').then(m => m.ResourcesComponent) },
  ]},
  { path: '**', redirectTo: '' }
];

All routes use lazy loading (loadComponent) so the initial bundle stays small.


Step 7 — Migrate the Home page

ng generate component pages/home --standalone

The home page has four main sections. Create a sub-component for each:

ng generate component pages/home/hero-section --standalone
ng generate component pages/home/featured-projects --standalone
ng generate component pages/home/tech-stack --standalone
ng generate component pages/home/why-me-carousel --standalone

Hero section — copy HTML from index.html's hero <section> block into hero-section.component.html. Replace <canvas id="bg"> particle canvas with the Angular particle wrapper (see Step 11).

Featured projects tabs — replace inline JavaScript tab-switching with an Angular signal:

// featured-projects.component.ts
selectedTab = signal<string>('user-guide');
tabs = ['user-guide', 'technical-article', 'workflow-diagram', 'instructional-video', 'white-paper'];
<!-- featured-projects.component.html -->
<div class="tab-buttons">
  @for (tab of tabs; track tab) {
    <button [class.active]="selectedTab() === tab" (click)="selectedTab.set(tab)">
      {{ tab | titlecase }}
    </button>
  }
</div>

Company / Education carousel — replace setInterval with an Angular @let + interval-driven signal:

// why-me-carousel.component.ts
import { Component, signal, OnInit, OnDestroy } from '@angular/core';

@Component({ standalone: true, ... })
export class WhyMeCarouselComponent implements OnInit, OnDestroy {
  activeIndex = signal(0);
  private timer?: ReturnType<typeof setInterval>;

  ngOnInit() {
    this.timer = setInterval(() => {
      this.activeIndex.update(i => (i + 1) % this.slides.length);
    }, 6000);
  }

  ngOnDestroy() { clearInterval(this.timer); }
}

Step 8 — Migrate the About page

ng generate component pages/about --standalone

Copy HTML from about/about.html into about.component.html. The About page has three static sections (intro, skills grid, timeline) — no dynamic data, so no service needed. Scoped styles go in about.component.scss.


Step 9 — Migrate Projects (Blogs, User Guides, etc.)

Define TypeScript interfaces (create angular-app/src/app/core/models/):

// blog.model.ts
export interface Blog {
  slug: string;
  title: string;
  description: string;
  date: string;
  tags: string[];
  coverImage: string;
  pdfPath?: string;
  content?: string;
}

// guide.model.ts
export interface Guide {
  slug: string;
  title: string;
  description: string;
  coverImage: string;
  pdfPath?: string;
}

Create data services:

ng generate service core/services/blog
ng generate service core/services/guide
// blog.service.ts
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { Blog } from '../models/blog.model';

@Injectable({ providedIn: 'root' })
export class BlogService {
  private http = inject(HttpClient);

  getAll(): Observable<Blog[]> {
    return this.http.get<Blog[]>('assets/data/blogs.json');
  }

  getBySlug(slug: string): Observable<Blog | undefined> {
    return this.getAll().pipe(
      map(blogs => blogs.find(b => b.slug === slug))
    );
  }
}

Copy projects/blogs/blogs.jsonangular-app/src/assets/data/blogs.json.
Copy projects/user_guides/manuals.jsonangular-app/src/assets/data/manuals.json.

Blogs list component:

ng generate component pages/projects/blogs/blogs-list --standalone
ng generate component pages/projects/blogs/blog-detail --standalone
// blogs-list.component.ts
@Component({ standalone: true, ... })
export class BlogsListComponent {
  blogs$ = inject(BlogService).getAll();
}
<!-- blogs-list.component.html -->
@if (blogs$ | async; as blogs) {
  @for (blog of blogs; track blog.slug) {
    <article class="blog-card">
      <img [src]="'assets/images/blogs/' + blog.coverImage" [alt]="blog.title">
      <h2>{{ blog.title }}</h2>
      <p>{{ blog.description }}</p>
      <a [routerLink]="['/projects/blogs', blog.slug]">Read more</a>
    </article>
  }
}

Repeat the same pattern for User Guides, White Papers, Content Strategy, and Visuals.


Step 10 — Migrate API Documentation section

The API docs section is the most complex — it has a three-column layout (sidebar, main content, ToC) with six pages.

ng generate component pages/api-docs --standalone
ng generate component pages/api-docs/api-sidebar --standalone
ng generate component pages/api-docs/getting-started --standalone
ng generate component pages/api-docs/authentication --standalone
ng generate component pages/api-docs/api-reference --standalone
ng generate component pages/api-docs/content-api --standalone
ng generate component pages/api-docs/guides --standalone
ng generate component pages/api-docs/resources --standalone

The ApiDocsComponent acts as a shell with the sidebar + <router-outlet> for the child route content:

<!-- api-docs.component.html -->
<div class="api-layout">
  <app-api-sidebar />
  <main class="api-main">
    <router-outlet />
  </main>
</div>

Migrate api-styles.cssapi-docs.component.scss (scoped to this shell).

Prism.js for code highlighting — install the Angular-friendly wrapper instead of the CDN link:

npm install ngx-prism --save
# OR use the raw prism package:
npm install prismjs --save
npm install @types/prismjs --save-dev

Add to angular.json under styles and scripts:

"styles": ["node_modules/prismjs/themes/prism-tomorrow.css"],
"scripts": ["node_modules/prismjs/prism.js"]

Step 11 — Migrate the particle background

The Three.js particles.js animation is a self-contained WebGL canvas. Wrap it in an Angular component:

ng generate component shared/particle-background --standalone
// particle-background.component.ts
import { Component, ElementRef, OnInit, OnDestroy, viewChild } from '@angular/core';
import * as THREE from 'three';

@Component({
  standalone: true,
  selector: 'app-particle-background',
  template: `<canvas #canvas class="particle-canvas"></canvas>`,
  styles: [`
    .particle-canvas {
      position: fixed;
      top: 0; left: 0;
      width: 100%; height: 100%;
      z-index: -1;
      pointer-events: none;
    }
  `]
})
export class ParticleBackgroundComponent implements OnInit, OnDestroy {
  canvas = viewChild.required<ElementRef<HTMLCanvasElement>>('canvas');
  private renderer?: THREE.WebGLRenderer;
  private animFrameId?: number;

  ngOnInit() {
    // Move the entire contents of particles.js here
    // Replace document.getElementById('bg') with this.canvas().nativeElement
    this.initParticles();
  }

  ngOnDestroy() {
    cancelAnimationFrame(this.animFrameId!);
    this.renderer?.dispose();
  }

  private initParticles() {
    // ... paste and adapt particles.js logic here ...
  }
}

Install Three.js as a proper npm dependency:

npm install three --save
npm install @types/three --save-dev

Step 12 — Migrate JSON data to Angular services

Current data files to migrate:

Old path New path Service
projects/blogs/blogs.json src/assets/data/blogs.json BlogService
projects/user_guides/manuals.json src/assets/data/manuals.json GuideService

Add provideHttpClient() to app.config.ts:

import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient } from '@angular/common/http';
import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes),
    provideHttpClient()
  ]
};

Step 13 — Replace Python scripts with Angular schematics

The scripts/update_blogs.py and scripts/update_buttons.py scripts exist to inject partials and regenerate HTML from JSON. In Angular, these are no longer needed because:

  • Header/footer are components — no injection needed.
  • Blog list is rendered dynamically from blogs.json via BlogService.
  • To add a new blog, edit src/assets/data/blogs.json — the UI updates on next build.

Document this change by adding a CONTENT_GUIDE.md inside angular-app/:

# Adding New Content

## Adding a Blog Post
1. Add a new entry to `src/assets/data/blogs.json` following the `Blog` interface.
2. Add the cover image to `src/assets/images/blogs/`.
3. (Optional) Add a PDF to `src/assets/projects/blogs/<slug>/`.
4. Run `ng build` and deploy.

## Adding a User Guide
Same pattern — edit `src/assets/data/manuals.json`.

Step 14 — Migrate Playwright tests

The existing Playwright tests in tests/portfolio.spec.js test the static HTML directly. Update them to test the Angular app:

Update playwright.config.js (or create angular-app/playwright.config.ts):

import { defineConfig } from '@playwright/test';

export default defineConfig({
  testDir: './e2e',
  use: { baseURL: 'http://localhost:4200' },
  webServer: {
    command: 'ng serve',
    port: 4200,
    reuseExistingServer: !process.env['CI']
  }
});

Migrate tests/portfolio.spec.jsangular-app/e2e/portfolio.spec.ts:

import { test, expect } from '@playwright/test';

test('home page renders hero section', async ({ page }) => {
  await page.goto('/');
  await expect(page.locator('app-hero-section')).toBeVisible();
});

test('dark mode toggle works', async ({ page }) => {
  await page.goto('/');
  await page.click('[data-testid="dark-mode-toggle"]');
  await expect(page.locator('body')).toHaveClass(/dark-mode/);
});

test('navigation to blogs page works', async ({ page }) => {
  await page.goto('/');
  await page.click('text=Blogs');
  await expect(page).toHaveURL('/projects/blogs');
});

Step 15 — Update CI/CD pipeline

Update .github/workflows/ci.yml to build and deploy the Angular app:

name: Build, Test & Deploy

on:
  push:
    branches: ['**']
  pull_request:

jobs:
  build-and-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Setup Node 20
        uses: actions/setup-node@v4
        with:
          node-version: 20

      - name: Install dependencies
        working-directory: ./angular-app
        run: npm ci

      - name: Build Angular app
        working-directory: ./angular-app
        run: npm run build -- --configuration production

      - name: Install Playwright
        working-directory: ./angular-app
        run: npx playwright install --with-deps chromium

      - name: Run E2E tests
        working-directory: ./angular-app
        run: npx playwright test

      - name: Deploy to GitHub Pages
        if: github.ref == 'refs/heads/main' && contains(github.event.head_commit.message, 'MAIN')
        working-directory: ./angular-app
        run: npx ng deploy --no-build
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Step 16 — Final validation and cutover

Before removing the old static files:

  • All 9 routes load correctly at http://localhost:4200
  • Dark mode toggle persists after page reload
  • Blogs list renders all entries from blogs.json
  • User Guides list renders all entries from manuals.json
  • Particle background animation runs on home page
  • API Docs sidebar navigation works (child routing)
  • All Playwright E2E tests pass
  • Angular production build completes without errors: ng build --configuration production
  • ng deploy pushes to gh-pages branch successfully
  • Live site at https://osaron.github.io/ displays the Angular app

Once all checks pass:

# Archive old static files (optional — keep for reference)
git mv index.html _old_static/index.html
git mv about/ _old_static/about/
git mv projects/ _old_static/projects/
git mv api-documentation/ _old_static/api-documentation/
git mv partials/ _old_static/partials/
git mv style.css _old_static/style.css
# ... etc
git commit -m "chore: archive old static files after Angular migration MAIN"

7. File Mapping Reference

Old static file New Angular component Notes
partials/header.html shared/header/header.component RouterLink replaces href
partials/footer.html shared/footer/footer.component Asset paths updated
darkmode.js core/services/dark-mode.service.ts Signal-based reactive state
particles.js shared/particle-background/particle-background.component Three.js via npm
style.css styles.scss + styles/_variables.scss SCSS with CSS custom properties
api-documentation/api-styles.css pages/api-docs/api-docs.component.scss Scoped to api-docs shell
index.html pages/home/home.component 4 child section components
about/about.html pages/about/about.component Static, no service needed
projects/blogs/blogs.html pages/projects/blogs/blogs-list.component Driven by BlogService
projects/blogs/[name]/index.html pages/projects/blogs/blog-detail.component Route param: slug
projects/user_guides/user_guides.html pages/projects/user-guides/guides-list.component Driven by GuideService
projects/user_guides/[name]/index.html pages/projects/user-guides/guide-detail.component Route param: slug
projects/white_papers/white_papers.html pages/projects/white-papers/white-papers.component Static
projects/content_strategy/content_strategy.html pages/projects/content-strategy/content-strategy.component Static
projects/visuals/visuals.html pages/projects/visuals/visuals.component Static
api-documentation/index.html pages/api-docs/getting-started.component Child route
api-documentation/authentication.html pages/api-docs/authentication.component Child route
api-documentation/api-reference.html pages/api-docs/api-reference.component Child route
api-documentation/content-api.html pages/api-docs/content-api.component Child route
api-documentation/guides.html pages/api-docs/guides.component Child route
api-documentation/resources.html pages/api-docs/resources.component Child route
projects/blogs/blogs.json src/assets/data/blogs.json Unchanged structure
projects/user_guides/manuals.json src/assets/data/manuals.json Unchanged structure
scripts/update_blogs.py (deleted — no longer needed) Angular handles dynamically
scripts/update_buttons.py (deleted — no longer needed) Angular handles dynamically

8. Known Gotchas

  1. Image paths with spaces — several image filenames contain spaces (e.g., Nuevo Logo Black.png, Github Logo White.png). In Angular templates, use URL encoding or rename files to kebab-case (nuevo-logo-black.png) and update all references.

  2. PDF download via hidden iframe — the current blogs.html opens PDFs using a hidden <iframe> print trick. In Angular, replace with a direct <a [href]="blog.pdfPath" target="_blank" download> link pointing to the asset path.

  3. Header/Footer fetch() injection — the current partials/ injection loads partials after DOMContentLoaded. In Angular this is replaced by components — delete all fetch() calls for header/footer from page scripts.

  4. dark-mode class on <body> — Angular's DOCUMENT injection token is required (not document directly) to manipulate the body class in a server-safe way. See Step 5.

  5. GitHub Pages base href — because this is a user page (osaron.github.io), the base href is /. If you ever move this to a project page (e.g., osaron.github.io/portfolio), add --base-href /portfolio/ to the ng build command.

  6. Three.js r128 → currentparticles.js uses Three.js r128 via CDN. The npm version will be more recent; check the Three.js migration guide if shader APIs have changed.

  7. Auto-deploy keyword MAIN — the current CI checks for MAIN in the commit message. Keep this convention or update the CI workflow condition when migrating.

  8. angular-cli-ghpages tokenng deploy needs a GITHUB_TOKEN secret in the repo settings to push to the gh-pages branch.


9. Progress Tracking

Use this checklist to track migration progress across sessions:

Foundation

  • Step 1 — Angular project bootstrapped in /angular-app/
  • Step 2 — GitHub Pages configuration complete (404.html redirect, deploy target)
  • Step 3 — Global styles and assets migrated

Shared Components

  • Step 4 — Header component
  • Step 4 — Footer component
  • Step 5 — DarkModeService
  • Step 6 — App routing configured

Pages

  • Step 7 — Home page (Hero, Featured Projects, Tech Stack, Why Me carousel)
  • Step 8 — About page
  • Step 9 — Blogs list + detail
  • Step 9 — User Guides list + detail
  • Step 9 — White Papers page
  • Step 9 — Content Strategy page
  • Step 9 — Visuals page
  • Step 10 — API Documentation section (6 sub-pages + sidebar)

Features

  • Step 11 — Particle background component
  • Step 12 — BlogService + GuideService (JSON data)
  • Step 13 — Content guide written, Python scripts removed

Quality

  • Step 14 — Playwright tests migrated and passing
  • Step 15 — CI/CD pipeline updated
  • Step 16 — Full validation checklist complete
  • Step 16 — Old static files archived
  • Live site live on https://osaron.github.io/

Generated by Claude Code on 2026-06-03. Branch: develop_2026.


10. Actual Implementation Notes

This section documents every place the real migration diverged from the plan above. Any AI agent picking this up should read this section first — it is the ground truth.

Angular 21 file naming (no .component. suffix)

The plan used home.component.ts / HomeComponent. Angular 21 dropped the .component. suffix:

Plan Reality
home.component.ts + HomeComponent home.ts + Home
dark-mode.service.ts + DarkModeService dark-mode.service.ts + DarkModeService (services kept suffix)

All loadComponent calls in app.routes.ts import from .../home and reference m.Home, etc.

Projects shell + child component pattern

The plan did not mention this explicitly. Before writing any page components, a Projects shell component was added at pages/projects/projects.ts. It renders a horizontal tab nav and a <router-outlet>. All project-type pages (blogs, user-guides, etc.) are child routes under /projects. This was introduced at the user's request before Step 2 was complete.

API Docs moved to a top-level route

The plan placed API Docs as a child route of /projects (i.e., /projects/api-docs). During implementation it was moved to its own top-level route /api-docs for two reasons:

  1. The original site's navbar had "API Docs" as a separate top-level item, not inside the Projects dropdown.
  2. The 3-column layout would have been nested inside the Projects shell's <router-outlet>, breaking the layout.

Header update: "API Docs" is now a standalone <a routerLink="/api-docs"> link in the navbar, separate from the Projects dropdown. The Projects dropdown no longer includes API Docs.

Route location: src/app/pages/api-docs/ (not pages/projects/api-docs/). The old pages/projects/api-docs/ placeholder is unused and can be deleted.

Steps 4 and 5 were done together

DarkModeService (Step 5) was created in the same pass as the Header and Footer (Step 4) because the Header directly depends on the service. The progress tracking checklist reflects this as a combined step.

API Docs CSS approach: ViewEncapsulation.None

The plan said to add api-styles.css to the global styles array. Instead, ViewEncapsulation.None was used on ApiDocs (pages/api-docs/api-docs.ts) so the full CSS lives in api-docs.scss and applies globally only while the component is mounted. This is cleaner — the styles are self-contained in one component directory and are automatically removed when you navigate away.

The body.api-docs-page class is added in ngOnInit and removed in ngOnDestroy via inject(DOCUMENT).

Two new shared infrastructure pieces for API Docs

These were not in the original plan:

New file Purpose
core/services/toc.service.ts Signal store — each page writes its TOC items here; the TOC component in the right sidebar reads them
core/services/api-page-base.ts Abstract @Directive that all 6 API docs page components extend — handles TocService.set() on init, Prism.highlightAll() on after-view-init, TOC clear on destroy, and scroll-spy via @HostListener('window:scroll')

Prism.js: npm package, not CDN

The plan mentioned adding CDN links. Instead, prismjs was installed as an npm package and added to angular.json scripts:

"scripts": [
  "node_modules/prismjs/prism.js",
  "node_modules/prismjs/components/prism-bash.min.js",
  ...
]

Called via the global Prism.highlightAll() in ApiPageBase.ngAfterViewInit().

THREE.Clock is deprecated in Three.js 0.184

The plan used THREE.Clock. The installed version is three@0.184.0 where Clock is deprecated. Replaced with performance.now():

// Init
this.startMs = performance.now();

// Animate loop
const t = (performance.now() - this.startMs) / 1000;

CSS budget raised to 24 kB warning / 32 kB error

The default Angular budget for component styles is 4 kB warning / 8 kB error. Three components legitimately exceed this:

Component Reason
home.scss (~4.3 kB) Rich hero, carousel, tech stack, and featured-projects styles
api-docs.scss (~19.8 kB) Full docs-site theme (ported verbatim from api-styles.css)

The budget in angular.json was raised to 24kB warning / 32kB error to avoid false failures.

Three.js bundle size (home chunk)

The home lazy chunk is ~532 kB (raw) / ~113 kB (gzipped) due to Three.js being included. This is expected and acceptable — Three.js is only loaded when the user visits the home page. To reduce it further in the future, consider using a lighter particle library or a custom WebGL solution without the full Three.js scene graph.

CI/CD split into two jobs

The original single test-and-deploy job was split into:

  • legacy-tests — runs the old Playwright tests against the static site (main branch only, to keep the current live site tested)
  • angular-build — installs Angular deps, builds, runs new E2E tests, deploys to gh-pages on MAIN keyword

Unused generated component

During scaffolding, ng generate component pages/api-docs/api-docs-shell was run but the resulting files at pages/api-docs/api-docs-shell/ are never imported. They can be safely deleted.

File Mapping delta — what changed from the table in §7

Original plan Actual location
pages/api-docs/api-docs.component (shell) pages/api-docs/api-docs.ts (top-level, not under projects)
Child routes under /projects/api-docs/... Child routes under /api-docs/...
API Docs in Projects dropdown API Docs as standalone nav link
THREE.Clock performance.now()
CDN Prism.js links npm prismjs in angular.json scripts

Portfolio v2.0 — Changelog (branch develop_2026)

All changes below were applied after the initial Angular migration was complete. Version tag: osaron_portfolio_v2.0

UX & Navigation

Change Details
"Homepage" label in navbar <span class="nav-home-label"> added next to the logo. Uses lang.t('nav.home') for bilingual support.
Arrow → "Explore" pill on hover .link-circle expands from 40 px circle to 100 px pill; arrow fades out, "Explore" / "Ver más" fades in via CSS transitions.
EN / ES language toggle See Internationalisation section below.

API Documentation

Change Details
Sidebars switched to position: fixed Resolves sidebars disappearing when scrolling near the footer. left / right use max(1.25rem, calc((100vw - 1480px) / 2 + 1.25rem)) to align with the centred grid.
Footer overlap fix @HostListener('window:scroll') in api-docs.ts computes footer.getBoundingClientRect().top and sets --sidebar-bottom-offset. Sidebars use bottom: var(--sidebar-bottom-offset) instead of a fixed height — they shrink as the footer enters the viewport.
TOC anchor links fixed toc.html links previously used [href]="'#' + id" which Angular's router intercepted and redirected to /. Fixed by adding (click)="scrollTo(id, $event)" that calls element.scrollIntoView({ behavior: 'smooth' }) and cancels the default event.
Testing Guide (Guides page rewrite) Full content rewrite covering: Load & Stress Testing (JMeter, k6, Locust, SoapUI, Karate), API Automation (Postman, REST Assured, TestNG, Karate, SoapUI), UI & E2E (Selenium, Cypress, Playwright, UFT), Mobile (Appium, Katalon), BDD & Gherkin, Languages (Python, Groovy, JS, Java, Gherkin, VBScript). TOC items updated in guides.ts.
Sidebar label "Guides" renamed to "Testing Guide" in sidebar.ts.

Content & Pages

Change Details
Home hero text Tagline changed to "QA Engineer & Senior Technical Writer". Text centred via text-align: center on .hero.
Videos page Replaced Content Strategy placeholder with a YouTube embed for the Geome User Guide walkthrough. Uses DomSanitizer.bypassSecurityTrustResourceUrl for the iframe src. Click-to-play pattern: thumbnail shown first, iframe swapped in on click using an Angular signal.
Footer version tag osaron_portfolio_v2.0 added as .version-tag (monospace, right-aligned, 35 % opacity).

Internationalisation (EN / ES)

A lightweight runtime translation system was built without external dependencies, using Angular signals for reactivity.

New files:

File Purpose
core/services/language.service.ts LanguageService — holds lang = signal<'en' | 'es'>('en'), exposes toggle() and t(key)
core/translations/translations.ts Flat Record<string, { en: string; es: string }> dictionary

How reactivity works: t(key) reads this.lang() (a signal) internally. Angular's template renderer tracks signal reads during rendering — so any component template that calls lang.t(...) is automatically marked dirty when lang.toggle() is called, without needing ChangeDetectionStrategy.OnPush or a pure: false pipe.

Components updated:

Component Translated content
header Logo label, nav items (Projects, API Docs, About), dropdown labels, mobile menu. Language toggle button (ES / EN) added next to dark-mode toggle.
home Hero tagline, greeting, sub-text; Featured Projects heading; tab labels, titles, and descriptions; Tech Stack heading and group headings; Why Me? heading, value label, and all three bullet points; carousel slide headings.
footer "Get in touch at" line and copyright notice.
projects All five nav tab labels (User Guides, Technical Articles, Diagrams, Videos, White Papers).
about Profile heading and body; all three skill names and descriptions; all five work-experience entries; all four education entries.

Data structure changes in home.ts:

  • slides array: heading string replaced by key: string pointing to a translation key.
  • stackGroups array: heading string replaced by key: string.
  • tabs array: label, title, description removed — resolved at render time via lang.t('tab.' + tab.id + '.label') etc.

Extending translations: Add a new key–value pair to translations.ts and call lang.t('your.key') in any template. No imports, no pipe, no module registration needed.

Known issues / next steps

  • API Docs pages are English onlyResolved in v2.2: all six API docs pages are now fully bilingual (EN/ES). Code blocks inside <pre><code> are intentionally left in English.
  • User-guide PDFs, blog articles, and white-paper PDFs are static assets and are not translated.
  • The language preference is not persisted across page reloads (no localStorage save). Add localStorage.setItem('lang', value) in toggle() and read it in the service constructor to persist.

Portfolio v2.1 — Changelog (branch develop_2026)

Changes applied in session dated 2026-06-04.

Videos section — multi-video refactor

Change Details
Array-based video list Videos component refactored from a single hardcoded video to a VideoCard[] array. Each entry holds its own WritableSignal<boolean> for play state and a SafeResourceUrl for the embed, so cards are fully independent.
New video added "Deck Family Farm — Account Setup" (YouTube ID 4JE0QlHdyR0) added as a second card with tag Tutorial.
Local thumbnail support VideoCard interface gained an optional thumbnail?: string field. When present, the card uses the local image; otherwise it falls back to https://img.youtube.com/vi/{id}/maxresdefault.jpg. The Deck Family Farm thumbnail (Deck Family Thumbnails.png) was copied to public/images/videos/deck-family-farm-thumbnail.png.

New / changed files:

File Change
src/app/pages/projects/videos/videos.ts VideoCard interface + videos[] array replacing single-video props
src/app/pages/projects/videos/videos.html @for loop over videos; [src] uses nullish-coalescing fallback
public/images/videos/deck-family-farm-thumbnail.png New local thumbnail asset

Workflow Diagrams — data-driven card system

The static info list was replaced with a fully data-driven card pattern matching the Blogs section.

Change Details
Diagram model New interface at core/models/diagram.model.ts with fields: title, summary, slug, date, tags, cover, tool, isExternal, link?, pdfPath?, problem?, steps? (array of DiagramStep).
DiagramService New service at core/services/diagram.service.ts — loads /data/diagrams.json via HttpClient.
diagrams.json New data file at public/data/diagrams.json. First entry: TravelHub Software Architecture Design.
WorkflowDiagrams component Now injects DiagramService and Router; renders cards via @for; clicking a card navigates to /projects/workflow-diagrams/:slug. The old static .visual-section tool list is preserved below the cards, separated by a border.
New route workflow-diagrams/:slug added as a sibling child route under projects in app.routes.ts, lazy-loading DiagramDetail.

New / changed files:

File Change
src/app/core/models/diagram.model.ts New — Diagram + DiagramStep interfaces
src/app/core/services/diagram.service.ts New — DiagramService
public/data/diagrams.json New — TravelHub entry with full problem + 6 development steps
public/images/diagrams/travel-hub-architecture.png New — architecture diagram image (copied from local Uniandes coursework)
src/app/pages/projects/workflow-diagrams/workflow-diagrams.ts Refactored to use service + Router navigation
src/app/pages/projects/workflow-diagrams/workflow-diagrams.html Card list via @for + preserved tools section
src/app/pages/projects/workflow-diagrams/workflow-diagrams.scss Card styles added; tools section given a top border separator
src/app/app.routes.ts Added workflow-diagrams/:slug route

Diagram Detail page — new routed page

A new standalone detail page renders at /projects/workflow-diagrams/:slug inside the existing Projects shell (sidebar stays visible, "Diagrams" tab stays active).

Layout:

  1. Hero — gradient banner with title + "Back to Diagrams" button on the same row (space-between); meta row below title shows date + all tags, each with an icon (fa-calendar-days for date, fa-pen-ruler for draw.io, fa-diagram-project for Visual Paradigm, fa-hashtag for all others).
  2. The Problemproblem field from JSON split on \n\n and rendered as paragraphs.
  3. Development Processsteps[] rendered as a CSS-counter ordered list; each item has a numbered circle (accent colour) + title + description.
  4. Architecture Diagram — full-width image with a rounded container and subtle shadow.

New files:

File Purpose
src/app/pages/projects/workflow-diagrams/diagram-detail/diagram-detail.ts Component — reads :slug from ActivatedRoute, finds matching diagram via service, exposes problemParagraphs getter and tagIcon(tag) method
src/app/pages/projects/workflow-diagrams/diagram-detail/diagram-detail.html Template — hero, problem, steps, full image
src/app/pages/projects/workflow-diagrams/diagram-detail/diagram-detail.scss Scoped styles — hero, step counter, full-width diagram wrapper

Portfolio v2.2 — Changelog (branch develop_2026)

Changes applied in session dated 2026-06-05.

Internationalisation — project pages fully translated

All project-type page components now use lang.t() for every user-visible string.

Component Content translated
blogs Page title, subtitle, card labels, tag filters, "Read more" links
user-guides Page title, subtitle, card labels, "View guide" links
videos Page title, subtitle, card titles, descriptions, tags, play-button labels
workflow-diagrams Page title, subtitle, card titles, descriptions, tags, tool labels, section headings
white-papers Page title, subtitle, card titles, descriptions, download links
diagram-detail Hero title, "Back" button, meta labels, "The Problem" heading, "Development Process" heading, step content

Internationalisation — API docs fully bilingual

All six API documentation pages are now bilingual (EN/ES). The implementation required a reactive architecture refactor to keep the right-sidebar TOC labels in sync with the active language.

New file:

File Purpose
src/app/core/translations/api-translations.ts ~200 translation keys for all API docs UI strings. Exported as API_TRANSLATIONS and spread into the main TRANSLATIONS object. Key namespaces: api.nav.*, api.toc.*, api.table.*, api.bc.*, api.gs.*, api.auth.*, api.ref.*, api.content.*, api.guide.*, api.res.*, api.sidebar.*.

ApiPageBase refactor (core/services/api-page-base.ts):

Before After
protected abstract tocItems: TocItem[] (static property) protected abstract getTocItems(): TocItem[] (abstract method)
lang not injected in base class lang = inject(LanguageService) — public, accessible from subclass templates
TOC set once in ngOnInit with hardcoded English labels effect(() => this.tocService.set(this.getTocItems())) in constructor — re-fires whenever lang.lang() signal changes

The effect() call in the base constructor tracks the lang.lang() signal. When the user toggles language, Angular re-runs the effect, which calls getTocItems() with the new language context — the TOC labels update without any additional wiring in subclasses.

API docs sidebar (pages/api-docs/sidebar/sidebar.ts):

The pages array was converted from a plain class field to a computed() signal:

readonly pages = computed<NavPage[]>(() => [
  { label: this.lang.t('api.nav.getting-started'), route: 'getting-started' },
  // ...
]);

Templates use pages() (with call parentheses). Nav labels update live on language toggle.

TOC header (pages/api-docs/toc/toc.html):

<span>On this page</span><span>{{ lang.t('api.toc.on-this-page') }}</span>.

Pages updated (TS + HTML):

Page Key changes
getting-started getTocItems() returns 5 labelled items via lang.t(); all prose uses lang.t()
authentication Same pattern; activeTab = signal('curl-auth') preserved
api-reference Same pattern; copyUrl() method preserved
content-api Same pattern
guides (Testing Guide) Same pattern; largest translation block (~80 keys)
resources Same pattern; activeTab, openFaqs, setTab, toggleFaq, isFaqOpen preserved

Code blocks inside <pre><code> are intentionally left in English across all pages.

API docs layout — content column widened

Property Before After
Outer container max-width 1480px 1700px
Fixed sidebar offset calculation calc((100vw - 1480px) / 2 + 1.25rem) calc((100vw - 1700px) / 2 + 1.25rem) (applied to both left and right sidebars)
Content section cap (.main-content > .api-section) max-width: 860px max-width: 1060px

The sidebar offset formula was updated in both the left-sidebar and right-sidebar position: fixed rules to keep them aligned with the new centred grid.

API docs — SDK card width

The sdk-grid and pattern-examples card grids had a minimum column width that was too narrow, causing code blocks inside cards to overflow horizontally. Increased the minmax floor:

Property Before After
.sdk-grid, .pattern-examples grid minmax 240px 320px

At the new content width (~1060px), this forces 3-wide columns (~342px each), giving inline code room to display without a scrollbar.

API docs — code block corner radius

.code-example border-radius reduced for a less pill-like appearance:

Property Before After
.code-example border-radius 22px 10px

Home page — "Why Me?" section flex layout fix

The .why-me-left and .why-me-right columns were collapsing to min-content width on smaller viewports. Fixed by replacing the width/flex shorthand with an explicit flex: 1 1 calc(50% - 1rem) on both columns so they share the row evenly and wrap correctly.

File changed: src/app/pages/home/home.scss