Publishing a Separate Astro Site on a Subdomain

I wanted to keep Roblox Studio development notes slightly separate from the current nosubject.io site.

Buying a new domain is one option, but starting with a subdomain such as create.nosubject.io is simpler. It lets the new site keep its own structure while still staying under the existing domain.

The main site currently uses Hugo, but I want to try Astro for the subdomain. Astro works well with Markdown and MDX, and it also makes it easy to build pages with components, so it should be a good fit for Roblox Studio screenshots and development logs.

This article uses create.nosubject.io as an example and walks through creating a separate Astro site, then connecting DNS, Nginx, HTTPS, and the GitHub Actions build workflow.

Environment

This article assumes the following environment:

  • The existing site is https://nosubject.io/
  • The new site is https://create.nosubject.io/
  • The subdomain site uses Astro
  • The production server is Nginx on a Raspberry Pi 5
  • Nginx runs through Docker Compose
  • Astro is built by a GitHub Actions self-hosted runner
  • DNS can be changed from Onamae.com Navi

Astro’s official documentation currently lists Node.js v22.12.0 or higher as a prerequisite. Check the runner on the Raspberry Pi 5 with:

node --version
npm --version

If the result looks like this, Node.js and npm are not installed yet:

-bash: node: command not found
-bash: npm: command not found

On Windows, this message means Node.js is either not installed or not available through PATH:

'node' is not recognized as an internal or external command,
operable program or batch file.
'npm' is not recognized as an internal or external command,
operable program or batch file.

Writing posts and previewing the site locally are assumed to happen on Windows 11. Nginx, HTTPS certificates, and publish directories are assumed to be handled on the Raspberry Pi 5.

Install Node.js

Astro needs Node.js both on the local Windows machine and on the Raspberry Pi 5 where the GitHub Actions self-hosted runner runs.

Astro currently requires Node.js v22.12.0 or higher, so installing the Node.js 24 LTS line is a straightforward choice.

Install On Windows 11

If winget is available on Windows 11, install Node.js LTS with:

winget install --id OpenJS.NodeJS.LTS --exact

After installing it, close and reopen Command Prompt or PowerShell. Existing terminals may not see the updated PATH immediately.

Then check the versions:

node --version
npm --version

When running npm --version in PowerShell, you may see an error saying that npm.ps1 cannot be loaded:

File C:\Program Files\nodejs\npm.ps1 cannot be loaded because running scripts is disabled on this system.
FullyQualifiedErrorId : UnauthorizedAccess

This can happen even when npm --version works in Command Prompt. It does not mean that Node.js failed to install. PowerShell is trying to load npm.ps1, and its execution policy is blocking .ps1 files.

If you only want to check npm immediately, run npm --version in Command Prompt or call npm.cmd from PowerShell:

npm.cmd --version

To use the normal npm command from PowerShell, set RemoteSigned for the current user:

Get-ExecutionPolicy -List
Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned
Get-ExecutionPolicy -List

Microsoft describes execution policy as a safety feature that controls the conditions under which PowerShell runs scripts. The CurrentUser scope affects only the current user. RemoteSigned allows locally written scripts to run without a signature, while requiring downloaded scripts to be signed.

After changing the setting, reopen PowerShell and check:

npm --version

In this environment, CurrentUser was initially Undefined.

CurrentUser       Undefined

After running Set-ExecutionPolicy, CurrentUser changed to RemoteSigned.

CurrentUser    RemoteSigned

After that, the normal npm --version command worked in PowerShell.

11.16.0

If winget is not available, download the LTS Windows Installer from the official Node.js website.

https://nodejs.org/

Install On Raspberry Pi 5

On the Raspberry Pi 5, install Node.js system-wide so the GitHub Actions self-hosted runner can use it easily.

When using the NodeSource Debian/Ubuntu repository, run:

sudo apt update
sudo apt install -y curl ca-certificates
curl -fsSL https://deb.nodesource.com/setup_24.x -o nodesource_setup.sh
sudo -E bash nodesource_setup.sh
sudo apt install -y nodejs

After installation, check the versions:

node --version
npm --version

On this environment, the Raspberry Pi 5 showed:

v24.18.0
11.16.0

Check the Windows 11 side in the same way:

node --version
npm --version

On this environment, Windows also showed:

v24.18.0
11.16.0

If the Node.js version is shown as v24.x.x, it satisfies Astro’s requirement.

Limitations

This article assumes that you already manage nosubject.io yourself and can edit the Nginx configuration on the server.

If you use shared hosting, GitHub Pages, Cloudflare Pages, Netlify, Vercel, or another managed hosting service, the DNS and HTTPS screens will be different. The overall idea is the same, but the Nginx server block and Docker Compose examples cannot be used as-is.

DNS propagation time, router port forwarding, IPv4 and IPv6 handling, and Let’s Encrypt certificate issuance can also vary by environment. This article focuses on how the pieces fit together when publishing a new Astro site on a subdomain, rather than covering every provider-specific screen.

Goal

The goal is as follows:

  • Keep the existing nosubject.io site as it is
  • Use create.nosubject.io for Roblox Studio development notes
  • Set Astro’s site option to the subdomain
  • Serve the subdomain from a separate Nginx server block
  • Build the new site into a separate output directory

Using a subdomain keeps the main blog categories and post list from mixing with the development diary. If the site later moves to its own domain, it is also easier to move as a separate site.

Choose A Directory For The New Site

First, keep the publish directory separate from the main site.

For example, on the Raspberry Pi:

/home/<user>/nosubject-docker/hugo-public
/home/<user>/nosubject-docker/create-public

hugo-public is for the existing nosubject.io site, and create-public is for the new create.nosubject.io site.

Astro builds to dist/ by default. In GitHub Actions, the contents of dist/ will be synced to /home/<user>/nosubject-docker/create-public.

On the Raspberry Pi 5, the directory that must exist is not create-nosubject, the Astro source directory, but create-public, the publish directory that Nginx reads.

mkdir -p /home/<user>/nosubject-docker/create-public

create-nosubject is the Astro project directory created on Windows. When building with GitHub Actions, actions/checkout fetches the repository into the runner’s working directory on the Raspberry Pi 5, so it is fine if there is no create-nosubject directory directly under the home directory.

Only clone the create-nosubject repository on the Raspberry Pi 5 if you also want to test the build manually there.

Create The Astro Site

Create a new repository or working directory for the new Astro site.

npm create astro@latest create-nosubject
cd create-nosubject

If the CLI asks which template to use, a blog template is a good starting point for a development diary.

For this project, I selected:

How would you like to start your new project?
  Use blog template

Install dependencies?
  Yes

Initialize a new git repository?
  Yes

When Initialize a new git repository? is set to Yes, Astro runs git init for the project. After completion, the CLI shows:

Project initialized!
Template copied
Dependencies installed
Git initialized

An npm notice about a new major version may appear at the end. The Astro project has still been created successfully, and it is fine to continue without updating npm immediately.

You can also start from a minimal template and add Markdown lists and tag pages later, but for a development diary, starting with the blog template is faster.

Set the final deployment URL in astro.config.mjs using the site option.

The file generated from the blog template already included @astrojs/mdx, @astrojs/sitemap, and local font settings. Keep those settings and confirm that site is set to https://create.nosubject.io.

// @ts-check

import mdx from '@astrojs/mdx';
import sitemap from '@astrojs/sitemap';
import { defineConfig } from 'astro/config';

export default defineConfig({
  site: 'https://create.nosubject.io',
  integrations: [mdx(), sitemap()],
});

The actual file may also include font settings using fontProviders.local(). Those settings control the template’s typography and can be left as-is.

import { defineConfig, fontProviders } from 'astro/config';

Astro uses site for things like sitemaps and canonical URLs. Because this site is deployed to a subdomain rather than a subdirectory, do not set base. integrations and fonts have different roles, so do not remove them when adding or checking site.

Add The First Post

If you chose a blog template, add Markdown under src/content/blog/.

src/content/blog/hello-create.md

For example:

---
title: 'Starting Creation Notes'
description: 'Creating a place for Roblox Studio notes'
pubDate: 2026-07-18
tags: ['Roblox Studio', 'Development Log']
---

I will keep notes here about things I try in Roblox Studio.

Preview it locally.

npm run dev -- --host 127.0.0.1 --port 4322

Astro’s development server uses 4321 by default. If that conflicts with another project, specify another port as shown above.

To generate the production static files, run:

npm run build

The build output is generated in dist/ by default.

In this environment, the build completed like this:

[build] output: "static"
[build] directory: C:\Users\kkitta\GitHub\create-nosubject\dist\
├─ /blog/hello-create/index.html
├─ /rss.xml
├─ /index.html
[@astrojs/sitemap] `sitemap-index.xml` created at `dist`
[build] 9 page(s) built in 1.16s
[build] Complete!

Because /blog/hello-create/index.html was generated, the new post is included in the build. The files generated in dist/ will later be copied to the Nginx publish directory.

Add The DNS Record

Because nosubject.io is managed at Onamae.com, add the DNS record for create.nosubject.io from Onamae.com Navi.

Onamae.com’s official guide, “DNS Record Settings”, shows this path for DNS record settings:

Log in to Onamae.com Navi
  -> Name Server/DNS
  -> Domain DNS Settings
  -> Domain DNS for the target domain
  -> DNS Record Settings
  -> Add Record

Choose the parent domain nosubject.io, not create.nosubject.io. To add a subdomain, add create as the host name under the parent domain’s DNS settings.

In this setup, nosubject.io already points to the same Raspberry Pi 5 server. A DNS update script also keeps nosubject.io updated when the externally reachable IP address for the Raspberry Pi changes.

For that reason, I did not point create.nosubject.io directly at the server IP address. Instead, I added a CNAME record that points to nosubject.io.

Host name: create
TYPE: CNAME
TTL: 86400
VALUE(TARGET): nosubject.io

In Onamae.com’s screen, enter only create as the host name, not the full create.nosubject.io.

With this setup, when the Raspberry Pi’s public IP address changes, the DNS update script only needs to update nosubject.io. create.nosubject.io will continue to follow it through the CNAME.

If you want to point the subdomain directly to your home server or VPS IP address, use an A record instead of CNAME.

Host name: create
TYPE: A
TTL: 86400
VALUE(TARGET): Your server's IPv4 address

If you also want to specify IPv6 directly, add an AAAA record too.

Host name: create
TYPE: AAAA
TTL: 86400
VALUE(TARGET): Your server's IPv6 address

After entering the record, click the add button, confirm that it appears in the record list, then continue to the confirmation screen and save the change.

If the domain protection application screen appears during the process, choose the option not to enable it if you do not need it. On the final confirmation screen, review the values and apply the change.

Onamae.com’s DNS record settings accept TTL values from 60 to 86400 seconds. If there is no specific reason to change it, 86400, the value used in the official example, is fine.

Check the DNS result.

dig create.nosubject.io

On Windows PowerShell, you can also use:

Resolve-DnsName create.nosubject.io

DNS changes may take some time to propagate. If the name does not resolve immediately after saving, wait a while and check again.

Immediately after saving the setting, I checked from the Raspberry Pi 5 and got NXDOMAIN:

status: NXDOMAIN
ANSWER: 0

QUESTION SECTION:
create.nosubject.io.           IN      A

AUTHORITY SECTION:
nosubject.io.           300     IN      SOA     01.dnsv.jp. hostmaster.dnsv.jp.

NXDOMAIN means that the name create.nosubject.io does not exist in DNS yet. Because the AUTHORITY SECTION shows the nosubject.io SOA at dnsv.jp, the query is reaching Onamae.com’s authoritative DNS, but the create record is not visible yet.

In that case, check the following:

  • The parent domain nosubject.io is selected in Onamae.com
  • The host name is create, not create.nosubject.io
  • The TYPE is CNAME
  • The VALUE(TARGET) is nosubject.io
  • You clicked through the final confirmation screen and applied the setting
  • You waited a while and ran dig create.nosubject.io again

Once the DNS record is visible, the ANSWER SECTION should show a CNAME for create.nosubject.io.

After waiting for a while, the Windows side resolved it like this:

Name                           Type   TTL   Section    NameHost
----                           ----   ---   -------    --------
create.nosubject.io            CNAME  3600  Answer     nosubject.io

Name       : nosubject.io
QueryType  : A
TTL        : 3600
Section    : Answer
IP4Address : 60.84.210.40

This confirms that create.nosubject.io points to nosubject.io, and that nosubject.io then resolves to the current IPv4 address.

Add An Nginx Server Block

In Nginx, create a separate server block for create.nosubject.io.

server {
    listen 80;
    listen [::]:80;

    server_name create.nosubject.io;

    root /var/www/create;
    index index.html;

    location / {
        try_files $uri $uri/ /index.html =404;
    }

    error_page 404 /404.html;

    location = /404.html {
        internal;
    }
}

Astro’s static output commonly creates index.html files for pages. Using try_files $uri $uri/ /index.html =404; makes regular static pages and client-side routed pages easier to serve.

If Nginx runs through Docker Compose, mount the host-side create-public directory into the container as /var/www/create.

services:
  web:
    image: nginx:stable
    volumes:
      - ./hugo-public:/var/www/html:ro
      - ./create-public:/var/www/create:ro
      - ./nginx/conf.d:/etc/nginx/conf.d:ro

The key point is to use a different root for the main site and the subdomain. If both server blocks use the same root, both domains will show the same site.

After editing the Nginx configuration, test it.

docker compose exec web nginx -t

If the test passes, reload Nginx.

docker compose exec web nginx -s reload

Create The HTTPS Certificate

The subdomain must be included in a certificate.

If you use Certbot with Nginx integration, add create.nosubject.io like this:

sudo certbot --nginx -d create.nosubject.io

Even if an existing certificate already covers nosubject.io and www.nosubject.io, it will not cover create.nosubject.io unless that name is included.

After issuing the certificate, open:

https://create.nosubject.io/

If Astro’s dist/ files have not been published yet, the page may still show a 404 or empty response. At this stage, the main thing to confirm is that HTTPS opens without a certificate warning.

Use A Separate GitHub Actions Build Destination

If the site is built automatically, sync Astro’s dist/ directory to a publish directory separate from the main site.

Before that, create an empty GitHub repository for create-nosubject.

When creating the repository on GitHub, do not add a README, .gitignore, or license, because the local repository already exists.

Repository name: create-nosubject
Visibility: Public or Private
Add a README file: off
Add .gitignore: None
Choose a license: None

After creating it, add the repository URL as origin and push the local repository.

git remote add origin https://github.com/<owner>/create-nosubject.git
git branch -M main
git push -u origin main

If you want to keep the local branch name as master, skip git branch -M main and push master instead. The workflow below is configured to run on both main and master.

If the GitHub Actions self-hosted runner is registered only for the existing nosubject-hugo repository, it will not automatically be available to the new create-nosubject repository. Register the runner from Settings -> Actions -> Runners in the new repository, or make sure it is available as a shared runner at the organization or account level.

When I tried to run ./config.sh --url https://github.com/usurageha/create-nosubject --token ... inside the existing ~/actions-runner directory on the Raspberry Pi 5, it showed:

Cannot configure the runner because it is already configured.
To reconfigure the runner, run 'config.cmd remove' or './config.sh remove' first.

This means that ~/actions-runner is already configured for another repository. If that existing runner is used by nosubject-hugo, running ./config.sh remove here may stop the existing site’s automated build.

For this setup, keep the existing runner and add another runner directory for create-nosubject.

cd ~
mkdir actions-runner-create
cd actions-runner-create

# Run the curl / tar commands shown in GitHub's runner setup screen.

./config.sh --url https://github.com/usurageha/create-nosubject --token <displayed-token> --name rpi5-create --labels rpi5,create

When the setup asks for a runner group, do not enter rpi5. Just press Enter and use Default.

Enter the name of the runner group to add this runner to: [press Enter for Default]

If you enter rpi5 here, the setup fails like this:

Could not find any self-hosted runner group named "rpi5".

rpi5 is not a runner group. It is a runner label passed with --labels rpi5,create. The workflow’s runs-on selector matches labels.

I ran ./config.sh again, pressed Enter without typing anything at the runner group prompt, and the runner registered successfully:

Enter the name of the runner group to add this runner to: [press Enter for Default]

√ Runner successfully added

Enter name of work folder: [press Enter for _work]

√ Settings Saved.

If you want the runner to stay running as a systemd service, install it as a separate service from this new directory.

sudo ./svc.sh install
sudo ./svc.sh start
sudo ./svc.sh status

If svc.sh status shows active (running), the runner is running as a service.

actions.runner.usurageha-create-nosubject.rpi5-create.service
Active: active (running)
√ Connected to GitHub
Listening for Jobs
Running job: build

On GitHub, rpi5-create also appeared under Settings -> Actions -> Runners.

The workflow uses runs-on: [self-hosted, Linux, ARM64, rpi5], so the new runner needs the rpi5 label. Adding a create label as well makes it easy to narrow the workflow later if needed.

Place a workflow like this in the new Astro site’s repository.

This workflow does not require a pre-existing create-nosubject folder in the Raspberry Pi 5 home directory. The Checkout step fetches the source from GitHub into the runner’s working directory, then npm run build creates dist/, and the publish step syncs it to ASTRO_DESTINATION.

name: Build Create Astro site

on:
  push:
    branches:
      - main
      - master
  workflow_dispatch:

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

permissions:
  contents: read

jobs:
  build:
    runs-on: [self-hosted, Linux, ARM64, rpi5]
    env:
      ASTRO_DESTINATION: ${{ vars.ASTRO_DESTINATION }}
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 24
          cache: npm

      - name: Install dependencies
        run: npm ci

      - name: Build
        run: npm run build

      - name: Check publish destination
        run: |
          test -n "$ASTRO_DESTINATION"
          case "$ASTRO_DESTINATION" in
            *create-public|*create-public/) ;;
            *) echo "ASTRO_DESTINATION must point to the create-public directory"; exit 1 ;;
          esac
          test -d "$ASTRO_DESTINATION"
          command -v rsync

      - name: Publish
        run: rsync -a --delete dist/ "$ASTRO_DESTINATION"/

The important part is syncing the contents of dist/ to create-public. To avoid writing the local user name directly in the workflow file, set the publish path as a GitHub Repository variable named ASTRO_DESTINATION.

ASTRO_DESTINATION: /home/<user>/nosubject-docker/create-public

If this points to the existing /home/<user>/nosubject-docker/hugo-public, the new site build may overwrite the main site output.

Check The Published Site

After the build and Nginx configuration are ready, check the site in this order.

curl -I https://create.nosubject.io/

If the response is HTTP/2 200 or HTTP/1.1 200 OK, the top page is being served.

Also check a URL that does not exist.

curl -I https://create.nosubject.io/not-found-test/

If the site has a 404 page, keep the status code as 404 while showing the page generated from Astro’s 404.astro.

Finally, check the Astro URLs.

  • The top page opens at https://create.nosubject.io/
  • Post URLs use https://create.nosubject.io/.../
  • Sitemap and canonical URLs point to create.nosubject.io, not nosubject.io
  • The main site at https://nosubject.io/ still works as before

After pushing the update to hello-create.md, I was able to open the article at:

https://create.nosubject.io/blog/hello-create/

This confirmed the full flow: pushing to the Astro repository, building with GitHub Actions, syncing to create-public on the Raspberry Pi 5, and serving the site through Nginx.

Register It With Search Tools

Search engines and analytics tools may treat subdomains as separate sites.

If needed, add create.nosubject.io to these services:

  • Google Search Console
  • Google Analytics
  • AdSense
  • Bing Webmaster Tools

You do not need to configure everything immediately, but registering the site in Search Console early makes it easier to check indexing later.

Summary

When publishing a separate Astro site on a subdomain such as create.nosubject.io, this order is easy to follow:

  1. Create a new Astro site
  2. Set site in astro.config.mjs to https://create.nosubject.io
  3. Add the create DNS record at Onamae.com
  4. Add an Nginx server block for create.nosubject.io
  5. Issue an HTTPS certificate
  6. Build with npm run build in GitHub Actions and sync dist/ to the dedicated directory
  7. Check the result with curl and a browser

A subdomain has less friction than buying a new domain. It should be a good first step for a Roblox Studio development diary, and if the site grows later, it can still be moved to its own domain.