Enhance website security and add SEO meta tags for better visibility

Implement robust security headers, including CSP with nonces, and integrate comprehensive SEO meta tags into the base template and homepage. Add inline styles for CSP compliance and improve theme management script for immediate theme application.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 48ecdb60-d0f0-4b75-95c9-34e409ef35fb
Replit-Commit-Checkpoint-Type: intermediate_checkpoint
This commit is contained in:
pac7
2025-09-22 16:06:47 +00:00
parent 95f94cc799
commit 6697d8890b
9 changed files with 315 additions and 52 deletions

View File

@@ -62,10 +62,6 @@ externalPort = 3000
localPort = 45245 localPort = 45245
externalPort = 3001 externalPort = 3001
[[ports]]
localPort = 46739
externalPort = 3002
[deployment] [deployment]
deploymentTarget = "autoscale" deploymentTarget = "autoscale"
run = [ run = [

View File

@@ -0,0 +1,97 @@
"""
Modern Security Headers Middleware for ThrillWiki
Implements Content Security Policy and other modern security headers.
"""
import secrets
import base64
from django.conf import settings
from django.utils.deprecation import MiddlewareMixin
class SecurityHeadersMiddleware(MiddlewareMixin):
"""
Middleware to add modern security headers to all responses.
"""
def _generate_nonce(self):
"""Generate a cryptographically secure nonce for CSP."""
# Generate 16 random bytes and encode as base64
return base64.b64encode(secrets.token_bytes(16)).decode('ascii')
def _modify_csp_with_nonce(self, csp_policy, nonce):
"""Modify CSP policy to include nonce for script-src."""
if not csp_policy:
return csp_policy
# Look for script-src directive and add nonce
directives = csp_policy.split(';')
modified_directives = []
for directive in directives:
directive = directive.strip()
if directive.startswith('script-src '):
# Add nonce to script-src directive
directive += f" 'nonce-{nonce}'"
modified_directives.append(directive)
return '; '.join(modified_directives)
def process_request(self, request):
"""Generate and store nonce for this request."""
# Generate a nonce for this request
nonce = self._generate_nonce()
# Store it in request so templates can access it
request.csp_nonce = nonce
return None
def process_response(self, request, response):
"""Add security headers to the response."""
# Content Security Policy with nonce support
if hasattr(settings, 'SECURE_CONTENT_SECURITY_POLICY'):
csp_policy = settings.SECURE_CONTENT_SECURITY_POLICY
# Apply nonce if we have one for this request
if hasattr(request, 'csp_nonce'):
csp_policy = self._modify_csp_with_nonce(csp_policy, request.csp_nonce)
response['Content-Security-Policy'] = csp_policy
# Cross-Origin Opener Policy
if hasattr(settings, 'SECURE_CROSS_ORIGIN_OPENER_POLICY'):
response['Cross-Origin-Opener-Policy'] = settings.SECURE_CROSS_ORIGIN_OPENER_POLICY
# Referrer Policy
if hasattr(settings, 'SECURE_REFERRER_POLICY'):
response['Referrer-Policy'] = settings.SECURE_REFERRER_POLICY
# Permissions Policy
if hasattr(settings, 'SECURE_PERMISSIONS_POLICY'):
response['Permissions-Policy'] = settings.SECURE_PERMISSIONS_POLICY
# Additional security headers
response['X-Content-Type-Options'] = 'nosniff'
response['X-Frame-Options'] = getattr(settings, 'X_FRAME_OPTIONS', 'DENY')
response['X-XSS-Protection'] = '1; mode=block'
# Cache Control headers for better performance
# Prevent caching of HTML pages to ensure users get fresh content
if response.get('Content-Type', '').startswith('text/html'):
response['Cache-Control'] = 'no-cache, no-store, must-revalidate'
response['Pragma'] = 'no-cache'
response['Expires'] = '0'
# Strict Transport Security (if SSL is enabled)
if getattr(settings, 'SECURE_SSL_REDIRECT', False):
hsts_seconds = getattr(settings, 'SECURE_HSTS_SECONDS', 31536000)
hsts_include_subdomains = getattr(settings, 'SECURE_HSTS_INCLUDE_SUBDOMAINS', True)
hsts_preload = getattr(settings, 'SECURE_HSTS_PRELOAD', False)
hsts_header = f'max-age={hsts_seconds}'
if hsts_include_subdomains:
hsts_header += '; includeSubDomains'
if hsts_preload:
hsts_header += '; preload'
response['Strict-Transport-Security'] = hsts_header
return response

View File

@@ -122,6 +122,7 @@ MIDDLEWARE = [
"django.middleware.cache.UpdateCacheMiddleware", "django.middleware.cache.UpdateCacheMiddleware",
"corsheaders.middleware.CorsMiddleware", # CORS middleware for API "corsheaders.middleware.CorsMiddleware", # CORS middleware for API
"django.middleware.security.SecurityMiddleware", "django.middleware.security.SecurityMiddleware",
"apps.core.middleware.security_headers.SecurityHeadersMiddleware", # Modern security headers
"whitenoise.middleware.WhiteNoiseMiddleware", "whitenoise.middleware.WhiteNoiseMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware", "django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware", "django.middleware.common.CommonMiddleware",

View File

@@ -34,3 +34,37 @@ SESSION_COOKIE_SAMESITE = env("SESSION_COOKIE_SAMESITE", default="Lax")
CSRF_COOKIE_SECURE = env.bool("CSRF_COOKIE_SECURE", default=False) CSRF_COOKIE_SECURE = env.bool("CSRF_COOKIE_SECURE", default=False)
CSRF_COOKIE_HTTPONLY = env.bool("CSRF_COOKIE_HTTPONLY", default=True) CSRF_COOKIE_HTTPONLY = env.bool("CSRF_COOKIE_HTTPONLY", default=True)
CSRF_COOKIE_SAMESITE = env("CSRF_COOKIE_SAMESITE", default="Lax") CSRF_COOKIE_SAMESITE = env("CSRF_COOKIE_SAMESITE", default="Lax")
# Content Security Policy (CSP) - Tightened security without unsafe directives
SECURE_CONTENT_SECURITY_POLICY = env(
"SECURE_CONTENT_SECURITY_POLICY",
default=(
"default-src 'self'; "
"script-src 'self' "
"https://unpkg.com https://cdnjs.cloudflare.com; "
"style-src 'self' "
"https://fonts.googleapis.com https://cdnjs.cloudflare.com; "
"img-src 'self' data: https: blob:; "
"font-src 'self' https://fonts.gstatic.com https://cdnjs.cloudflare.com; "
"connect-src 'self'; "
"media-src 'self'; "
"object-src 'none'; "
"frame-src 'none'; "
"worker-src 'self'; "
"manifest-src 'self'; "
"base-uri 'self'; "
"form-action 'self'; "
"upgrade-insecure-requests;"
)
)
# Additional modern security headers
SECURE_CROSS_ORIGIN_OPENER_POLICY = env("SECURE_CROSS_ORIGIN_OPENER_POLICY", default="same-origin")
SECURE_REFERRER_POLICY = env("SECURE_REFERRER_POLICY", default="strict-origin-when-cross-origin")
SECURE_PERMISSIONS_POLICY = env(
"SECURE_PERMISSIONS_POLICY",
default="geolocation=(), camera=(), microphone=(), payment=()"
)
# X-Frame-Options alternative - more flexible
X_FRAME_OPTIONS = env("X_FRAME_OPTIONS", default="DENY")

View File

@@ -0,0 +1,29 @@
/* Inline styles that were moved from the base template for CSP compliance */
[x-cloak] {
display: none !important;
}
.dropdown-menu {
position: absolute;
right: 0;
margin-top: 0.5rem;
width: 12rem;
border-radius: 0.375rem;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1),
0 4px 6px -2px rgba(0, 0, 0, 0.05);
z-index: 50;
overflow: hidden;
}
.htmx-indicator {
display: none;
}
.htmx-request .htmx-indicator {
display: block;
}
.htmx-request.htmx-indicator {
display: block;
}

View File

@@ -2957,6 +2957,11 @@
left: calc(var(--spacing) * 4); left: calc(var(--spacing) * 4);
} }
} }
.focus\:z-50 {
&:focus {
z-index: 50;
}
}
.focus\:border-blue-500 { .focus\:border-blue-500 {
&:focus { &:focus {
border-color: var(--color-blue-500); border-color: var(--color-blue-500);

16
static/js/theme.js Normal file
View File

@@ -0,0 +1,16 @@
/**
* Theme management script
* Prevents flash of wrong theme by setting theme class immediately
*/
(function() {
let theme = localStorage.getItem("theme");
if (!theme) {
theme = window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light";
localStorage.setItem("theme", theme);
}
if (theme === "dark") {
document.documentElement.classList.add("dark");
}
})();

View File

@@ -5,30 +5,68 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="csrf-token" content="{{ csrf_token }}" /> <meta name="csrf-token" content="{{ csrf_token }}" />
<title>{% block title %}ThrillWiki{% endblock %}</title>
<!-- Google Fonts --> <!-- SEO Meta Tags -->
<title>{% block title %}ThrillWiki{% endblock %}</title>
<meta name="description" content="{% block meta_description %}Your ultimate guide to theme parks and attractions worldwide. Discover thrilling rides, explore amazing parks, and share your adventures with fellow enthusiasts.{% endblock %}" />
<meta name="keywords" content="{% block meta_keywords %}theme parks, roller coasters, attractions, rides, amusement parks, Disney, Universal, Cedar Point{% endblock %}" />
<meta name="author" content="ThrillWiki" />
<meta name="robots" content="{% block meta_robots %}index, follow{% endblock %}" />
<link rel="canonical" href="{% block canonical_url %}{{ request.scheme }}://{{ request.get_host }}{{ request.path }}{% endblock %}" />
<!-- Open Graph / Facebook -->
<meta property="og:type" content="{% block og_type %}website{% endblock %}" />
<meta property="og:url" content="{% block og_url %}{{ request.build_absolute_uri|default:'' }}{% endblock %}" />
<meta property="og:title" content="{% block og_title %}ThrillWiki{% endblock %}" />
<meta property="og:description" content="{% block og_description %}Your ultimate guide to theme parks and attractions worldwide. Discover thrilling rides, explore amazing parks, and share your adventures with fellow enthusiasts.{% endblock %}" />
<meta property="og:image" content="{% block og_image %}{% load static %}{{ request.scheme }}://{{ request.get_host }}{% static 'images/placeholders/default-park.jpg' %}{% endblock %}" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
<meta property="og:site_name" content="ThrillWiki" />
<meta property="og:locale" content="en_US" />
<!-- Twitter -->
<meta name="twitter:card" content="{% block twitter_card %}summary_large_image{% endblock %}" />
<meta name="twitter:url" content="{% block twitter_url %}{{ request.build_absolute_uri|default:'' }}{% endblock %}" />
<meta name="twitter:title" content="{% block twitter_title %}ThrillWiki{% endblock %}" />
<meta name="twitter:description" content="{% block twitter_description %}Your ultimate guide to theme parks and attractions worldwide. Discover thrilling rides, explore amazing parks, and share your adventures with fellow enthusiasts.{% endblock %}" />
<meta name="twitter:image" content="{% block twitter_image %}{% load static %}{{ request.scheme }}://{{ request.get_host }}{% static 'images/placeholders/default-park.jpg' %}{% endblock %}" />
<meta name="twitter:creator" content="@ThrillWiki" />
<meta name="twitter:site" content="@ThrillWiki" />
<!-- Resource Hints for Performance -->
<link rel="dns-prefetch" href="//fonts.googleapis.com" />
<link rel="dns-prefetch" href="//fonts.gstatic.com" />
<link rel="dns-prefetch" href="//unpkg.com" />
<link rel="dns-prefetch" href="//cdnjs.cloudflare.com" />
{% block extra_dns_prefetch %}{% endblock %}
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
{% block extra_preconnect %}{% endblock %}
<!-- Preload Critical Resources -->
{% block critical_resources %}
<link rel="preload" href="{% static 'css/tailwind.css' %}" as="style" />
<link rel="preload" href="{% static 'js/theme.js' %}?v={{ version|default:'1.0' }}" as="script" />
<link rel="preload" href="{% static 'js/alpine.min.js' %}?v={{ version|default:'1.0' }}" as="script" />
<link rel="preload" href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap" as="style" />
{% endblock %}
<!-- Module Preload for Modern Browsers -->
{% block module_preload %}{% endblock %}
<!-- Google Fonts with performance optimizations -->
<link <link
href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap" href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap"
rel="stylesheet" rel="stylesheet"
/> />
<!-- Prevent flash of wrong theme --> <!-- Prevent flash of wrong theme -->
<script> <script src="{% static 'js/theme.js' %}?v={{ version|default:'1.0' }}"></script>
let theme = localStorage.getItem("theme");
if (!theme) {
theme = window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light";
localStorage.setItem("theme", theme);
}
if (theme === "dark") {
document.documentElement.classList.add("dark");
}
</script>
<!-- HTMX --> <!-- HTMX -->
<script src="https://unpkg.com/htmx.org@1.9.6"></script> <script src="https://unpkg.com/htmx.org@1.9.6" integrity="sha384-FYYOtJ1BoGZ1EZbdLzmaydhwRKh5zxCwWA0jzNEzSJYTzFxN2wjCjOj3gLyQYZGC" crossorigin="anonymous"></script>
<!-- Alpine.js Components (must load before Alpine.js) --> <!-- Alpine.js Components (must load before Alpine.js) -->
<script src="{% static 'js/alpine-components.js' %}?v={{ version|default:'1.0' }}"></script> <script src="{% static 'js/alpine-components.js' %}?v={{ version|default:'1.0' }}"></script>
@@ -43,75 +81,85 @@
<link href="{% static 'css/tailwind.css' %}" rel="stylesheet" /> <link href="{% static 'css/tailwind.css' %}" rel="stylesheet" />
<link href="{% static 'css/components.css' %}" rel="stylesheet" /> <link href="{% static 'css/components.css' %}" rel="stylesheet" />
<link href="{% static 'css/alerts.css' %}" rel="stylesheet" /> <link href="{% static 'css/alerts.css' %}" rel="stylesheet" />
<link href="{% static 'css/inline-styles.css' %}" rel="stylesheet" />
<!-- Font Awesome --> <!-- Font Awesome -->
<link <link
rel="stylesheet" rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"
integrity="sha512-9usAa10IRO0HhonpyAIVpjrylPvoDwiPUiKdWk5t3PyolY1cOd4DSE0Ga+ri4AuTroPR5aQvXU9xC6qOPnzFeg=="
crossorigin="anonymous"
/> />
<style>
[x-cloak] { <!-- Structured Data (JSON-LD) -->
display: none !important; {% block structured_data %}
<script type="application/ld+json" nonce="{{ request.csp_nonce }}">
{
"@context": "https://schema.org",
"@type": "WebSite",
"name": "ThrillWiki",
"description": "Your ultimate guide to theme parks and attractions worldwide",
"url": "{{ request.scheme }}://{{ request.get_host }}",
"potentialAction": {
"@type": "SearchAction",
"target": {
"@type": "EntryPoint",
"urlTemplate": "{{ request.scheme }}://{{ request.get_host }}/search/?q={search_term_string}"
},
"query-input": "required name=search_term_string"
},
"author": {
"@type": "Organization",
"name": "ThrillWiki"
} }
.dropdown-menu { }
position: absolute; </script>
right: 0; {% endblock %}
margin-top: 0.5rem;
width: 12rem;
border-radius: 0.375rem;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1),
0 4px 6px -2px rgba(0, 0, 0, 0.05);
z-index: 50;
overflow: hidden;
}
.htmx-indicator {
display: none;
}
.htmx-request .htmx-indicator {
display: block;
}
.htmx-request.htmx-indicator {
display: block;
}
</style>
{% block extra_head %}{% endblock %} {% block extra_head %}{% endblock %}
</head> </head>
<body <body
class="flex flex-col min-h-screen text-gray-900 bg-gradient-to-br from-white via-blue-50 to-indigo-50 dark:from-gray-950 dark:via-indigo-950 dark:to-purple-950 dark:text-white" class="flex flex-col min-h-screen text-gray-900 bg-gradient-to-br from-white via-blue-50 to-indigo-50 dark:from-gray-950 dark:via-indigo-950 dark:to-purple-950 dark:text-white"
{% block body_attributes %}{% endblock %}
> >
<!-- Skip to content link for accessibility -->
<a href="#main-content" class="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:z-50 px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors">Skip to main content</a>
<!-- Enhanced Header --> <!-- Enhanced Header -->
{% include 'components/layout/enhanced_header.html' %} {% include 'components/layout/enhanced_header.html' %}
<!-- Flash Messages --> <!-- Flash Messages -->
{% if messages %} {% if messages %}
<div class="fixed top-0 right-0 z-50 p-4 space-y-4"> <div class="fixed top-0 right-0 z-50 p-4 space-y-4" role="alert" aria-live="polite" aria-label="Notifications">
{% for message in messages %} {% for message in messages %}
<div <div
class="alert {% if message.tags %}alert-{{ message.tags }}{% endif %}" class="alert {% if message.tags %}alert-{{ message.tags }}{% endif %}"
role="alert"
aria-describedby="alert-{{ forloop.counter }}"
> >
{{ message }} <span id="alert-{{ forloop.counter }}">{{ message }}</span>
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
{% endif %} {% endif %}
<!-- Main Content --> <!-- Main Content -->
<main class="container flex-grow px-6 py-8 mx-auto"> <main id="main-content" class="container flex-grow px-6 py-8 mx-auto" role="main">
{% block content %}{% endblock %} {% block content %}{% endblock %}
</main> </main>
<!-- Footer --> <!-- Footer -->
<footer <footer
class="mt-auto border-t bg-white/90 dark:bg-gray-800/90 backdrop-blur-lg border-gray-200/50 dark:border-gray-700/50" class="mt-auto border-t bg-white/90 dark:bg-gray-800/90 backdrop-blur-lg border-gray-200/50 dark:border-gray-700/50"
role="contentinfo"
aria-label="Site footer"
> >
<div class="container px-6 py-6 mx-auto"> <div class="container px-6 py-6 mx-auto">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div class="text-gray-600 dark:text-gray-400"> <div class="text-gray-600 dark:text-gray-400">
<p>&copy; {% now "Y" %} ThrillWiki. All rights reserved.</p> <p>&copy; {% now "Y" %} ThrillWiki. All rights reserved.</p>
</div> </div>
<div class="space-x-4"> <nav class="space-x-4" role="navigation" aria-label="Footer links">
<a <a
href="{% url 'terms' %}" href="{% url 'terms' %}"
class="text-gray-600 transition-colors hover:text-primary dark:text-gray-400 dark:hover:text-primary" class="text-gray-600 transition-colors hover:text-primary dark:text-gray-400 dark:hover:text-primary"
@@ -122,7 +170,7 @@
class="text-gray-600 transition-colors hover:text-primary dark:text-gray-400 dark:hover:text-primary" class="text-gray-600 transition-colors hover:text-primary dark:text-gray-400 dark:hover:text-primary"
>Privacy</a >Privacy</a
> >
</div> </nav>
</div> </div>
</div> </div>
</footer> </footer>
@@ -137,10 +185,6 @@
<script src="{% static 'js/main.js' %}?v={{ version|default:'1.0' }}"></script> <script src="{% static 'js/main.js' %}?v={{ version|default:'1.0' }}"></script>
<script src="{% static 'js/alerts.js' %}?v={{ version|default:'1.0' }}"></script> <script src="{% static 'js/alerts.js' %}?v={{ version|default:'1.0' }}"></script>
<!-- Cache control meta tag -->
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
{% block extra_js %}{% endblock %} {% block extra_js %}{% endblock %}
</body> </body>
</html> </html>

View File

@@ -4,6 +4,47 @@
{% block title %}ThrillWiki - Theme Parks & Attractions Guide{% endblock %} {% block title %}ThrillWiki - Theme Parks & Attractions Guide{% endblock %}
{% block meta_description %}Discover the world's best theme parks and thrilling rides. Explore amazing parks, find detailed ride information, and share your adventures with fellow theme park enthusiasts.{% endblock %}
{% block meta_keywords %}theme parks, roller coasters, attractions, rides, amusement parks, Disney World, Universal Studios, Cedar Point, Six Flags, thrill rides{% endblock %}
{% block og_title %}ThrillWiki - Your Ultimate Theme Park & Attractions Guide{% endblock %}
{% block og_description %}Discover the world's best theme parks and thrilling rides. Explore amazing parks, find detailed ride information, and share your adventures with fellow theme park enthusiasts.{% endblock %}
{% block og_type %}website{% endblock %}
{% block twitter_title %}ThrillWiki - Your Ultimate Theme Park & Attractions Guide{% endblock %}
{% block twitter_description %}Discover the world's best theme parks and thrilling rides. Explore amazing parks, find detailed ride information, and share your adventures.{% endblock %}
{% block structured_data %}
<script type="application/ld+json" nonce="{{ request.csp_nonce }}">
{
"@context": "https://schema.org",
"@type": "WebSite",
"name": "ThrillWiki",
"description": "Your ultimate guide to theme parks and attractions worldwide",
"url": "{{ request.scheme }}://{{ request.get_host }}",
"potentialAction": {
"@type": "SearchAction",
"target": {
"@type": "EntryPoint",
"urlTemplate": "{{ request.scheme }}://{{ request.get_host }}/search/?q={search_term_string}"
},
"query-input": "required name=search_term_string"
},
"author": {
"@type": "Organization",
"name": "ThrillWiki",
"description": "The ultimate theme park and attractions database"
},
"mainEntity": {
"@type": "ItemList",
"name": "Featured Theme Parks and Attractions",
"description": "Top-rated theme parks and thrilling rides from around the world"
}
}
</script>
{% endblock %}
{% block content %} {% block content %}
<!-- Hero Section --> <!-- Hero Section -->
<div class="mb-12 bg-white border border-gray-200 rounded-lg shadow-lg dark:bg-gray-800 dark:border-gray-700"> <div class="mb-12 bg-white border border-gray-200 rounded-lg shadow-lg dark:bg-gray-800 dark:border-gray-700">