diff --git a/posts/img/post-index-build-script.webm b/posts/img/post-index-build-script.webm
new file mode 100644
index 0000000..30f3708
Binary files /dev/null and b/posts/img/post-index-build-script.webm differ
diff --git a/posts/img/systemsobscure-forgejo-runners.png b/posts/img/systemsobscure-forgejo-runners.png
new file mode 100644
index 0000000..4361b91
Binary files /dev/null and b/posts/img/systemsobscure-forgejo-runners.png differ
diff --git a/posts/self-hosting-this-site.md b/posts/self-hosting-this-site.md
new file mode 100644
index 0000000..7613dcb
--- /dev/null
+++ b/posts/self-hosting-this-site.md
@@ -0,0 +1,136 @@
+---
+title: "Self-hosting this site"
+slug: /self-hosting-this-site/
+date: 2025-07-21
+tags: ["self-hosting"]
+---
+
+[Previously](https://systemsobscure.blog/posts/how-I-deploy-this-site) this site
+was deployed as follows. I used [Gatsby.js](https://gatsbyjs.com) to generate a
+static website from React JavaScript. When changes to the `main` branch were
+pushed to the remote GitHub repository, a GitHub action would execute, building
+the source files and transferring them to an S3 bucket. The HTML generated was
+then served using AWS Cloud Front.
+
+As I am now trying to self-host as many services as I can, it was time remove
+the dependency on AWS and GitHub and instead serve the site from my VPS and
+manage deployment via my
+[self-hosted Forgejo instance](https://forgejo.systemsobscure.net/thomasabishop).
+
+As part of this process I went on a bit of a side-quest to rebuild the site
+without Gatsby.
+
+When I tried to update to the lastest version of Gatsby I faced several problems
+mostly due to Gatsby trying to foist server-side rendering on every project. For
+such a small blog site with next to zero traffic this is unnecessary. Moreover,
+I realised that I could also do without all the bloat that Gatsby adds via its
+plugin ecosystem. (Perhaps if frontend bundles weren't so large, SSR wouldn't be
+needed lol.)
+
+So I decided to simplify the blog and build it as a [Vite](https://vite.dev)
+React application that gets its content from a JSON index that is generated via
+a pre-build script.
+
+I
+[wrote a script](https://forgejo.systemsobscure.net/thomasabishop/systems-obscure/src/branch/main/scripts/generate-post-index.js)
+in JavaScript that loops through all the blog posts in Markdown format from a
+`/posts` directory at the project root. It creates a JSON array with an entry
+for each post. For example, this post would be represented as follows:
+
+```json
+[
+ {
+ "slug": "self-hosting-this-site",
+ "title": "Self-hosting this site",
+ "date": "2025-07-22T00:00:00.00Z",
+ "tags": ["self-hosting"],
+ "html": "
Previously this site was..."
+ }
+]
+```
+
+To convert the raw markdown to HTML I used
+[marked](https://github.com/markedjs/marked). I also added the following steps:
+
+- convert `` tags into syntax highlighted blocks using
+ [shiki](https://shiki.style/)
+- compress and resize images and then transfer them to the `public/` directory
+ where Vite sources static assets when deployed
+- find and replace all image `src` attributes in the resulting HTML with the
+ `public/` file path
+
+When I run `npm run build:posts` this generates the `post-index.json` file and
+saves it to `/public` so that the React application can read from it when
+rendering the site content.
+
+
+
+ Running the pre-build script to generate the site content.
+
+
+I created a custom hook called `usePosts` that fetches the index and saves it to
+the session storage to avoid unnecessary network requests. You can see this
+being invoked in the
+[template component for blog posts](https://forgejo.systemsobscure.net/thomasabishop/systems-obscure/src/commit/43eec03edd2e6330362cc5605a48a91387eb49d3/src/templates/BlogTemplate.tsx).
+
+Having rebuilt the site without the dependency on Gatsby, the next step was to
+deploy it.
+
+I wanted to retain the "push and deploy" model that I previously achieved via
+the GitHub Action. This was easy because the syntax for Forgejo Actions is
+practically identical. You place the YAML declaration in the `.forejo/`
+directory of the project and Forejo picks it up on each push to the remote.
+
+Here's the file:
+
+```yaml
+name: Deploy Blog
+on:
+ push:
+ branches: [main]
+jobs:
+ deploy:
+ runs-on: ubuntu-latest
+ container: node:18
+ steps:
+ - uses: actions/checkout@v3
+ - run: npm install
+ - run: npm run build:posts
+ - run: npm run build
+ - run: |
+ echo "${{ secrets.SSH_FORGEJO_KEY }}" > /tmp/ssh_key
+ chmod 600 /tmp/ssh_key
+ ssh -i /tmp/ssh_key -o StrictHostKeyChecking=no ${{ vars.VPS_USER }} "bash -c 'rm -rf /var/www/systemsobscure.blog/*'"
+ scp -i /tmp/ssh_key -o StrictHostKeyChecking=no -r dist/* ${{ vars.VPS_USER }}:/var/www/systemsobscure.blog/
+ rm /tmp/ssh_key
+```
+
+Before compiling the build I first run the pre-build script to generate the post
+index. Then I remove the existing source files for the site in `var/www/` over
+`ssh` and then transfer the new source files using `scp`. (While I run most
+third-party services on my VPS via Docker, I prefer to keep it simple with my
+own applications and not use containers.)
+
+
+
+In order for the site to be served from `var/www/systemsobscure.blog`, I had to
+give my Docker instance of nginx running on the VPS access to this directory by
+[adding it to the volume mappings](https://forgejo.systemsobscure.net/thomasabishop/self-host/commit/8c380b71735f278f4309bbd14ad96cb9a29104b7)
+in the `docker-compose.`
+
+Then I added an
+[nginx `.conf` file for the blog](https://forgejo.systemsobscure.net/thomasabishop/self-host/src/branch/main/proxy/nginx/conf.d/systemsobscure.conf).
+This file specifies the SSL certificate to use and sets `index.html` as the
+document root. It also adds some default caching and compression, along with
+security headers.
+
+Finally I
+[updated my SSL certificate generation script](https://forgejo.systemsobscure.net/thomasabishop/self-host/commit/61cdbe43c2041d5961ef74416270794eb6fb91c6)
+to include `systemsobscure.blog`.
+
+And that's it. My personal website is now self-hosted on my VPS and is
+automatically deployed via pushes to my self-hosted Git forge. No more GitHub,
+no more AWS.
diff --git a/scripts/process-blog-imgs.js b/scripts/process-blog-imgs.js
index 97bc91c..4e62593 100644
--- a/scripts/process-blog-imgs.js
+++ b/scripts/process-blog-imgs.js
@@ -15,23 +15,27 @@ const processBlogImages = async () => {
fs.mkdirSync(destDir, { recursive: true })
}
- // Copy all images
const files = fs.readdirSync(srcDir)
- const imageExtensions = [".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg"]
+ const imageExtensions = [
+ ".jpg",
+ ".jpeg",
+ ".png",
+ ".gif",
+ ".webp",
+ ".svg",
+ ".webm",
+ ]
- // Use for...of loop instead of forEach to work with async/await
for (const file of files) {
const ext = path.extname(file).toLowerCase()
if (imageExtensions.includes(ext)) {
const inputPath = path.join(srcDir, file) // Define inputPath and outputPath
const outputPath = path.join(destDir, file)
- if (ext === ".svg") {
- // SVGs just get copied
+ if (ext === ".svg" || ext === ".webm") {
fs.copyFileSync(inputPath, outputPath)
console.info(`📸 Copied ${file}`)
} else {
- // Process other images
await sharp(inputPath)
.resize(1200, 800, {
fit: "inside",