Skip to main content

Deploying An Eleventy Site to NeoCities with GitLab CI/CD

As I've already written about before, I love NeoCities. It is a free web hosting service embarcing the spirit of the early web, taking its name from the tragically-defunct GeoCities.

For the sake of ease, I'm currently hosting my site with Netlify, but I thought it would be a fun side quest to use GitLab's CI/CD pipeline to upload the rendered _site output of my static site to NeoCities via their API. Easy enough, right?

There were several reasons I wanted to try this. First, it would be a handy backup, particularly for someone like me who's trying to follow the 3-2-1 rule. I would also just love to join the community, and maybe in the future I'll create a site that's specifically created for NeoCities rather than just a mirror.

The service also has a generous free tier! 1 GB of storage and 200 GB of bandwidth, with no server management or any sort of complex configuration. You just drag-and-drop files and they're hosted. (Or remotely upload the files, in my case.)

There are limitations, of course, particularly with filetypes. I'll get into that.

So, this was ultimately my plan:

  1. Automatically deploy my 11ty site to NeoCities with each commit
  2. Support multiple authentication methods (username/password or API key)
  3. Only upload modified files to ensure no wasted bandwidth
  4. Filter out any unsupported file types automatically
  5. Ensure robust error-handling and logging

Although, number five happened organically due to how many times I messed up and had no idea why. I am a really good web developer, I swear.

Here's the full .gitlab-ci.yml handling everything. It ended up rather extensive and lengthy. Everything below will be referencing this script.

The NeoCities API

NeoCities provides a REST API with two authentication methods:

1. Basic Authentication (Username/Password)

curl -u "username:password" https://neocities.org/api/info

2. Bearer Token (API Key)

curl -H "Authorization: Bearer YOUR_API_KEY" https://neocities.org/api/info

You can get your API key at: https://neocities.org/api/key

Supported File Types

NeoCities free accounts support a specific set of file extensions. The complete list includes:

apng asc atom avif bin cjs css csv dae eot epub geojson gif glb 
glsl gltf gpg htm html ico jpeg jpg js json jxl key kml knowl 
less manifest map markdown md mf mid midi mjs mtl obj opml osdx 
otf pdf pgp pls png py rdf resolveHandle rss sass scss sf2 svg 
text toml ts tsv ttf txt webapp webmanifest webp woff woff2 xcf 
xml yaml yml

Files like .pf_fragment, .pf_meta, and .wasm (generated by Pagefind for my site's search) are not supported on free accounts.

Setting Up Environment Variables

First, add your credentials to GitLab CI/CD variables (Settings → CI/CD → Variables):

  • NEOCITIES_USERNAME
  • NEOCITIES_PASSWORD
  • NEOCITIES_API_KEY

Mark these as Masked for security.

When Deployments Run

One thing I learned the hard way is that you don't always want to deploy every single file. After some trial and error (mostly error), I set up three triggers for the pipeline:

  • Automatic: Every commit to the main branch (since that's usually when I've actually fixed something)
  • Manual: You can trigger it from GitLab's web interface - handy for those "oops, I need to push that again" moments
  • Forced full deploy: Set FORCE_FULL_DEPLOY=true when you need to upload everything (great for first-time setup or when things get weird)

For the very first deployment, the script uploads ALL supported files. This makes sense since you can't upload "modified" files when there's nothing there yet! The same thing happens for manual triggers or when you force a full deploy.

How It Figures Out What to Upload

Eleventy builds your src/ folder into _site/, but Git tracks changes in src/. The script needs to map between these! It does this by simply stripping the src/ prefix from file paths.

# Remove src/ prefix if it exists
if [[ "$file" == src/* ]]; then
  built_file="${file#src/}"
else
  built_file="$file"
fi

The script also gives you way more debug output than you probably need:

DEBUG: Modified source files:
src/posts/my-new-post.md
src/assets/css/new-style.css

DEBUG: New source files:
src/images/cat-photo.jpg

DEBUG: Files to upload after mapping:
posts/my-new-post.md
assets/css/new-style.css
images/cat-photo.jpg

It even counts how many files it's about to upload, which is satisfying to see in the logs.

GitLab CI Extras

Since we're using GitLab, there are a couple of nice-to-have features I enabled. These don't affect the deployment itself:

  • Artifacts: The _site folder gets saved, so you can download the built site if needed
  • Environment: GitLab tracks this as a "production" environment with your NeoCities URL

Features

The script tries username/password first, then falls back to the API key to provide robustness. The test_auth function validates credentials before attempting upload, and provides error messages if authentication fails.

Both jq (if available) and grep patterns are used to parse JSON responses, handing variations in whitespace and formatting:

# Flexible grep pattern allows for whitespace
grep -q '"result":[[:space:]]*"success"'

Only modified files are uploaded after the first deployment. This speeds up deployments from minutes to seconds:

# Get changed files between commits
git diff --name-only HEAD~1 HEAD

The regex automatically filters to only supported file types, preventing errors from trying to upload things like the Pagefind search index files and anything else unsupported:

grep -E "\.($SUPPORTED_EXTENSIONS)$"

Error Handling

When things go wrong, the script tries outputting useful information. For authentication failures, it extracts the actual error message from NeoCities:

Error type: invalid_auth
Message: Invalid username or password

One particularly helpful debug feature shows just enough of your API key to verify it's correct without exposing the whole thing:

API Key length: 64 characters
First 8 chars: abcd1234...
Last 8 chars: ...efgh5678

The script also uses Node.js 18 in the Docker image, which matters if you're using newer JavaScript features in your build process.

Drawbacks

As it can be seen from above, certain features don't work with this mirror, such as the search and contact form. In the future, I might create a separate branch for a NeoCities-friendly version of the site that doesn't automatically mirror any broken functionality. But I think this is good enough, for now.

Debugging Parsing Errors

Curl's progress meter ended up interfering with response parsing, so I used -sS flags and reted stderr:

curl -sS -w "\nHTTP_CODE:%{http_code}" [...] 2>/dev/null

In addition, storing curl flags in variables causes word splitting issues. We can use conditional curl commands directly instead of storing flags:

if [ "$method" = "Username/Password" ]; then
  response=$(curl -sS -u "${USER}:${PASS}" [...])
else
  response=$(curl -sS -H "Authorization: Bearer ${KEY}" [...])
fi

Multi-line string assignments cause YAML parsing errors, which means we need to use echo -e with \n:

MODIFIED_FILES=$(echo -e "$MODIFIED_FILES\n$NEW_FILES")

Testing Deployment

The script is written so that authentication is tested before uploading atttempts. GitLab CI provides detailed logs for deployment. You can visit your NeoCities site to confirm uploads, and make small changes to ensure only modified files are uploaded.

Conclusion

Despite how seemingly simple and obvious this pipeline probably seems, it took a lot of trial and error for me. For whatever reason, when I attempted only API key to deploy I kept getting failures, using username and password seems a lot more reliable.

Certain aspects of syntax will probably be different if you're not hosting your repository with GitLab. Sophie of localghost.dev has a write-up specific for GitHub deployment which I relied on for some debugging.

If you do use GitLab, it seems important to make sure your variables are, first and foremost, stored in GitLab settings instead of being present anywhere in your repository (this would give access of your password/API key to anybody). But also, ensure neither the "protect variable" nor the "expand variable reference" flags are checked.

That's about it, for now!


Do you host your site on NeoCities? I'd love to hear from other developers. Leave a comment in my guestbook or email me at hi at brennan brown dot ca!

Resources


Webmentions

No webmentions yet. Be the first to send one!


Related Posts

↑ TOP