Riad Kilani
  • Bio
  • Portfolio
  • Blog
  • Contact
  • Accessibility
  • Case Studies
  • CSS
  • Design
  • Front-End
  • HTML
  • JavaScript
  • News
  • Productivity
  • Random Thoughts
  • SEO
  • Themes
  • Trends
  • Tutorials
  • TypeScript
  • TypeSCript From The Ground Up
  • UX Engineering
  • Web Development
  • Wordpress
Home » Case Studies » From Portfolio to Platform: How I Built a SPA + WordPress Ecosystem

From Portfolio to Platform: How I Built a SPA + WordPress Ecosystem

August 8, 2025
riad-kilani-portfolio

I didn’t want “just a portfolio.” I wanted something I could grow—a place to show both my work and how I work. That became a two-part setup: a vanilla-JS Single Page App (SPA) for the portfolio and a custom WordPress site for the blog. They run independently, but feel like one product. This is just a brief overview. Not all code shown is exact and is just to display for the article.

Why this approach works: the SPA stays fast and interactive, while WordPress makes publishing long-form content simple and great for SEO. Best of both worlds.


The Plan (in plain English)

  • Show real engineering: clean code, smart structure, and reliable performance
  • Make life easier later: components, shared tokens, a single build pipeline
  • Delight users: smooth motion, quick loads, consistent UI
  • Be inclusive: accessibility from day one
  • Stay cohesive: portfolio + blog look and feel like the same brand

Two Sites, One Experience

  • riadkilani.com (SPA): vanilla JavaScript, component-based, smooth transitions
  • blog.riadkilani.com (WordPress): custom theme, easy publishing, SEO-friendly

What this buys me:
Performance isolation, safer publishing (content without touching app code), independent scaling, and the right tool for each job.


Inside the SPA (vanilla JS)

I kept the SPA lean but structured like a framework app.

riadkilani.com/
├─ components/   # home, bio, portfolio, contact (HTML/JS/SCSS per view)
├─ scss/         # tokens, base, typography, style.scss
├─ js/           # app.js, router.js, script.js
└─ assets/       # images, fonts

The tiny router (human-sized, no framework):

function loadPage(page) {
  fadeOutContent(() => {
    fetch(`components/${page}/${page}.html`)
      .then(res => res.text())
      .then(html => {
        const main = document.getElementById('main-content');
        main.innerHTML = html;
        document.title = page[0].toUpperCase() + page.slice(1) + ' – Riad Kilani';
        loadPageScript(page);
        fadeInContent();
      })
      .catch(() => main.innerHTML = '<p>Page not found.</p>');
  });
}

Why it matters: faster navigation, page-specific scripts, and SEO-friendly hash URLs—without dragging in a big framework.


The Blog (custom WordPress theme)

The blog keeps writing and organization simple, while staying on-brand.

wp-content/themes/blog/
├─ index.php • single.php • page.php • archive.php
├─ header.php • footer.php • sidebar.php • functions.php
├─ scss/  (_variables.scss, _base.scss, _layout.scss, _widgets.scss, _syntax-highlight.scss, _responsive.scss)
├─ js/    (main.js, main-vanilla.js)
└─ assets/images/

Helpful touches:

  • REST add-on: expose featured image URLs for future integrations
  • Auto-pagination: split very long posts into readable chunks
  • Widget polish: cleaner tag clouds with counts and better styles

(All custom, lightweight, and easy to maintain.)


One Design System, Two Apps

Both sites use the same SCSS tokens (colors, type, spacing, radii) and a shared Gulp build.

// Tokens (excerpt)
$primary: #2b72c9;
$primary-dark: #217dbb;
$accent: #8e24aa;
$text: #333;
$font-heading: "TrajanProBold", serif;
$font-body: "Inter", sans-serif;
$radius: 8px;
$gap: 1rem;
// gulpfile.js (excerpt)
function styles() {
  return gulp.src('./scss/**/*.scss')
    .pipe(sourcemaps.init())
    .pipe(sass().on('error', sass.logError))
    .pipe(postcss([autoprefixer()]))
    .pipe(cleanCSS())
    .pipe(sourcemaps.write('.'))
    .pipe(gulp.dest('./css'));
}

Why it matters: updates roll out consistently to both sites with one set of tokens and one pipeline. Less drift, less rework.


Navigation on Mobile (with accessibility baked in)

Menu interactions are keyboard-friendly, escape-to-close works, and focus returns to the toggle. Small detail, big UX win.


How I Built It (6 simple phases)

  1. Plan: map goals, pick SPA vs WP roles, sketch user journeys
  2. Design system: tokens, components, responsive rules, WCAG 2.1 AA
  3. Build: SPA components + router; custom WP theme + features
  4. Test: cross-browser/device, Lighthouse, keyboard/screen reader passes
  5. Optimize: images, minify, cache, keep plugins lean
  6. Ship & maintain: Git workflow, docs, regular updates

Challenges → Solutions

  • Keeping visuals identical across stacks: shared tokens + patterns
  • Flexible WP theme without mess: modular templates, enhanced widgets, REST tweaks
  • Performance parity: SPA uses code splitting and lazy images; WP uses caching and fewer plugins
  • Consistent mobile UX: same breakpoints and nav patterns in both apps

Shared breakpoints:

$mobile: 480px;
$tablet: 768px;
$desktop: 1024px;

CORS & CSP Headers (challenges and fixes).

Running the SPA on riadkilani.com and WordPress on blog.riadkilani.com surfaced two common gotchas: CORS (fonts, images, and REST calls blocked by default across origins) and a strict CSP that accidentally broke inline scripts/styles from WordPress and some SPA interactions. The fix was to be explicit. For CORS, I returned precise response headers from the WordPress origin—allowing only the portfolio origin, listing the methods/headers I actually use, adding Vary: Origin, and handling OPTIONS preflights. For CSP, I started in Report-Only, then rolled out a policy that whitelists my own domains/CDNs and uses nonces for inline scripts (so WP and small inline inits keep working) while avoiding blanket unsafe-inline. Together, these keep the app secure without breaking legitimate cross-site requests.

Minimal header examples (optional):

Apache (.htaccess on blog.riadkilani.com)

# CORS Headers for Cross-Origin Resource Sharing
<IfModule mod_headers.c>
    # Remove any existing CORS headers first
    Header unset Access-Control-Allow-Origin
    Header unset Access-Control-Allow-Methods
    Header unset Access-Control-Allow-Headers
    Header unset Access-Control-Allow-Credentials
    
    # Set specific CORS headers for allowed origins - all domain variations
    SetEnvIf Origin "^https://www\.riadkilani\.com$" ALLOW_ORIGIN=1
    SetEnvIf Origin "^https://riadkilani\.com$" ALLOW_ORIGIN=1
    
    # Set headers dynamically based on origin
    Header set Access-Control-Allow-Origin "%{HTTP_ORIGIN}e" env=ALLOW_ORIGIN
    Header set Access-Control-Allow-Methods "GET, POST, OPTIONS" env=ALLOW_ORIGIN
    Header set Access-Control-Allow-Headers "Content-Type, Authorization, X-Requested-With" env=ALLOW_ORIGIN
    Header set Access-Control-Allow-Credentials "true" env=ALLOW_ORIGIN
    Header always set Referrer-Policy "strict-origin-when-cross-origin"

    # Allow font loading from any origin
    <FilesMatch "\.(woff|woff2|ttf|eot)$">
        Header set Access-Control-Allow-Origin "*"
    </FilesMatch>

WordPress CSP with nonces (functions.php on blog.riadkilani.com)

add_action('send_headers', function () {
  // Generate a per-request nonce
  $nonce = base64_encode(random_bytes(16));

  // Store it (so we can attach to enqueued <script> tags)
  $GLOBALS['csp_nonce'] = $nonce;

  header("Content-Security-Policy: ".
    "default-src 'self'; ".
    "script-src 'self' 'nonce-{$nonce}' https://riadkilani.com; ".
    "style-src 'self' 'unsafe-inline'; ".
    "img-src 'self' data: https:; ".
    "font-src 'self' https://riadkilani.com; ".
    "connect-src 'self' https://riadkilani.com https://blog.riadkilani.com; ".
    "frame-ancestors 'self'; object-src 'none'; base-uri 'self';");
});

// Attach the nonce to all enqueued scripts
add_filter('script_loader_tag', function ($tag) {
  if (!empty($GLOBALS['csp_nonce'])) {
    $tag = str_replace('<script ', '<script nonce="'.$GLOBALS['csp_nonce'].'" ', $tag);
  }
  return $tag;
}, 10, 1);

Tips:
• Prefer an exact origin over * (especially if you use credentials).
• Add Access-Control-Max-Age to cache preflights if needed.
• Start CSP in Report-Only to collect violations, then enforce.
• Replace remaining inline scripts/styles with nonced or external files over time.


Results (current test numbers)

  • Portfolio (SPA): Perf ~96, FCP ~1.2s, LCP ~2.1s, CLS ~0.08, TTI ~2.3s
  • Blog (WP): Perf ~92, Mobile ~89, SEO ~97, Accessibility ~95
  • Together: ~98% visual consistency, <500ms load-time variance, unified navigation

(Numbers will evolve as I keep tuning, but the baseline is strong.)


What This Shows

  • Frontend: HTML/CSS/JS (ES6+), SCSS, components, routing, responsive design
  • Backend (WP): PHP, hooks/filters, REST, performance-aware choices
  • Tooling: Gulp, PostCSS/Autoprefixer, asset optimization, Git
  • UX & Design: systems thinking, motion, accessibility, prototyping
  • PM: documentation, analytics, integration strategy, iteration

What’s Next

  • SPA: PWA features, richer Web Animations, TypeScript, Vite/Webpack
  • Blog: optional headless routes, ACF layouts, i18n, deeper analytics
  • Cross-app: unified API, global search, end-to-end analytics, CI/CD

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

← Previous Post Exploring New Features in CSS: What You Need to Know in 2025
Next Post → Emmet Tips & Tricks for Beginners (That You’ll Actually Use)

Categories

  • Accessibility
  • Case Studies
  • CSS
  • Design
  • Front-End
  • HTML
  • JavaScript
  • News
  • Productivity
  • Random Thoughts
  • SEO
  • Themes
  • Trends
  • Tutorials
  • TypeScript
  • TypeSCript From The Ground Up
  • UX Engineering
  • Web Development
  • Wordpress

Recent Posts

  • Native CSS Is Quietly Replacing Sass, But It Isn’t Replacing the “Need” for Sass
  • Everyday Types Explained (From the Ground Up)
  • 2026 CSS Features You Must Know (Shipped Late 2025–Now)
  • 60 JavaScript Projects in 60 Days
  • JavaScript vs TypeScript: What Actually Changes

Tags

accessibility accessible web design ADA compliance async components Career Journey cascade layers code splitting composables composition api computed properties container queries css Design Inspiration Design Systems disability access File Organization Front-End Development Frontend frontend development immutability javascript JavaScript reducers lazy loading Material Design Modern CSS performance Personal Growth react React useReducer Redux Resume screen readers seo Suspense Teleport TypeScript UI/UX UI Engineering UX UX Engineering Vue Router WCAG web accessibility Web Development Web Performance

Riad Kilani Front-End Developer

© 2026 Riad Kilani. All rights reserved.