rtrim((string) env('SITE_URL', 'http://localhost:4321'), '/'), /* | How the frontend maps a locale onto a URL prefix. The default locale is | served without a prefix, which is what every i18n router this side has | met does out of the box. Turn it on if the frontend prefixes all of them. */ 'prefix_default_locale' => (bool) env('SITE_PREFIX_DEFAULT_LOCALE', false), /* | Path segment per content type, per locale, so the API can build complete | URLs instead of leaving the frontend to guess them. */ 'paths' => [ 'page' => ['es' => '', 'en' => '', 'ca' => ''], 'project' => ['es' => 'proyectos', 'en' => 'projects', 'ca' => 'projectes'], 'member' => ['es' => 'equipo', 'en' => 'team', 'ca' => 'equip'], 'call' => ['es' => 'colabora', 'en' => 'contribute', 'ca' => 'col-labora'], 'service' => ['es' => 'servicios', 'en' => 'services', 'ca' => 'serveis'], ], /* |-------------------------------------------------------------------------- | Preview |-------------------------------------------------------------------------- | | Shared secret that lets the frontend request unpublished content. Leave | it empty to disable previewing entirely — an unset value never grants | access. Generate one with: | php -r "echo bin2hex(random_bytes(24)), PHP_EOL;" | */ 'preview_token' => env('SITE_PREVIEW_TOKEN'), /* |-------------------------------------------------------------------------- | Public API |-------------------------------------------------------------------------- */ // Seconds a shared cache may serve a public response without revalidating. 'cache_seconds' => (int) env('SITE_CACHE_SECONDS', 60), /* | Requests per minute allowed on the public API. | | With an SSR frontend every visitor's page view arrives from the same | server IP, so this budget is for the whole site at once, not per person. | It must be generous, and `trusted_ips` lets the frontend bypass it. */ 'rate_limit' => (int) env('SITE_RATE_LIMIT', 600), 'trusted_ips' => array_values(array_filter( explode(',', (string) env('SITE_TRUSTED_IPS', '')) )), /* |-------------------------------------------------------------------------- | Telemetry |-------------------------------------------------------------------------- | | The frontend reporting back what it could not resolve. Redirects only | ever knew about slugs renamed here; addresses from the previous website, | from print, or from somebody else's link fail over there and were never | heard about at all. | | Token-gated rather than open like the read endpoints: this one writes, | and an unauthenticated write endpoint is a way to fill a table with | whatever a stranger likes. An empty token disables reporting entirely — | it never means "everybody may report", exactly as with the preview | secret. Generate one with: | php -r "echo bin2hex(random_bytes(24)), PHP_EOL;" | */ 'telemetry' => [ 'token' => env('SITE_INGEST_TOKEN'), // Reports per minute, per address. Generous because the frontend // batches, and because a burst of 404s is precisely the moment this // wants to be listening. 'rate_limit' => (int) env('SITE_TELEMETRY_RATE_LIMIT', 60), // Paths per request. A crawler walking a dead section produces dozens // in a second, and one request is cheaper than dozens. 'max_batch' => (int) env('SITE_TELEMETRY_MAX_BATCH', 50), /* | Never recorded, whatever the frontend reports. Most of what a public | site is asked for is somebody looking for an admin panel that was | never here, and a list where real broken links are buried under that | is a list nobody opens. Matched with fnmatch against the path. */ 'ignore' => [ '/wp-*', '/wordpress*', '/.env*', '/.git*', '/vendor/*', '/cgi-bin/*', '/xmlrpc.php', '/*.php', '/*.asp*', '/*.cgi', ], ], /* |-------------------------------------------------------------------------- | Submissions |-------------------------------------------------------------------------- | | The only place the public may write. Everything else on the public API is | read-only, so these settings are the whole attack surface of it. | */ 'submissions' => [ // A switch that closes the forms without a deploy, for when a spam run // gets past the rest. The frontend reads it from /site and hides the // form instead of showing one that will be refused. 'enabled' => (bool) env('SITE_ACCEPT_SUBMISSIONS', true), /* | Name of the field no human ever fills in. Bots complete every input | they find, so anything arriving with this set is filed as spam — | filed, not dropped, because an over-eager password manager can trip | it too and a real person must not vanish because of that. */ 'honeypot' => (string) env('SITE_HONEYPOT_FIELD', 'website'), /* | Submissions per minute from the same e-mail address, and per hour | across the whole site. | | Note what is *not* here: a per-IP budget. With an SSR frontend every | submission reaches this API from the frontend server's address, so a | per-IP limit would either throttle everybody at once or, since that | address is in `trusted_ips`, nothing at all. */ 'rate_limit' => (int) env('SITE_SUBMISSION_RATE_LIMIT', 3), 'hourly_limit' => (int) env('SITE_SUBMISSION_HOURLY_LIMIT', 60), /* | Window during which the same person answering the same call again is | treated as a double-click rather than a second submission. */ 'deduplicate_minutes' => (int) env('SITE_SUBMISSION_DEDUPE_MINUTES', 1440), // Also e-mail the panel users who can read the inbox. Off by default: // the notification bell is always on, and mail needs a working mailer. 'notify_by_mail' => (bool) env('SITE_SUBMISSION_MAIL', false), ], /* |-------------------------------------------------------------------------- | Expiry reminders |-------------------------------------------------------------------------- | | Days ahead of a call closing, or of published content coming off the | site, on which `content:expiring` tells the people who edit it. | */ 'expiry_reminders' => [7, 1], ];