Static site generators (SSGs) are an ideal match for Tor hidden service hosting. A static site has no server-side execution, no database, and no application runtime - just pre-built HTML, CSS, and JavaScript files served by nginx. This minimal stack drastically reduces the attack surface: there is no PHP interpreter to exploit, no database to exfiltrate, no application server to crash. This guide covers deploying Hugo, Jekyll, Gatsby, and Eleventy static sites as Tor hidden services, with nginx configuration optimized for Tor's characteristics.
Need this done for your project?
We implement, you ship. Async, documented, done in days.
Static sites have fundamental security advantages for .onion hosting: no server-side code execution means no remote code execution vulnerabilities, no database means no SQL injection, no session state means no session hijacking. The server only needs to serve pre-built files - nginx handling static files is extremely robust and well-audited. For performance: nginx serving static files handles thousands of requests per second even on minimal hardware, and since Tor is the bottleneck (not the server), even a small VPS serves a static site as fast as a powerful dedicated server. For content integrity: static sites can be fully signed (hash-verified) at build time, ensuring file integrity. The build process runs on a separate, potentially airgapped machine - the production server only hosts the output. This separation of concerns is architecturally cleaner than dynamic CMS deployments where both editing and serving happen on the same server.
Hugo Site Building and Deployment to .onion
Hugo is the fastest static site generator, building thousands of pages in seconds. For .onion deployment, set the baseURL in config.toml to your .onion address: baseURL = 'http://youronion.onion'. Build with hugo --minify to generate the public/ directory. The --minify flag reduces file sizes (removing whitespace from HTML/CSS/JS), important for improving load times over Tor's limited bandwidth. Deploy the public/ directory to the server via SCP over Tor: torsocks scp -r public/ user@server.onion:/var/www/html/. Set up nginx to serve the public/ directory as the document root. Hugo's fast build times allow frequent rebuilds for content updates without significant delay. For a content workflow: edit markdown files locally, run hugo to build, and deploy the public/ directory to the server. The server never runs Hugo - it only serves pre-built files, keeping the production server's attack surface minimal. Configure nginx cache headers appropriately: Cache-Control: max-age=3600 for HTML files (1 hour, since content updates every few hours at most) and Cache-Control: max-age=86400 for static assets (images, CSS, JS) which change less frequently.
Jekyll Configuration for .onion Publishing
Jekyll is the traditional static site generator, popular for GitHub Pages and documentation sites. For .onion deployment: set url in _config.yml to http://youronion.onion and baseurl to '' (empty string for root deployment). Build with JEKYLL_ENV=production bundle exec jekyll build which generates the _site/ directory. Jekyll builds are slower than Hugo (Ruby runtime vs Go binary) but the output is identical static files. Jekyll plugins can make external HTTP requests during build - audit your Gemfile for plugins that fetch external data. Run Jekyll builds on a separate machine, not the production server, to keep the server minimal. For the data-fetching pattern (if you fetch external data at build time): route Jekyll's HTTP requests through Tor during the build process (torsocks bundle exec jekyll build) so external requests from the build machine do not reveal the eventual server's location.
Gatsby and Next.js Static Export for .onion
Gatsby generates static sites with React components. For .onion deployment, run gatsby build which outputs to the public/ directory. Configure gatsby-config.js with pathPrefix if deploying to a subdirectory of the .onion address. Gatsby fetches data at build time (from GraphQL sources, APIs, CMS) - these build-time fetches reveal the build machine's IP (or Tor exit if routed through Tor), not the production server. This is acceptable for most workflows. Avoid Gatsby plugins that embed external service tokens or tracking scripts in the output HTML (Gatsby adds Google Analytics, etc. as plugins - these create clearnet connections in visitors' browsers, de-anonymizing them). Review gatsby-config.js plugins list carefully and remove any that load external scripts. Next.js with next export generates static output compatible with nginx. Note that Next.js static export does not support API routes or SSR - only static pages. For .onion static sites, this limitation is acceptable since API functionality requires a backend server anyway.
Nginx Configuration for Tor-Optimized Static Serving
Optimize nginx for serving static sites over Tor's characteristics. Enable gzip compression for text files (HTML, CSS, JS, SVG): gzip on; gzip_types text/html text/css application/javascript image/svg+xml. This reduces file sizes by 60-80% for text content, significantly improving load times over Tor's limited bandwidth. Enable TCP optimizations: tcp_nopush on; tcp_nodelay on; sendfile on; - these reduce kernel syscall overhead for file serving. Set appropriate keepalive values: keepalive_timeout 65; - this keeps Tor circuits alive between requests from the same client (reduces circuit setup overhead). Configure error pages to serve custom error HTML (404, 500) without revealing nginx version or server details: error_page 404 /404.html; with a custom 404.html in the document root. Set send_timeout and client_header_timeout to handle Tor's variable latency: use higher values (90-120s) to accommodate slow Tor circuits. The resulting nginx configuration serves a static site optimally over Tor with minimal resource usage on the server.