Maintaining a personal portfolio is often a dilemma: you want the flexibility of React to create an interactive and modern landing page, but for writing content, nothing beats the simplicity of a Markdown-based static site generator like Jekyll.

Why choose one when you can have both? In this post, I’ll show you how I configured my site so that the portfolio lives at the root (/) and the blog in a sub-path (/blog), using GitHub Actions to make the magic happen on its own.


The Project Structure

The biggest challenge was organizing the files so they wouldn’t overlap. My structure looks like this:

.
├── .github/workflows/   <-- Where the CI/CD configuration lives
├── blog/                <-- The entire Jekyll site
│   ├── _posts/
│   ├── _config.yml
│   └── index.md
├── src/                 <-- React source code (living at the root)
├── public/              <-- React static assets
├── package.json         <-- React dependencies
└── tailwind.config.js

Configuring Jekyll for a Sub-path

For Jekyll to know it won’t be at the root of the domain, we need to adjust the _config.yml. This is vital so that the generated file paths don’t break:

# blog/_config.yml
title: My Blog
baseurl: "/blog" # <--- Vital to serve from my-domain.com/blog
url: "https://osgioia.dev"

This ensures that all internal links and Jekyll CSS files are correctly served from /blog/assets/....


The Magic: GitHub Actions

This is where the real integration happens. We need a workflow that detects what changed and builds only what’s necessary.

1. Smart Change Detection

We don’t want to build React if we only wrote a Markdown post. We use a bash script in the workflow to detect affected directories:

- name: Check for changes
  id: changes
  run: |
    DIFF_OUTPUT="$(git diff --name-only $ $)"
    
    if echo "$DIFF_OUTPUT" | grep -E '^(src/|public/|package\.json)' > /dev/null; then
      echo "react_changed=true" >> "$GITHUB_OUTPUT"
    fi

    if echo "$DIFF_OUTPUT" | grep -E '^blog/' > /dev/null; then
      echo "blog_changed=true" >> "$GITHUB_OUTPUT"
    fi

2. Conditional Builds

Then, we use that output to run build steps only when needed:

# React Build (only if frontend changed)
- name: Build React portfolio
  if: steps.changes.outputs.react_changed == 'true'
  run: |
    npm ci
    npm run build

# Jekyll Build (only if blog changed)
- name: Build Jekyll blog
  if: steps.changes.outputs.blog_changed == 'true'
  run: |
    cd blog
    bundle exec jekyll build --destination ../_site/blog

3. Merging the Sites

The final step is to merge both results into a single _site folder that GitHub Pages can serve:

- name: Prepare deployment directory
  run: |
    mkdir -p _site
    
    # Copy React build (goes to the root /)
    if [ -d "build" ]; then
      cp -r build/* _site/
    fi

    # The Jekyll build was already done inside _site/blog/
    # so the final structure is perfect.

Why This Solution?

  • React for UX: My landing page uses animations and dynamic components.
  • Jekyll for SEO: Blog posts are pure static HTML, perfect for indexing.
  • Zero Cost: The best part is that all of this is 100% free. GitHub Pages hosts the site at no cost, and GitHub Actions gives us free execution minutes to build the code every time we push.

I hope this technical breakdown helps you set up your own “Frankenstein” of web technologies without spending a single cent!