I wanted a simple place to document my side projects, explain why I built them, and keep notes on the technical trade-offs. I did not want to maintain a database or deal with a heavy CMS. This site is just a static SvelteKit 5 app that rebuilds on every git push and serves from m-r-t.xyz.
Almost the entire setup was done straight from the terminal. We created the GitHub repository, provisioned the Cloudflare DNS zone, pushed the DNS records, and enabled HTTPS certificates without clicking around web dashboards.
1. Static Prerendering with SvelteKit 5
The site runs on Svelte 5 and SvelteKit using @sveltejs/adapter-static. Because +layout.ts exports prerender = true and trailingSlash = "always", Vite compiles the whole app into static HTML, CSS, and small client chunks inside build/.
When the site first went up on the default github.io project URL (/mrt-blog), it needed a subpath prefix. Once the custom domain (m-r-t.xyz) went live, it needed to serve from the root path (/). Reading BASE_PATH from the build environment let us switch between the two without touching any route links:
import adapter from '@sveltejs/adapter-static';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
const config = {
preprocess: vitePreprocess(),
kit: {
adapter: adapter({
pages: 'build',
assets: 'build',
fallback: '404.html',
strict: true
}),
paths: {
base: process.argv.includes('dev') ? '' : (process.env.BASE_PATH ?? '')
}
}
};
export default config;2. Content as Code
I wanted to keep project cards and write-ups right next to the code. Instead of hooking up an external CMS, all projects and posts live in src/lib/data/content.ts as typed TypeScript objects.
Adding a new project write-up takes three quick steps:
- Project card: Add an entry to the
projectsarray insrc/lib/data/content.tswith its GitHub repo URL, status, and stack tags. - Post content: Add a post entry to the
postsarray with anoverviewcard, sections, code snippets, andteamcredits. - Build-time discovery: SvelteKit reads every post slug via an
entries()generator and prerenders/posts/[slug]/index.htmlduringnpm run build.
3. CI/CD with GitHub CLI & GitHub Actions
We initialized the repository and turned on GitHub Pages from the terminal using the GitHub CLI (gh repo create and gh api /repos/:owner/:repo/pages -f build_type=workflow).
On every push to main, .github/workflows/deploy.yml installs dependencies, checks actions/configure-pages@v5 for the active base_path, runs the build, and deploys the static artifact:
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- name: Configure GitHub Pages
id: pages
uses: actions/configure-pages@v5
- name: Build static SvelteKit site
env:
BASE_PATH: ${{ steps.pages.outputs.base_path }}
run: npm run build
- uses: actions/upload-pages-artifact@v3
with:
path: 'build'
- uses: actions/deploy-pages@v44. Custom Domain Setup: Namecheap to Cloudflare to GitHub Pages
I registered m-r-t.xyz on Namecheap, but I prefer keeping DNS management on Cloudflare so all of my domains are in one place and programmable from the CLI.
Each layer in the chain handles one job:
- Registrar (Namecheap): Holds the domain registration and points custom nameservers to Cloudflare.
- DNS Provider (Cloudflare): Hosts the DNS zone and serves
A,AAAA, andCNAMErecords pointing to GitHub Pages. - Origin & TLS (GitHub Pages / Fastly CDN): Serves the static build based on
static/CNAMEand provisions a Let's Encrypt TLS certificate.
Deep Dive: Bypassing the 60-day ICANN transfer lock
Newly registered domains have a mandatory 60-day ICANN lock before you can transfer the registrar itself to Cloudflare Registrar.
You do not actually need to wait 60 days to use Cloudflare DNS. By leaving Namecheap as the registrar of record and updating the domain's custom nameservers to the pair assigned by Cloudflare, 100% of DNS management moves over to Cloudflare's API right away.
5. Automating Cloudflare DNS via the REST API
Instead of manually entering nine DNS records in a dashboard, we put a scoped Cloudflare API token in a local, gitignored .env file (with Zone:Edit and DNS:Edit permissions) and scripted the zone setup with curl.
We also set proxied: false (DNS-only mode) on every record. That lets GitHub Pages verify domain ownership directly against its Fastly edge IPs and issue its Let's Encrypt certificate without running into Cloudflare SSL proxy conflicts:
# 1. Create the zone in Cloudflare
curl -s -X POST "https://api.cloudflare.com/client/v4/zones" \
-H "Authorization: Bearer $CF_API_TOKEN" \
-H "Content-Type: application/json" \
--data '{"name":"m-r-t.xyz","type":"full"}'
# 2. Point apex A & AAAA records + www CNAME to GitHub Pages
for ip in 185.199.108.153 185.199.109.153 185.199.110.153 185.199.111.153; do
curl -s -X POST "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records" \
-H "Authorization: Bearer $CF_API_TOKEN" \
-H "Content-Type: application/json" \
--data "{\"type\":\"A\",\"name\":\"m-r-t.xyz\",\"content\":\"$ip\",\"proxied\":false}"
done
curl -s -X POST "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records" \
-H "Authorization: Bearer $CF_API_TOKEN" \
-H "Content-Type: application/json" \
--data '{"type":"CNAME","name":"www","content":"mrtoronto.github.io","proxied":false}'6. Verifying Propagation & Enforcing HTTPS
After updating the nameservers on Namecheap, we just had to wait about 30 minutes for the .xyz TLD registry (CentralNic) to publish the NS delegation.
Once the A records resolved publicly, we ran one GitHub API call to trigger certificate verification and enforce HTTPS redirects across the site:
# Set custom domain and enforce HTTPS via GitHub CLI
gh api --method PUT /repos/mrtoronto/mrt-blog/pages \
-f cname="m-r-t.xyz" \
-F https_enforced=true \
-f build_type="workflow"Deep Dive: Why dig @a.nic.xyz NS +short looked empty during propagation
While polling the .xyz TLD server directly with dig @a.nic.xyz m-r-t.xyz NS +short, the output stayed completely blank even after delegation was live.
That happens because TLD referral servers return delegated NS records in the AUTHORITY section of the DNS packet rather than the ANSWER section, and +short only prints the ANSWER section. Dropping +short (or querying a recursive resolver like dig @1.1.1.1 m-r-t.xyz NS +short) confirmed the Cloudflare nameservers were already active.