About this portfolio

SEO

100 words

  • Unity
  • Multiplayer
  • LiveOps
  • C#

Leonardo Lycan is a Lead Game Developer specializing in Unity game development, multiplayer architecture, gameplay systems, LiveOps, and technical production. This portfolio brings together published projects, playable prototypes, product studies, and articles on game engineering, applied AI, Web3, onboarding, and digital experiences. The work combines team leadership, C# programming, service integration, performance optimization, and clear communication across design, art, and business. For projects, consulting, or collaboration, Leonardo takes a practical approach: understand the problem, build a robust solution, and turn complex requirements into memorable experiences for players and digital products with measurable, lasting impact for teams, audiences, and product goals.

Back

Engineering · August 26, 2026

From main to the domain: Astro, GitHub Actions and Cloudflare Pages

How I built one publication flow for an Astro portfolio: a validated GitHub Actions build, Cloudflare Pages deployment, a custom domain and language selection without a visible redirect.

From main to the domain: Astro, GitHub Actions and Cloudflare Pages

Publishing a portfolio sounds simple until the task includes everything at once: local code, a remote repository, a reproducible build, credentials, a custom domain, DNS, HTTPS and two languages. The risk was not running a deployment command. It was building a system in which the next release would be predictable and would not require repeating manual decisions.

This article records the architecture I used for my own portfolio. The goal was to reach one simple rule:

every approved change enters main; automation validates the project and publishes one version; domains and languages are different ways to reach that same release.

This is not a universal recipe. It is an implementation case with explicit boundaries, including what remains a next step.

The final shape

push to main
  → GitHub Actions
  → npm ci
  → build + release validation
  → Wrangler uploads dist/ to Cloudflare Pages
  → Cloudflare serves the custom domain

The project remains a static Astro site. I did not have to convert the whole application to SSR to gain continuous deployment or run one small decision at the edge. The build generates dist/, and Wrangler publishes that directory to the existing Cloudflare Pages project. This route is also covered by the official guides for deploying Astro to Cloudflare and Direct Upload with Wrangler.

One production source

The first principle was to avoid two competing processes. GitHub does not host one copy while Cloudflare hosts another. GitHub stores the code and runs automation; Cloudflare Pages receives the production artifact.

The workflow runs only on a push to main or through workflow_dispatch:

on:
  push:
    branches: [main]
  workflow_dispatch:

concurrency:
  group: cloudflare-pages-production
  cancel-in-progress: true

The branch filter makes main the publication contract. Concurrency prevents two production deployments from racing toward the same destination. GitHub documents both push branch filters and the use of environments and concurrency for deployments.

Validate before uploading

Automation does not begin with the upload. It reproduces the installation and validates what will be published:

- name: Install dependencies
  run: npm ci

- name: Build and validate release
  run: npm run release:check

- name: Deploy to Cloudflare Pages
  uses: cloudflare/wrangler-action@v4
  with:
    apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
    accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
    command: pages deploy dist --project-name=portfolio

In this project, release:check combines the Astro build with output validation. That second step checks the bilingual inventory, metadata, internal links, assets and the absence of editorial routes from the public artifact. Deployment starts only when the command succeeds.

The token and account identifier do not live in YAML. They are read from GitHub Actions Secrets. Besides keeping credentials out of version control, this separation makes it possible to restrict the token to the permissions the deployment actually requires. GitHub documents how to create and use secrets in workflows, and its security guidance reinforces the principle of least privilege.

A custom domain is a DNS change, not a build change

After the pages.dev address responded correctly, I connected the primary domain to the same Pages project. That order separated two diagnostic questions:

  1. is the artifact correct and reachable on Cloudflare?
  2. is the domain delegated and associated correctly?

For an apex domain such as example.com, Cloudflare requires the zone to exist in the same account as the Pages project and its nameservers to point to Cloudflare. The correct flow also includes adding the hostname under Custom domains; manually creating a CNAME is not a substitute for that association. The sequence is documented in the Cloudflare Pages guide to custom domains.

There was another operational constraint: migrate the website without deleting records used by other services. Before changing DNS authority, I reviewed and preserved the existing email and verification records. A working website with broken email is not a successful deployment.

One release, multiple domains

Additional domains do not need additional builds. The intended architecture is:

  • one canonical domain for indexing and sharing;
  • secondary domains as entry points;
  • permanent redirects from secondary domains to the canonical one;
  • one origin, one operational certificate path and one content pipeline.

At the time of this record, the primary domain is active. The .com.br domain remains a next step because it is still under another DNS authority. It is not presented here as completed. Publishing it requires adding the zone, preserving its records, changing nameservers at the registrar and only then enabling the redirect to the canonical domain.

The hidden loading state on first access

The most visible problem appeared after the domain went live. The / route was a static Astro redirect to /en-us/. On a fast connection that could look acceptable. During a real Safari check, however, the browser displayed the intermediate “Redirecting from / to /en-us/” page before the site loaded.

The requirement changed: detect the language preference without changing the initial URL and without making the browser perform a second navigation.

The solution kept the already generated localized pages and added one Pages Function at the root:

GET /
  → read Accept-Language
  → pt: internally serve /pt-br/artigos/
  → en or fallback: internally serve /en-us/insights/
  → return 200 while keeping the URL at /

In Cloudflare Pages, functions/index.js maps to the root route. The Function can request a static asset from the same project through env.ASSETS.fetch(). There is no Location header and no new browser navigation. The response also declares Content-Language and Vary: Accept-Language.

This approach uses capabilities documented in the official Pages guides for Function routing, env.ASSETS.fetch() and Accept-Language localization.

Explicit /pt-br/... and /en-us/... routes still exist. They remain useful for links, sharing, alternates and manual language selection. What disappears is the mandatory jump on the first visit to the domain.

Cost and scale

For this type of website, purely static requests on Pages do not consume the Functions quota. The / route does because it invokes a Function, so it counts against the Workers quota. Current Cloudflare documentation states that the free plan shares a daily allowance between Workers and Pages Functions; cost should therefore be reviewed if traffic grows or more dynamic routes are added. The current details live in Pages Functions pricing.

On GitHub, cost depends on repository visibility, the selected runner and the account plan. I did not treat “free CI” as a permanent guarantee; the reliable reference is the current GitHub Actions billing documentation.

The deployment completion checklist

The work is complete only when every layer responds:

  • npm ci reproduces installation;
  • the build uses the Node version declared by the project;
  • release:check succeeds;
  • the main workflow completes successfully;
  • the pages.dev address responds;
  • the canonical domain responds over HTTPS;
  • email and verification DNS records remain present;
  • / returns 200 without a Location header;
  • Accept-Language: pt-BR serves Portuguese;
  • Accept-Language: en-US serves English;
  • no credential appears in the repository or logs.

The main lesson

Deployment automation is more than “publish on merge.” It makes the contracts among code, build, credentials, infrastructure and the entry experience explicit.

The design became simpler when every system received one responsibility:

  • Git stores history;
  • main defines what may enter production;
  • GitHub Actions reproduces and validates;
  • Wrangler transports the artifact;
  • Cloudflare Pages serves the site;
  • DNS connects names without duplicating applications;
  • the root Function resolves only the initial language choice.

This separation prevents every new domain, language or release from becoming a parallel project. The desired result remains one system: change, validate and publish with evidence.