Files
landing-api/config/site.php
Josep Piguillem 773d3901c6 Warn about calls closing and content coming off the site
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>
2026-10-01 10:16:24 +02:00

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],
];