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.
- Current Architecture Summary
- Why Migrate?
- Recommended Technology: Angular + Static Export
- Alternatives Evaluated
- Pre-Migration Checklist
- Step-by-Step Migration
- Step 1 — Bootstrap the Angular project
- Step 2 — Configure for GitHub Pages
- Step 3 — Migrate global styles and assets
- Step 4 — Create shared components (Header, Footer)
- Step 5 — Migrate the dark-mode service
- Step 6 — Set up routing
- Step 7 — Migrate the Home page
- Step 8 — Migrate the About page
- Step 9 — Migrate Projects (Blogs, User Guides, etc.)
- Step 10 — Migrate API Documentation section
- Step 11 — Migrate the particle background
- Step 12 — Migrate JSON data to Angular services
- Step 13 — Replace Python scripts with Angular schematics
- Step 14 — Migrate Playwright tests
- Step 15 — Update CI/CD pipeline
- Step 16 — Final validation and cutover
- File Mapping Reference
- Known Gotchas
- Progress Tracking
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.
| 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 |
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 deployplugin 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) |
| 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.
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_2026branch (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 agh-pagesbranch - Option B: migrate fully — the repo root becomes the Angular workspace, old HTML files removed after migration
- Option A (recommended): new
This guide uses Option A to allow a side-by-side comparison while migrating.
# 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-appWhy
--ssr=falsehere? 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 appearGitHub 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-ghpagesConfigure 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
mainbranch 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 dedicatedgh-pagesbranch 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>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;
}cd angular-app
ng generate component shared/header --standalone
ng generate component shared/footer --standaloneheader.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">ng generate service core/services/dark-modedark-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);
}
}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.
ng generate component pages/home --standaloneThe 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 --standaloneHero 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); }
}ng generate component pages/about --standaloneCopy 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.
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.json → angular-app/src/assets/data/blogs.json.
Copy projects/user_guides/manuals.json → angular-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.
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 --standaloneThe 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.css → api-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-devAdd to angular.json under styles and scripts:
"styles": ["node_modules/prismjs/themes/prism-tomorrow.css"],
"scripts": ["node_modules/prismjs/prism.js"]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-devCurrent 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()
]
};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.jsonviaBlogService. - 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`.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.js → angular-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');
});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 }}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 deploypushes togh-pagesbranch 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"| 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 |
-
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. -
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. -
Header/Footer
fetch()injection — the currentpartials/injection loads partials afterDOMContentLoaded. In Angular this is replaced by components — delete allfetch()calls for header/footer from page scripts. -
dark-modeclass on<body>— Angular'sDOCUMENTinjection token is required (notdocumentdirectly) to manipulate the body class in a server-safe way. See Step 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 theng buildcommand. -
Three.js
r128→ current —particles.jsuses Three.js r128 via CDN. The npm version will be more recent; check the Three.js migration guide if shader APIs have changed. -
Auto-deploy keyword
MAIN— the current CI checks forMAINin the commit message. Keep this convention or update the CI workflow condition when migrating. -
angular-cli-ghpagestoken —ng deployneeds aGITHUB_TOKENsecret in the repo settings to push to thegh-pagesbranch.
Use this checklist to track migration progress across sessions:
- 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
- Step 4 — Header component
- Step 4 — Footer component
- Step 5 — DarkModeService
- Step 6 — App routing configured
- 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)
- Step 11 — Particle background component
- Step 12 — BlogService + GuideService (JSON data)
- Step 13 — Content guide written, Python scripts removed
- 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.
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.
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.
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.
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:
- The original site's navbar had "API Docs" as a separate top-level item, not inside the Projects dropdown.
- 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.
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.
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).
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') |
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().
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;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.
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.
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 togh-pagesonMAINkeyword
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.
| 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 |
All changes below were applied after the initial Angular migration was complete. Version tag: osaron_portfolio_v2.0
| 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. |
| 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. |
| 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). |
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:
slidesarray:headingstring replaced bykey: stringpointing to a translation key.stackGroupsarray:headingstring replaced bykey: string.tabsarray:label,title,descriptionremoved — resolved at render time vialang.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.
API Docs pages are English only— Resolved 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
localStoragesave). AddlocalStorage.setItem('lang', value)intoggle()and read it in the service constructor to persist.
Changes applied in session dated 2026-06-04.
| 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 |
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 |
A new standalone detail page renders at /projects/workflow-diagrams/:slug inside the existing Projects shell (sidebar stays visible, "Diagrams" tab stays active).
Layout:
- 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-daysfor date,fa-pen-rulerfordraw.io,fa-diagram-projectforVisual Paradigm,fa-hashtagfor all others). - The Problem —
problemfield from JSON split on\n\nand rendered as paragraphs. - Development Process —
steps[]rendered as a CSS-counter ordered list; each item has a numbered circle (accent colour) + title + description. - 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 |
Changes applied in session dated 2026-06-05.
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 |
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.
| 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.
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.
.code-example border-radius reduced for a less pill-like appearance:
| Property | Before | After |
|---|---|---|
.code-example border-radius |
22px |
10px |
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