mirror of
https://github.com/pacnpal/thrillwiki_django_no_react.git
synced 2025-12-20 03:51:09 -05:00
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:
4
.replit
4
.replit
@@ -62,10 +62,6 @@ externalPort = 3000
|
||||
localPort = 45245
|
||||
externalPort = 3001
|
||||
|
||||
[[ports]]
|
||||
localPort = 46739
|
||||
externalPort = 3002
|
||||
|
||||
[deployment]
|
||||
deploymentTarget = "autoscale"
|
||||
run = [
|
||||
|
||||
97
apps/core/middleware/security_headers.py
Normal file
97
apps/core/middleware/security_headers.py
Normal 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
|
||||
@@ -122,6 +122,7 @@ MIDDLEWARE = [
|
||||
"django.middleware.cache.UpdateCacheMiddleware",
|
||||
"corsheaders.middleware.CorsMiddleware", # CORS middleware for API
|
||||
"django.middleware.security.SecurityMiddleware",
|
||||
"apps.core.middleware.security_headers.SecurityHeadersMiddleware", # Modern security headers
|
||||
"whitenoise.middleware.WhiteNoiseMiddleware",
|
||||
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||
"django.middleware.common.CommonMiddleware",
|
||||
|
||||
@@ -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_HTTPONLY = env.bool("CSRF_COOKIE_HTTPONLY", default=True)
|
||||
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")
|
||||
|
||||
29
static/css/inline-styles.css
Normal file
29
static/css/inline-styles.css
Normal 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;
|
||||
}
|
||||
@@ -2957,6 +2957,11 @@
|
||||
left: calc(var(--spacing) * 4);
|
||||
}
|
||||
}
|
||||
.focus\:z-50 {
|
||||
&:focus {
|
||||
z-index: 50;
|
||||
}
|
||||
}
|
||||
.focus\:border-blue-500 {
|
||||
&:focus {
|
||||
border-color: var(--color-blue-500);
|
||||
|
||||
16
static/js/theme.js
Normal file
16
static/js/theme.js
Normal 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");
|
||||
}
|
||||
})();
|
||||
@@ -5,30 +5,68 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="csrf-token" content="{{ csrf_token }}" />
|
||||
|
||||
<!-- 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" />
|
||||
|
||||
<!-- Google Fonts -->
|
||||
<!-- 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
|
||||
href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
|
||||
<!-- Prevent flash of wrong theme -->
|
||||
<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>
|
||||
<script src="{% static 'js/theme.js' %}?v={{ version|default:'1.0' }}"></script>
|
||||
|
||||
<!-- 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) -->
|
||||
<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/components.css' %}" rel="stylesheet" />
|
||||
<link href="{% static 'css/alerts.css' %}" rel="stylesheet" />
|
||||
<link href="{% static 'css/inline-styles.css' %}" rel="stylesheet" />
|
||||
|
||||
<!-- Font Awesome -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
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] {
|
||||
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;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- Structured Data (JSON-LD) -->
|
||||
{% 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"
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_head %}{% endblock %}
|
||||
</head>
|
||||
<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"
|
||||
{% 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 -->
|
||||
{% include 'components/layout/enhanced_header.html' %}
|
||||
|
||||
<!-- Flash 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 %}
|
||||
<div
|
||||
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>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- 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 %}
|
||||
</main>
|
||||
|
||||
<!-- 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"
|
||||
role="contentinfo"
|
||||
aria-label="Site footer"
|
||||
>
|
||||
<div class="container px-6 py-6 mx-auto">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="text-gray-600 dark:text-gray-400">
|
||||
<p>© {% now "Y" %} ThrillWiki. All rights reserved.</p>
|
||||
</div>
|
||||
<div class="space-x-4">
|
||||
<nav class="space-x-4" role="navigation" aria-label="Footer links">
|
||||
<a
|
||||
href="{% url 'terms' %}"
|
||||
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"
|
||||
>Privacy</a
|
||||
>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
@@ -137,10 +185,6 @@
|
||||
<script src="{% static 'js/main.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 %}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -4,6 +4,47 @@
|
||||
|
||||
{% 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 %}
|
||||
<!-- Hero Section -->
|
||||
<div class="mb-12 bg-white border border-gray-200 rounded-lg shadow-lg dark:bg-gray-800 dark:border-gray-700">
|
||||
|
||||
Reference in New Issue
Block a user