Журнал изменений
Align the alphabetical listing (op=liste) across all eight modules that provide it: empty results now return a proper 404 page, the letter filter is case-insensitive, and the unreachable empty_alert is removed.
Core changes:
- Empty list -> 404 (modules/{faq,files,help,links,media,news,pages,shop}/index.php):
liste(): "if (!$rows) setError(404)" replaces the partial "num > 1" guard
- Empty letter filter (e.g. let=w) now yields the branded 404 page like num=99
- Case-insensitive letter filter:
Both the result query and the pager count query use UCASE(title) LIKE BINARY UCASE(:let)
- let=p and let=P (and Cyrillic) now match identically
- Fixes result/pager parity (pager previously did not UCASE the title)
- Dead code removal:
Dropped the now-unreachable 'empty_alert' key from the liste() content-list call (empty result is caught by the 404 before the list renders)
Also folds a minor pre-existing spacing fix in the file upload note (_ADDFNOTE2).
Benefits:
- Predictable 404 semantics for missing/empty listings
- Letter filter works regardless of case; count and results stay consistent
Bring the self-contained 502/504 fallback page in line with the unified .sl-alert component and make its recovery links work even while the local server is down.
Core changes:
- Error markup (error.html):
Error text now uses .sl-alert sl-alert-warn with an inline SVG icon
- Self-contained: no icon font or external CSS (page must render when PHP is down)
- Solid exclamation-triangle icon, soft tinted box, accent left bar, subtle shake
Recovery links (brand, home, search) point to the always-up vendor site so they remain usable during an outage of the local server
Benefits:
- Error page matches the rest of the system visually
- Links stay functional when the user's server is unavailable
Replace the divergent per-theme alert styles (.sl-warn/.sl-info and the admin-only .sl-callout-*) with a single .sl-alert component shared by both themes and the Markdown parser, add a 5-second flash auto-hide, and emit the favicon link for the admin area as well.
Core changes:
- Alert component (templates/lite/assets/css/theme.css, templates/admin/assets/css/theme.css):
One .sl-alert vocabulary with five tones (info, success, warn, error, accent)
- Icon via ::before, tone-bound looping animation, prefers-reduced-motion guard
- Identical class names in both themes; theme keeps its own visual style
Removed dead legacy alert CSS
- Dropped admin .sl-callout-* and the legacy .sl-warn/.sl-info component
- Dropped unused .sl-table-block
- Fragments (templates/lite|admin/fragments/alert.html, blockquote.html):
- Alert fragment renders .sl-alert with type/is_warn fallback (contract preserved)
- Blockquote GFM callouts render as .sl-alert sl-alert-<tone>
- Parser (core/classes/parser.php):
Map callout kind to semantic tone (NOTE->info, TIP->success, IMPORTANT->accent, WARNING->warn, CAUTION->error); styled fallback when no template engine
- Flash auto-hide (templates/admin/assets/js/alerts.js, core/helpers.php, core/system.php):
- Timer reduced from 15s to 5s (JS default and data-sl-autohide callers)
- Branded head (core/system.php, favicon.ico):
- setHead() now emits the favicon link for admin too (was frontend-only)
- Added root favicon.ico generated from the theme favicon for legacy probes
- Editor plugin (plugins/editors/toastui/assets/slaed-upload.js):
- Upload status messages use .sl-alert sl-alert-warn / -info
Benefits:
- Single alert vocabulary reused everywhere, less duplicated CSS
- Consistent alert/callout look and behavior across themes and content
- Favicon served on every surface; no more /favicon.ico fallback miss
Breaking: .sl-callout-* classes removed (no external consumers).
Remove the XML <![CDATA[ ]]> markers around the inlined logo's inner <style>. They are unnecessary in an HTML document (style content is already raw text) and were flagged by the HTML validator. The logo renders identically since the .fil* rules remain in the <style> block.
Route every web-server error code to the SLAED branded page and add a fully self-contained static fallback for 502/504, so visitors always get the themed error UI instead of a bare nginx/Apache page even when PHP is down. Several modules that silently fell back to the home page now return SEO-correct 404s.
Core changes:
- Static 502/504 fallback (error.html):
New self-contained page served by the web server when PHP is unavailable
- Inline CSS (theme rules), inline SVG logo and icons, no external file
- Inline JS sets the current year; 2026 is the no-JS fallback
- noindex; renders identically to the live branded error page
- ?error= contract and status reasons (core/security.php):
Whitelist ?error= codes 400 401 402 403 404 500 502 503 504
- Forged or unknown codes fall through and cannot emit arbitrary statuses
- Extend the setError() status-reason map (401, 402, 502, 504)
- SEO-correct 404s instead of soft home redirects:
- index.php: unknown, inactive or unservable module now setError(404)
- modules/changelog/index.php: out-of-range page now setError(404)
- Web-server wiring and documentation:
- .htaccess: ErrorDocument 502/504 to /error.html for Apache parity
docs/PERFORMANCE.md and admin/info/editor/ru.md: nginx/Apache recipes, fastcgi_intercept_errors off, the ?error= whitelist and the static page
- Error-page card styling (templates/lite/assets/css/theme.css):
- Adjust .sl-msg-search and .sl-msg-foot widths and spacing to fit the card
- Regression guard (tests/ErrorPageContractTest.php):
- Assert error.html is self-contained (no external CSS/JS/img) and branded
- Assert the ?error= whitelist is fully covered by setError() reasons
Benefits:
- Correct HTTP status on every error path, improving SEO and crawler signals
- Branded, no-store error UI even during a PHP-FPM outage
- One error-rendering contract shared by nginx and Apache
Technical notes:
- error.html is generated from the lite theme with only used rules and tokens
nginx still serves real 502/504 from the static file; 502/504 are whitelisted only so a manual ?error= still renders when PHP is alive
- No database or schema changes; backward compatible
Uncaught exceptions, fatal errors, and the DB-connection failure now produce a consistent response: the full error detail in debug mode, a clean 500 in production, and never an HTML page inside a non-HTML response. Detail goes to the log; nothing internal is shown to visitors in production.
Core changes:
- Error responder (core/security.php):
- Add setErrorOut(): recursion-guarded; skips non-HTML requests (go=1/2/3/4/5/asset/captcha/rss/xsl or headers already sent) with a status-only 500; debug shows the detail via setExit() (status 200, so nginx never intercepts it); production renders setError(500)
- set_exception_handler() now routes through setErrorOut() and logs the full trace; rendering is decoupled from security.error_log so a clean 500 page appears even with logging off
- register_shutdown_function() logs fatals (when enabled) and sets a 500 status without a heavy render (process state may be broken)
- setError() status map gains 503 Service Unavailable
- Database connection (core/classes/pdo.php):
- On PDOException, gate the detail by security.error: debug shows it (setExit), production renders setError(500); the detail is logged in both cases
Technical notes:
- App-emitted errors render the SLAED page with Cache-Control: no-store; status via http_response_code() (HTTP/2 safe)
- Debug detail is served as 200 so it survives nginx without fastcgi_intercept_errors off; the full stack trace is written to the log
- Behavior change: uncaught errors no longer fall through to raw PHP output; production no longer leaks DB internals to the page
Document that PHP-generated 404/403/503/500 must pass through (fastcgi_intercept_errors off) so SLAED's branded page and no-store headers reach the client, and separate app-emitted 5xx from infrastructure 5xx (502/504) that only nginx can answer.
Core changes:
- Performance guide (docs/PERFORMANCE.md):
- New "PHP error pages" subsection under the web-server configuration section
A DB-connection failure and the site-closed gate returned 200, which misleads crawlers and monitoring. They now carry the correct status, and the DB error detail is logged instead of shown to visitors.
Core changes:
- Database connection (core/classes/pdo.php):
- On PDOException, log the detail via Logger and render setError(500) instead of setExit() (was 200; also stops leaking the raw DB message to the page)
- Maintenance gate (index.php):
- Closed-site response now sends 503 Service Unavailable while keeping the _CLOSE_TEXT page
- Error helper (core/security.php):
- setError() status map gains 503 Service Unavailable
Technical notes:
- App-emitted 5xx render the SLAED page (status via http_response_code(), Cache-Control: no-store)
- captcha JSON 503 left as-is; infra 5xx (502/504) remain nginx's responsibility
Align the error page presentation with the removed auto-redirect.
Core changes:
- Error page (templates/lite/pages/message.html):
- Render "<title> - <sitename>" when a page title is set
- Localization (lang/*.php):
- _ERROR_PAGE now invites returning home or using search instead of announcing a redirect (de/en/fr/pl/ru/uk)
Missing content, out-of-range list pagination and access-restricted pages now emit proper 404/403 instead of redirecting or returning 200, so crawlers stop indexing soft-error pages. Error rendering is consolidated into one setError() helper.
Core changes:
- Error helper (core/security.php):
- Add setError(int $code): status via http_response_code(), conditional logging, standard error page
- Drop the meta-refresh auto-redirect from setExit() (soft-404 / WCAG 2.2.1 anti-pattern)
- Route the bootstrap $_GET['error'] handler through setError(), removing the 40-line $http status map
- Frontend modules (modules/*/index.php):
- view(): 404 when the item does not exist
- list/liste(): 404 when a page beyond the first yields no rows
- forum: 404 for out-of-range topic pages and unpublished topics, 403 when category read is denied
- broken()/loading(): 404 on invalid requests
- Module gates (index.php):
- view=1 / view=2 access denials now send 403
Technical notes:
- http_response_code() is HTTP/2-safe; error responses keep Cache-Control: no-store
- Backward compatible; php -l and phpstan clean