This site is generated as a static Next.js export and published on shared hosting (Altervista, after a first attempt with another provider). The process looks simple (build, zip, upload), but between the first build and the site actually going live there were three real problems, none of them easy to diagnose. I report them here with the same method as the other guides: symptom, diagnosis, cause, fix.
The bug that blocked every upload: folders with $ in the name
Symptom
The hosting panel refused to extract the zip with a generic "an error occurred while extracting the files". Switching to an FTP upload, the error became more precise:
Remote file copy failed.
Unable to change directory to /homelab/accesso-remoto-vpn/__next.homelab/$d$slug/__PAGE__.txt: Permission deniedDiagnosis
My first suspicion, a malformed zip, turned out to be wrong: recreated with correct paths (/ instead of \, the typical Compress-Archive error of PowerShell on Windows), the extraction still failed. The FTP message, however, pointed precisely at the culprit: an attempt to cd into a folder called $d$slug.
find out/ -name '*$*' | wc -l
# 6969 folders, one for each dynamic route of the site (posts, projects, homelab entries, tags, categories), all with the same pattern: __next.<section>/$d$slug/__PAGE__.txt. These are files generated by Next.js for client-side link prefetching: when the visitor hovers over a link, the browser downloads these payloads in advance to make navigation instant instead of a full page load.
Cause
The hosting (both the panel's zip extractor and the FTP server) refuses to create or traverse paths containing the $ character, almost certainly out of caution toward server-side scripts that might interpret it as the start of a shell variable.
Fix
The prefetch files are not essential: if they are missing, the Next.js router simply does a normal navigation instead of the instant one, with no visible errors for people visiting the site. The solution was to remove them from the build before publishing it, automatically and repeatably on every build:
function removeDollarPaths(dir: string): number {
let removed = 0;
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name);
if (entry.name.includes("$")) {
fs.rmSync(full, { recursive: true, force: true });
removed += 1;
} else if (entry.isDirectory()) {
removed += removeDollarPaths(full);
}
}
return removed;
}Result: 543 files before the cleanup, 474 after, zero failed uploads since.
Note
The initial symptom (a generic error during zip extraction) and the later one (permission denied over FTP) were the same cause seen through two different tools. Changing the upload method helped me get a more precise error message, not solve the problem on its own.
The unwritten limit: no security headers
The site generates an .htaccess with the standard security headers (Content-Security-Policy, X-Frame-Options, Strict-Transport-Security, etc.), also verified on the local preview server. Once online, though, none of these headers reached the browser: only Server, Content-Type, Vary, nothing else.
The other directives in the same file (ErrorDocument, the HTTPS redirect, the MIME type forced on the Open Graph image) worked normally, so the file was being read. The suspicion, a missing Apache module, was confirmed by the hosting's official documentation: the list of supported .htaccess directives includes ErrorDocument, DirectoryIndex, RewriteEngine/RewriteRule/RewriteCond, Redirect* and AddHandler. The Header directive (the mod_headers module, which all the security headers depend on) is not there: it is not an option turned off somewhere, it is simply absent for every user of this hosting.
There is no way around it on the server side. The alternatives I evaluated:
- a
<meta http-equiv="Content-Security-Policy">tag covers only part of the policy (noX-Frame-Options,HSTSorPermissions-Policy, which exist only as real HTTP headers); - a proxy like Cloudflare in front of the domain, with a Worker that adds the missing headers at the edge, would remain the only complete solution.
For now the limit is only documented, not hidden: it is a conscious trade-off of free hosting, not a configuration error.
A layout bug: tables that broke words in half
Pages with dense tables (the hardware specs of the nodes, for example) showed a curious problem: words like "NVIDIA" ended up split mid-word ("NVIDI" / "A") instead of wrapping cleanly.
The cause was a combination of two CSS rules: the table was constrained to width: 100% of its container, and the cell text had overflow-wrap: anywhere (inherited from the general text rules, meant to stop very long URLs from breaking the layout). With many narrow columns, the browser preferred to break words to stay within the assigned width, instead of wrapping between one word and the next.
The fix: overflow-wrap: normal on the cells (it wraps only between words, like a normal paragraph) and, for the densest tables, a structure with fewer columns (nodes as columns, attributes as rows, instead of the reverse) so the content has more horizontal room without needing to scroll.
The lesson
The three problems share the same pattern: a generic symptom (an extraction error, missing headers, malformed text), a non-obvious cause tied to a specific limit of the target environment, and a fix that has to be made permanent in the build process, not a manual intervention to repeat at every publish. The check:launch check and the postbuild.ts script of this site exist precisely for this: turning a problem solved once into a problem that does not come back.