# Static Service The Static service provides a way to serve static content through a pre-configured Nginx setup. It balances simplicity with the flexibility needed for modern web applications. ## Quick Start Add a Static service to your project by including this in your `zerops.yaml`: ```yaml title="zerops.yaml" zerops: - setup: app run: os: alpine base: static ``` ## Routing & Configuration ### Default Behavior Every Static service in Zerops comes with built-in defaults optimized for modern web applications, including Single Page Applications. By default, for any incoming request, the service will: 1. Try to serve the exact path (`$uri`) 2. Try with .html extension (`$uri.html`) 3. Look for an index.html in the directory (`$uri/index.html`) 4. Fall back to `/index.html` (suitable for SPAs) 5. Return 404 if none of the above exist :::important SPAs Single Page Applications work out of the box without any additional configuration. The built-in fallback to `/index.html` ensures that client-side routing functions properly. ::: ### Custom Routing Configuration The Static service allows you to configure additional URL routing and redirects through simple YAML configuration, abstracting away the complexity of Nginx configuration. Configure custom routing beyond the default behavior in the `run.routing` section of your `zerops.yaml`: ```yaml title="zerops.yaml" run: routing: redirects: # Only needed for custom redirect scenarios - from: /special-path/* to: /specific-landing-page status: 302 ``` ### Redirects #### Relative Redirects :::note Remember that SPA routing is already built into the default behavior. You don't need to add any custom redirects for client-side routing to work. ::: Use relative redirects to route paths within your application. When both `from` and `to` are relative paths, you can omit the `status` code to create a masked redirect that shows the content of the target page while preserving the original URL: ```yaml title="zerops.yaml" routing: redirects: # Masked redirect - URL stays the same but shows content from about-us - from: /about to: /about-us # Standard redirect with status code - from: /old-page to: /new-page status: 301 # Preserve the path when redirecting between directories - from: /blog/* to: /articles/ preservePath: true status: 302 # Preserve both path and query parameters - from: /posts/* to: /blog/ preservePath: true preserveQuery: true status: 302 ``` :::caution Important When using `preservePath` with wildcards, ensure the `to` path ends with a `/` to maintain proper path concatenation. For example, `/blog/*` to `/new-blog/` will correctly redirect `/blog/hello.html` to `/new-blog/hello.html`, while `/new-blog` would result in `/new-bloghello.html`. ::: #### Absolute Redirects For redirecting between domains or to external URLs, use absolute redirects by including `http://` or `https://`. When using absolute URLs in either `from` or `to`, you must specify a `status` code: ```yaml title="zerops.yaml" routing: redirects: # Redirect an old domain to a new one - from: https://old-domain.com/* to: https://new-domain.com status: 301 preserveQuery: true # Optional: maintain query parameters # Redirect with path preservation - from: https://old-site.com/* to: https://new-site.com/ status: 301 preservePath: true ``` #### Wildcard Matching Use `*` as a wildcard in your paths: - **At the end of a path**: Matches any subsequent content - **At the start of a domain** (after `https://`): Enables regex matching for subdomains Example of domain management: ```yaml title="zerops.yaml" run: routing: redirects: # Redirect a specific domain to an article - from: https://promo-domain.com/* to: https://main-site.com/special-offer status: 302 # Redirect all subdomains to main site - from: https://*.old-domain.com/* to: https://main-site.com status: 302 ``` #### Matching Priority When multiple redirects are configured, they follow Nginx's matching priority system: 1. Exact matches are checked first 2. Simple path matches (without wildcards) are checked next 3. Pattern matches (with wildcards) are checked last For example: ```yaml title="zerops.yaml" routing: redirects: # Exact match for homepage - standard redirect - from: / to: /home status: 302 # Simple path match - masked redirect - from: /about to: /about-us # Pattern match with path preservation - from: /blog/* to: /articles/ preservePath: true status: 302 ``` In this configuration: - `/` will redirect to `/home` with a 302 status - `/about` will show content from `/about-us` but keep the URL as `/about` - `/blog/post-123.html` will redirect to `/articles/post-123.html` - Any other path will use the [default behavior](#default-behavior) #### Common Redirect Patterns **Domain Migration** ```yaml title="zerops.yaml" routing: redirects: - from: https://old-domain.com/* to: https://new-domain.com status: 301 ``` Use permanent (301) redirects when permanently moving content to maintain SEO value. **Multiple Domain Management** ```yaml title="zerops.yaml" run: routing: redirects: # Product-specific domain - from: https://product-promo.com/* to: https://main-site.com/products status: 302 # Campaign domain - from: https://special-offer.com/* to: https://main-site.com/campaign status: 302 # Legacy domains and subdomains - from: https://*.legacy-domain.com/* to: https://main-site.com status: 302 ``` ### CORS Configuration You can enable CORS for your static service by adding a `cors` directive: ```yaml title="zerops.yaml" run: routing: # Simple case - automatically converted to '*' cors: "*" # Full syntax with proper quoting cors: "'*' always" ``` The `cors` directive sets the following headers: - `Access-Control-Allow-Origin` - `Access-Control-Allow-Methods` - `Access-Control-Allow-Headers` - `Access-Control-Expose-Headers` :::note The `cors` directive has a special case: if you specify just `"*"`, it's automatically converted to `'*'`. For any other values, you need to include the proper Nginx syntax including quotes. ::: ### Custom Headers For more control over HTTP headers, use the `headers` directive: ```yaml title="zerops.yaml" run: routing: headers: - for: "/" values: # All values need proper quoting since they're inserted directly into Nginx X-Frame-Options: "'DENY'" # Values with internal quotes need proper YAML escaping Content-Security-Policy: '"default-src ''self''"' ``` :::important Header values are inserted directly into the Nginx configuration **without** additional quotes, which means: 1. **All values must include their own quotes** (typically single quotes) 2. If you need single quotes inside your header value, you must escape them in YAML (using double single quotes) 3. To include the `always` directive, add it after your quoted value 4. For complex values, you can use YAML's block scalar notation (`>-`) for better readability ::: Here are examples for different header scenarios: ```yaml title="zerops.yaml" headers: - for: "/" values: # Simple header with proper quoting X-Frame-Options: "'DENY'" # Header with 'always' directive X-XSS-Protection: "'1; mode=block' always" # Header with internal single quotes - need double single quotes for escaping Content-Security-Policy: '"default-src ''self'' https://cdn.example.com"' # Complex header with block scalar notation for better readability Content-Security-Policy: >- "default-src 'self' https://cdn.example.com; script-src 'self' 'unsafe-inline'; img-src * data:" always ``` When this configuration is processed, it translates to the following Nginx directives: ``` add_header X-Frame-Options 'DENY'; add_header X-XSS-Protection '1; mode=block' always; add_header Content-Security-Policy "default-src 'self' https://cdn.example.com"; add_header Content-Security-Policy "default-src 'self' https://cdn.example.com; script-src 'self' 'unsafe-inline'; img-src * data:" always; ``` :::important Path Handling When you specify headers for a path that doesn't have an existing location block, the Static service automatically creates a location with the same [default behavior](#default-behavior) as the root path (trying files in order: `$uri`, `$uri.html`, `$uri/index.html`, `/index.html` or returning 404). If you add headers for a path that already has a location block, your headers will be merged with the existing configuration. ::: ### Combining CORS and Custom Headers You can use both CORS and custom headers together: ```yaml title="zerops.yaml" run: routing: cors: "'*' always" headers: - for: "/" values: X-Frame-Options: "'DENY'" ``` The `cors` directive sets default Access-Control headers for all routes, while the `headers` directive allows you to set additional headers for specific paths. :::important If you specify Access-Control headers in the `headers` directive, they will override the ones set by `cors` for that specific path. ::: ## SEO with Prerender Single Page Applications render content with JavaScript, which most crawlers can't process—they see an empty page instead of your content. This affects traditional search engines, social media platforms, and AI tools like ChatGPT, Perplexity, and Claude. The Static service includes built-in support for Prerender.io, which automatically detects crawlers and serves them pre-rendered HTML while your users get the full interactive experience. ### Setup 1. Set the `PRERENDER_TOKEN` secret variable with your Prerender.io token 2. The service automatically configures necessary rewrites based on user agents ### Custom Prerender Host If you're using a custom Prerender host, add it to environment variables in `zerops.yaml`: ```yaml title="zerops.yaml" run: envVariables: - PRERENDER_HOST=your.prerender.host ``` :::note Default The default host is `service.prerender.io` if not specified. ::: ## Framework Integration The Static service handles static builds from any modern framework. Here's the typical deployment pattern: ```yaml title="zerops.yaml" zerops: - setup: app build: base: nodejs@20 buildCommands: - npm install - npm run build deployFiles: - dist/~ # Your framework's output directory run: base: static ``` The key is pointing `deployFiles` to wherever your framework outputs its built files (`dist/`, `build/`, `.output/public/`, etc.). This configuration: 1. Uses Node.js for building the application 2. Installs dependencies and builds the application 3. Deploys the resulting static files to the Static service You can enhance this basic setup with: - Custom redirects for URL management - Prerender.io integration for SEO - Additional routing rules as needed For framework-specific examples, check out our [recipe collection](https://github.com/zeropsio/recipe-analog-static). ## Advanced Topics ### Switching to Full Nginx If you need more control over your Nginx configuration: 1. Go to your Static service overview in the UI 2. Click the three vertical dots in the left panel 3. Select **Need to switch to full Nginx service?** 4. Copy the generated Nginx configuration 5. Use this configuration as a starting point for a full Nginx service :::tip This allows you to move to a more customizable setup while maintaining your existing routing logic. ::: ### Complex Multi-Domain Setups For advanced scenarios involving multiple domains and complex routing: ```yaml title="zerops.yaml" run: routing: redirects: # Product-specific domain - from: https://product-promo.com/* to: https://main-site.com/products status: 302 # Campaign domain - from: https://special-offer.com/* to: https://main-site.com/campaign status: 302 # Legacy domains and subdomains - from: https://*.legacy-domain.com/* to: https://main-site.com status: 302 ``` ## Complete Examples ### Development Setup Configuration for local development with CORS and API proxying: ```yaml title="zerops.yaml" run: routing: # CORS with proper quoting cors: "'*' always" redirects: # API requests - from: /api/* to: https://api.your-domain.com status: 302 ``` ### Production Setup with Security Security-enhanced configuration for production environments: ```yaml title="zerops.yaml" run: routing: headers: # Custom headers for default location - for: "/*" values: X-Frame-Options: "'DENY' always" X-Content-Type-Options: "'nosniff' always" # Note the proper escaping of single quotes Content-Security-Policy: '"default-src ''self''" always' ```