Illustration created by the author. | GitLab Logo | NeoCities Logo
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:
- Automatically deploy my 11ty site to NeoCities with each commit
- Support multiple authentication methods (username/password or API key)
- Only upload modified files to ensure no wasted bandwidth
- Filter out any unsupported file types automatically
- 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_USERNAMENEOCITIES_PASSWORDNEOCITIES_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=truewhen 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
_sitefolder 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!