My new website: from Wordpress to Astro

Luis Serrano
My new website: from Wordpress to Astro

Share

Share on social networks

I recently launched the latest version of my website, moving away from the old WordPress-based version. Although it had served me well for many years, I felt it was time for a change.

The previous version had a few issues. It still relied on jQuery, the theme code was disorganized, and certain PHP methods were marked as deprecated, preventing me from upgrading to the latest PHP versions. Originally created in 2016, the theme was not built from scratch. Instead, I had purchased a commercial theme and heavily customized it to achieve the desired look and feel, which included a cool 6-minute background video showcasing my wife and me traveling the world.

The website itself is relatively small. It consists of a homepage with a “typing” effect, followed by an About section where I describe myself. The Resume page displays my work experience and peer recommendations, and there’s a contact page as well. Later on, I added a blog section, although my activity in maintaining it hasn’t been very consistent. Overall, the website experiences minimal changes over time, except when I need to update my resume, add a new recommendation, or write an occasional blog post.

In the previous WordPress site, I had two types of content: blog posts and pages. Blog posts were used for the obvious purpose, while everything else was treated as pages. However, managing updates to my resume, adding new work experiences or recommendations, or including new certifications and trainings became quite cumbersome. These elements weren’t organized as taxonomies, making it impossible to simply “Add new work experience” and have it seamlessly appear in the timeline of my resume.

Choosing the new framework

To create my site, and keeping in mind that it barely changes, I considered static site generators like Gatsby. But the learning curve was a bit steep, and although I love React, I didn’t really want to invest too much time or effort on development. I work 8 hours a day and I’ve got two kids, so my time is very, very limited.

I accidentally stumped into Astro, and after reading about it I put it on my “review list”. Here’s what caught my attention:

  • Blends static and dynamic rendering: Astro allows developers to combine static and dynamic rendering in a single project. It enables you to build parts of your website as static files, while also incorporating dynamic components when needed.
  • Component-oriented architecture: Astro follows a component-oriented approach, where reusable components are at the core of the development process. It allows developers to create components using their preferred framework or library and seamlessly integrate them into the Astro project.
  • Agnostic data fetching: Astro allows developers to use their preferred data fetching techniques, whether it’s server-side rendering, client-side rendering, or static data. This flexibility empowers developers to choose the most suitable method for different parts of their website.
  • Performance optimization: With Astro, you can optimize performance by selectively rendering only the necessary components on the server-side, reducing the payload sent to the client. This approach enhances the overall speed and user experience of the website.

Also, while reading about Astro, I discovered the frontmatter. It refers to the metadata that can be added at the beginning of a file to provide additional information or configuration options. It is commonly used in static site generators to define attributes for individual pages or components.

The frontmatter in Astro follows the YAML syntax, which is a human-readable data serialization format. It consists of key-value pairs enclosed in triple dashes (---). Here’s an example of how frontmatter is structured in an Astro file:

---
title: "My Page Title"
description: "This is a description of my page."
date: 2023-06-26
---

In this example, the frontmatter section includes three key-value pairs: title, description, and date. Each key represents a specific attribute, while the corresponding value provides the associated information. The values can be of different types, such as strings, numbers, booleans, or dates, depending on your requirements.

Once the frontmatter is defined, you can access these values within your Astro components or pages. For instance, you can use them to dynamically generate page titles, display the description, or utilize the date for specific functionalities.

The frontmatter is often utilized in CMS platforms like Ghost and Netlify CMS, where it helps define attributes and settings for content entries.

I found creating my site with Astro like if I was writting simple HTML/CSS, old school. Although it’s not really HTML, but JSX. Anyway, pretty similar experience. I didn’t need to worry about routing, I could define common layouts and components to reuse as much as possible, and I barely use Javascript at all in the client. It’s actually only used to emulate the typing effect you see in the Homepage. Everything else is just HTML and (really cool) CSS animations. I also don’t use any cookies anymore, I have ditched Google Analytics for good and I now use PostHog. So my shiny cool new site is also GDPR compliant and privacy friendly. There are also other goodies like scoring a 98 in Lighthouse/Google Page insights scores. Ironically, it’s not a 100 because of Google Fonts!

Finally, I implemented full dark mode support, because I love to read at night and it helps my eyes a lot. I am offering the same courtesy to my visitors.

Adding / Modifying content

I opted to integrate Astro with Netlify CMS and utilize local markdown files instead of relying on a headless CMS or traditional database. While Astro has the capability to fetch data from various sources via APIs, I preferred a straightforward solution for maintaining my site. My primary goal was to have a user-friendly interface for adding and editing content, especially when I’m away from my computer and code editor. Setting up authentication with an external provider was also straightforward, although I won’t disclose the specific provider for security reasons.

Adding taxonomies

After successfully integrating the CMS, my first step was to set up “taxonomies” (or “collections”, in Astro’s terminology). This allowed me to define different content types, each with its own set of properties, that could be added or edited. To add content, I simply created a new markdown file and started writing. The CMS editor also provided a convenient feature for easily dropping images into the markdown. The editor automatically copied the image to the appropriate folder and generated the correct image path.

Furthermore, I implemented image optimization functionality, eliminating the need for manual optimization. Now, when I add images, the CMS automatically handles the optimization process, ensuring optimal image performance on the website.

To illustrate the contrast, let me show you how I used to add a new work experience with my previous website:

Adding a new work experience in my Wordpress site

Managing the resume section, including items related to education, training, and recommendations from former colleagues, was a cumbersome task. It involved dealing with a large block of code that combined HTML with shortcodes. Whenever I needed to add or modify something, I had to navigate through this messy code and manually locate the “insertion point”. Not ideal, huh?

Not only those pieces of content are now separate entities that I can add/edit/remove. I came up with something called “blocks”. The block content type is meant for stuff that I want to have in markdown files, and it can be displayed in different parts of the site. In the frontmatter of those files, I have a “section” property, and then from each section I get all blocks filtering by the section name. So I can have blocks displayed in the Resumen, but also in the Contact and About pages. The also have an “order” property, in case I want them displayed in a specific order. Every other piece of content is sorted by date.

The learning curve

I must say that building the entire site with Astro was surprisingly easy, even without any prior experience or exposure to the framework. Admittedly, my site is relatively simple, and I’m aware that there are more complex examples out there that required a significant amount of work to put together. Overall, my main source of frustration stemmed from using the latest version of Astro, which sometimes made it challenging to find examples or documentation for accomplishing simple tasks. As you can imagine, neither Copilot or chatGPT were helpful in my mission. They provided outdated snippets that weren’t correct anymore.

For instance, I encountered difficulties with basic features like pagination, which were not thoroughly documented. It took a lot of trial and error to figure out the necessary steps to make certain things work. Fortunately, I found helpful solutions from enthusiastic YouTubers who are passionate about Astro and shared their knowledge in their videos. Their insights saved the day and provided the solutions I needed.

Let me post here some of those videos, which are great resources if you’re learning:

And one thing that didn’t work:

I wanted to add page transitions, the same way my old site had. But in my Wordpress site, all the content was in the same page, and then with some CSS/Javascript I was animating the whole screen when changing sections. Here, going from one page to another, a full page refresh is done because the content is split in different HTML pages. There are experimental features that will enable page transitions in the future, but they’re not ready yet. So it’ll have to wait.

The integration with Netlify CMS works well but don’t get your hopes up: it’s not a perfect solution. It will help you quickly to edit content from a tablet or phone, but it’s really really basic and it feels unfinished, not very polished, slow and with some bugs. Also, you can’t see an exact preview of your content as you’d see it in your page when using the CMS. Besides, technically speaking, it wasn’t hard to integrate but you’ll need to dig quite a lot in the documentation to find working examples of some use cases.

The migration

I created two scripts to bring my content from Wordpress to Astro. I will share it at the end of this post, in case it helps anyone. I also added a custom youtube plugin to embed responsive Youtube videos in my markdown files, like this:

Conclussion

In conclusion, I am quite happy with Astro, to the point that I believe I could even embark on a side business creating marketing sites using this framework. Just imagine the possibilities, such as real estate websites. With Astro, it would be incredibly straightforward to extract text files from a folder hierarchy, convert them into markdown along with images, and effortlessly generate static and visually appealing HTML pages that showcase property details, photos, and a user-friendly contact form. The potential for creating stunning websites with ease is truly exciting.

If you feel like sharing your thoughts, asking questions, or throwing exciting business opportunities my way, feel free to head over to the Contact Page. I'm all ears and ready for some juicy discussions!

Custom code

Importing data from Wordpress to Astro/Markdown

Export your blog posts (or any other content type except media) as an XML file from Wordpress. You’ll find the Export section under “Tools”. Once you’re done, process the XML file with this script:

/* import-wp.js */
import fs from "fs";
import path from "path";
import { XMLParser } from "fast-xml-parser";
import TurndownService from "turndown";
import axios from "axios";

const turndownService = new TurndownService();
const INPUT_FILE = process.argv[2] || "wordpress.xml"; // it accepts a filename as parameter, or wordpress.xml by default
const MD_FOLDER = "./src/content/blog";
const IMAGES_FOLDER = "./src/assets";
if (!fs.existsSync(MD_FOLDER)) {
  fs.mkdirSync(MD_FOLDER);
}

if (!fs.existsSync(IMAGES_FOLDER)) {
  fs.mkdirSync(IMAGES_FOLDER);
}

fs.readFile(INPUT_FILE, "utf8", async (err, xmlData) => {
  if (err) {
    console.error(`Error reading file from disk: ${err}`);
    return;
  }

  const parser = new XMLParser({
    ignoreAttributes: false,
    attributeNamePrefix: "",
  });
  const jsonData = parser.parse(xmlData);

  const items = jsonData.rss.channel.item;
  const posts = items.filter((item) => item["wp:post_type"] === "post");

  for (const post of posts) {
    const html = post["content:encoded"];
    const markdown = turndownService.turndown(html);
    const slug = post["wp:post_name"];
    const author = post["dc:creator"];
    const title = post.title;
    const description = post.description;
    const pubDate = post.pubDate;
    const heroImage = post["wp:postmeta"].find(
      (meta) => meta["wp:meta_key"] === "_thumbnail_id"
    )?.["wp:meta_value"];

    let heroImageUrl = "";
    if (heroImage) {
      const imageItem = items.find((item) => item["wp:post_id"] === heroImage);
      if (imageItem) {
        heroImageUrl = imageItem["wp:attachment_url"];
        await downloadImage(heroImageUrl, slug);
      }
    }

    // You will need to adapt this to your content structure
    const frontmatter = `---
title: "${title}"
author: ${author.replace(/\s+/g, "-").toLowerCase()}
description: "${description}"
pubDate: ${new Date(pubDate).toISOString()}
heroImage: ${
      heroImageUrl ? path.join(IMAGES_FOLDER, path.basename(heroImageUrl)) : ""
    }
---
`;

    fs.writeFileSync(
      path.join(MD_FOLDER, `${slug}.md`),
      frontmatter + "\n" + markdown
    );
  }
});

async function downloadImage(url, slug) {
  const response = await axios({
    method: "GET",
    url: url,
    responseType: "stream",
  });

  const imagePath = path.join(IMAGES_FOLDER, `${slug}-${path.basename(url)}`);
  const writer = fs.createWriteStream(imagePath);

  response.data.pipe(writer);

  return new Promise((resolve, reject) => {
    writer.on("finish", resolve);
    writer.on("error", reject);
  });
}

To import your media library from Wordpress, you’ll need to export selecting Media like this:

Exporting Media library from Wordpress

Then process the resulting XML file with this script:

/* import-wp-media.js */
import fs from "fs";
import path from "path";
import { XMLParser } from "fast-xml-parser";
import axios from "axios";

const http = axios.create({
  proxy: false, // this is important if you're behind a proxy
  maxRedirects: 15, // increase as needed
});

const INPUT_FILE = process.argv[2] || "media.xml"; // it accepts a filename as parameter, or media.xml by default
const IMAGES_FOLDER = "./src/assets";

if (!fs.existsSync(IMAGES_FOLDER)) {
  fs.mkdirSync(IMAGES_FOLDER);
}

fs.readFile(INPUT_FILE, "utf8", async (err, xmlData) => {
  if (err) {
    console.error(`Error reading file from disk: ${err}`);
    return;
  }

  const parser = new XMLParser({
    ignoreAttributes: false,
    attributeNamePrefix: "",
  });
  const jsonData = parser.parse(xmlData);
  const items = jsonData.rss.channel.item;
  const images = items.filter((item) => item["wp:post_type"] === "attachment");

  for (const image of images) {
    const imageUrl = image["wp:attachment_url"];
    const imageName = path.basename(imageUrl);
    await downloadImage(imageUrl, imageName);
  }
});

async function downloadImage(url, imageName) {
  console.log("Attempting to download: " + url);
  const response = await http({
    method: "GET",
    url: url,
    responseType: "stream",
  });

  const imagePath = path.join(IMAGES_FOLDER, imageName);
  const writer = fs.createWriteStream(imagePath);

  response.data.pipe(writer);

  return new Promise((resolve, reject) => {
    writer.on("finish", resolve);
    writer.on("error", reject);
  });
}

Youtube embedding pluggin

I created this simple “shortcode” to embed responsive Youtube videos. The syntax is:

[youtube:VIDEO ID]

That it’s replace with an iframe containing the video.

/* youtube-plugin.js */
import { visit } from "unist-util-visit";

export default function remarkYouTubePlugin() {
  function transformer(tree) {
    visit(tree, "text", function visitor(node, index, parent) {
      const regex = /\[youtube:(\w+)\]/g;
      let value = node.value;
      let match;

      while ((match = regex.exec(value)) !== null) {
        const videoId = match[1];
        const iframe = `<div class="youtube-video"><div class="youtube-video-inner"><iframe src="https://www.youtube.com/embed/${videoId}" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen></iframe></div></div>`;

        const before = value.slice(0, match.index);
        const after = value.slice(match.index + match[0].length);

        value = before + iframe + after;
        regex.lastIndex += iframe.length - match[0].length;
      }

      if (value !== node.value) {
        parent.children.splice(index, 1, { type: "html", value });
      }
    });
  }

  return transformer;
}

Share

Share on social networks