Building My Website

In this post, I discuss how I built this website, and why I made the technology choices I did. You can view the source code on GitHub here.

Astro

I selected Astro as my front-end framework. Astro’s elegant way of using a code fence to distinguish between server and client code, and Markdown being a first-class citizen, made it a top choice for me.

Here’s an example .astro file from my website:

---
import { AstroSeo } from "@astrolib/seo";
import BaseLayout from "./BaseLayout.astro";
const { frontmatter } = Astro.props;
---

<AstroSeo
  title={frontmatter.title}
  description={frontmatter.description}
/>
<BaseLayout>
  <div class="cust-markdown mx-auto max-w-7xl m-4 p-4 prose sm:prose-sm lg:prose-lg xl:prose-xl dark:prose-invert prose-img:rounded-lg bg-gray-100 dark:bg-gray-900 rounded-2xl">
    <slot />
  </div>
</BaseLayout>

<style>
  .cust-markdown {
    font-family: "system-ui";
  }
</style>

At first glance, aside from the code-fenced server code, it looks unremarkable. It almost looks like plain html. What you see on the client is much different though. For example, if you inspect element, you wont find an internal CSS element. Instead you will find this:

<script type="module" src="/src/layouts/MdLayout.astro?astro&type=style&index=0&lang.css"></script>

Astro has parsed the source file, and created a style sheet with the style from the block. It does the same thing with inline scripts. This feature has a convenient side-effect: if you choose to add a CSP header blocking inline scripts (which is a XSS vulnerability), you won’t have to go back and fix your code.

Markdown files in src/pages are automatically made pages with a route, all you need to do to get them rendered is put them in a layout, simply by adding the following to the Markdown file’s frontmatter:

layout: ../../layouts/MdLayout.astro

For styling, I used the convenient Astro Tailwind plugin which gives you powerful styling capabilities by simply applying classes to your html elements.

Because I wanted markdown to use whatever system font a client uses, I simply set the font family for markdown to system-ui.

SST

SST was the obvious choice for self-hosting my website since it provides a construct, AstroSite, that needs minimal configuration. The site is hosted on Lambda and CloudFront, which are practically free for low-traffic websites. All I had to do was pass it my domain and deploy to AWS, which I do via GitHub Actions.

Creating the site in the SST stack:

const johnDomain = "johnlien.me";
let domainName = johnDomain;
if (app.local) {
  domainName = `dev.${johnDomain}`;
} else if (app.stage !== "prod") {
  domainName = `${app.stage}.${johnDomain}`;
}

const site = new AstroSite(stack, "site", {
  customDomain: {
    domainName,
    hostedZone: johnDomain,
  },
});
stack.addOutputs({
  url: site.customDomainUrl || site.url,
});

Deploying with GitHub Actions:

jobs:
  deploy:
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      packages: read
      actions: write
    steps:
      - uses: actions/checkout@v3
      - uses: pnpm/action-setup@v2
        with:
          version: 8
      - uses: actions/setup-node@v3
        with:
          node-version: 18
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - uses: aws-actions/configure-aws-credentials@v2
        with:
          aws-region: ${{ secrets.AWS_REGION }}
          role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT }}:role/gh-actions
          role-session-name: JohnWww_${{ github.run_id }}_${{ github.run_attempt }}
      - run: pnpm sst deploy --stage 'prod'

Note that I use the secure OIDC authentication to my AWS account. I have already told IAM that my website’s repository is allowed to deploy to my account so I don’t need to give an access key.

Custom Font

I really wanted a location symbol for my header, but it isn’t in your average fonts. I knew I needed a nerd font but there wasn’t a simple npm package or CDN file I could simply import. Instead, I needed to figure out how to import my font in to my website manually.

First, I added the font .ttf file in public/fonts.

Then I updated my tailwind.config.cjs with

const defaultTheme = require("tailwindcss/defaultTheme");
module.exports = {
  ...
  theme: {
    fontFamily: {
      sans: ['"Arimo Nerd Font"', ...defaultTheme.fontFamily.sans],
    },
  },
  ...
};

Finally, I added a global style sheet with the following

@tailwind base;
@tailwind components;
@tailwind utilities;

@layer base {
  @font-face {
    font-family: "Arimo Nerd Font";
    src: url(/fonts/ArimoNerdFont-Regular.ttf);
  }
}

Linting

I used a community maintained Astro plugin to add ESLint to my project. However, I found there were a few tweaks I had to make they didn’t mention in their guide.

Starting with their default .eslintrc.js file, I had to make some changes:

  1. Update the extension to .cjs since my project is of type “module”
  2. pnpm add -D @typescript-eslint/eslint-plugin
  3. Update the config to add:
module.exports = {
  ...
  rules: {
    "semi": ["error", "always"],
  },
  overrides: [
    ...
    {
      files: ['*.ts'],
      parser: '@typescript-eslint/parser',
      extends: ['plugin:@typescript-eslint/recommended'],
      rules: {
        '@typescript-eslint/no-unused-vars': [
          'error',
          { argsIgnorePattern: '^_', destructuredArrayIgnorePattern: '^_' },
        ],
        '@typescript-eslint/no-non-null-assertion': 'off',
      },
    },
  ],
}

I also added a global rule on top of the recommended to enforce semicolons. It annoys me to no end that Javascript lets you omit semicolons (maybe that’s the C# programmer in me).