Site.json config file Last updated: 20. Oct 2026
Configuration reference for site.json – controls minification, HTTP headers, URL rules, image policy, security level, template syntax, the sign-in card and the site router per site.
Configuration file placed at the root of a site (#/site.json). Provides control over caching, minification, HTTP headers, image policy, security level and template syntax per site.
Structure
File #/site.json
{
"cache": {
"enabled": true
},
"headers": {
"X-Custom-Header": "value"
},
"minify": {
"enabled": true,
"provider": "nuglify",
"js": true,
"css": true,
"html": true,
"cleanInvisible": 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": [ ... ],
"abuseBlocking": "true"
},
"hashjs": {
"compatibilityMode": true
}
}
All sections are optional. Missing sections mean default behaviour is preserved.
A section can also be its own file in the # folder — #/images.json, #/security.json, #/headers.json, #/cache.json, #/minify.json, #/hashjs.json, #/login.json, #/router.json — containing just the section. If a section is in both places, they are merged and the file wins for a key set in both; the site console warns when the two have different values for the same key. A site built on an app inherits the app's settings key by key — see the note below and Configure site settings and inherit them from the app.
headers
Key/value pairs added as HTTP headers on all responses from the site. They override Docly's default headers. If the site's #/headers.json sets the same header, headers.json wins — it is the more specific file — and site.json only fills in the headers it does not set. A site inherits the app's headers key by key; set a header to null to remove it, also one Docly would otherwise send by default.
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.
cache
enabled(bool) – Overrides the publication's Enable caching setting for minification and compression of generated pages. Without acacheblock the publication setting applies. Acacheblock withoutenabledcounts asfalse.
Note: the server-side page cache itself (cached HTML and HTTP 304) follows the publication's Enable caching setting.
minify
Minification only runs when caching is on — the publication's Enable caching, or
cache.enabledabove. With caching off, pages are sent as rendered, whateverminifysays.
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. Always done with NUglify, whateverprovidersays.css(bool, default:true) – Minify CSS. Always done with NUglify, whateverprovidersays.html(bool, default:false) – Minify HTML.provideronly applies to HTML.cleanInvisible(bool, default:true) – Removes invisible characters (zero-width, bidi control and tag characters) from the output. Applies even whenenabledisfalse; set it tofalseto keep them.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: when caching is on, HTML is compressed with Zeta as the legacy provider.
In an app: a site inherits the app's settings key by key. For each section Docly merges four layers, the most specific last: the app's site.json, the app's section file (for example headers.json in the template's root), the site's own #/site.json and the site's own section file (for example #/headers.json). Objects are merged key by key, a list replaces the app's list as a whole, and null removes a value. "$inherit": false in a section in the site makes the site ignore the app for that section. Docly does not copy the app's site.json into the site: the settings Docly writes itself go to the site's #/images.json. Details: Configure site settings and inherit them from the app.
Security headers
The headers property in site.json is the recommended place to set HTTP security headers for a single site. For headers that every site on an app should share, put them in the app — in headers.json in the template's root, or in the template's site.json — see the note at the top of this page. 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> | Deprecated — use |
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> | Obsolete — removed from the standard and ignored by current browsers. Use |
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 | Obsolete — current browsers ignore it. Use the CSP directive |
>
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: strict-origin-when-cross-origin
The full URL is sent with requests within your own site. Other sites only get the origin (for example https://example.com/), and nothing is sent from HTTPS to HTTP. This is the default in current browsers and the value Docly sends on its own pages.
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": "strict-origin-when-cross-origin",
"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.
The settings from the Image sizes tab and the sizes Docly discovers are written to the site's #/images.json — never to site.json, and never to the app. An images section in site.json still works, but Docly moves it to #/images.json the first time it saves image settings. A site's allowedSizes replaces the app's list as a whole.
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 the site's #/images.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 |
abuseBlocking |
| Whether the site refuses addresses that Docly's IP data provider has marked as abusive. |
{
"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 or through their app. It is normal unless Docly has chosen otherwise, and it is never higher than elevated, so it never removes a site from search results.
- The site's own
levelalways wins, then the app's. 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.
hashjs — template syntax (compatibility mode)
The optional hashjs block decides how the site's .hash pages are compiled. By default they run in compatibility mode: plain JavaScript plus Docly's older template syntax — bracket conditions such as #[Field]#…#[/Field]#, the text operators ^=, ^!, ~=, ~! and $=, function names in any letter case, and .Length / .Count.
Set compatibilityMode to false and the pages are compiled as plain JavaScript, the same way files under #/API/ always are.
Recommended for new sites: start with compatibilityMode set to false — the strictest mode. See Start new sites in strict JavaScript mode.
A site on an app only needs the block. A site inherits the app's settings key by key, so a #/site.json with only hashjs in it keeps the template's headers, image policy and security level (see the note at the top of this page).
Key | Values | Description |
|---|---|---|
compatibilityMode |
|
|
{
"hashjs": {
"compatibilityMode": false
}
} Compatibility mode (default) | compatibilityMode: false | |
|---|---|---|
| Bitwise XOR / NOT | Bitwise XOR / NOT |
| XOR assignment, when it stands alone on a line (since October 2026 — before that the line did nothing) | XOR assignment |
| Contains / does not contain ( | Not available — |
| Bracket condition | Syntax error |
| Syntax error | Allowed |
| Work (case-insensitive names, legacy helpers) | Use |
What it covers. The site's own .hash pages, including display templates such as #/Blog post.hash, and the files they include. It does not cover templates stored in a schema — those have their own Modern syntax setting on each template, because they are also rendered outside the site (downloads, previews, PDF). See Create display templates for your schemas.
Before you switch it off on a live site, make sure no page uses the bracket syntax or the text operators: a page that does stops compiling and returns an error. The compile check in the editor reads the same setting, so open and save the pages after changing the value to see which ones fail. It reads the site.json in the workspace itself — a value that only comes from an app is not seen by the check.
login — sign-in card
The login block (or the file #/login.json) holds the name and logo on the Docly sign-in page.
"login": {
"name": "Acme Customer Portal",
"logo": "/assets/acme-icon.png"
}
router — the site router
The router block (or the file #/router.json) names a script in #/Services/ that runs before every request to the site. It lets the request through, serves another path, redirects it or answers it itself — and can set cookies and headers on the way.
"router": { "script": "Router.js" }
A path inside #/Services/ — Router.js, or with folders, Routing/Router.js — using letters, digits, ., _, - and /, ending in .js; no part may start with .. An invalid path, a missing file or a router section that cannot be read answers every request with 500. On a publication that requires sign-in, Docly's sign-in comes first and the router runs after it. See The site router.
For workspace-wide assets and the home page, see The Root folder.