Site.json config file Last updated: 28. Sep 2026
Configuration reference for site.json – controls minification, HTTP headers, URL rules, image policy and security level per site.
Configuration file placed at the root of a site (#/site.json). Provides control over minification, HTTP headers, URL rules, image policy and security level per site.
Structure
File #/site.json
{
"urls": {
"cleanUrls": false
},
"headers": {
"X-Custom-Header": "value"
},
"minify": {
"enabled": true,
"provider": "nuglify",
"js": true,
"css": true,
"html": true,
"settings": {
"html": { ... },
"js": { ... },
"css": { ... }
}
},
"images": {
"sizePolicy": "auto",
"onDisallowed": "nearest",
"maxDimension": 2000,
"defaultFormat": "webp",
"quality": { "webp": 78 },
"allowedSizes": [ ... ],
"discoveredSizes": [ ... ]
},
"security": {
"level": "normal",
"clearanceMinutes": 60,
"until": null,
"exclude": [ ... ]
}
}
All sections are optional. Missing sections mean default behaviour is preserved.
headers
Key/value pairs added as HTTP headers on all responses from the site. In case of conflict with existing headers, site.json wins.
See the Security headers chapter below for recommended values for common security-related headers such as Content-Security-Policy, Strict-Transport-Security and Permissions-Policy.
minify
enabled(bool, default:false) – Enables minification via the selected provider. Whenfalse, legacy compression is used (as before).provider(string, default:"zeta") –"zeta"or"nuglify".js(bool, default:true) – Minify JavaScript (requires NUglify).css(bool, default:true) – Minify CSS (requires NUglify).html(bool, default:false) – Minify HTML.settings(object) – Provider-specific settings (see below).
Note: JS and CSS are currently only minified for the HTML pipeline. Standalone JS/CSS files go through the binary path and are not yet supported.
settings (NUglify)
Three optional subsections – html, js and css – mapped directly to NUglify's settings classes. Unknown properties are ignored; missing properties receive NUglify defaults.
Without settings, NUglify's standard defaults are used.
{
"settings": {
"html": {
"removeAttributeQuotes": false,
"removeOptionalTags": false,
"minifyJs": false,
"minifyCss": false
},
"js": {
"preserveImportantComments": true
},
"css": {
"commentMode": "Important"
}
}
}
A full list of available properties can be found in the NUglify documentation for HtmlSettings, CodeSettings and CssSettings.
Examples
NUglify with defaults:
{
"minify": { "enabled": true, "provider": "nuglify", "html": true }
}
NUglify with custom settings:
{
"minify": {
"enabled": true,
"provider": "nuglify",
"html": true,
"settings": {
"html": { "removeOptionalTags": true },
"js": { "preserveImportantComments": false }
}
}
}
Behaviour without site.json
Existing sites without site.json are completely unaffected. All behaviour is identical to before, and HTML is compressed with Zeta as the legacy provider.
Security headers
The headers property in site.json is the recommended place to set HTTP security headers for your site. The following headers protect against common web vulnerabilities such as Cross-Site Scripting (XSS), Clickjacking, MIME type sniffing and insecure transport, and they are equipped with sensible Docly defaults.
A full, recommended site.json example using these headers is shown at the end of this chapter.
>
Content-Security-Policy
The "Content-Security-Policy" header is designed to prevent Cross-Site Scripting (XSS) and data injection attacks. It allows web administrators to control which resources can be loaded and executed, thus enhancing the website's security.
Docly™ recommendation: Content-Security-Policy: default-src 'self';
This default setting restricts all content types to load only from the origin of the served document, implementing a strong same-origin policy.
Value | Description |
|---|---|
default-src 'self' | Restricts all content types to load only from the origin of the served document (same-origin policy). |
script-src 'self' | Allows scripts to be loaded from the same origin as the document. |
script-src <source> | Specifies a whitelist of sources for scripts. |
img-src 'self' | Permits loading images from the same origin. |
img-src <source> | Defines valid sources for loading images. |
object-src 'none' | Disallows <object>, <embed>, and <applet> elements, blocking plugins. |
style-src 'self' | Allows stylesheets to be loaded from the same origin. |
style-src 'unsafe-inline' | Permits the use of inline styles in the document. |
font-src 'self' | Restricts font loading to the same origin. |
connect-src 'self' | Limits where the document can connect to via scripts (e.g., WebSocket, fetch). |
frame-src 'self' | Restricts sourcing of frames to the same origin. |
sandbox | Applies extra restrictions to the content in <iframe> tags. |
report-uri <URI> | Specifies where the browser should send reports if the content security policy is violated. |
child-src 'self' | Defines valid sources for web workers and nested browsing contexts. |
form-action 'self' | Restricts the URLs which can be used as the action of HTML forms. |
frame-ancestors 'self' | Specifies valid parents that may embed a page. |
plugin-types <type> | Restricts the set of plugins that can be embedded into a document. |
base-uri <source> | Defines a base URI for relative URIs within the document. |
>
X-Frame-Options
The "X-Frame-Options" HTTP header is used to control whether a browser should allow a webpage to be rendered in a <frame>, <iframe>, <embed>, or <object>. Its main function is to protect against 'clickjacking' attacks, where a malicious site tricks a user into clicking something different from what the user perceives, potentially revealing confidential information or taking control of their account.
Docly™ recommendation: X-Frame-Options: SAMEORIGIN
This setting ensures that pages can only be displayed in frames or iframes that are from the same origin as the page itself. It's a widely supported and effective measure for protecting against clickjacking attacks.
Value | Description |
|---|---|
DENY | The page cannot be displayed in a frame on any site, including the site it belongs to. |
SAMEORIGIN | The page can only be displayed in a frame on the same origin as the page itself. |
ALLOW-FROM uri | The page can be displayed in a frame, but only on the specified uri. This is less commonly used due to limited browser support. |
>
X-Content-Type-Options
The "X-Content-Type-Options" HTTP header is used to instruct the browser to stop automatically detecting and interpreting the content type of the resources it loads. This is particularly important for preventing MIME type sniffing vulnerabilities.
Docly™ default: X-Content-Type-Options: nosniff
This default value is universally applicable and is crucial for enhancing security by instructing the browser to strictly follow the declared content type in the MIME headers, thus preventing MIME type sniffing attacks.
Value | Description |
|---|---|
nosniff | Instructs the browser to refrain from MIME-sniffing a response away from the declared content-type. This setting helps prevent attacks based on MIME-type confusion. |
>
Strict-Transport-Security
The "Strict-Transport-Security" (HSTS) HTTP header is used to enforce secure connections to the server. Its primary function is to tell browsers that they should only interact with the server using secure HTTPS connections and not via the insecure HTTP protocol. This is a critical security measure for preventing "man-in-the-middle" attacks, where an attacker could intercept or alter the communication between the user and the website if transmitted over an unencrypted HTTP connection.
Docly™ recommendation: Strict-Transport-Security: max-age=31536000;
Value | Description |
|---|---|
max-age=<seconds> | Specifies the time, in seconds, that the browser should remember to access the site only over HTTPS. |
includeSubDomains | Instructs the browser to apply HSTS to all subdomains of the site's domain, enforcing HTTPS on the entire site. |
preload | An optional directive used to indicate the site's inclusion in browsers' preload lists for HSTS. |
>
Referrer-Policy
The "Referrer-Policy" HTTP header controls how much referrer information (the URL of the referring page) should be included with requests. This header is essential for privacy and security, as it governs the amount of information sent to the server when a user navigates from one page to another. Proper configuration can help prevent leakage of sensitive information through URLs.
Docly™ recommendation: Referrer-Policy: no-referrer-when-downgrade
This setting ensures that the full URL of the referring document is sent to the destination when the protocol security level is the same (HTTPS to HTTPS) or lower (HTTPS to HTTP). However, if the protocol security level is downgraded (from HTTPS to HTTP), it will not send any referrer information, thus preventing potential data leakage in unsecured environments.
Value | Description |
|---|---|
no-referrer | No referrer information will be sent along with requests. |
no-referrer-when-downgrade | Full URL is sent with requests to the same protocol security level. No referrer is sent when downgrading from HTTPS to HTTP. |
origin | Only the origin of the referring document is sent as referrer information. |
origin-when-cross-origin | Full URL is sent to same-origin requests, but only the origin is sent for cross-origin requests. |
same-origin | Full referrer is sent for same-origin requests but no referrer for cross-origin requests. |
strict-origin | Only the origin is sent as the referrer to a more secure destination (HTTPS -> HTTPS), but no referrer to a less secure destination (HTTPS -> HTTP). |
strict-origin-when-cross-origin | Full referrer is sent to same-origin requests and only the origin to more secure destinations, but no referrer to less secure destinations. |
unsafe-url | Full URL, including the path and query string, is sent with all requests regardless of origin. |
>
Permissions-Policy
The "Permissions-Policy" HTTP header (formerly known as "Feature Policy") is a mechanism that allows website administrators to control which browser features and APIs can be used in the browser. This header can enable or disable powerful features like geolocation, camera, microphone access, and more, within the site's own pages and for iframes.
This header is instrumental in enhancing security and privacy by restricting features that could be exploited by malicious content. For example, it can prevent third-party content embedded in iframes from accessing sensitive device features or user data without explicit permission.
By specifying the allowed features and the contexts in which they can be used, the Permissions-Policy header provides granular control over the execution environment of the web content, thereby contributing to a more secure and user-centric web experience.
Docly™ recommendation: Permissions-Policy: geolocation=(self), microphone=(), camera=()
Feature | Values | Description |
|---|---|---|
geolocation | (self), (*), (none) | Controls access to geolocation data. |
microphone | (self), (*), (none) | Manages access to the device's microphone. |
camera | (self), (*), (none) | Determines access to the device's camera. |
fullscreen | (self), (*), (none) | Controls the ability to display content in fullscreen mode. |
payment | (self), (*), (none) | Manages access to payment request APIs. |
>
A note on X-XSS-Protection
The X-XSS-Protection header is deprecated and should no longer be used. Modern browsers either ignore it or have removed support, and in some older browser versions the built-in XSS filter has itself been a source of vulnerabilities. Rely on a strong Content-Security-Policy instead.
>
Example: recommended site.json security headers
{
"headers": {
"Content-Security-Policy": "default-src 'self';",
"X-Frame-Options": "SAMEORIGIN",
"X-Content-Type-Options": "nosniff",
"Strict-Transport-Security": "max-age=31536000;",
"Referrer-Policy": "no-referrer-when-downgrade",
"Permissions-Policy": "geolocation=(self), microphone=(), camera=()"
}
} images
The optional images block controls image-size policy (which scaling sizes are allowed on image URLs), the default output format for scaled images and the encoder quality used when they are encoded. It is shared per folder — several templates and aliases share the same site.json, so the policy is managed in one place.
For full details on the size policy and the admin 'Image sizes' tab, see Image scaling & allowed sizes. For a step-by-step walkthrough of turning the policy on and registering the sizes, see the KB article Restrict and register image sizes. For output formats, see Image output formats (WebP / AVIF).
Key | Values | Description |
|---|---|---|
defaultFormat |
| Default output format for all scaled images — |
quality | number 1–100, or | Encoder quality for scaled images. A plain number applies to every format; the object form sets it per format with |
sizePolicy |
| Enforcement of scaling sizes on image URLs. See the allowed-sizes section. |
onDisallowed |
| What to do with a disallowed size. Empty → per policy: |
maxDimension | integer (px) | Hard cap on W/H in all modes (including |
allowedSizes |
| Owner-maintained list. Hand-edited; never touched by the system. |
discoveredSizes |
| Machine-maintained (auto + Scan). Can be cleared to reset. |
{
"images": {
"sizePolicy": "auto",
"onDisallowed": "nearest",
"maxDimension": 2000,
"defaultFormat": "webp",
"quality": { "jpeg": 85, "webp": 78, "avif": 50 },
"allowedSizes": ["1200x800x0", "400x400x1"],
"discoveredSizes": ["800x600x0", "150x150x1"]
}
} The WxHxM key: W = width in px (0 = free), H = height in px (0 = free), M = scaling mode (0 = ScaleToFit default, 1 = ScaleToFill, …). The key is the URL segment: /photo.jpg/1200x800x0/name.jpg. "1200x800" is treated as mode 0.
>
Quality
quality sets the encoder quality used when Docly re-encodes a scaled image. It applies to the lossy formats — JPEG, WebP and AVIF. PNG is lossless, so quality is ignored there.
Write it either as a single number that covers every format, or as an object with a value per format:
// one value for every format
{ "images": { "quality": 78 } }
// per format
{ "images": { "quality": { "jpeg": 90, "webp": 78, "avif": 50 } } }
// per format, with a fallback for the rest
{ "images": { "quality": { "webp": 78, "default": 70 } } } Lookup order: the key for the format being written → the plain number or the default key → the built-in default. jpg is accepted as an alias for jpeg.
Built-in defaults: JPEG 85, WebP 78, AVIF 50. The AVIF scale is not comparable to JPEG/WebP — 50 gives roughly the same perceived quality as JPEG 85 at a much smaller file size.
Where | Quality used |
|---|---|
URL scaling — |
|
scaleImage without a |
|
scaleImage with a | The value in the call. An explicit quality always wins. |
Quality can never be set from a URL. This is the same ownership model as allowedSizes: an arbitrary quality in the URL would mean one cache entry per (size × quality), so any visitor could grow the cache without limit.
Invalid values are ignored rather than fatal. A non-number or a value of 0 or less falls through to the next level; anything above 100 is clamped to 100. A typo in site.json will not break image serving.
Changing quality, defaultFormat or sizePolicy takes effect automatically. These settings are part of the image cache key, so URLs that have already been served are regenerated on the next request. There is no need to clear the site cache.
The quality fields are also editable in the Image sizes tab in the workspace folder properties, one field per format plus one for all other formats. An empty field means not set — the placeholder shows the built-in default that applies. Clearing every field removes the quality key from site.json.
Note for existing sites: URL-scaled JPEGs used to be encoded at quality 90, while scaleImage used 85. Both now use 85, so URL-scaled JPEGs come out slightly smaller than before. Set images.quality.jpeg: 90 if you want the previous behaviour back.
security — security level
The optional security block sets how strictly the site checks its visitors. From elevated and up, the visitors the level covers must pass a Cloudflare Turnstile check before the site serves anything — including pages that are already in the site cache. A visitor who passes gets a clearance that lasts clearanceMinutes, and browses normally until it runs out.
Raise the level while a site is being hammered by bots or scrapers, and lower it again when the attack is over — or set until so the site goes back to the platform default by itself (see Platform default below).
level | Who must pass the check | Search engines |
|---|---|---|
| Nobody. The firewall and automatic blocking work as always. | Indexed |
| Requests for known attack URLs the site does not have (such as | Indexed |
| Everyone except visitors from countries with high trust. | Blocked |
| Everyone. | Blocked |
Key | Values | Description |
|---|---|---|
level |
| Who must pass the check, see the table above. An unknown value counts as not set, and the platform default applies. |
clearanceMinutes | integer, default | How long a passed check lasts. 0 or negative → 60; capped at one week (10080). Shortening it applies immediately, also to clearances already given. |
until | date and time, e.g. | When your own level ends by itself; after that the platform default applies. Without a time zone, the server's local time is used. Empty → the level stays until someone changes it. A value that cannot be read is ignored, and the level stays. |
exclude |
| Paths on the site that never get the check — webhooks, payment callbacks, uptime monitoring. A trailing |
{
"security": {
"level": "underAttack",
"clearanceMinutes": 60,
"until": "2026-09-27T12:00",
"exclude": ["/api/webhooks/*", "/health"]
}
} Known attack URLs. From elevated and up, a request for a URL that scanners probe for — wp-login.php, .env, /.git/config, /wp-admin, /admin and similar — that your site does not have gets the check instead of the 404 page, even on the first request from an address. Every such request is counted against the visitor's IP, and an address that keeps probing is eventually blocked by the firewall. A visitor who passes the check gets the site's normal 404.
If your site really has such a page — an old .php page, or a page at /admin — nothing changes for it: it is served like any other page, and visiting it never counts against anyone. You do not need to add it to exclude.
Platform default. Docly can set a default level for all sites that do not set level themselves. It is normal unless Docly has chosen otherwise, and it is never higher than elevated, so it never removes a site from search results.
- Your own
site.jsonalways wins. To opt out of the platform default, set"level": "normal". - A misspelled
levelcounts as not set, and gives you the platform default. untilonly ends your own level. After it, the platform default applies.excludeandclearanceMinutesapply to the platform default too, so you can exclude paths without choosing a level yourself.
Search engines cannot get in from high and up. Crawlers cannot pass the check, so they only see the challenge page (marked noindex) or a 403. The same goes for monitoring, uptime checks, webhooks and other machine clients calling the site — put their paths in exclude. Keep the level up only as long as you need it, preferably with until — otherwise the site drops out of search results.
What a visitor without clearance gets:
- Page views (a normal page load in the browser) get a challenge page with the Turnstile check, HTTP 403. When the check passes, the page reloads with the content.
- Everything else —
fetchand API calls, images, files, form posts — gets HTTP 403 with{"ResultCode": -213, "Message": "Verification required."}. - Both responses are sent with
Cache-Control: no-store, so neither the browser nor a CDN in front of the site keeps them.
The clearance is a signed cookie, docly_clr_<site id>: HttpOnly, Secure on HTTPS, SameSite=Lax, and valid for this site only. It is not tied to the visitor's IP address, so people on mobile networks keep it when their address changes. Clearances are reset when the server restarts; visitors then pass the check again.
Reserved path: while the level is above normal, <site>/__docly/clearance is used by the challenge page and is not served from the site.
The country is taken from Docly's cache of earlier lookups — the check never waits for a new lookup. A visitor whose country is not known yet counts as neutral: checked on high, not on elevated. Which countries have high and low trust is set centrally by Docly.
Earlier form: "underAttack": true still works, and means the same as "level": "underAttack". It only applies when level is not set.
Keys in site.json are not case-sensitive; the documented form is camelCase.
Limits: single published documents (not a published folder) are not covered. Responses a browser or CDN already cached with a max-age before the level was raised cannot be recalled by Docly.
Web app manifest: browsers fetch <link rel="manifest"> without cookies, so the clearance is not sent and the manifest gets a 403 while the level is up — the page itself works, but "add to home screen" loses its name and icon. Either put the path in exclude (for example "/assets/icons/*"), or add crossorigin="use-credentials" to the link tag.
For workspace-wide assets and the home page, see The "Root" folder.