The site now speaks German, French, Spanish, Ukrainian, Russian, Chinese, Korean and Hindi alongside English — 623 strings per language, 5,607 translations, covering the landing page, the key exchange, the chat, group calls and every error along the way. Each locale is a real page at a real URL (/de/, /fr/, …), generated at build time from locales/*.json. That is the whole point: the app is client-rendered, so a language that only swaps strings at runtime has no address for a crawler to index and no link anyone can share. Each page carries its own canonical, a reciprocal hreflang cluster and translated schema.org. The URL decides the language, always. A stored preference only applies where the address does not say, and nothing ever redirects on Accept-Language — that is how sites become invisible to search engines outside one country. robots.txt and sitemap.xml did not exist before; both are generated now. Bugs found on the way, each with a test that would have caught it: - partner logos used page-relative paths and 404'd from any /xx/ page - post-build stamped ?v= only into the root shell, leaving locales behind - "Back online" appeared on every page load, not just after being offline - update timestamps were hard-coded to US format for every reader - t() threw on a partial window, taking whole components down with it
249 lines
8.3 KiB
ApacheConf
249 lines
8.3 KiB
ApacheConf
# SecureBit.chat - Apache Configuration
|
|
# Comprehensive caching configuration for forced updates
|
|
|
|
# ============================================
|
|
# MIME TYPES - MUST BE FIRST (before other rules)
|
|
# ============================================
|
|
# Critical: Set MIME types BEFORE any other rules to ensure correct Content-Type headers
|
|
<IfModule mod_mime.c>
|
|
# JavaScript modules - explicit order matters
|
|
AddType application/javascript .jsx
|
|
AddType application/javascript .mjs
|
|
AddType application/javascript .js
|
|
AddType application/json .json
|
|
|
|
# Fonts
|
|
AddType font/woff .woff
|
|
AddType font/woff2 .woff2
|
|
AddType application/font-woff .woff
|
|
AddType application/font-woff2 .woff2
|
|
|
|
# Service Worker
|
|
AddType application/manifest+json .webmanifest
|
|
</IfModule>
|
|
|
|
# Force Content-Type headers (override any server defaults)
|
|
<IfModule mod_headers.c>
|
|
# All JavaScript files including JSX - CRITICAL for ES modules
|
|
<FilesMatch "\.(js|mjs|jsx)$">
|
|
Header always set Content-Type "application/javascript; charset=utf-8"
|
|
</FilesMatch>
|
|
</IfModule>
|
|
|
|
# Enable mod_rewrite
|
|
<IfModule mod_rewrite.c>
|
|
RewriteEngine On
|
|
RewriteBase /
|
|
</IfModule>
|
|
|
|
# ============================================
|
|
# CRITICAL FILES - NO CACHING
|
|
# ============================================
|
|
|
|
# meta.json - versioning file (never cache)
|
|
<FilesMatch "meta\.json$">
|
|
<IfModule mod_headers.c>
|
|
Header set Cache-Control "no-cache, no-store, must-revalidate, max-age=0"
|
|
Header set Pragma "no-cache"
|
|
Header set Expires "0"
|
|
Header set X-Content-Type-Options "nosniff"
|
|
</IfModule>
|
|
</FilesMatch>
|
|
|
|
# HTML files - always fresh
|
|
<FilesMatch "\.(html|htm)$">
|
|
<IfModule mod_headers.c>
|
|
Header set Cache-Control "no-cache, no-store, must-revalidate, max-age=0"
|
|
Header set Pragma "no-cache"
|
|
Header set Expires "0"
|
|
# Remove ETag for validation
|
|
Header unset ETag
|
|
FileETag None
|
|
</IfModule>
|
|
</FilesMatch>
|
|
|
|
# Service Worker - no cache
|
|
<FilesMatch "sw\.js$">
|
|
<IfModule mod_headers.c>
|
|
Header set Cache-Control "no-cache, no-store, must-revalidate, max-age=0"
|
|
Header set Pragma "no-cache"
|
|
Header set Expires "0"
|
|
Header set Service-Worker-Allowed "/"
|
|
</IfModule>
|
|
</FilesMatch>
|
|
|
|
# manifest.json - no cache
|
|
<FilesMatch "manifest\.json$">
|
|
<IfModule mod_headers.c>
|
|
Header set Cache-Control "no-cache, no-store, must-revalidate, max-age=0"
|
|
Header set Pragma "no-cache"
|
|
Header set Expires "0"
|
|
</IfModule>
|
|
</FilesMatch>
|
|
|
|
# ============================================
|
|
# STATIC RESOURCES - AGGRESSIVE CACHING
|
|
# ============================================
|
|
|
|
# JavaScript files in dist/ - no cache (for updates)
|
|
<FilesMatch "^dist/.*\.(js|mjs)$">
|
|
<IfModule mod_headers.c>
|
|
Header set Cache-Control "no-cache, no-store, must-revalidate, max-age=0"
|
|
Header set Pragma "no-cache"
|
|
Header set Expires "0"
|
|
Header set X-Content-Type-Options "nosniff"
|
|
</IfModule>
|
|
</FilesMatch>
|
|
|
|
# JavaScript files with hashes in other locations - long cache
|
|
<FilesMatch "\.(js|mjs)$">
|
|
<IfModule mod_headers.c>
|
|
# Files with hashes in name - cache for one year
|
|
Header set Cache-Control "public, max-age=31536000, immutable"
|
|
Header set X-Content-Type-Options "nosniff"
|
|
</IfModule>
|
|
</FilesMatch>
|
|
|
|
# The dictionary is imported by a bare path from a plain browser module, so nothing
|
|
# busts its cache. It must revalidate or translations freeze at whatever a visitor
|
|
# first loaded.
|
|
<FilesMatch "^(index|generated)\.js$">
|
|
<IfModule mod_headers.c>
|
|
Header set Cache-Control "no-cache, must-revalidate"
|
|
</IfModule>
|
|
</FilesMatch>
|
|
|
|
# CSS files - long cache
|
|
<FilesMatch "\.css$">
|
|
<IfModule mod_headers.c>
|
|
Header set Cache-Control "public, max-age=31536000, immutable"
|
|
</IfModule>
|
|
</FilesMatch>
|
|
|
|
# Images - long cache
|
|
<FilesMatch "\.(jpg|jpeg|png|gif|webp|svg|ico)$">
|
|
<IfModule mod_headers.c>
|
|
Header set Cache-Control "public, max-age=31536000, immutable"
|
|
</IfModule>
|
|
</FilesMatch>
|
|
|
|
# Fonts - long cache
|
|
<FilesMatch "\.(woff|woff2|ttf|otf|eot)$">
|
|
<IfModule mod_headers.c>
|
|
Header set Cache-Control "public, max-age=31536000, immutable"
|
|
Header set Access-Control-Allow-Origin "*"
|
|
</IfModule>
|
|
</FilesMatch>
|
|
|
|
# Audio/Video - long cache
|
|
<FilesMatch "\.(mp3|mp4|webm|ogg)$">
|
|
<IfModule mod_headers.c>
|
|
Header set Cache-Control "public, max-age=31536000, immutable"
|
|
</IfModule>
|
|
</FilesMatch>
|
|
|
|
# Crawler-facing files - short cache so search engines re-read them
|
|
<FilesMatch "^(robots\.txt|sitemap\.xml)$">
|
|
<IfModule mod_headers.c>
|
|
Header set Cache-Control "public, max-age=3600"
|
|
</IfModule>
|
|
</FilesMatch>
|
|
|
|
# ============================================
|
|
# SECURITY
|
|
# ============================================
|
|
|
|
# XSS Protection
|
|
<IfModule mod_headers.c>
|
|
Header set X-XSS-Protection "1; mode=block"
|
|
Header set X-Content-Type-Options "nosniff"
|
|
Header set Referrer-Policy "strict-origin-when-cross-origin"
|
|
Header set X-Frame-Options "DENY"
|
|
# Force HTTPS (2 years + preload) to close the first-visit SSL-strip window.
|
|
Header set Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
|
|
# Restrict powerful features; camera + microphone kept for QR scanning and calls.
|
|
Header set Permissions-Policy "camera=(self), microphone=(self), geolocation=(), payment=(), usb=(), magnetometer=(), gyroscope=(), accelerometer=()"
|
|
</IfModule>
|
|
|
|
# Content Security Policy (frame-ancestors and report-uri only work in HTTP headers, not meta tags)
|
|
<IfModule mod_headers.c>
|
|
Header set Content-Security-Policy "frame-ancestors 'none'; report-uri /csp-report; report-to csp-endpoint;"
|
|
</IfModule>
|
|
|
|
# ============================================
|
|
# GZIP COMPRESSION
|
|
# ============================================
|
|
|
|
<IfModule mod_deflate.c>
|
|
# Compress text files
|
|
AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css text/javascript application/javascript application/json application/xml
|
|
|
|
# Compress fonts
|
|
AddOutputFilterByType DEFLATE font/woff font/woff2 application/font-woff application/font-woff2
|
|
</IfModule>
|
|
|
|
|
|
# ============================================
|
|
# CLOUDFLARE RULES
|
|
# ============================================
|
|
|
|
# Cloudflare can cache static files, but should not cache:
|
|
# - meta.json
|
|
# - index.html
|
|
# - sw.js
|
|
# - manifest.json
|
|
|
|
# These rules are applied at Cloudflare Page Rules level
|
|
# (see CLOUDFLARE_SETUP.md documentation)
|
|
|
|
# ============================================
|
|
# SPA FALLBACK
|
|
# ============================================
|
|
|
|
# If file not found, redirect to index.html (for SPA routing)
|
|
<IfModule mod_rewrite.c>
|
|
# Locale shells first: a deep link under /de/ must fall back to the German page,
|
|
# not the English one. Rewritten by scripts/build-i18n.js from locales/site.json.
|
|
# BEGIN generated locale shells
|
|
RewriteCond %{REQUEST_FILENAME} !-f
|
|
RewriteCond %{REQUEST_FILENAME} !-d
|
|
RewriteRule ^de(/.*)?$ /de/index.html [L]
|
|
RewriteCond %{REQUEST_FILENAME} !-f
|
|
RewriteCond %{REQUEST_FILENAME} !-d
|
|
RewriteRule ^fr(/.*)?$ /fr/index.html [L]
|
|
RewriteCond %{REQUEST_FILENAME} !-f
|
|
RewriteCond %{REQUEST_FILENAME} !-d
|
|
RewriteRule ^es(/.*)?$ /es/index.html [L]
|
|
RewriteCond %{REQUEST_FILENAME} !-f
|
|
RewriteCond %{REQUEST_FILENAME} !-d
|
|
RewriteRule ^uk(/.*)?$ /uk/index.html [L]
|
|
RewriteCond %{REQUEST_FILENAME} !-f
|
|
RewriteCond %{REQUEST_FILENAME} !-d
|
|
RewriteRule ^ru(/.*)?$ /ru/index.html [L]
|
|
RewriteCond %{REQUEST_FILENAME} !-f
|
|
RewriteCond %{REQUEST_FILENAME} !-d
|
|
RewriteRule ^zh(/.*)?$ /zh/index.html [L]
|
|
RewriteCond %{REQUEST_FILENAME} !-f
|
|
RewriteCond %{REQUEST_FILENAME} !-d
|
|
RewriteRule ^ko(/.*)?$ /ko/index.html [L]
|
|
RewriteCond %{REQUEST_FILENAME} !-f
|
|
RewriteCond %{REQUEST_FILENAME} !-d
|
|
RewriteRule ^hi(/.*)?$ /hi/index.html [L]
|
|
# END generated locale shells
|
|
|
|
RewriteCond %{REQUEST_FILENAME} !-f
|
|
RewriteCond %{REQUEST_FILENAME} !-d
|
|
RewriteCond %{REQUEST_URI} !^/meta\.json$
|
|
RewriteCond %{REQUEST_URI} !^/sw\.js$
|
|
RewriteCond %{REQUEST_URI} !^/manifest\.json$
|
|
RewriteRule ^(.*)$ /index.html [L]
|
|
</IfModule>
|
|
|
|
# ============================================
|
|
# LOGGING (optional)
|
|
# ============================================
|
|
|
|
# Uncomment for debugging
|
|
# LogLevel rewrite:trace3
|
|
|