Need to leave this site fast? Press Esc twice, or tap Quick exit. It opens a weather page in place of this one. It does not erase your history; here is how. Quick exit

Guide · October 4, 2026 · Dr. Safi Mojidi

Implementation plan: moving a nonprofit website to files you own

The technical plan we followed to replace a hosted website builder with a static site on AWS, published by GitHub Actions. De-identified, with the four mistakes that cost us the most time so you can skip them.

This is the plan we actually executed, with our account identifiers removed. It assumes a nonprofit with a domain name it controls, a free GitHub account, and an AWS account with a credit card on file (the bill at small-nonprofit traffic is cents per month). The same shape works on other clouds; the pieces are generic.

The target architecture

Five pieces, each doing one job.

  • A Git repository holds the source: a build/ folder with the page sources, a redirects.json map of old URLs to new ones, an infrastructure template, and two automation workflows. The generated site is not committed; it is rebuilt on every change.
  • A build script (a few hundred lines of Python) turns the sources into finished HTML. It writes the shared header, footer, navigation, and calls-to-action once, inlines the stylesheet, resizes photos, writes the sitemap, and refuses to publish any page that still contains placeholder text.
  • Object storage (an S3 bucket) holds the finished files. The bucket is private; only the content delivery network can read it.
  • A content delivery network (a CloudFront distribution) serves the files over HTTPS with a free certificate, rewrites clean URLs like /donate/ to the right file, and answers the old URLs with permanent redirects so nothing you ever shared goes dead.
  • A publishing job (GitHub Actions) runs on every push to the main branch: build, check, upload, refresh the cache, and smoke-test three pages. It signs in to AWS with a short-lived identity token, so there are no long-lived access keys anywhere.

Phase 1: the content (half a day)

  1. Inventory the old site. List every page URL. You will need this twice: once to decide what to keep, once to build the redirect map. Export any text you cannot easily recreate.
  2. Decide the page list and the navigation. We went from a sprawl of pages to thirty-seven with a six-item navigation. Each page got a title, a one-sentence description, and a reason to exist.
  3. Write the pages as sources, not as finished HTML. Some of ours are wireframe exports; most are short Python calls that produce a hero, a card grid, and a call to action from text. The point is that a volunteer edits sentences, never layout.
  4. Mark every unknown with a visible placeholder in a fixed format, and have the build log them to a file. Ours is a bracketed tag that starts with the word SWAP and says what belongs there. The build replaces each with honest interim copy (“Pricing announced soon”) and writes the original intent to NEEDS-INPUT.md. The checker refuses to publish any page where a raw placeholder survived.
  5. Build the redirect map. For every old URL, pick the new page it should land on. Ours has fifty-seven entries. This is the single most valuable file for anyone who has ever put a link in a grant report.

Phase 2: the infrastructure (two hours, once)

  1. Write the infrastructure as a template, not as clicks in a console. Ours is one CloudFormation file that creates the bucket, the distribution, the certificate, the URL-rewrite function, and the deploy role. Keep it in the repository. A small script regenerates the template from redirects.json, so adding a redirect never means editing infrastructure by hand.
  2. Create the stack in us-east-1 (certificates used by the CDN must live there) with the staging hostname, for example next.yourdomain.org. While it creates, the certificate service shows a validation record to add at your DNS provider. Add it; the stack waits until it resolves.
  3. Point the staging hostname at the distribution with a CNAME record.
  4. Connect GitHub to AWS by storing four values as repository variables: the deploy role’s identifier, the bucket name, the distribution identifier, and the site URL. The workflow reads them. Nothing secret is stored.
  5. Push. The first run publishes the site to the staging hostname.

Phase 3: cutover (one hour, when ready)

  1. Update the stack so the primary hostname is your apex domain and the additional hostname is www. A new certificate is issued; add its two validation records.
  2. Change the site URL constant in the build (it drives canonical links and social previews) and the SITE_URL variable, and push.
  3. At the DNS provider, point the apex at the distribution (an ALIAS or ANAME record, or your provider’s equivalent) and www as a CNAME. The distribution redirects www to the apex.
  4. Leave every mail record alone: MX, SPF, DKIM, DMARC. Your mailboxes do not know or care where the website lives.
  5. Do not renew the old platform. Keep the domain registration; it is separate from the website plan and usually cheap.

The four mistakes that cost us the most time

The identity provider already existed. If your AWS account has ever connected to GitHub before, the OpenID Connect provider is already there and a template that tries to create it again fails and rolls back. Make it a parameter and default it to “no”.

Certificate validation options without a hosted zone. If your DNS is not in Route 53, the certificate resource must omit its validation-options block entirely. Including it with an empty zone fails with an unhelpful message about a null domain.

GitHub changed the shape of its identity token. The subject claim used to read repo:Owner/Repo:ref:refs/heads/main. It now stamps numeric identifiers into it: repo:Owner@12345/Repo@67890:ref:refs/heads/main. A trust policy that matches only the old pattern is refused with “not authorized to perform sts:AssumeRoleWithWebIdentity”. Match both patterns with a wildcard condition. Check the sign-in audit log for the exact subject your run presented; it tells you in one line.

Waiting on a cache refresh you are not allowed to read. Our deploy role could create a cache invalidation but not read its status, so the “wait until done” step failed after the site was already live. Either grant the read permission or do not wait; the refresh completes in about a minute regardless.

Operating it afterwards

  • A text change is an edit on the code host’s website and a three-minute wait. No tools.
  • A new photo is a file dropped into a folder plus one line naming it.
  • A new redirect is one line in redirects.json.
  • Undo is one command, because every publish is a commit.
  • Review the old-URL traffic for a month after cutover and add any redirect you missed.

What this is not

It is not a content management system with roles, drafts, and a visual editor. If your organization has many non-technical editors publishing daily, you may still want one. For a small nonprofit with a few pages that change a few times a month, this is less work, far cheaper, and entirely yours.

The full source is in our repository, and the student project brief in this section turns the plan into a semester-long service-learning project.

guideawsgithubstatic-sitedns

More from Sustainable Security

Weekly email

Get this in your inbox once a week.

One email, every week: what we built, what it cost, what broke, and what we learned running a small nonprofit on technology we own. No tracking pixels. Unsubscribe in one click.

Get the weekly email

This form opens here shortly. Until then, email us and a person will answer: Email hello@hackingtheworkforce.org

How you can help

Keep the guides coming.

Every guide here is free because someone funded the hours to write it.

Our team's experience spans

NASA
U.S. Air Force
U.S. Space Force
Slack
Salesforce
Fairfax County Public Schools
Pluralsight
TransTech Social Enterprises

Where our work has taken us