Build a Portfolio with GitHub Pages

Build an interactive portfolio and deploy it automatically with GitHub Pages.

Introduction

30 Second Summary

Your best work can end up scattered across old links or forgotten folders. Someone reviewing your skills may never see the full story.

In this project, you will build a responsive personal portfolio for your professional story. A GitHub Actions CI/CD workflow will publish it to GitHub Pages after each update reaches main.

What You'll Build

You'll open your personal site to see your work presented as a polished story that is ready to share.

By the end of this project, you'll have:

  • A responsive homepage that gives visitors a clear professional summary on desktop or mobile.
  • A print-ready résumé that opens cleanly from the browser.
  • An interactive project gallery that filters your work by category while the visible count updates.
  • Secret Mission: Add keyword search that works together with the category filters.

Are there any prerequisites?

Basic familiarity with HTML, CSS, or JavaScript will help you follow the code.

This project assumes you have a GitHub account plus Git, Visual Studio Code, and Microsoft Edge on Windows.

Before We Start

Before the hands-on work begins, this is your moment to define the living portfolio you are building and why it matters to the people who view it.

Create Your Personal Site Repository

Your portfolio needs a public home before its local files can become a live website. A correctly named GitHub Pages repository connects that future website to your personal address.

You'll create the correctly named repository on GitHub. A README.md file gives the remote repository its first tracked file.

You'll use Git to clone that repository into a Visual Studio Code workspace. Microsoft Edge gives you the first visible proof that index.html works.

In this step, get ready to:
  • Create the public repository for your personal website.
  • Prepare a local workspace from the repository.
  • Verify the starter page in Microsoft Edge.
Create the public repository

A personal GitHub Pages repository uses your lowercase GitHub username followed by .github.io. This exact naming pattern prepares the repository for your personal website address.

  • Go to GitHub in your browser.
  • Sign in with your existing account.
  • Select the create menu in the upper-right corner.
  • Click New repository.

Keep Personal Details Intentional

This repository is public because the free GitHub Pages path uses a public repository. Add only résumé details or contact information that you intentionally want visitors to see.

  • Choose your personal account from the Owner menu.
  • Enter YOUR-USERNAME.github.io in the Repository name field.
  • Select Public as the repository visibility.
  • Toggle Add README to On.
  • Click Create repository.

Your portfolio now has its public GitHub repository. You should see README.md in the repository file list.

Repository Name Unavailable?

  • Confirm that you replaced YOUR-USERNAME with your lowercase GitHub username.
  • Check your GitHub repositories for an existing personal-site repository with the same name.
  • Use the existing correctly named repository if you already created it for your account.
  • Help me check the correct name for my GitHub Pages personal-site repository.
Clone and open the repository

A clone is a local copy of your remote repository. PowerShell downloads the repository to your Desktop so every file stays easy to find.

  • Press the Windows key to open the search bar.
  • Type PowerShell and press Enter to open it.
  • Move to your Desktop by running this command:
cd ~/Desktop

What Does This Command Do?

The cd command changes PowerShell's current folder. The ~/Desktop path points to the Desktop inside your Windows user account.

  • Replace YOUR-USERNAME in the command below with your lowercase GitHub username.
  • Replace YOUR-REPOSITORY with YOUR-USERNAME.github.io in the command below.
  • Clone the personal-site repository by running the completed command:
git clone https://github.com/YOUR-USERNAME/YOUR-REPOSITORY

What Does This Command Do?

  • The git clone command downloads the remote repository with its Git history.
  • The final part of the address identifies your GitHub account and personal-site repository.
  • Git creates a local folder named YOUR-USERNAME.github.io on your Desktop.

PowerShell shows download progress before returning to the prompt. Your local repository now exists on the Desktop.

  • Move PowerShell into the cloned repository by running this command:
cd YOUR-USERNAME.github.io

Why Change Folders?

Git commands work against the repository in PowerShell's current folder. Moving into YOUR-USERNAME.github.io ensures the final status check examines your portfolio files.

  • Press the Windows key to open the search bar.
  • Type Visual Studio Code and press Enter to open it.

Visual Studio Code may show a Workspace Trust prompt after you select the folder. This prompt lets you confirm that you trust the code inside the workspace.

  • Click File in the top menu.
  • Click Open Folder....
  • Select your Desktop in the folder picker.
  • Select the YOUR-USERNAME.github.io folder.
  • Click Select Folder.
  • Select Yes, I trust the authors if Workspace Trust appears.

Your remote repository now has a local workspace. The Explorer sidebar should show README.md inside YOUR-USERNAME.github.io.

Clone or Folder Missing?

  • Check that the clone command uses your actual lowercase GitHub username.
  • Confirm that YOUR-REPOSITORY was replaced with the complete YOUR-USERNAME.github.io repository name.
  • Look for the cloned folder directly on your Desktop.
  • Help me troubleshoot why my GitHub repository did not clone or open in Visual Studio Code.
Create and verify the starter page

A valid Semantic HTML document gives the browser a clear page structure. This starter page provides a small visible result before you build the full portfolio shell.

  • Click New File in the Explorer toolbar.
  • Enter index.html and press Enter.

You should see index.html beside README.md in the Explorer sidebar.

  • Add the starter page to index.html by pasting this code:
<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Portfolio Setup</title>
</head>
<body>
  <p>Portfolio setup ready</p>
</body>
</html>

What Does This Page Do?

  • The document declaration tells the browser to use modern HTML rules.
  • The <head> section defines the character set and mobile viewport.
  • The <title> element names the browser tab Portfolio Setup.
  • The <body> displays Portfolio setup ready as visible page content.
  • Press Ctrl+S to save index.html.
  • Press the Windows key to open the search bar.
  • Type Microsoft Edge and press Enter to open it.

Before you open the file, what text do you expect Microsoft Edge to display?

  • Press Ctrl+O in Microsoft Edge to open the file picker.
  • Select index.html inside the YOUR-USERNAME.github.io folder on your Desktop.
  • Click Open.

There it is. Microsoft Edge displays Portfolio setup ready, which proves your first local portfolio page works.

Page Blank or File Missing?

  • Confirm that the file is named exactly index.html.
  • Check that index.html sits directly beside README.md.
  • Return to Visual Studio Code to confirm that the file is saved.
  • Help me debug why Microsoft Edge does not display Portfolio setup ready from my local index.html file.

✔️ Awesome, I've got everything!

Great. Double check that index.html is saved in the same folder as README.md.

ⓧ I'd like to double check the full code

Compare your saved index.html with this complete starter file.

<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Portfolio Setup</title>
</head>
<body>
  <p>Portfolio setup ready</p>
</body>
</html>

How to Use This Reference

Check the document structure and text character by character. Your saved file should match this reference exactly.

Before the final check, do you expect Git to treat index.html as an existing tracked file or a new untracked file?

  • Switch back to the PowerShell window from earlier.
  • Check the repository's file state by running this command:
git status

What Should You See?

  • The git status command compares the working folder with Git's tracked state.
  • The output identifies index.html as a new untracked file.
  • The existing README.md file stays outside the untracked-file list because it came from the cloned repository.

Your local page works and Git can see the new file. The repository is ready for the complete portfolio shell.

Git Status Missing index.html?

  • Confirm that PowerShell's current folder is YOUR-USERNAME.github.io.
  • Check that index.html appears beside README.md in Visual Studio Code.
  • Save index.html before running the status check again.
  • Help me understand why git status does not show my new index.html file.

Your repository and starter page are ready. Next up, you'll replace this small setup message with the responsive shell for your summary, résumé, and project gallery.

Build the Portfolio Shell

Your repository now has a local page that Microsoft Edge can display. The starter message proves that your editing and preview loop works.

A useful portfolio needs clear sections for your story, résumé, and projects. Semantic HTML gives each section a meaningful role. This step also lets the browser expose what happens when project cards depend on copied markup.

In this step, get ready to:
  • Replace the starter markup with semantic portfolio sections.
  • Apply responsive and print-ready styles.
  • Test repeated project markup in the browser.
Build the semantic page structure

The document head describes the page to browsers. It also connects the page to the stylesheet that controls its appearance.

  • Switch back to index.html in Visual Studio Code.
  • Replace the current <head> element with the code below:
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta name="description" content="Personal portfolio with a professional summary, résumé, and project gallery.">
  <title>Your Name | Portfolio</title>
  <link rel="stylesheet" href="styles.css">
</head>

What does this code do?

  • The description summarizes the portfolio for services that inspect the page metadata.
  • The title sets the text shown in the browser tab.
  • The stylesheet link tells the browser to load presentation rules from styles.css.
  • Save index.html.
  • Refresh the existing Microsoft Edge tab.

You'll see Your Name | Portfolio in the browser tab. The starter message remains in the page body.

Does the browser tab still show the old title?

  • Confirm that index.html is saved in the cloned repository.
  • Check that the new <title> element sits inside <head>.
  • Refresh the Microsoft Edge tab that displayed your starter page.
  • Help me check why my updated HTML title is not appearing in Microsoft Edge.

The body now needs landmarks that visitors can scan. The header provides navigation. The main area carries your introduction and professional summary.

  • Replace everything from <body> through </body> with the code below:
<body>
  <header class="site-header">
    <a class="brand" href="#home">Your Name</a>
    <nav aria-label="Primary navigation">
      <a href="#about">Summary</a>
      <a href="#resume">Résumé</a>
      <a href="#projects">Projects</a>
    </nav>
  </header>
  <main>
    <section class="hero" id="home">
      <p class="eyebrow">Cloud and DevOps learner</p>
      <h1>I build practical projects and improve them continuously.</h1>
      <p class="hero-copy">Replace this text with a short summary of your focus, strengths, and the kind of opportunity you want.</p>
      <div class="hero-actions">
        <a class="button primary" href="#projects">View projects</a>
        <a class="button secondary" href="#resume">View résumé</a>
      </div>
    </section>
    <section class="section" id="about">
      <p class="section-label">Summary</p>
      <h2>About me</h2>
      <p>I enjoy turning cloud, automation, and web-development ideas into working projects. Replace this paragraph with your own experience, interests, and goals.</p>
    </section>
  </main>
  <footer class="site-footer"><p>Built by Your Name and continuously deployed with GitHub Actions.</p></footer>
</body>

How is the page organized?

  • The header contains links to the page sections identified by home, about, resume, and projects.
  • The hero introduces your professional direction before visitors reach the detailed sections.
  • The summary gives you a dedicated place to explain your interests and goals.
  • The footer states that the finished site uses GitHub Actions for continuous deployment.
  • Save index.html.
  • Refresh the existing Microsoft Edge tab.

You'll see your navigation links above a large introduction. The summary appears beneath the hero content.

Is the starter message still visible?

  • Confirm that the old <p>Portfolio setup ready</p> line has been removed.
  • Check that the replacement begins with <body>.
  • Check that the replacement ends with </body>.
  • Help me find why Microsoft Edge still shows my old starter HTML.

A résumé section turns the page into something visitors can review or print. Its three cards separate experience, skills, and education.

  • Find the closing tag for the summary section in index.html.
  • Insert the résumé section immediately after that closing tag:
    <section class="section resume" id="resume">
      <div class="section-heading">
        <div><p class="section-label">Résumé</p><h2>Experience and skills</h2></div>
        <button class="button secondary" id="print-resume" type="button">Print résumé</button>
      </div>
      <div class="resume-grid">
        <article><h3>Experience</h3><p class="item-title">Your role or learning track</p><p>Describe one measurable responsibility, project result, or learning milestone.</p></article>
        <article><h3>Skills</h3><ul class="skill-list"><li>Git and GitHub</li><li>CI/CD fundamentals</li><li>HTML, CSS, and JavaScript</li></ul></article>
        <article><h3>Education</h3><p class="item-title">Your qualification or program</p><p>Add the institution, learning provider, or expected completion date.</p></article>
      </div>
    </section>

What does the résumé section contain?

  • The section heading pairs the résumé title with a print button.
  • The resume-grid container groups three professional information cards.
  • The skills list gives short technical terms their own visual treatment.
  • Save index.html.
  • Refresh the existing Microsoft Edge tab.

You'll see an Experience and skills section below the summary. It contains three unstyled résumé entries and a Print résumé button.

Is the résumé missing?

  • Confirm that the résumé section sits inside <main>.
  • Place the résumé section after the summary section.
  • Check that the section uses id="resume" so the navigation link can reach it.
  • Help me debug a missing résumé section in my portfolio HTML.
Style the responsive portfolio

CSS turns the document structure into a readable visual hierarchy. The stylesheet also adapts the cards to narrow screens and isolates the résumé for printing.

  • Create styles.css beside index.html using the file creation control above the file list in Visual Studio Code.
  • Add the complete responsive stylesheet by pasting this code:
:root { --background:#08111f; --surface:#111d2e; --text:#edf4ff; --muted:#a9b8cc; --accent:#66e3c4; --accent-dark:#0c352d; --border:#2b3b52; --max-width:1080px; }
* { box-sizing:border-box; }
html { scroll-behavior:smooth; }
body { margin:0; background:var(--background); color:var(--text); font-family:Inter,"Segoe UI",sans-serif; line-height:1.6; }
a { color:inherit; }
button,a { font:inherit; }
button:focus-visible,a:focus-visible,input:focus-visible { outline:3px solid var(--accent); outline-offset:3px; }
.site-header { align-items:center; background:rgba(8,17,31,.94); border-bottom:1px solid var(--border); display:flex; justify-content:space-between; padding:1rem max(1.25rem,calc((100% - var(--max-width)) / 2)); position:sticky; top:0; z-index:10; }
.brand { font-weight:800; text-decoration:none; }
nav,.hero-actions,.filters,.project-links,.skill-list { display:flex; flex-wrap:wrap; gap:.75rem; }
nav a { color:var(--muted); text-decoration:none; }
nav a:hover { color:var(--accent); }
main,.site-footer { margin:0 auto; max-width:var(--max-width); padding-inline:1.25rem; }
.hero { min-height:72vh; padding:7rem 0 4rem; }
.eyebrow,.section-label,.project-category { color:var(--accent); font-size:.8rem; font-weight:800; letter-spacing:.12em; text-transform:uppercase; }
h1 { font-size:clamp(2.5rem,7vw,5rem); line-height:1.05; margin:.5rem 0 1.5rem; max-width:900px; }
h2 { font-size:clamp(1.8rem,4vw,2.8rem); margin:.25rem 0 1.5rem; }
.hero-copy,.section > p,.resume article p,.project-summary { color:var(--muted); }
.button,.filter-button { border:1px solid var(--border); border-radius:999px; cursor:pointer; display:inline-block; padding:.7rem 1rem; text-decoration:none; }
.button.primary,.filter-button.active { background:var(--accent); border-color:var(--accent); color:#05110e; }
.button.secondary,.filter-button { background:var(--surface); color:var(--text); }
.section { border-top:1px solid var(--border); padding:4.5rem 0; }
.section-heading { align-items:center; display:flex; gap:1rem; justify-content:space-between; }
.resume-grid,.project-grid { display:grid; gap:1rem; grid-template-columns:repeat(3,minmax(0,1fr)); }
.resume article,.project-card { background:var(--surface); border:1px solid var(--border); border-radius:1rem; padding:1.35rem; }
.skill-list { list-style:none; padding:0; }
.skill-list li,.project-skills li { background:var(--accent-dark); border-radius:999px; color:var(--accent); font-size:.85rem; padding:.35rem .7rem; }
.site-footer { border-top:1px solid var(--border); color:var(--muted); padding-block:2rem; }
@media (max-width:760px) { .site-header,.section-heading { align-items:flex-start; flex-direction:column; } .resume-grid,.project-grid { grid-template-columns:1fr; } .hero { min-height:auto; padding-top:5rem; } }
@media print { body { background:#fff; color:#000; } .site-header,.hero,#about,#projects,.site-footer,#print-resume { display:none; } main,.resume { margin:0; max-width:none; padding:0; } .resume-grid { grid-template-columns:1fr; } .resume article { background:#fff; border-color:#ccc; break-inside:avoid; color:#000; } .resume article p { color:#222; } }

How does the stylesheet shape the portfolio?

  • The variables at the top keep the color palette and maximum width consistent.
  • The layout rules turn navigation links, buttons, résumé entries, and project entries into reusable visual groups.
  • The focus rules give keyboard users a visible outline around interactive controls.
  • The responsive rule changes the card grids to one column at widths below 760px.
  • The print rule hides the site navigation and project content so the résumé becomes the printable focus.
  • Save styles.css.
  • Refresh the existing Microsoft Edge tab.

You'll see a dark page with a mint accent color. The résumé entries now appear as three bordered cards.

  • Narrow the Microsoft Edge window to test the responsive layout.

You'll see the header stack vertically. The résumé cards also collapse into one column.

Does the page still look unstyled?

  • Confirm that styles.css sits beside index.html.
  • Check that the document head contains href="styles.css".
  • Confirm that styles.css is saved before refreshing Microsoft Edge.
  • Help me find why styles.css is not changing my portfolio page.

✔️ Awesome, I've got everything!

Your responsive styles are saved in styles.css. The portfolio now has its final visual foundation.

ⓧ I'd like to double check the full code

:root { --background:#08111f; --surface:#111d2e; --text:#edf4ff; --muted:#a9b8cc; --accent:#66e3c4; --accent-dark:#0c352d; --border:#2b3b52; --max-width:1080px; }
* { box-sizing:border-box; }
html { scroll-behavior:smooth; }
body { margin:0; background:var(--background); color:var(--text); font-family:Inter,"Segoe UI",sans-serif; line-height:1.6; }
a { color:inherit; }
button,a { font:inherit; }
button:focus-visible,a:focus-visible,input:focus-visible { outline:3px solid var(--accent); outline-offset:3px; }
.site-header { align-items:center; background:rgba(8,17,31,.94); border-bottom:1px solid var(--border); display:flex; justify-content:space-between; padding:1rem max(1.25rem,calc((100% - var(--max-width)) / 2)); position:sticky; top:0; z-index:10; }
.brand { font-weight:800; text-decoration:none; }
nav,.hero-actions,.filters,.project-links,.skill-list { display:flex; flex-wrap:wrap; gap:.75rem; }
nav a { color:var(--muted); text-decoration:none; }
nav a:hover { color:var(--accent); }
main,.site-footer { margin:0 auto; max-width:var(--max-width); padding-inline:1.25rem; }
.hero { min-height:72vh; padding:7rem 0 4rem; }
.eyebrow,.section-label,.project-category { color:var(--accent); font-size:.8rem; font-weight:800; letter-spacing:.12em; text-transform:uppercase; }
h1 { font-size:clamp(2.5rem,7vw,5rem); line-height:1.05; margin:.5rem 0 1.5rem; max-width:900px; }
h2 { font-size:clamp(1.8rem,4vw,2.8rem); margin:.25rem 0 1.5rem; }
.hero-copy,.section > p,.resume article p,.project-summary { color:var(--muted); }
.button,.filter-button { border:1px solid var(--border); border-radius:999px; cursor:pointer; display:inline-block; padding:.7rem 1rem; text-decoration:none; }
.button.primary,.filter-button.active { background:var(--accent); border-color:var(--accent); color:#05110e; }
.button.secondary,.filter-button { background:var(--surface); color:var(--text); }
.section { border-top:1px solid var(--border); padding:4.5rem 0; }
.section-heading { align-items:center; display:flex; gap:1rem; justify-content:space-between; }
.resume-grid,.project-grid { display:grid; gap:1rem; grid-template-columns:repeat(3,minmax(0,1fr)); }
.resume article,.project-card { background:var(--surface); border:1px solid var(--border); border-radius:1rem; padding:1.35rem; }
.skill-list { list-style:none; padding:0; }
.skill-list li,.project-skills li { background:var(--accent-dark); border-radius:999px; color:var(--accent); font-size:.85rem; padding:.35rem .7rem; }
.site-footer { border-top:1px solid var(--border); color:var(--muted); padding-block:2rem; }
@media (max-width:760px) { .site-header,.section-heading { align-items:flex-start; flex-direction:column; } .resume-grid,.project-grid { grid-template-columns:1fr; } .hero { min-height:auto; padding-top:5rem; } }
@media print { body { background:#fff; color:#000; } .site-header,.hero,#about,#projects,.site-footer,#print-resume { display:none; } main,.resume { margin:0; max-width:none; padding:0; } .resume-grid { grid-template-columns:1fr; } .resume article { background:#fff; border-color:#ccc; break-inside:avoid; color:#000; } .resume article p { color:#222; } }

How should I use this reference?

Compare this reference with your saved styles.css file. Matching selectors and values produce the intended responsive layout.

Expose the hardcoded card problem

The Projects section needs entries that visitors can scan. This first approach places each entry directly in the HTML.

  • Find the closing tag for the résumé section in index.html.
  • Insert the Projects section immediately after that closing tag:
    <section class="section" id="projects">
      <p class="section-label">Portfolio</p><h2>Selected projects</h2>
      <div class="project-grid">
        <article class="project-card"><p class="project-category">NextWork</p><h3 class="project-title">My First NextWork Project</h3><p class="project-summary">Replace this summary with the problem you solved, the service or skill you used, and the result you produced.</p></article>
        <article class="project-card"><p class="project-category">NextWork</p><h3 class="project-title">My First NextWork Project</h3><p class="project-summary">Replace this summary with the problem you solved, the service or skill you used, and the result you produced.</p></article>
      </div>
    </section>

How are the projects represented?

  • The project-grid container controls the card layout.
  • Each project-card article contains its category, title, and summary directly in the page markup.
  • Adding another project with this approach requires another complete article element.
  • Save index.html.

Before you refresh, what do you think the Projects section does with two cards that contain the same text?

  • Refresh the existing Microsoft Edge tab.

You'll see the styled summary above the résumé. The résumé appears as a row of cards on a wide screen.

The Projects section shows the same NextWork card twice. That visible duplication is the intended shortfall of hardcoded project markup.

Why does this approach become difficult?

Every new project requires another copied article. Updating the card structure later means editing every copy.

The duplicate cards make that maintenance problem visible before the gallery grows.

Do the project cards look different?

  • Compare both project-card article elements in index.html.
  • Confirm that both cards use the title My First NextWork Project.
  • Confirm that both cards use the same summary text.
  • Help me check why my two hardcoded project cards do not match.

✔️ Awesome, I've got everything!

Your portfolio shell is complete. Double check that index.html is saved.

ⓧ I'd like to double check the full code

<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta name="description" content="Personal portfolio with a professional summary, résumé, and project gallery.">
  <title>Your Name | Portfolio</title>
  <link rel="stylesheet" href="styles.css">
</head>
<body>
  <header class="site-header">
    <a class="brand" href="#home">Your Name</a>
    <nav aria-label="Primary navigation">
      <a href="#about">Summary</a>
      <a href="#resume">Résumé</a>
      <a href="#projects">Projects</a>
    </nav>
  </header>
  <main>
    <section class="hero" id="home">
      <p class="eyebrow">Cloud and DevOps learner</p>
      <h1>I build practical projects and improve them continuously.</h1>
      <p class="hero-copy">Replace this text with a short summary of your focus, strengths, and the kind of opportunity you want.</p>
      <div class="hero-actions">
        <a class="button primary" href="#projects">View projects</a>
        <a class="button secondary" href="#resume">View résumé</a>
      </div>
    </section>
    <section class="section" id="about">
      <p class="section-label">Summary</p>
      <h2>About me</h2>
      <p>I enjoy turning cloud, automation, and web-development ideas into working projects. Replace this paragraph with your own experience, interests, and goals.</p>
    </section>
    <section class="section resume" id="resume">
      <div class="section-heading">
        <div><p class="section-label">Résumé</p><h2>Experience and skills</h2></div>
        <button class="button secondary" id="print-resume" type="button">Print résumé</button>
      </div>
      <div class="resume-grid">
        <article><h3>Experience</h3><p class="item-title">Your role or learning track</p><p>Describe one measurable responsibility, project result, or learning milestone.</p></article>
        <article><h3>Skills</h3><ul class="skill-list"><li>Git and GitHub</li><li>CI/CD fundamentals</li><li>HTML, CSS, and JavaScript</li></ul></article>
        <article><h3>Education</h3><p class="item-title">Your qualification or program</p><p>Add the institution, learning provider, or expected completion date.</p></article>
      </div>
    </section>
    <section class="section" id="projects">
      <p class="section-label">Portfolio</p><h2>Selected projects</h2>
      <div class="project-grid">
        <article class="project-card"><p class="project-category">NextWork</p><h3 class="project-title">My First NextWork Project</h3><p class="project-summary">Replace this summary with the problem you solved, the service or skill you used, and the result you produced.</p></article>
        <article class="project-card"><p class="project-category">NextWork</p><h3 class="project-title">My First NextWork Project</h3><p class="project-summary">Replace this summary with the problem you solved, the service or skill you used, and the result you produced.</p></article>
      </div>
    </section>
  </main>
  <footer class="site-footer"><p>Built by Your Name and continuously deployed with GitHub Actions.</p></footer>
</body>
</html>

How should I use this reference?

Compare this reference with your saved index.html file. The sections should appear in the same order.

Your responsive portfolio shell now makes the hardcoded-card problem impossible to miss. Next, you'll replace repeated markup with reusable project data and JavaScript rendering.

Render Interactive Projects from Data

Your portfolio now has a responsive summary and a printable résumé. The duplicated project card also exposed the maintenance problem with copied markup.

In this step, you will move each project into a reusable JavaScript array. One renderer will turn that data into distinct cards with category filters and a live project count.

In this step, get ready to:
  • Store distinct NextWork and Personal projects in a reusable data array.
  • Generate project cards from the reusable data.
  • Filter the cards by category and open the printable résumé.
Create reusable project data

A data array gives every project the same structure. Adding another project becomes a new object instead of another copied card.

  • In Visual Studio Code, select the file-creation icon at the top of the left file sidebar.
  • Enter projects.js as the filename.
  • Press Enter.
  • Add the reusable project data to projects.js by pasting this code:
const projects = [
  {
    title: "My First NextWork Project",
    category: "NextWork",
    summary: "Replace this summary with the problem you solved, the service or skill you used, and the result you produced.",
    skills: ["Cloud", "Documentation", "Problem solving"],
    repoUrl: "",
    demoUrl: ""
  },
  {
    title: "My Personal Project",
    category: "Personal",
    summary: "Replace this summary with what the project does, why you built it, and what you learned.",
    skills: ["HTML", "CSS", "JavaScript"],
    repoUrl: "",
    demoUrl: ""
  }
];

What does this data represent?

  • The projects array becomes the gallery's single source of project content.
  • Each object describes one project. The properties provide its title and category.
  • The skills property gives the renderer a reusable list of skills.
  • The empty repoUrl and demoUrl values keep unavailable links out of the card.
  • Save projects.js.
  • Confirm projects.js appears beside index.html in the left file sidebar.

Is the project data file missing?

  • Check that the filename is exactly projects.js.
  • Move the file beside index.html if it was created inside another folder.
  • Check that the array ends with ];.
  • Help me check my project data file.

✔️ Awesome, I've got everything!

Your reusable project data is saved in projects.js.

ⓧ I'd like to double check the full code

const projects = [
  {
    title: "My First NextWork Project",
    category: "NextWork",
    summary: "Replace this summary with the problem you solved, the service or skill you used, and the result you produced.",
    skills: ["Cloud", "Documentation", "Problem solving"],
    repoUrl: "",
    demoUrl: ""
  },
  {
    title: "My Personal Project",
    category: "Personal",
    summary: "Replace this summary with what the project does, why you built it, and what you learned.",
    skills: ["HTML", "CSS", "JavaScript"],
    repoUrl: "",
    demoUrl: ""
  }
];

Compare this reference with your saved file. Keep every property name and value in the same position.

Build the reusable renderer

The DOM represents the page as elements that JavaScript can create or update. Your renderer will use those elements to build one card for each project object.

  • Select the file-creation icon at the top of the left file sidebar.
  • Enter script.js as the filename.
  • Press Enter.
  • Add the page references and card builder to script.js by pasting this code:
const projectGrid = document.getElementById("project-grid");
const projectStatus = document.getElementById("project-status");
const filterButtons = document.querySelectorAll(".filter-button");
const printResumeButton = document.getElementById("print-resume");

function createProjectCard(project) {
  const card = document.createElement("article");
  card.className = "project-card";
  const category = document.createElement("p");
  category.className = "project-category";
  category.textContent = project.category;
  const title = document.createElement("h3");
  title.className = "project-title";
  title.textContent = project.title;
  const summary = document.createElement("p");
  summary.className = "project-summary";
  summary.textContent = project.summary;
  const skills = document.createElement("ul");
  skills.className = "project-skills";
  project.skills.forEach((skill) => { const item = document.createElement("li"); item.textContent = skill; skills.appendChild(item); });
  const links = document.createElement("div");
  links.className = "project-links";
  [["Repository", project.repoUrl], ["Live demo", project.demoUrl]].forEach(([label, url]) => { if (url) { const link = document.createElement("a"); link.href = url; link.target = "_blank"; link.rel = "noreferrer"; link.textContent = label; links.appendChild(link); } });
  if (!project.repoUrl && !project.demoUrl) { const note = document.createElement("span"); note.textContent = "Add repository or demo links when available"; links.appendChild(note); }
  card.append(category, title, summary, skills, links);
  return card;
}

How does the card builder work?

  • The four constants keep references to the gallery and its controls.
  • The createProjectCard() function creates one article for the project object it receives.
  • The function turns each skill into a list item.
  • The function adds links only when the project data contains a destination.
  • Save script.js.
  • Confirm script.js appears beside projects.js in the left file sidebar.

Is the card builder showing errors?

  • Check that every string uses matching quotation marks.
  • Confirm the closing brace for createProjectCard() appears after return card;.
  • Compare the property names with the objects in projects.js.
  • Help me fix my project card builder.

The card builder produces one article at a time. The next part decides which projects are visible and places those cards inside the gallery.

  • Place your cursor after the closing brace of createProjectCard().
  • Add the renderer and interaction listeners by pasting this code:
function renderProjects(filter = "All") {
  const visibleProjects = filter === "All" ? projects : projects.filter((project) => project.category === filter);
  projectGrid.textContent = "";
  visibleProjects.forEach((project) => projectGrid.appendChild(createProjectCard(project)));
  const label = visibleProjects.length === 1 ? "project" : "projects";
  projectStatus.textContent = `${visibleProjects.length} ${label} shown`;
}
filterButtons.forEach((button) => { button.addEventListener("click", () => { const selectedFilter = button.dataset.filter; filterButtons.forEach((candidate) => { const isActive = candidate === button; candidate.classList.toggle("active", isActive); candidate.setAttribute("aria-pressed", String(isActive)); }); renderProjects(selectedFilter); }); });
printResumeButton.addEventListener("click", () => { window.print(); });
renderProjects();

How do the interactions work?

  • The renderProjects() function selects projects that match the requested category.
  • The renderer clears the old cards before adding the current results.
  • Each filter button updates the active styling. Its aria-pressed value changes at the same time.
  • The résumé button calls window.print() when a visitor selects it.
  • Save script.js.
  • Check script.js for red error underlines.

You should see no syntax errors in the editor. The final line should be renderProjects();.

Does the renderer show an error?

  • Confirm renderProjects() begins after the closing brace of createProjectCard().
  • Check that the filter listener uses selectedFilter when it calls the renderer.
  • Keep renderProjects(); as the final line.
  • Help me check my renderer and event listeners.

✔️ Awesome, I've got everything!

Your script.js file now contains the card builder and every required interaction.

ⓧ I'd like to double check the full code

const projectGrid = document.getElementById("project-grid");
const projectStatus = document.getElementById("project-status");
const filterButtons = document.querySelectorAll(".filter-button");
const printResumeButton = document.getElementById("print-resume");

function createProjectCard(project) {
  const card = document.createElement("article");
  card.className = "project-card";
  const category = document.createElement("p");
  category.className = "project-category";
  category.textContent = project.category;
  const title = document.createElement("h3");
  title.className = "project-title";
  title.textContent = project.title;
  const summary = document.createElement("p");
  summary.className = "project-summary";
  summary.textContent = project.summary;
  const skills = document.createElement("ul");
  skills.className = "project-skills";
  project.skills.forEach((skill) => { const item = document.createElement("li"); item.textContent = skill; skills.appendChild(item); });
  const links = document.createElement("div");
  links.className = "project-links";
  [["Repository", project.repoUrl], ["Live demo", project.demoUrl]].forEach(([label, url]) => { if (url) { const link = document.createElement("a"); link.href = url; link.target = "_blank"; link.rel = "noreferrer"; link.textContent = label; links.appendChild(link); } });
  if (!project.repoUrl && !project.demoUrl) { const note = document.createElement("span"); note.textContent = "Add repository or demo links when available"; links.appendChild(note); }
  card.append(category, title, summary, skills, links);
  return card;
}
function renderProjects(filter = "All") {
  const visibleProjects = filter === "All" ? projects : projects.filter((project) => project.category === filter);
  projectGrid.textContent = "";
  visibleProjects.forEach((project) => projectGrid.appendChild(createProjectCard(project)));
  const label = visibleProjects.length === 1 ? "project" : "projects";
  projectStatus.textContent = `${visibleProjects.length} ${label} shown`;
}
filterButtons.forEach((button) => { button.addEventListener("click", () => { const selectedFilter = button.dataset.filter; filterButtons.forEach((candidate) => { const isActive = candidate === button; candidate.classList.toggle("active", isActive); candidate.setAttribute("aria-pressed", String(isActive)); }); renderProjects(selectedFilter); }); });
printResumeButton.addEventListener("click", () => { window.print(); });
renderProjects();

Compare this reference with your saved file from the first constant through the final render call.

Connect the page and test each interaction

The project data and renderer are ready. The page now needs controls that match the JavaScript selectors and an empty container for the generated cards.

  • Switch back to the existing index.html tab.
  • Click inside the editor.
  • Use the editor's select-all command.
  • Replace the selected page with this updated HTML:
<!doctype html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><meta name="description" content="Personal portfolio with a professional summary, résumé, and project gallery."><title>Your Name | Portfolio</title><link rel="stylesheet" href="styles.css"><script src="projects.js" defer></script><script src="script.js" defer></script></head><body><header class="site-header"><a class="brand" href="#home">Your Name</a><nav aria-label="Primary navigation"><a href="#about">Summary</a><a href="#resume">Résumé</a><a href="#projects">Projects</a></nav></header><main><section class="hero" id="home"><p class="eyebrow">Cloud and DevOps learner</p><h1>I build practical projects and improve them continuously.</h1><p class="hero-copy">Replace this text with a short summary of your focus, strengths, and the kind of opportunity you want.</p><div class="hero-actions"><a class="button primary" href="#projects">View projects</a><a class="button secondary" href="#resume">View résumé</a></div></section><section class="section" id="about"><p class="section-label">Summary</p><h2>About me</h2><p>I enjoy turning cloud, automation, and web-development ideas into working projects. Replace this paragraph with your own experience, interests, and goals.</p></section><section class="section resume" id="resume"><div class="section-heading"><div><p class="section-label">Résumé</p><h2>Experience and skills</h2></div><button class="button secondary" id="print-resume" type="button">Print résumé</button></div><div class="resume-grid"><article><h3>Experience</h3><p class="item-title">Your role or learning track</p><p>Describe one measurable responsibility, project result, or learning milestone.</p></article><article><h3>Skills</h3><ul class="skill-list"><li>Git and GitHub</li><li>CI/CD fundamentals</li><li>HTML, CSS, and JavaScript</li></ul></article><article><h3>Education</h3><p class="item-title">Your qualification or program</p><p>Add the institution, learning provider, or expected completion date.</p></article></div></section><section class="section" id="projects"><p class="section-label">Portfolio</p><h2>Selected projects</h2><div class="filters" aria-label="Filter projects"><button class="filter-button active" type="button" data-filter="All" aria-pressed="true">All</button><button class="filter-button" type="button" data-filter="NextWork" aria-pressed="false">NextWork</button><button class="filter-button" type="button" data-filter="Personal" aria-pressed="false">Personal</button></div><p class="project-status" id="project-status" aria-live="polite"></p><div class="project-grid" id="project-grid"></div></section></main><footer class="site-footer"><p>Built by Your Name and continuously deployed with GitHub Actions.</p></footer></body></html>

How does the page connect to the renderer?

  • The page loads projects.js before script.js so the project data exists when rendering begins.
  • The filter buttons contain values that match the project categories.
  • The empty project-grid container gives the renderer one destination for every card.
  • The live status element gives visitors the current result count.

Before you refresh, do you expect the duplicated hardcoded card to remain after JavaScript takes control of the gallery?

  • Save index.html.
  • Switch back to the portfolio tab in Microsoft Edge.
  • Refresh the page.

You should see two distinct cards titled My First NextWork Project and My Personal Project. The status should read 2 projects shown.

Do the project cards stay empty?

  • Confirm projects.js appears before script.js in index.html.
  • Check that the gallery uses id="project-grid".
  • Confirm renderProjects(); is the final line in script.js.
  • Help me debug my empty project gallery.

✔️ Awesome, I've got everything!

The copied markup is gone. Your browser now displays two cards generated from reusable data.

ⓧ I'd like to double check the full code

<!doctype html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><meta name="description" content="Personal portfolio with a professional summary, résumé, and project gallery."><title>Your Name | Portfolio</title><link rel="stylesheet" href="styles.css"><script src="projects.js" defer></script><script src="script.js" defer></script></head><body><header class="site-header"><a class="brand" href="#home">Your Name</a><nav aria-label="Primary navigation"><a href="#about">Summary</a><a href="#resume">Résumé</a><a href="#projects">Projects</a></nav></header><main><section class="hero" id="home"><p class="eyebrow">Cloud and DevOps learner</p><h1>I build practical projects and improve them continuously.</h1><p class="hero-copy">Replace this text with a short summary of your focus, strengths, and the kind of opportunity you want.</p><div class="hero-actions"><a class="button primary" href="#projects">View projects</a><a class="button secondary" href="#resume">View résumé</a></div></section><section class="section" id="about"><p class="section-label">Summary</p><h2>About me</h2><p>I enjoy turning cloud, automation, and web-development ideas into working projects. Replace this paragraph with your own experience, interests, and goals.</p></section><section class="section resume" id="resume"><div class="section-heading"><div><p class="section-label">Résumé</p><h2>Experience and skills</h2></div><button class="button secondary" id="print-resume" type="button">Print résumé</button></div><div class="resume-grid"><article><h3>Experience</h3><p class="item-title">Your role or learning track</p><p>Describe one measurable responsibility, project result, or learning milestone.</p></article><article><h3>Skills</h3><ul class="skill-list"><li>Git and GitHub</li><li>CI/CD fundamentals</li><li>HTML, CSS, and JavaScript</li></ul></article><article><h3>Education</h3><p class="item-title">Your qualification or program</p><p>Add the institution, learning provider, or expected completion date.</p></article></div></section><section class="section" id="projects"><p class="section-label">Portfolio</p><h2>Selected projects</h2><div class="filters" aria-label="Filter projects"><button class="filter-button active" type="button" data-filter="All" aria-pressed="true">All</button><button class="filter-button" type="button" data-filter="NextWork" aria-pressed="false">NextWork</button><button class="filter-button" type="button" data-filter="Personal" aria-pressed="false">Personal</button></div><p class="project-status" id="project-status" aria-live="polite"></p><div class="project-grid" id="project-grid"></div></section></main><footer class="site-footer"><p>Built by Your Name and continuously deployed with GitHub Actions.</p></footer></body></html>

Compare this reference with your saved page. Focus on the script order and the complete Projects section.

Before you test the filters, which project count do you expect after selecting the Personal category?

  • Select NextWork in the project filters.

You should see only My First NextWork Project. The status should read 1 project shown.

  • Select Personal in the project filters.

You should see only My Personal Project. The Personal button should use the active styling.

  • Select All in the project filters.

You should see both project cards again. The status should return to 2 projects shown.

Before you test the résumé action, what browser control do you expect the button to open?

  • Select Print résumé in the résumé section.

Microsoft Edge should open its print dialog with the résumé prepared for printing.

  • Close the print dialog.

Are the filters or print button unresponsive?

  • Confirm each category button uses the filter-button class.
  • Check that each data-filter value matches a category in projects.js.
  • Verify the résumé button uses id="print-resume".
  • Help me debug my portfolio interactions.

That is the duplication problem solved. Your portfolio now produces distinct cards and interactive views from one maintainable data source.

Your local portfolio now behaves like a living project gallery. Next, you will publish it with GitHub Actions and GitHub Pages so visitors can use the same interactive site online.

Deploy with GitHub Actions and GitHub Pages

The interactive portfolio now turns reusable data into filtered project cards. Visitors still cannot reach it because the files exist only on your computer.

In this step, GitHub Actions turns each push to main into a deployment. GitHub Pages publishes the static files at your personal website address through a visible CI/CD pipeline.

In this step, get ready to:
  • Create the GitHub Pages deployment workflow.
  • Set GitHub Pages to use GitHub Actions as its deployment source.
  • Push the portfolio to main through Visual Studio Code.
Create the deployment workflow

A YAML workflow describes the automated deployment process. This workflow starts when a commit reaches main.

Why use a custom workflow?

A custom workflow makes the deployment stages visible. You can inspect how GitHub checks out the repository.

You can also inspect how the files become a Pages artifact. The final job shows whether publication succeeded.

  • In the file tree sidebar in Visual Studio Code, right-click the top-level repository folder.
  • Choose the option for creating a new file.
  • Enter .github/workflows/deploy.yml as the complete file path.
  • Confirm the new file path.

You should see deploy.yml inside the nested .github/workflows folders.

  • Add the deployment workflow to .github/workflows/deploy.yml by pasting this code:
name: Deploy to GitHub Pages

on:
  push:
    branches:
      - main

permissions:
  contents: read
  pages: write
  id-token: write

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    steps:
      - name: Checkout
        uses: actions/checkout@v6
      - name: Setup Pages
        uses: actions/configure-pages@v5
      - name: Upload artifact
        uses: actions/upload-pages-artifact@v4
        with:
          path: '.'
      - name: Deploy to GitHub Pages
        id: deployment
        uses: actions/deploy-pages@v4

What does this workflow do?

  • The push trigger starts the workflow when a commit reaches main.
  • The permissions allow the runner to read the repository. They also allow it to create the Pages deployment.
  • The github-pages environment records the live address produced by the deployment.
  • The actions/checkout@v6 step retrieves the repository files.
  • The actions/configure-pages@v5 step prepares GitHub Pages.
  • The actions/upload-pages-artifact@v4 step packages the static files from path: '.'.
  • The actions/deploy-pages@v4 step publishes the uploaded artifact.
  • Save .github/workflows/deploy.yml from the Visual Studio Code file menu.
  • Expand the .github folder in the file tree sidebar.
  • Expand the workflows folder.
  • Confirm that deploy.yml appears inside .github/workflows.

Seeing a YAML warning?

  • Check that the file path is exactly .github/workflows/deploy.yml.
  • Compare the indentation beneath jobs with the full workflow below.
  • Confirm that the filename ends with .yml.

Help me compare my workflow with the expected YAML structure.

✔️ Awesome, I've got everything!

Your saved deploy.yml file contains the complete deployment workflow.

ⓧ I'd like to double check the full code

name: Deploy to GitHub Pages

on:
  push:
    branches:
      - main

permissions:
  contents: read
  pages: write
  id-token: write

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    steps:
      - name: Checkout
        uses: actions/checkout@v6
      - name: Setup Pages
        uses: actions/configure-pages@v5
      - name: Upload artifact
        uses: actions/upload-pages-artifact@v4
        with:
          path: '.'
      - name: Deploy to GitHub Pages
        id: deployment
        uses: actions/deploy-pages@v4
Configure GitHub Pages

GitHub Pages needs a publication source. Selecting GitHub Actions allows the workflow to publish your site.

  • Return to your YOUR-USERNAME.github.io repository on GitHub.
  • Select Settings from the repository navigation.
  • Select Pages in the left sidebar.
  • Find the Build and deployment section.
  • Set Source to GitHub Actions.

The Source field now shows GitHub Actions. This confirms that Pages accepts deployments from your workflow.

Publish the portfolio

A Git commit stores the portfolio files with the new workflow. Pushing that commit to main starts the deployment.

  • Switch back to Source Control in Visual Studio Code by pressing Ctrl+Shift+G.
  • Select the plus icon beside Changes.

Your portfolio files and deploy.yml should move into Staged Changes.

  • Enter Deploy portfolio with GitHub Pages in the commit message field.
  • Select Commit.

Your local repository now contains a commit with the complete portfolio. The commit also includes the deployment workflow.

The first push may trigger a GitHub authorization request. This gives Visual Studio Code permission to upload your commit.

Before you push, do you expect the workflow to start when the commit reaches main?

  • Select More Actions (...) in Source Control.
  • Select Push.
  • Approve the GitHub authorization request in your browser if prompted.

The pushed commit reaches main. GitHub now creates a deployment run from deploy.yml.

  • Return to your personal-site repository on GitHub.
  • Select Actions from the repository navigation.
  • Open the newest Deploy to GitHub Pages workflow run.

The first publication can take up to 10 minutes. A queued or running status during that window is normal.

  • Wait until the newest workflow run shows a successful result.

That successful run confirms that GitHub packaged your static files. It also confirms that the files reached the github-pages environment.

Before you open the live address, do you expect its project filters to match the local portfolio?

  • Switch back to Microsoft Edge from earlier.
  • Enter https://YOUR-USERNAME.github.io in the address bar using your lowercase GitHub username.
  • Press Enter.

Your styled portfolio should load from its public GitHub Pages address. The project status should display 2 projects shown.

  • Select the NextWork category filter.

You should see the NextWork project card. The project status should change to 1 project shown.

Workflow failed or site missing?

  • Check that the workflow file is saved at .github/workflows/deploy.yml.
  • Confirm that Source still shows GitHub Actions in the Pages settings.
  • Wait up to 10 minutes before refreshing a newly published site.

Help me diagnose my failed workflow or missing GitHub Pages site.

That is the deployment complete. Your interactive portfolio is live through a workflow you can inspect after every push. Next, you'll prove continuous delivery by publishing one more project entry.

Prove Continuous Delivery

Your interactive portfolio is already live through GitHub Actions. That first launch proves the deployment works once.

Continuous delivery earns its name when a small content change reaches the same website through the same pipeline. This step tests that process with one new project entry.

In this step, get ready to:
  • Add a third project object to projects.js.
  • Confirm the existing renderer displays the new project locally.
  • Push the content update through the existing GitHub Pages workflow.
Add a third project entry

The projects array is the portfolio's single source of project content. Adding one object should create another card without changing the renderer.

  • In Visual Studio Code, select projects.js from the Explorer sidebar.
  • Replace the file contents with the complete three-object array by pasting this code:
const projects = [
  {
    title: "My First NextWork Project",
    category: "NextWork",
    summary: "Replace this summary with the problem you solved, the service or skill you used, and the result you produced.",
    skills: ["Cloud", "Documentation", "Problem solving"],
    repoUrl: "",
    demoUrl: ""
  },
  {
    title: "My Personal Project",
    category: "Personal",
    summary: "Replace this summary with what the project does, why you built it, and what you learned.",
    skills: ["HTML", "CSS", "JavaScript"],
    repoUrl: "",
    demoUrl: ""
  },
  {
    title: "My Second NextWork Project",
    category: "NextWork",
    summary: "Use this third entry to prove that a content update can travel through the same deployment pipeline.",
    skills: ["Git", "GitHub Actions", "CI/CD"],
    repoUrl: "",
    demoUrl: ""
  }
];

What does this change do?

  • The third object gives renderProjects() another project to display.
  • The NextWork category makes the project visible under the matching category filter.
  • The existing script.js renderer turns the new object into a card automatically.
  • The deployment workflow remains unchanged because this test isolates a content-only update.
  • Save projects.js.
  • Return to the local index.html page from earlier in Microsoft Edge.
  • Select All in the project filters.

Before you refresh, make a quick prediction about how many project cards the existing renderer should show.

  • Refresh the local page.

You should see three cards with 3 projects shown above the gallery. The new card is titled My Second NextWork Project.

Don't see the third project?

  • Check that every project object except the last one ends with a comma.
  • Confirm the array still ends with ];.
  • Make sure the new project's category value is spelled NextWork.
  • Refresh the local page again after saving projects.js.
  • Help me debug why my third project object is not appearing.

✔️ Awesome, I've got everything!

Great work. Your data-driven gallery now accepts another project without any duplicated card markup.

ⓧ I'd like to double check the full code

Your complete projects.js should match this reference.

const projects = [
  {
    title: "My First NextWork Project",
    category: "NextWork",
    summary: "Replace this summary with the problem you solved, the service or skill you used, and the result you produced.",
    skills: ["Cloud", "Documentation", "Problem solving"],
    repoUrl: "",
    demoUrl: ""
  },
  {
    title: "My Personal Project",
    category: "Personal",
    summary: "Replace this summary with what the project does, why you built it, and what you learned.",
    skills: ["HTML", "CSS", "JavaScript"],
    repoUrl: "",
    demoUrl: ""
  },
  {
    title: "My Second NextWork Project",
    category: "NextWork",
    summary: "Use this third entry to prove that a content update can travel through the same deployment pipeline.",
    skills: ["Git", "GitHub Actions", "CI/CD"],
    repoUrl: "",
    demoUrl: ""
  }
];
Push the content update

The existing workflow watches pushes to main. This commit changes only project data, which makes it a clean test of the same deployment path.

  • Press Ctrl+Shift+G to return to the Source Control panel.
  • Select the plus icon beside projects.js to move it into Staged Changes.
  • Confirm projects.js is the only file under Staged Changes.
  • Enter Add second NextWork project in the commit message box.
  • Select Commit.

Before you push, make a quick prediction about whether a change limited to projects.js triggers the same workflow.

  • Select More Actions (...) in the Source Control panel.
  • Select Push.

Your content commit is now on main. The unchanged workflow starts automatically because it watches pushes to that branch.

  • Return to your personal-site repository on GitHub.
  • Select Actions from the repository navigation.
  • Locate the newest Deploy to GitHub Pages run for the Add second NextWork project commit.

Publication can take up to 10 minutes after a push. A short wait here means GitHub Pages is processing the update.

  • Wait for the newest workflow run to succeed.
  • Compare the newest run with the previous successful run.
  • Confirm both runs use the Deploy to GitHub Pages workflow.

You should now see two successful deployments from the same workflow. The second run is tied to your content-only commit.

Didn't get a second successful run?

  • Confirm the newest run is associated with the Add second NextWork project commit.
  • Check that the run has finished before testing the live site.
  • Return to Visual Studio Code if the newest commit does not appear in the repository.
  • Help me diagnose why my second GitHub Pages deployment did not succeed.
  • Return to https://YOUR-USERNAME.github.io in Microsoft Edge.

Before you refresh, make a quick prediction about whether the existing address will show the new project data.

  • Refresh the live site.
  • Select All in the project filters.

You should see My Second NextWork Project as the third card. You should also see 3 projects shown above the gallery.

That's continuous delivery proven. Your existing pipeline carried a content-only change to the same live address without another hosting configuration.

Secret mission

Add Combined Project Search

Your category filters work well while the gallery is small. Add keyword search that works together with the active category so visitors can find relevant projects as your portfolio grows.

Clean Up Your Resources

Clean Up Your Resources

Your public GitHub Pages site costs $0 to keep while its repository remains public. Decide whether to keep publishing updates, unpublish the site for now, or remove the project entirely.

Resources you used:

  • The public YOUR-USERNAME.github.io repository on GitHub. It contains your portfolio files plus .github/workflows/deploy.yml.
  • The GitHub Actions workflow that deploys each accepted update to main.
  • The GitHub Pages deployment available at https://YOUR-USERNAME.github.io.
  • The local YOUR-USERNAME.github.io folder containing your cloned portfolio files.

Keep everything running

No action needed. Choose this if you want your portfolio to stay public while you add more projects.

  • Keep the YOUR-USERNAME.github.io repository public.
  • Keep the deployment workflow enabled so future pushes to main continue updating the site.
  • Visit https://YOUR-USERNAME.github.io whenever you want to confirm the published portfolio is available.

Pause - I'll come back to this later

Unpublish the website while preserving your repository and workflow. You can publish the same project again when you are ready to continue.

  • Return to the YOUR-USERNAME.github.io repository on GitHub.
  • Select Settings in the repository navigation.
  • Select Pages in the repository settings.
  • Choose Unpublish site.

Your repository settings remain available. The public deployment is removed.

  • Visit https://YOUR-USERNAME.github.io to confirm the portfolio is no longer published.

Delete - I don't want to use this again

Repository deletion is permanent. Your local copy gives you one last chance to preserve any portfolio code you value.

  • Copy any portfolio files you want to retain into a separate folder.
  • Return to the YOUR-USERNAME.github.io repository on GitHub.
  • Select Settings in the repository navigation.
  • Select Pages in the repository settings.
  • Choose Unpublish site to remove the public deployment.

The public website is now separated from the repository cleanup. Next, remove the source that powered its workflow.

  • Return to the repository settings.
  • Scroll to the repository deletion controls at the bottom of the settings page.
  • Choose the control for permanently deleting the repository.
  • Complete the confirmation checks that GitHub displays.

Deleting the repository removes its portfolio files and deployment workflow from GitHub.

  • Close the local repository in Visual Studio Code.
  • Use your Windows file manager to locate the YOUR-USERNAME.github.io folder inside the folder where you keep projects.
  • Delete the local YOUR-USERNAME.github.io folder.
  • Search the same projects folder for YOUR-USERNAME.github.io to confirm the local clone no longer appears.

Your public site, remote repository, workflow, and local clone are now removed.

Nice Work!

Nice Work!

You did it! Your responsive portfolio is live at https://YOUR-USERNAME.github.io with a repeatable deployment workflow behind it.

You've learned how to:

  • Build a responsive portfolio with a professional summary plus a print-ready résumé.
  • Replace duplicated card markup with a data-driven UI that renders reusable project objects through the DOM. Use category filters to update the visible projects.
  • Publish updates through GitHub Actions to GitHub Pages. This CI/CD workflow updates the same public site after each push to main.
  • Complete the optional Secret Mission by adding combined project search that works with the category filters.

Ready to quiz yourself?