Recreating my portfolio website in Astro


Tutorial on building a Astro portfolio and open-sourcing it

21 Jul, 2021 · 8 min read

My portfolio website was a bit of a mix between plain HTML, Eleventy, and bootstrap.

However, it was a bit unmaintainable to edit the content. Since I was looking at Astro already, I decided to refactor my portfolio right away.

And the cool part: I open-sourced it for everyone to use!

Portfolio build in Astro

Note: This article doesn’t describe the build step-by-step but showcases some important process highlights.

Rethinking the base

The portfolio was already a one-page, so there was nothing new about that, but thinking about how it would look as components, I quickly came up with the following approach.

  • Header
  • Intro
  • Skills
    • Comfortable
    • Mastering
  • Portfolio
  • Journey
  • Testimonials
  • Footer/Contact

And if you have a look at the source files, you can see all these components written down.

As for the parts that needed to be dynamic:

  • Skills
  • Portfolio
  • Journey
  • Testimonials
  • Socials

One cool aspect of Astro is that it can quickly read from a JSON file, so in our case, we are using that as our “database”.

Let’s dive right into building it!

Creating the project

Astro has a super quick setup guide, and we can go ahead and create a folder and init Astro like this:

mkdir portfolio && cd portfolio
npm init astro

You can choose a starter template if you like, but I tend to go for the starter kit.

Open this project up in your favorite editor.

Let’s start with our data source. I’ll add a site.json file inside a data directory in the src of the project.

The basic structure of this JSON looks like this.

  "title": "Chris Bongers | Senior Full Stack Developer",
  "description": "Chris Bongers - Portfolio, Full stack developer",
  "url": "",
  "author": "Chris Bongers",
  "profile_pic": "/assets/chris.jpg",
  "resume": "/assets/resume.pdf",
  "nav": [
    { "link": "#skills", "name": "Skills" },
    { "link": "#work", "name": "Work" },
    { "link": "#footer", "name": "Contact" },
      "link": "",
      "name": "Blog",
      "target": "_blank"
  "skills": {
    "comfortable": [{ "name": "HTML5", "icon": "html5" }],
    "mastering": [{ "name": "Flutter", "icon": "flutter" }]
  "work": [
      "title": "Yaatree",
      "description": "Our amazing puppy, who goes on all kinds of cool adventures",
      "image": "/assets/yaatree.jpg"
  "journey": {
    "2030": [
        "title": "Senior full stack remote developer ☁️",
        "description": "It's my dream to be a senior full-stack developer who can work remotely."
  "testimonials": [
      "image": "/assets/nicole.jpg",
      "name": "Nicole Bongers",
      "title": "~ Wife",
      "quote": "Chris is an amazing husband and dog father!"
  "socials": [
      "link": "",
      "title": "Codepen",
      "icon": "codepen"

As you can see, I’ve omitted some data, but you can make this your own. Feel free to add socials, testimonials, and work elements as you find needed.

The index page in Astro

Now let’s head over to the index.astro page. This is the actual index and, for us, the only page we have.

// Component Imports
import MainHead from '../components/MainHead.astro';
import Header from '../components/Header.astro';
import Intro from '../components/Intro.astro';
import Skills from '../components/Skills.astro';
import Work from '../components/Work.astro';
import Journey from '../components/Journey.astro';
import Testimonials from '../components/Testimonials.astro';
import Footer from '../components/Footer.astro';
<html lang="en">
    <MainHead />
    <Header />
      <Intro />
      <Skills />
      <Work />
      <Journey />
      <Testimonials />
      <Footer />
<style lang="scss">
main {
  padding-top: 150px;
  width: 100%;
  max-width: 700px;
  margin: 0 auto;

Don’t worry. We are importing a lot of components here. We’ll make those in a bit.

I just want to highlight some things here. Remember how I said we could easily split things out into components? Well, here they are. Every section has its own component, so the code keeps readable for us.

Another cool thing is that Astro allows us to use inline styling, and even in the form of scss.

We also include a component called the MainHead component in the head of our structure. Let’s see what that component looks like.

import site from "../data/site.json";
const {title, description} = site;
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="Description" content={description}>
<link rel="stylesheet" type="text/css" href="/global.css">
<link rel="stylesheet" href="[email protected]/devicon.min.css">
<link rel="icon" href="/favicon.ico" />

As you can see, this is one way to load data from our JSON file. And use it in the component.

I find this approach super convenient and easy to use. The file above has all the stuff we want to have in our head section.

Looping data in Astro

Another important thing we need to do is loop over our sample arrays.

Let’s take the skills as an example.

At the top of our Skills.astro component, we can define the data we need:

import site from "../data/site.json";
import SkillItem from './SkillItem.astro';
const title = `These are skills I've mastered over the years`;
const comfortable = `Skills I'm super comfortable with`;
const mastering = `Skills I'm mastering`;
const {skills} = site;

Now let’s use these skills in the HTML part.

<div class="skills">
    { => (
        <SkillItem item={item} />
<div class="skills">
    { => (
        <SkillItem item={item} />

As you can see, I loop over the two arrays we have inside our skills dataset, being the comfortable and mastering arrays.

For each of those items, I render something called a SkillItem and pass an item variable.

Let’s quickly have a look at a SkillItem to see how this item variable is used:

const { item } = Astro.props;
<div class="skills-item tooltip-container">
    <span class="tooltip">{}</span>
    <i class={`devicon-${item.icon}-${(item.type?item.type:'plain')}`}></i>
<style lang="scss">
.skills-item {
    background: #fff;
    padding: 10px;
    margin: 5px;
    border-radius: 5px;
    display: flex;
    align-items: center;
    justify-content: center;
    i {
        color: var(--color-purple);
        font-size: 2rem;

As you can see, the item variable is retrieved using the Astro.props.

And we can now render a super small part of our application in its component. It also uses its own styles, so it’s clear where these belong.

Rendering JavaScript inside a component

Another thing Astro can do is, of course, render React/Svelte/Vue components and use those JavaScript.

However, my site was previously built in plain HTML, so I already had some plain JavaScript.

The cool part is that Astro gives us the option to use it inside Astro files!

So let’s look at the Header component to see how we can embed a little piece of JavaScript for a single component.

import site from "../data/site.json";
const {nav} = site;
<header id="header">
        { => (
                <a href={} target={} rel="noopener noreferrer">{}</a>
<script type="text/javascript">
const header = document.getElementById("header");
window.onscroll = function() {
  if (document.body.scrollTop > 50 || document.documentElement.scrollTop > 50) {
  } else {

This JavaScript will only get attached when the component is rendered; however, it doesn’t matter much since everything is on one page.

Using static assets

You might have spotted that I’ve used some images in my portfolio, as many static generators also come with a public folder.

This folder will always be available on our build. For the portfolio, I placed images inside a assets folder.

We can link these by using the following syntax:

<img src="/assets/image.jpg" />

If you are keen to see how it looks compiled, then visit my portfolio.

Or maybe you instead want to see the complete code, then check out this GitHub public repository.

Thank you for reading, and let’s connect!

Thank you for reading my blog. Feel free to subscribe to my email newsletter and connect on Facebook or Twitter

Spread the knowledge with fellow developers on Twitter
Tweet this tip
Powered by Webmentions - Learn more

Read next 📖

Computed Nano Stores

6 Sep, 2022 · 2 min read

Computed Nano Stores

Astro Nano Stores maps

5 Sep, 2022 · 3 min read

Astro Nano Stores maps

Join 2099 devs and subscribe to my newsletter