A dashboard widget lists what expires in the next two weeks, and a daily command reminds whoever edits it a week and a day ahead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
182 lines
7.2 KiB
PHP
182 lines
7.2 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
return [
|
|
|
|
/*
|
|
|--------------------------------------------------------------------------
|
|
| Public frontend
|
|
|--------------------------------------------------------------------------
|
|
|
|
|
| The public site is a separate, server-rendered application reading from
|
|
| /api/public/v1. Which framework it is built with is deliberately not
|
|
| recorded anywhere here: these settings describe the relationship, not the
|
|
| technology on the other end.
|
|
|
|
|
*/
|
|
|
|
// Canonical origin of the public site. Used to build absolute URLs and
|
|
// hreflang alternates the frontend can emit verbatim.
|
|
'url' => 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],
|
|
|
|
];
|