loading please wait.
Welcome to my blog

Welcome to my blog

4 min read

Introduction

Welcome to my blog reeceharris.net. Initially, this site was meant to host my portfolio and provide information about my latest projects. However, in an effort to expand my reach and improve SEO, I decided to add a blog section.

As of writing this, I'm 21 years old and, like many others, I aspire to achieve financial success. But my dreams go beyond just wealth—I want to provide for my future family and build a financially secure future. To do that, I'm going to create side hustles that can generate passive income. With over 7 years of development experience (3 of those in a professional capacity), I plan to create apps and services that are accessible to everyone.

To start that journey, I need to connect with a community. Inspired by Marc Lou, I’ll be creating small apps and services. Making a good first impression is key, and that’s exactly what this blog and portfolio are here to help me achieve.

How I Built a Fully-Featured Blog with Zero Expenses

Like many others, I can't justify spending money on databases, CMSs, and other services for a blog that may never generate income. My goal is to create a blog system that offers the same features as a paid service—but for free.

To achieve this, I plan to use static site generation, leveraging Svelte in combination with Obsidian. Obsidian? Yes, the markdown note-taking app! It’s simple: I’ll create a vault within the website's source code and, during the build process, compile all markdown files into a single source that SvelteKit can read and render. Fortunately, I can automate this with GitHub Actions.

Before Vercel clones the repo for the build, we need to run a GitHub action. This is simple: just have Vercel pull from an 'after-process' branch. The action will then process the posts, create the new file, and push it to the 'after-process' branch:

import fs from 'fs';
import path from 'path';
import slugify from 'slugify';
import readingTime from 'reading-time';
import moment from 'moment';

const directoryPath = path.join('posts');
let parsedPosts = [];

fs.readdir(directoryPath, (err, files) => {
    if (err) {
        return console.log('Unable to scan directory: ' + err);
    }

    const mdFiles = files.filter(file => path.extname(file) === '.md');

    mdFiles.forEach(file => {
        const filePath = path.join(directoryPath, file);
        const data = fs.readFileSync(filePath, 'utf8');

        const title = file.replace('.md', '');
        let createdAt = fs.statSync(filePath).birthtime;
        let updatedAt = fs.statSync(filePath).mtime;

        const parts = data.split('---');

        for (let i = 0; i < parts[0].split('\n').length; i++) {
            const line = parts[0].split('\n')[i];

            if (line.startsWith('createdAt:')) {
                const dateParts = line.replace('createdAt:', '').trim().split("/");
                createdAt = new Date(`${dateParts[2]}-${dateParts[1]}-${dateParts[0]}`);
            }

            if (line.startsWith('updatedAt:')) {
                const dateParts = line.replace('updatedAt:', '').trim().split("/");
                updatedAt = new Date(`${dateParts[2]}-${dateParts[1]}-${dateParts[0]}`);
            }

        }

        const description = parts[1];
        const contentArray = parts;
        const content = contentArray.length > 2 ? contentArray.slice(2).join('---') : '';

        parsedPosts.push({
            slug: slugify(title),
            title: title,
            description: description,
            createdAt: createdAt,
            updatedAt: updatedAt,
            content: content,
            readTime: readingTime(content),
            createdAtFormatted: moment(createdAt).format("MMM Do YYYY"),
            updatedAtFormatted: moment(updatedAt).format("MMM Do YYYY")
        });
    });

    parsedPosts.sort((a, b) => {
        return new Date(b.createdAt) - new Date(a.createdAt);
    })

    fs.writeFileSync(path.join('postsData.json'), JSON.stringify(parsedPosts, null, 2));
    console.log('Posts data has been saved.');
});

Next, I need to create custom Obsidian plugins to manage the site's SEO. One of the plugins I developed uses the SEMRush API to audit content, helping to improve the overall ranking of an article. This allows me to analyse and optimise each post’s SEO performance directly from within Obsidian, making it easier to enhance visibility without leaving my workspace.

Analytics and Performance Managment

Vercel offers analytics and site speed tools for one project per free account, which I use to track engagement, visits, and other data. I combine this with Google Analytics 4 and Google Search Console to monitor click-through rates, and SEMrush to track backlinks.

These metrics can be complicated, but once you understand how they work, improving a site’s SEO becomes much easier. Fortunately, working at a web development company has taught me a lot about SEO and how to rank higher over the years.

The key is to focus on ranking for non-competitive keywords, avoid using AI-generated content, and prioritise user satisfaction with quality posts. Otherwise, search engines are likely to rank your site lower.

Over and Out

This is a starter post to experiment with different writing styles. Over the next few posts, the style and structure may vary as I explore what I prefer and what performs best in terms of ranking.

I don’t plan on cranking out a large volume of blog posts, as I want to avoid diluting the content. Posts will be far and between, but I aim to write one for each app or service I create. These posts will offer insights into how they're built, along with tips and tricks I want to share with others.