ADAM DJ BRETT

Home / Blog / Two Domains, One Build: Hosting .ngo and .ong Together with Eleventy and xmit

There is a very particular kind of joy that comes from solving a web problem whose final answer is smaller than the problem felt.

I wanted the American Indian Law Alliance website to live at both aila.ngo and aila.ong. The pairing matters: .ngo is the primary address, while .ong is its parallel form. I did not want the second domain to bounce visitors to the first. If somebody requests /give/, a blog post, an image, a PDF, or any other site resource from either hostname, that hostname should serve the resource itself.

This sounds ordinary until the requirements are stated precisely. It is not a redirect rule. It is not an alias that only works for the homepage. It is the entire generated site—hundreds of pages and a large document archive—available under two fully qualified domains.

When I finally had it working, I felt euphoric. Not because the solution was elaborate, but because it was not.

One canonical build #

The site is built with Eleventy. Eleventy writes the finished static site to dist, so there is no application server and no runtime routing layer to duplicate. The first important decision was to build only once.

I set the production URL to the primary domain:

- name: Build Eleventy site
  run: npm run build
  env:
    URL: https://aila.ngo

That keeps canonical links, Open Graph URLs, feeds, and the sitemap anchored to aila.ngo. Search engines get one authoritative address even though human beings can use either domain. The parallel domain serves the same HTML, but the metadata continues to identify the primary.

This distinction cleared up much of my confusion: “available at two domains” does not have to mean “build two versions.”

Let xmit launch the same bundle twice #

xmit is unusually well suited to this because it treats a static deployment as a content-addressed bundle. It hashes the files and uploads only content it does not already have. My site includes a substantial collection of PDFs, images, fonts, Pagefind indexes, and ordinary HTML. Uploading the same unchanged bytes twice would be absurd. xmit recognizes that they are the same bytes.

The domain-to-team syntax is also wonderfully small. Team 389 is written after the hostname:

xmit "aila.ngo@389" dist
xmit "aila.ong@389" dist

That is the heart of the solution. Both commands launch the complete dist directory. Neither command contains a path list because none is needed. /give/ was my first test, but it is not a special case. Every post, page, image, stylesheet, font, feed, search index, and document goes along with it.

The production workflow adds retries because an 800-plus-megabyte site should not fail permanently over one transient network error:

env:
  XMIT_PRIMARY_SITE: aila.ngo
  XMIT_PARALLEL_SITE: aila.ong

steps:
  - name: Deploy to xmit
    env:
      XMIT_KEY: ${{ secrets.XMIT_KEY }}
    run: |
      for site in "$XMIT_PRIMARY_SITE" "$XMIT_PARALLEL_SITE"; do
        deployed=false
        for attempt in 1 2 3; do
          if ./node_modules/.bin/xmit "$site@389" dist; then
            deployed=true
            break
          fi
          sleep $((attempt * 15))
        done
        [ "$deployed" = true ] || exit 1
      done

The DNS records for both domains point to xmit, and xmit serves each requested hostname directly. A request to https://aila.ong/give/ stays on aila.ong; it is not sent to aila.ngo. Yet both responses come from one Eleventy build.

The speed came from doing less #

The other satisfying part was making repeat deployments faster without inventing a new build system. Eleventy’s image pipeline already maintains a cache, so GitHub Actions only needed to preserve it:

- uses: actions/cache@v6
  with:
    path: .cache/@11ty/img
    key: eleventy-images-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
    restore-keys: eleventy-images-${{ runner.os }}-

That cache is about 53 MB. Restoring it avoids regenerating responsive images on every clean runner. Pagefind takes less than a second; it was never the bottleneck. Dependency installation was already cached by setup-node. xmit already avoids resending unchanged content. The fast solution was to let each tool do the job it already knew how to do.

There are more sophisticated architectures for multi-domain publishing. I do not need one. I need a single canonical Eleventy build, two real hostnames, one xmit team, and a workflow boring enough that I will understand it six months from now.

That is what produced the joy: not merely seeing both domains resolve, but realizing that the correct model had been sitting in front of me all along. Build the site once. Name the primary. Launch the same artifact wherever it belongs.

Tags : 11ty xmit static-sites web-development domains

Webmentions

No webmentions yet.

Previous

Writing Support Resources

Free supports for academic writing: weekly writing communities, the Academic Phrasebank, citation alerts, and tools your university library already provides.