Building This Site: Astro, Amplify, and a Few Gotchas
A retrospective on setting up jcamping.com — the stack, the deploy pipeline, and the small things that tripped me up along the way.
Every developer eventually builds their own personal site, and I figured it was my turn. This post is a quick writeup of how jcamping.com came together — the stack, the deploy pipeline, and a few things that tripped me up that might save someone else some time.
The Stack
Nothing exotic here:
- Astro for the static site framework. Markdown-first, minimal JavaScript shipped to the browser, and it gets out of your way. Adding a blog post is just dropping a
.mdfile insrc/pages/blog/. - GitHub for source control.
- AWS Amplify Hosting for the actual hosting, build pipeline, and CDN.
- Route 53 for DNS.
The decision between Astro and Hugo was pretty close, but Astro won out for the nicer component model and the ability to drop in React later if I ever want interactivity on a page.
Why Amplify Over S3 + CloudFront
I considered the classic S3 + CloudFront + ACM + GitHub Actions combo. It’s the more “AWS-native” approach and gives you fine-grained control over everything. But for a personal site, it’s a lot of moving parts to wire up: bucket policies, OAI or OAC, a CloudFront distribution, an ACM cert in us-east-1, a deploy workflow that syncs to S3 and invalidates the cache…
Amplify collapses all of that into “connect your repo and click deploy.” It auto-detects Astro, configures the build, provisions an SSL cert, and redeploys on every push to main. The free tier is generous enough that this site will probably cost me pennies per month.
I briefly considered AWS SAM too, but that’s designed for serverless application backends (Lambda, API Gateway, DynamoDB) — not static sites. It would have meant writing CloudFormation YAML to do what Amplify does for free. Maybe later if I add a contact form or dynamic piece.
The Deploy Pipeline
The workflow is about as simple as it gets:
# write a new post
vim src/pages/blog/new-thing.md
# push it
git add .
git commit -m "new post: the new thing"
git push
Amplify picks up the push, runs npm ci && npm run build, and deploys the dist/ folder. Takes about a minute end to end. Here’s the amplify.yml it auto-generated:
version: 1
frontend:
phases:
preBuild:
commands:
- npm install
build:
commands:
- npm run build
artifacts:
baseDirectory: dist
files:
- '**/*'
cache:
paths:
- node_modules/**/*
Clean and obvious. I left it alone.
Gotchas Worth Flagging
A few things came up during setup that aren’t obvious from the docs:
Commit your package-lock.json
At first I wondered if committing the lock file would prevent my dependencies from ever updating. It doesn’t — it just means Amplify builds with the same exact versions you tested locally. When you want to update, you do it intentionally:
npm update # upgrade within semver ranges
npm install astro@latest # upgrade to latest of a specific package
…then commit the updated lock file. Without a lock file, Amplify could silently pull in a breaking version at the worst possible moment.
Add a .gitignore before your first push
Easy to forget until you notice your repo is 200 MB. Node projects need this at minimum:
# dependencies
node_modules/
# build output
dist/
.astro/
# environment
.env
.env.local
.env.*.local
# editor / OS
.vscode/
.idea/
.DS_Store
If you’ve already committed node_modules/, remove it from tracking with:
git rm -r --cached node_modules
git commit -m "remove node_modules from tracking"
Amplify needs an IAM role to write to Route 53
This one cost me some time. When Amplify tries to set up your custom domain, it needs to create DNS records in your Route 53 hosted zone — specifically, ACM validation CNAMEs and the A/CNAME records pointing to its CloudFront distribution. But it can’t do that without explicit permission.
The error message mentions a role name like AWSAmplifyDomainRole-Z02551591J3062UOA73KG (the suffix is your hosted zone ID). You have to create this role yourself:
aws iam create-role \
--role-name "AWSAmplifyDomainRole-<ZONE_ID>" \
--assume-role-policy-document '{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {"Service": "amplify.amazonaws.com"},
"Action": "sts:AssumeRole"
}]
}'
aws iam attach-role-policy \
--role-name "AWSAmplifyDomainRole-<ZONE_ID>" \
--policy-arn "arn:aws:iam::aws:policy/AmazonRoute53FullAccess"
AmazonRoute53FullAccess is broader than strictly necessary — you could scope it down to route53:ChangeResourceRecordSets on just your hosted zone — but for a personal account it’s fine.
Clean up old DNS records before adding a custom domain
If your domain has been used before, stale records will cause the Amplify SSL activation to fail with something like:
One or more of the CNAMEs you provided are already associated with a different resource.
The culprits are typically:
- Old
AorCNAMErecords on the root domain andwwwpointing to a previous host - Leftover ACM validation CNAMEs like
_abc123.yourdomain.comfrom previous cert attempts CAArecords that restrict issuers to a CA other than Amazon
In my case, I had an old CloudFront distribution still wired up (from a previous attempt), plus a few orphaned _xxx.jcamping.com ACM validation CNAMEs. Once I deleted those, the activation went through clean.
Records you want to keep during this cleanup: NS, SOA, and anything related to email (MX, DKIM, SES verification TXT records). Don’t nuke the whole zone.
The apex domain doesn’t use CNAME
Standard DNS doesn’t allow CNAMEs at the zone apex (the root jcamping.com with no subdomain). Route 53 handles this with ALIAS records, which look like A records but resolve dynamically to AWS targets. Amplify creates these for you automatically — you don’t need to configure it manually. If you’re coming from a registrar that doesn’t support ALIAS/ANAME, the workaround is to use www as the primary and redirect the apex.
What I’d Do Differently
Honestly, not much. The only real friction was cleaning up the old DNS records and figuring out the IAM role requirement — both of which are one-time setup costs. The day-to-day workflow of “write Markdown, push to GitHub, done” is exactly what I wanted.
If I ever need dynamic features — a contact form that emails me, a newsletter signup, or comments — I’ll reach for SAM to add a Lambda or two behind API Gateway, and keep the static site on Amplify. That combo gives you nearly unlimited flexibility while staying cheap.
The Meta Bit
This post — and the site it lives on — was built with Claude. Not “Claude wrote a template and I tweaked it,” but actual back-and-forth: describe what I want, see a preview, ask for changes, iterate. The Astro project, the dark theme, the blog layout, the sample posts, all generated from conversation.
That said, the experience wasn’t entirely hands-off, and I think it’s worth being honest about where the AI made things fast and where human judgment still mattered.
Where Claude sped things up
The initial build was the big win. I described the stack I wanted (static site framework, dark theme, resume up top, blog posts below), uploaded a screenshot of my LinkedIn so it could populate real experience, and got a working project in a few minutes. Hand-writing the Astro config, the layouts, the CSS tokens, the sample posts — that would have been an evening of work. It was maybe ten minutes of conversation.
The iteration was also frictionless. “Remove the skill tags, simplify the experience section, drop dates from the homepage post cards” — one message, done. The equivalent of opening six files and making the edits manually, without the tab-switching.
Where I had to steer
A few things needed real input from me, and I think they’d need input from anyone doing this:
Knowing what to ask for. Claude asked me up front about framework preference, visual style, and homepage layout — but I had to have an opinion. “Dark theme, minimal, resume first” is a real design direction. “Make it look professional” would have given me generic output. The quality of the starting point depended on me having some idea of what I wanted.
Knowing when it was wrong. Early on, the tarball came out with a stray {src folder — a bash brace-expansion bug from the initial mkdir -p command. I only caught it because I actually looked at what was in the archive. If you blindly trust output without sanity-checking file trees, build output, or deploy logs, you’ll ship the bugs.
Domain and AWS knowledge. The SSL activation failure on Amplify was the most involved troubleshooting step, and it needed actual understanding to resolve. Claude correctly identified the likely causes (stale ACM validation CNAMEs, old CloudFront A-records, CAA issues), but I had to read my own Route 53 zone and decide which records to delete without wiping out my email setup. An LLM can’t tell you with certainty whether the _amazonses.jcamping.com TXT record is important to you — you have to know that SES was set up for a reason and keep it.
The IAM role for Route 53. Amplify threw an error about a missing role. Claude helped me write the aws iam create-role commands, but understanding why the role was needed (Amplify assuming a role to write DNS records in my hosted zone on my behalf) required knowing how AWS cross-service permissions work. The LLM accelerated the fix; it didn’t replace the mental model.
The pattern I settled into
A loop that worked well: let Claude scaffold, preview the output, deploy to a real environment, surface the errors that come up, share them back verbatim. The real AWS error messages were far more useful than asking hypothetically “what could go wrong?” Each gotcha in the list above came from an actual failure I hit and pasted into chat.
The last thing worth saying: at a certain point, chat iteration becomes clunky compared to editing files in your IDE and pushing. Claude is great for the initial build and for troubleshooting specific problems. Day-to-day updates — writing new posts, tweaking copy, fixing a typo — those live better in a normal git workflow. If I need AI help in that phase, Claude Code in the terminal is the better fit.
Feels like the right kind of recursive to write this post on the site the post is about.