1 Commits

Author SHA1 Message Date
codex_a
95abbdc0ac fix: update existing default site in seed command instead of hardcoding 127.0.0.1
Wagtail initialises the default site with hostname 'localhost'. The previous
get_or_create on '127.0.0.1' left the localhost site intact (still pointing
to the Welcome page), so browsers got the wrong root page.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-02-28 20:30:22 +00:00
31 changed files with 182 additions and 935 deletions

View File

@@ -2,9 +2,6 @@ name: CI
on:
pull_request:
push:
branches:
- main
schedule:
- cron: "0 2 * * *"
@@ -191,15 +188,3 @@ jobs:
- name: Remove CI image
if: always()
run: docker image rm -f "$CI_IMAGE" || true
deploy:
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- name: Deploy to lintel-prod-01
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.PROD_SSH_HOST }}
username: deploy
key: ${{ secrets.PROD_SSH_KEY }}
script: bash /srv/sum/nohype/app/deploy/deploy.sh

View File

@@ -116,16 +116,17 @@ class Command(BaseCommand):
legal_index.add_child(instance=privacy)
privacy.save_revision().publish()
# Point every existing Site at the real home page and mark exactly one
# as the default. Wagtail's initial migration creates a localhost:80
# site that matches incoming requests by hostname before the
# is_default_site fallback is ever reached, so we must update *all*
# sites, not just the is_default_site one.
Site.objects.all().update(root_page=home, site_name="No Hype AI", is_default_site=False)
site = Site.objects.first()
# Update the existing default site (whatever hostname Wagtail created it with)
# rather than creating a new 127.0.0.1 entry that leaves localhost pointing
# to the Welcome page.
site = Site.objects.filter(is_default_site=True).first()
if site is None:
site = Site(hostname="localhost", port=80)
site.root_page = home
site.is_default_site = True
site.site_name = "No Hype AI"
site.save()
# Remove any other conflicting default-site entries left by test fixtures
Site.objects.exclude(pk=site.pk).filter(is_default_site=True).update(is_default_site=False)
self.stdout.write(self.style.SUCCESS("Seeded E2E content."))

View File

@@ -25,9 +25,9 @@ class SecurityHeadersMiddleware:
response["Content-Security-Policy"] = (
f"default-src 'self'; "
f"script-src 'self' 'nonce-{nonce}'; "
"style-src 'self' https://fonts.googleapis.com; "
"style-src 'self'; "
"img-src 'self' data: blob:; "
"font-src 'self' https://fonts.gstatic.com; "
"font-src 'self'; "
"connect-src 'self'; "
"object-src 'none'; "
"base-uri 'self'; "

View File

@@ -142,13 +142,6 @@ X_CONTENT_TYPE_OPTIONS = "nosniff"
CSRF_TRUSTED_ORIGINS = [u for u in os.getenv("CSRF_TRUSTED_ORIGINS", "http://localhost:8035").split(",") if u]
TRUSTED_PROXY_IPS = [ip.strip() for ip in os.getenv("TRUSTED_PROXY_IPS", "").split(",") if ip.strip()]
STORAGES = {
"default": {
"BACKEND": "django.core.files.storage.FileSystemStorage",
},
"staticfiles": {
"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
},
}
STATICFILES_STORAGE = "whitenoise.storage.CompressedManifestStaticFilesStorage"
TAILWIND_APP_NAME = "theme"

View File

@@ -4,21 +4,6 @@ DEBUG = True
INTERNAL_IPS = ["127.0.0.1"]
# Drop WhiteNoise in dev — it serves from STATIC_ROOT which is empty without
# collectstatic, so it 404s every asset. Django's runserver serves static and
# media files natively when DEBUG=True (via django.contrib.staticfiles + the
# media URL pattern in urls.py).
MIDDLEWARE = [m for m in MIDDLEWARE if m != "whitenoise.middleware.WhiteNoiseMiddleware"]
STORAGES = {
"default": {
"BACKEND": "django.core.files.storage.FileSystemStorage",
},
"staticfiles": {
"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
},
}
try:
import debug_toolbar # noqa: F401

View File

@@ -2,16 +2,8 @@ from .base import * # noqa
DEBUG = False
# Behind Caddy: trust the forwarded proto header so Django knows it's HTTPS.
# SECURE_SSL_REDIRECT is intentionally off — Caddy handles HTTPS redirects
# before the request reaches Django; enabling it here causes redirect loops.
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
USE_X_FORWARDED_HOST = True
SECURE_SSL_REDIRECT = False
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
CSRF_TRUSTED_ORIGINS = [
"https://nohypeai.net",
"https://www.nohypeai.net",
]

View File

@@ -1,5 +1,3 @@
from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
from django.urls import include, path
from django.views.generic import RedirectView
@@ -23,6 +21,3 @@ urlpatterns = [
path("admin/", RedirectView.as_view(url="/cms/", permanent=False)),
path("", include(wagtail_urls)),
]
if settings.DEBUG:
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

View File

@@ -1,23 +0,0 @@
nohypeai.net, www.nohypeai.net {
encode gzip zstd
header {
X-Content-Type-Options nosniff
X-Frame-Options DENY
Referrer-Policy strict-origin-when-cross-origin
Permissions-Policy "geolocation=(), microphone=(), camera=()"
X-Forwarded-Proto https
}
handle_path /static/* {
root * /srv/sum/nohype/static
file_server
}
handle_path /media/* {
root * /srv/sum/nohype/media
file_server
}
reverse_proxy localhost:8001
}

View File

@@ -1,33 +0,0 @@
#!/usr/bin/env bash
# Deploy script for No Hype AI — runs on lintel-prod-01 as deploy user.
# Called by CI after a successful push to main.
set -euo pipefail
SITE_DIR=/srv/sum/nohype
APP_DIR=${SITE_DIR}/app
cd "${SITE_DIR}"
echo "==> Pulling latest code"
git -C "${APP_DIR}" pull origin main
echo "==> Updating compose file"
cp "${APP_DIR}/docker-compose.prod.yml" "${SITE_DIR}/docker-compose.prod.yml"
echo "==> Ensuring static/media directories exist"
mkdir -p "${SITE_DIR}/static" "${SITE_DIR}/media"
echo "==> Rebuilding and recreating web container"
docker compose -f "${SITE_DIR}/docker-compose.prod.yml" up -d --no-deps --build --force-recreate web
echo "==> Waiting for health check"
for i in $(seq 1 30); do
if curl -fsS -H "Host: nohypeai.net" http://localhost:8001/ >/dev/null 2>&1; then
echo "==> Site is up"
exit 0
fi
sleep 3
done
echo "ERROR: site did not come up after 90s" >&2
docker compose -f "${SITE_DIR}/docker-compose.prod.yml" logs --tail=50 web
exit 1

View File

@@ -1,22 +0,0 @@
#!/bin/sh
set -e
python manage.py tailwind install --no-input
python manage.py tailwind build
python manage.py migrate --noinput
python manage.py collectstatic --noinput
# Set Wagtail site hostname from first entry in ALLOWED_HOSTS
python manage.py shell -c "
from wagtail.models import Site
import os
hostname = os.environ.get('ALLOWED_HOSTS', 'localhost').split(',')[0].strip()
Site.objects.update(hostname=hostname, port=443, site_name='No Hype AI')
"
exec gunicorn config.wsgi:application \
--workers 3 \
--bind 0.0.0.0:8000 \
--access-logfile - \
--error-logfile - \
--capture-output

View File

@@ -1,26 +0,0 @@
[Unit]
Description=No Hype AI (Docker Compose)
Requires=docker.service
After=docker.service network-online.target
[Service]
Type=simple
User=deploy
Group=www-data
WorkingDirectory=/srv/sum/nohype
ExecStartPre=docker compose -f docker-compose.prod.yml pull --ignore-pull-failures
ExecStart=docker compose -f docker-compose.prod.yml up --build
ExecStop=docker compose -f docker-compose.prod.yml down
Restart=always
RestartSec=10
TimeoutStartSec=300
TimeoutStopSec=30
StandardOutput=journal
StandardError=journal
SyslogIdentifier=sum-nohype
[Install]
WantedBy=multi-user.target

View File

@@ -1,36 +0,0 @@
services:
web:
build: app
working_dir: /app
command: /app/deploy/entrypoint.prod.sh
env_file: .env
environment:
DJANGO_SETTINGS_MODULE: config.settings.production
volumes:
- /srv/sum/nohype/static:/app/staticfiles
- /srv/sum/nohype/media:/app/media
ports:
- "127.0.0.1:8001:8000"
depends_on:
db:
condition: service_healthy
restart: unless-stopped
db:
image: postgres:16-alpine
env_file: .env
environment:
POSTGRES_DB: nohype
POSTGRES_USER: nohype
volumes:
- nohype_pg:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U nohype -d nohype"]
interval: 5s
timeout: 5s
retries: 10
start_period: 10s
restart: unless-stopped
volumes:
nohype_pg:

View File

@@ -3,13 +3,11 @@ services:
build: .
working_dir: /app
command: >
sh -c "python manage.py tailwind install --no-input &&
python manage.py tailwind build &&
python manage.py migrate --noinput &&
python manage.py seed_e2e_content &&
sh -c "python manage.py migrate --noinput &&
python manage.py runserver 0.0.0.0:8000"
volumes:
- .:/app
- /opt/playwright-tools/browsers:/opt/playwright-tools/browsers:ro
ports:
- "8035:8000"
environment:
@@ -24,6 +22,7 @@ services:
EMAIL_BACKEND: django.core.mail.backends.console.EmailBackend
DEFAULT_FROM_EMAIL: hello@nohypeai.com
NEWSLETTER_PROVIDER: buttondown
PLAYWRIGHT_BROWSERS_PATH: /opt/playwright-tools/browsers
depends_on:
db:
condition: service_healthy

View File

@@ -6,9 +6,6 @@
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{% block title %}No Hype AI{% endblock %}</title>
{% block head_meta %}{% endblock %}
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;500&family=Inter:wght@400;500;600&family=Space+Grotesk:wght@500;700;900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="{% static 'css/styles.css' %}" />
<script nonce="{{ request.csp_nonce|default:'' }}">
(function(){try{if(localStorage.getItem('theme')==='light'){document.documentElement.classList.remove('dark');}}catch(e){}})();
@@ -18,18 +15,17 @@
<script src="{% static 'js/prism.js' %}" defer></script>
<script src="{% static 'js/newsletter.js' %}" defer></script>
</head>
<body class="bg-brand-light dark:bg-brand-dark text-brand-dark dark:text-brand-light antialiased min-h-screen flex flex-col relative">
<div class="fixed inset-0 bg-grid-pattern pointer-events-none z-[-1]"></div>
<body>
{% include 'components/nav.html' %}
{% include 'components/cookie_banner.html' %}
{% if messages %}
<section aria-label="Messages" class="max-w-7xl mx-auto px-6 py-2">
<section aria-label="Messages">
{% for message in messages %}
<p class="font-mono text-sm py-2 px-4 bg-brand-cyan/10 text-brand-cyan border border-brand-cyan/20 mb-2">{{ message }}</p>
<p>{{ message }}</p>
{% endfor %}
</section>
{% endif %}
<main class="flex-grow w-full max-w-7xl mx-auto px-6 py-8">{% block content %}{% endblock %}</main>
<main>{% block content %}{% endblock %}</main>
{% include 'components/footer.html' %}
</body>
</html>

View File

@@ -1,40 +1,14 @@
{% extends 'base.html' %}
{% load wagtailimages_tags wagtailcore_tags %}
{% block title %}{{ page.title }} | No Hype AI{% endblock %}
{% block content %}
<!-- Page Header -->
<div class="py-8 md:py-12 border-b border-zinc-200 dark:border-zinc-800 mb-12">
<h1 class="font-display font-black text-4xl md:text-6xl mb-4">{{ page.title }}</h1>
{% if page.mission_statement %}
<p class="text-xl md:text-2xl text-zinc-600 dark:text-zinc-400 font-medium max-w-2xl">{{ page.mission_statement }}</p>
{% endif %}
</div>
<!-- Body -->
<div class="grid grid-cols-1 lg:grid-cols-12 gap-12">
<div class="lg:col-span-8 prose prose-lg dark:prose-invert max-w-none
prose-headings:font-display prose-headings:font-bold
prose-a:text-brand-cyan hover:prose-a:text-brand-pink prose-a:transition-colors prose-a:no-underline hover:prose-a:underline">
{{ page.body|richtext }}
</div>
{% if page.featured_author %}
<aside class="lg:col-span-4">
<div class="bg-brand-surfaceLight dark:bg-brand-surfaceDark border border-zinc-200 dark:border-zinc-800 p-6 relative overflow-hidden">
<div class="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-brand-cyan to-brand-pink"></div>
<h1>{{ page.title }}</h1>
<p>{{ page.mission_statement }}</p>
{{ page.body|richtext }}
{% if page.featured_author %}
<h2>{{ page.featured_author.name }}</h2>
<p>{{ page.featured_author.bio }}</p>
{% if page.featured_author.avatar %}
<div class="w-20 h-20 mb-4 overflow-hidden border border-zinc-200 dark:border-zinc-800">
{% image page.featured_author.avatar fill-80x80 class="w-full h-full object-cover" %}
</div>
{% else %}
<div class="w-20 h-20 mb-4 bg-gradient-to-tr from-brand-cyan to-brand-pink"></div>
{% image page.featured_author.avatar fill-200x200 %}
{% endif %}
<h2 class="font-display font-bold text-xl mb-1">{{ page.featured_author.name }}</h2>
{% if page.featured_author.role %}<p class="font-mono text-xs text-zinc-500 mb-3">{{ page.featured_author.role }}</p>{% endif %}
{% if page.featured_author.bio %}<p class="text-sm text-zinc-600 dark:text-zinc-400">{{ page.featured_author.bio }}</p>{% endif %}
</div>
</aside>
{% endif %}
</div>
{% endif %}
{% endblock %}

View File

@@ -10,39 +10,26 @@
<meta property="og:url" content="{{ canonical }}" />
{% endblock %}
{% block content %}
<!-- Page Header -->
<div class="py-8 md:py-12 border-b border-zinc-200 dark:border-zinc-800 mb-12">
<h1 class="font-display font-black text-4xl md:text-6xl mb-6">{{ page.title }}</h1>
<!-- Tag Filters -->
<div class="flex flex-wrap gap-3">
<a href="/articles/" class="px-4 py-2 font-mono text-sm font-bold border transition-colors {% if not active_tag %}bg-brand-dark text-brand-light dark:bg-brand-light dark:text-brand-dark border-transparent{% else %}bg-transparent text-zinc-600 dark:text-zinc-400 border-zinc-300 dark:border-zinc-700 hover:text-brand-dark dark:hover:text-brand-light hover:border-brand-dark dark:hover:border-brand-light{% endif %}" {% if not active_tag %}aria-current="page"{% endif %}>All</a>
<h1>{{ page.title }}</h1>
<section>
<h2>Filter by tag</h2>
<a href="/articles/" {% if not active_tag %}aria-current="page"{% endif %}>All</a>
{% for tag in available_tags %}
<a href="/articles/?tag={{ tag.slug }}" class="px-4 py-2 font-mono text-sm font-bold border transition-colors {% if active_tag == tag.slug %}bg-brand-dark text-brand-light dark:bg-brand-light dark:text-brand-dark border-transparent{% else %}bg-transparent text-zinc-600 dark:text-zinc-400 border-zinc-300 dark:border-zinc-700 hover:text-brand-dark dark:hover:text-brand-light hover:border-brand-dark dark:hover:border-brand-light{% endif %}" {% if active_tag == tag.slug %}aria-current="page"{% endif %}>{{ tag.name }}</a>
<a href="/articles/?tag={{ tag.slug }}" {% if active_tag == tag.slug %}aria-current="page"{% endif %}>{{ tag.name }}</a>
{% endfor %}
</div>
</div>
<!-- Article List -->
<div class="space-y-8">
{% for article in articles %}
</section>
{% for article in articles %}
{% include 'components/article_card.html' with article=article %}
{% empty %}
<p class="font-mono text-zinc-500 py-12 text-center">No articles found.</p>
{% endfor %}
</div>
<!-- Pagination -->
{% if articles.has_previous or articles.has_next %}
<nav aria-label="Pagination" class="mt-12 flex justify-center items-center gap-4 font-mono text-sm">
{% empty %}
<p>No articles found.</p>
{% endfor %}
<nav aria-label="Pagination">
{% if articles.has_previous %}
<a href="?page={{ articles.previous_page_number }}{% if active_tag %}&tag={{ active_tag }}{% endif %}" class="px-6 py-3 border border-zinc-300 dark:border-zinc-700 hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-colors">Previous</a>
<a href="?page={{ articles.previous_page_number }}{% if active_tag %}&tag={{ active_tag }}{% endif %}">Previous</a>
{% endif %}
<span class="text-zinc-500">Page {{ articles.number }} of {{ paginator.num_pages }}</span>
<span>Page {{ articles.number }} of {{ paginator.num_pages }}</span>
{% if articles.has_next %}
<a href="?page={{ articles.next_page_number }}{% if active_tag %}&tag={{ active_tag }}{% endif %}" class="px-6 py-3 border border-zinc-300 dark:border-zinc-700 hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-colors">Next</a>
<a href="?page={{ articles.next_page_number }}{% if active_tag %}&tag={{ active_tag }}{% endif %}">Next</a>
{% endif %}
</nav>
{% endif %}
{% endblock %}

View File

@@ -1,5 +1,5 @@
{% extends 'base.html' %}
{% load wagtailcore_tags wagtailimages_tags seo_tags core_tags %}
{% load wagtailcore_tags wagtailimages_tags seo_tags %}
{% block title %}{{ page.title }} | No Hype AI{% endblock %}
{% block head_meta %}
{% canonical_url page as canonical %}
@@ -17,210 +17,75 @@
{% if og_image %}<meta name="twitter:image" content="{{ og_image }}" />{% endif %}
{% endblock %}
{% block content %}
<!-- Breadcrumb -->
<div class="mb-8 font-mono text-sm text-zinc-500">
<a href="/" class="hover:text-brand-cyan transition-colors">Home</a> /
<a href="/articles/" class="hover:text-brand-cyan transition-colors">Articles</a> /
<span class="text-brand-dark dark:text-brand-light">{{ page.title|truncatechars:40 }}</span>
</div>
<!-- Article Header -->
<header class="mb-12 border-b border-zinc-200 dark:border-zinc-800 pb-12">
<div class="flex gap-3 mb-6 items-center flex-wrap">
{% for tag in page.tags.all %}
<span class="text-xs font-mono font-bold px-2 py-1 {{ tag|get_tag_css }} border border-current/20">{{ tag.name }}</span>
{% endfor %}
<span class="text-sm font-mono text-zinc-500">{{ page.first_published_at|date:"M j, Y" }}</span>
<span class="text-sm font-mono text-zinc-500">{{ page.read_time_mins }} min read</span>
</div>
<h1 class="font-display font-black text-4xl md:text-6xl lg:text-7xl leading-tight mb-8">{{ page.title }}</h1>
<div class="flex items-center gap-4">
<div class="w-12 h-12 bg-gradient-to-tr from-brand-cyan to-brand-pink shrink-0"></div>
<div>
<div class="font-bold font-display text-lg">{{ page.author.name }}</div>
{% if page.author.role %}<div class="font-mono text-xs text-zinc-500">{{ page.author.role }}</div>{% endif %}
</div>
</div>
</header>
{% if page.hero_image %}
<div class="mb-12 border border-zinc-200 dark:border-zinc-800 overflow-hidden">
{% image page.hero_image width-1200 class="w-full h-auto" %}
</div>
{% endif %}
<!-- Main Content Layout -->
<div class="grid grid-cols-1 lg:grid-cols-12 gap-12">
<!-- Article Body -->
<article class="lg:col-span-8 prose prose-lg dark:prose-invert max-w-none
prose-headings:font-display prose-headings:font-bold
prose-a:text-brand-cyan hover:prose-a:text-brand-pink prose-a:transition-colors prose-a:no-underline hover:prose-a:underline
prose-img:border prose-img:border-zinc-200 dark:prose-img:border-zinc-800
prose-blockquote:border-l-brand-pink prose-blockquote:bg-brand-pink/5 prose-blockquote:py-2 prose-blockquote:not-italic
prose-code:font-mono prose-code:text-brand-cyan prose-code:bg-zinc-100 dark:prose-code:bg-zinc-900 prose-code:px-1 prose-code:py-0.5 prose-code:rounded prose-code:before:content-none prose-code:after:content-none">
<article>
<h1>{{ page.title }}</h1>
<p>{{ page.read_time_mins }} min read</p>
{% if page.hero_image %}
{% image page.hero_image fill-1200x630 %}
{% endif %}
{{ page.body }}
{% article_json_ld page %}
</article>
<!-- Sidebar -->
<aside class="lg:col-span-4 space-y-8">
<div class="sticky top-28">
<!-- Share -->
<section aria-label="Share this article" class="mb-8">
<h3 class="font-display font-bold text-lg mb-4 uppercase tracking-widest text-zinc-500 text-sm">Share Article</h3>
<div class="flex gap-2">
<a href="https://x.com/intent/post?url={{ request.build_absolute_uri|urlencode }}&text={{ page.title|urlencode }}" target="_blank" rel="noopener noreferrer"
class="w-10 h-10 flex items-center justify-center bg-brand-surfaceLight dark:bg-brand-surfaceDark border border-zinc-200 dark:border-zinc-800 hover:border-brand-cyan transition-colors hover:text-brand-cyan" aria-label="Share on X">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-4.714-6.231-5.401 6.231H2.748l7.73-8.835L1.254 2.25H8.08l4.259 5.63L18.244 2.25zm-1.161 17.52h1.833L7.084 4.126H5.117L17.083 19.77z"/></svg>
</a>
<a href="https://www.linkedin.com/sharing/share-offsite/?url={{ request.build_absolute_uri|urlencode }}" target="_blank" rel="noopener noreferrer"
class="w-10 h-10 flex items-center justify-center bg-brand-surfaceLight dark:bg-brand-surfaceDark border border-zinc-200 dark:border-zinc-800 hover:border-blue-600 transition-colors hover:text-blue-600" aria-label="Share on LinkedIn">
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433a2.062 2.062 0 0 1-2.063-2.065 2.064 2.064 0 1 1 2.063 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/></svg>
</a>
<button type="button" data-copy-link data-copy-url="{{ request.build_absolute_uri }}"
class="w-10 h-10 flex items-center justify-center bg-brand-surfaceLight dark:bg-brand-surfaceDark border border-zinc-200 dark:border-zinc-800 hover:border-brand-pink transition-colors hover:text-brand-pink" aria-label="Copy link">
<svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M13.19 8.688a4.5 4.5 0 0 1 1.242 7.244l-4.5 4.5a4.5 4.5 0 0 1-6.364-6.364l1.757-1.757m13.35-.622 1.757-1.757a4.5 4.5 0 0 0-6.364-6.364l-4.5 4.5a4.5 4.5 0 0 0 1.242 7.244" /></svg>
</button>
</div>
</section>
<!-- Newsletter -->
<div class="bg-brand-dark text-brand-light dark:bg-brand-light dark:text-brand-dark p-6 border border-transparent dark:border-zinc-700 shadow-solid-dark dark:shadow-solid-light">
<h3 class="font-display font-bold text-xl mb-2">Subscribe for Updates</h3>
<p class="text-sm opacity-80 mb-4">Get our latest articles and coding benchmarks delivered to your inbox every week.</p>
{% include 'components/newsletter_form.html' with source='article' label='Subscribe' %}
</div>
</div>
</aside>
</div>
<!-- Related Articles -->
{% if related_articles %}
<section class="mt-16 md:mt-24 pt-12 border-t border-zinc-200 dark:border-zinc-800">
<div class="flex items-center justify-between mb-8">
<h3 class="font-display font-bold text-3xl">Related Articles</h3>
<a href="/articles/" class="font-mono text-sm text-brand-cyan hover:underline">View All</a>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
{% for article in related_articles %}
<article class="group flex flex-col h-full bg-brand-surfaceLight dark:bg-brand-surfaceDark border border-zinc-200 dark:border-zinc-800 hover:-translate-y-2 hover:shadow-solid-dark dark:hover:shadow-solid-light transition-all duration-300">
<a href="{{ article.url }}" class="h-48 overflow-hidden relative bg-zinc-900 flex items-center justify-center border-b border-zinc-200 dark:border-zinc-800 shrink-0 block">
{% if article.hero_image %}
{% image article.hero_image fill-400x300 class="w-full h-full object-cover grayscale group-hover:grayscale-0 transition-all duration-500" %}
{% else %}
<svg class="w-16 h-16 text-brand-cyan opacity-40 group-hover:opacity-80 transition-all duration-500 group-hover:scale-110" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M9.75 3.104v5.714a2.25 2.25 0 0 1-.659 1.591L5 14.5M9.75 3.104c-.251.023-.501.05-.75.082m.75-.082a24.301 24.301 0 0 1 4.5 0m0 0v5.714c0 .597.237 1.17.659 1.591L19.8 15.3M14.25 3.104c.251.023.501.05.75.082M19.8 15.3l-1.57.393A9.065 9.065 0 0 1 12 15a9.065 9.065 0 0 1-6.23-.693L5 14.5m14.8.8 1.402 1.402c1 1 .03 2.798-1.442 2.798H4.24c-1.47 0-2.44-1.798-1.442-2.798L4.2 15.3" /></svg>
{% endif %}
</a>
<div class="p-6 flex flex-col flex-grow">
<div class="flex gap-2 mb-3 flex-wrap">
{% for tag in article.tags.all %}
<span class="text-xs font-mono font-bold px-2 py-1 {{ tag|get_tag_css }}">{{ tag.name }}</span>
{% endfor %}
</div>
<a href="{{ article.url }}">
<h4 class="font-display font-bold text-xl mb-2 group-hover:text-brand-cyan transition-colors">{{ article.title }}</h4>
</a>
<p class="text-zinc-600 dark:text-zinc-400 text-sm mb-6 line-clamp-2">{{ article.summary }}</p>
<div class="mt-auto pt-4 border-t border-zinc-100 dark:border-zinc-800 flex items-center gap-2 text-sm font-bold font-mono group-hover:text-brand-cyan transition-colors">
Read Article
<svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="m4.5 19.5 15-15m0 0H8.25m11.25 0v11.25" /></svg>
</div>
</div>
</article>
{% endfor %}
</div>
</article>
<section aria-label="Share this article">
<h2>Share</h2>
<a href="https://x.com/intent/post?url={{ request.build_absolute_uri|urlencode }}&text={{ page.title|urlencode }}" target="_blank" rel="noopener noreferrer">Share on X</a>
<a href="https://www.linkedin.com/sharing/share-offsite/?url={{ request.build_absolute_uri|urlencode }}" target="_blank" rel="noopener noreferrer">Share on LinkedIn</a>
<button type="button" data-copy-link data-copy-url="{{ request.build_absolute_uri }}">Copy link</button>
</section>
{% endif %}
<!-- Comments -->
<section>
<h2>Related</h2>
{% for article in related_articles %}
<a href="{{ article.url }}">{{ article.title }}</a>
{% endfor %}
</section>
<aside>
<h2>Newsletter</h2>
{% include 'components/newsletter_form.html' with source='article' label='Never miss a post' %}
</aside>
{% if page.comments_enabled %}
<section class="mt-16 pt-12 border-t border-zinc-200 dark:border-zinc-800">
<h2 class="font-display font-bold text-3xl mb-8">Comments</h2>
{% if approved_comments %}
<div class="space-y-8 mb-12">
<section>
<h2>Comments</h2>
{% for comment in approved_comments %}
<article id="comment-{{ comment.id }}" class="bg-brand-surfaceLight dark:bg-brand-surfaceDark border border-zinc-200 dark:border-zinc-800 p-6">
<div class="flex items-center gap-3 mb-3">
<div class="w-8 h-8 bg-gradient-to-tr from-brand-cyan to-brand-pink shrink-0"></div>
<div>
<div class="font-display font-bold text-sm">{{ comment.author_name }}</div>
<div class="font-mono text-xs text-zinc-500">{{ comment.created_at|date:"M j, Y" }}</div>
</div>
</div>
<p class="text-zinc-700 dark:text-zinc-300 text-sm leading-relaxed">{{ comment.body }}</p>
<article id="comment-{{ comment.id }}">
<p><strong>{{ comment.author_name }}</strong></p>
<p>{{ comment.body }}</p>
{% for reply in comment.replies.all %}
<article id="comment-{{ reply.id }}" class="mt-6 ml-8 bg-zinc-50 dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 p-4">
<div class="flex items-center gap-3 mb-2">
<div class="w-6 h-6 bg-gradient-to-tr from-brand-pink to-brand-cyan shrink-0"></div>
<div>
<div class="font-display font-bold text-sm">{{ reply.author_name }}</div>
<div class="font-mono text-xs text-zinc-500">{{ reply.created_at|date:"M j, Y" }}</div>
</div>
</div>
<p class="text-zinc-700 dark:text-zinc-300 text-sm leading-relaxed">{{ reply.body }}</p>
<article id="comment-{{ reply.id }}">
<p><strong>{{ reply.author_name }}</strong></p>
<p>{{ reply.body }}</p>
</article>
{% endfor %}
<form method="post" action="{% url 'comment_post' %}" class="mt-4 pt-4 border-t border-zinc-100 dark:border-zinc-800">
<form method="post" action="{% url 'comment_post' %}">
{% csrf_token %}
<input type="hidden" name="article_id" value="{{ page.id }}" />
<input type="hidden" name="parent_id" value="{{ comment.id }}" />
<div class="flex gap-3 mb-3">
<input type="text" name="author_name" required placeholder="Your name"
class="flex-1 bg-transparent border border-zinc-300 dark:border-zinc-700 px-3 py-2 font-mono text-sm focus:outline-none focus:border-brand-pink transition-colors" />
<input type="email" name="author_email" required placeholder="your@email.com"
class="flex-1 bg-transparent border border-zinc-300 dark:border-zinc-700 px-3 py-2 font-mono text-sm focus:outline-none focus:border-brand-pink transition-colors" />
</div>
<textarea name="body" required placeholder="Write a reply..." rows="2"
class="w-full bg-transparent border border-zinc-300 dark:border-zinc-700 px-3 py-2 font-mono text-sm focus:outline-none focus:border-brand-pink transition-colors mb-3 resize-none"></textarea>
<input type="text" name="author_name" required />
<input type="email" name="author_email" required />
<textarea name="body" required></textarea>
<input type="text" name="honeypot" style="display:none" />
<button type="submit" class="px-4 py-2 bg-zinc-200 dark:bg-zinc-800 font-display font-bold text-sm hover:bg-brand-pink hover:text-white transition-colors">Reply</button>
<button type="submit">Reply</button>
</form>
</article>
{% empty %}
<p>No comments yet.</p>
{% endfor %}
{% if comment_form and comment_form.errors %}
<div aria-label="Comment form errors">
{{ comment_form.non_field_errors }}
{% for field in comment_form %}
{{ field.errors }}
{% endfor %}
</div>
{% else %}
<p class="font-mono text-sm text-zinc-500 mb-12">No comments yet. Be the first to comment.</p>
{% endif %}
{% if comment_form and comment_form.errors %}
<div aria-label="Comment form errors" class="mb-6 p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 font-mono text-sm text-red-600 dark:text-red-400">
{{ comment_form.non_field_errors }}
{% for field in comment_form %}{{ field.errors }}{% endfor %}
</div>
{% endif %}
<div class="bg-brand-surfaceLight dark:bg-brand-surfaceDark border border-zinc-200 dark:border-zinc-800 p-6">
<h3 class="font-display font-bold text-xl mb-6">Post a Comment</h3>
<form method="post" action="{% url 'comment_post' %}" class="space-y-4">
<form method="post" action="{% url 'comment_post' %}">
{% csrf_token %}
<input type="hidden" name="article_id" value="{{ page.id }}" />
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label class="block font-mono text-xs text-zinc-500 mb-1 uppercase tracking-wider">Name *</label>
<input type="text" name="author_name" value="{% if comment_form %}{{ comment_form.author_name.value|default:'' }}{% endif %}" required
class="w-full bg-transparent border border-zinc-300 dark:border-zinc-700 px-4 py-2 font-mono text-sm focus:outline-none focus:border-brand-pink transition-colors" />
</div>
<div>
<label class="block font-mono text-xs text-zinc-500 mb-1 uppercase tracking-wider">Email *</label>
<input type="email" name="author_email" value="{% if comment_form %}{{ comment_form.author_email.value|default:'' }}{% endif %}" required
class="w-full bg-transparent border border-zinc-300 dark:border-zinc-700 px-4 py-2 font-mono text-sm focus:outline-none focus:border-brand-pink transition-colors" />
</div>
</div>
<div>
<label class="block font-mono text-xs text-zinc-500 mb-1 uppercase tracking-wider">Comment *</label>
<textarea name="body" required rows="5"
class="w-full bg-transparent border border-zinc-300 dark:border-zinc-700 px-4 py-2 font-mono text-sm focus:outline-none focus:border-brand-pink transition-colors resize-none">{% if comment_form %}{{ comment_form.body.value|default:'' }}{% endif %}</textarea>
</div>
<input type="text" name="author_name" value="{% if comment_form %}{{ comment_form.author_name.value|default:'' }}{% endif %}" required />
<input type="email" name="author_email" value="{% if comment_form %}{{ comment_form.author_email.value|default:'' }}{% endif %}" required />
<textarea name="body" required>{% if comment_form %}{{ comment_form.body.value|default:'' }}{% endif %}</textarea>
<input type="text" name="honeypot" style="display:none" />
<button type="submit" class="px-6 py-3 bg-brand-dark text-brand-light dark:bg-brand-light dark:text-brand-dark font-display font-bold hover:-translate-y-1 hover:shadow-solid-dark dark:hover:shadow-solid-light transition-all">Post comment</button>
<button type="submit">Post comment</button>
</form>
</div>
</section>
{% endif %}
{% endblock %}

View File

@@ -1,20 +1,4 @@
<div class="bg-zinc-100 dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 p-6 my-8 flex items-start gap-4
{% if value.icon == 'warning' %}border-l-4 border-l-yellow-400{% elif value.icon == 'trophy' %}border-l-4 border-l-brand-pink{% elif value.icon == 'tip' %}border-l-4 border-l-green-500{% else %}border-l-4 border-l-brand-cyan{% endif %}">
<div class="shrink-0 mt-0.5">
{% if value.icon == 'warning' %}
<svg class="w-6 h-6 text-yellow-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z" /></svg>
{% elif value.icon == 'trophy' %}
<svg class="w-6 h-6 text-brand-pink" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M16.5 18.75h-9m9 0a3 3 0 0 1 3 3h-15a3 3 0 0 1 3-3m9 0v-3.375c0-.621-.503-1.125-1.125-1.125h-.871M7.5 18.75v-3.375c0-.621.504-1.125 1.125-1.125h.872m5.007 0H9.497m5.007 0a7.454 7.454 0 0 1-.982-3.172M9.497 14.25a7.454 7.454 0 0 0 .981-3.172M5.25 4.236c-.982.143-1.954.317-2.916.52A6.003 6.003 0 0 0 7.73 9.728M5.25 4.236V4.5c0 2.108.966 3.99 2.48 5.228M5.25 4.236V2.721C7.456 2.41 9.71 2.25 12 2.25c2.291 0 4.545.16 6.75.47v1.516M7.73 9.728a6.726 6.726 0 0 0 2.748 1.35m8.272-6.842V4.5c0 2.108-.966 3.99-2.48 5.228m2.48-5.492a46.32 46.32 0 0 1 2.916.52 6.003 6.003 0 0 1-5.395 4.972m0 0a6.726 6.726 0 0 1-2.749 1.35m0 0a6.772 6.772 0 0 1-3.044 0" /></svg>
{% elif value.icon == 'tip' %}
<svg class="w-6 h-6 text-green-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M12 18v-5.25m0 0a6.01 6.01 0 0 0 1.5-.189m-1.5.189a6.01 6.01 0 0 1-1.5-.189m3.75 7.478a12.06 12.06 0 0 1-4.5 0m3.75 2.383a14.406 14.406 0 0 1-3 0M14.25 18v-.192c0-.983.658-1.823 1.508-2.316a7.5 7.5 0 1 0-7.517 0c.85.493 1.509 1.333 1.509 2.316V18" /></svg>
{% else %}
<svg class="w-6 h-6 text-brand-cyan" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z" /></svg>
{% endif %}
</div>
<div class="min-w-0">
{% if value.heading %}
<h4 class="font-display font-bold text-lg mb-2">{{ value.heading }}</h4>
{% endif %}
<div class="text-zinc-700 dark:text-zinc-300 text-sm leading-relaxed">{{ value.body }}</div>
</div>
<div class="callout icon-{{ value.icon }}">
<h3>{{ value.heading }}</h3>
{{ value.body }}
</div>

View File

@@ -1,19 +1,5 @@
{% load wagtailcore_tags %}
<div class="my-8 overflow-hidden bg-[#0d1117] border border-zinc-800 shadow-xl">
<div class="flex items-center justify-between px-4 py-2 bg-[#161b22] border-b border-zinc-800">
<div class="flex gap-2">
<div class="w-3 h-3 rounded-full bg-red-500"></div>
<div class="w-3 h-3 rounded-full bg-yellow-500"></div>
<div class="w-3 h-3 rounded-full bg-green-500"></div>
</div>
{% if value.filename %}
<div class="font-mono text-xs text-zinc-500">{{ value.filename }}</div>
{% else %}
<div class="font-mono text-xs text-zinc-500">{{ value.language }}</div>
{% endif %}
<div class="w-8"></div>
</div>
<div class="overflow-x-auto">
<pre data-lang="{{ value.language }}" class="p-6 text-sm"><code class="language-{{ value.language }} font-mono text-zinc-300">{{ value.raw_code }}</code></pre>
</div>
<div class="code-block">
{% if value.filename %}<div>{{ value.filename }}</div>{% endif %}
<pre data-lang="{{ value.language }}"><code class="language-{{ value.language }}">{{ value.raw_code }}</code></pre>
</div>

View File

@@ -1,5 +1,5 @@
{% extends 'base.html' %}
{% load wagtailimages_tags seo_tags core_tags %}
{% load seo_tags %}
{% block title %}No Hype AI{% endblock %}
{% block head_meta %}
{% canonical_url page as canonical %}
@@ -11,146 +11,21 @@
<meta property="og:url" content="{{ canonical }}" />
{% endblock %}
{% block content %}
<!-- Featured Article -->
{% if featured_article %}
<section class="mb-12 md:mb-16">
<div class="flex items-center gap-2 mb-6">
<span class="w-2 h-2 rounded-full bg-brand-pink animate-pulse"></span>
<span class="font-mono text-sm font-bold uppercase tracking-widest text-zinc-500">Featured Article</span>
</div>
<article class="group cursor-pointer grid grid-cols-1 lg:grid-cols-2 gap-8 items-center bg-brand-surfaceLight dark:bg-brand-surfaceDark border border-zinc-200 dark:border-zinc-800 p-4 md:p-6 hover:border-brand-cyan dark:hover:border-brand-cyan hover:shadow-solid-dark dark:hover:shadow-solid-light transition-all duration-300">
<a href="{{ featured_article.url }}" class="w-full h-64 md:h-[400px] overflow-hidden relative bg-zinc-100 dark:bg-zinc-900 order-2 lg:order-1 border border-zinc-200 dark:border-zinc-800 block">
{% if featured_article.hero_image %}
{% image featured_article.hero_image fill-800x600 class="w-full h-full object-cover grayscale group-hover:grayscale-0 transition-all duration-700 group-hover:scale-105" %}
{% else %}
<div class="absolute inset-0 flex items-center justify-center">
<svg class="w-20 h-20 text-brand-cyan opacity-30" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M9.75 3.104v5.714a2.25 2.25 0 0 1-.659 1.591L5 14.5M9.75 3.104c-.251.023-.501.05-.75.082m.75-.082a24.301 24.301 0 0 1 4.5 0m0 0v5.714c0 .597.237 1.17.659 1.591L19.8 15.3M14.25 3.104c.251.023.501.05.75.082M19.8 15.3l-1.57.393A9.065 9.065 0 0 1 12 15a9.065 9.065 0 0 1-6.23-.693L5 14.5m14.8.8 1.402 1.402c1 1 .03 2.798-1.442 2.798H4.24c-1.47 0-2.44-1.798-1.442-2.798L4.2 15.3" /></svg>
</div>
<section>
{% if featured_article %}
<h2>{{ featured_article.title }}</h2>
<p>{{ featured_article.author.name }}</p>
<p>{{ featured_article.read_time_mins }} min read</p>
{% endif %}
</a>
<div class="flex flex-col py-2 order-1 lg:order-2">
<div class="flex gap-3 mb-4 items-center flex-wrap">
{% for tag in featured_article.tags.all %}
<span class="text-xs font-mono font-bold px-2 py-1 {{ tag|get_tag_css }}">{{ tag.name }}</span>
{% endfor %}
<span class="text-sm font-mono text-zinc-500">{{ featured_article.read_time_mins }} min read</span>
</div>
<a href="{{ featured_article.url }}">
<h2 class="font-display font-black text-3xl md:text-5xl mb-4 group-hover:text-brand-cyan transition-colors leading-[1.1]">{{ featured_article.title }}</h2>
</a>
<p class="text-zinc-600 dark:text-zinc-400 mb-8 text-lg md:text-xl line-clamp-3">{{ featured_article.summary }}</p>
<div class="mt-auto flex items-center justify-between pt-4 border-t border-zinc-200 dark:border-zinc-800">
<div class="flex items-center gap-3">
<div class="w-10 h-10 bg-gradient-to-tr from-brand-cyan to-brand-pink"></div>
<div>
<div class="text-sm font-bold font-display">{{ featured_article.author.name }}</div>
<div class="text-xs font-mono text-zinc-500">{{ featured_article.first_published_at|date:"M j, Y" }}</div>
</div>
</div>
<a href="{{ featured_article.url }}" class="text-sm font-bold font-mono group-hover:text-brand-cyan transition-colors flex items-center gap-1">
Read
<svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M13.5 4.5 21 12m0 0-7.5 7.5M21 12H3" /></svg>
</a>
</div>
</div>
</article>
</section>
{% endif %}
<!-- 2-Column Editorial Layout -->
<div class="grid grid-cols-1 lg:grid-cols-12 gap-12">
<!-- Main Feed -->
<div class="lg:col-span-8">
<div class="flex items-center justify-between mb-8 pb-4 border-b border-zinc-200 dark:border-zinc-800">
<h3 class="font-display font-bold text-3xl">Latest Articles</h3>
<a href="/articles/" class="font-mono text-sm text-brand-cyan hover:underline flex items-center gap-1">View All</a>
</div>
<div class="space-y-8">
<section>
{% for article in latest_articles %}
<article class="group flex flex-col md:flex-row gap-6 items-start pb-8 border-b border-zinc-200 dark:border-zinc-800 last:border-0">
<a href="{{ article.url }}" class="w-full md:w-48 h-48 md:h-32 shrink-0 overflow-hidden relative bg-zinc-100 dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-800 flex items-center justify-center block">
{% if article.hero_image %}
{% image article.hero_image fill-200x130 class="w-full h-full object-cover grayscale group-hover:grayscale-0 transition-all duration-500" %}
{% else %}
<svg class="w-12 h-12 text-brand-cyan opacity-40 group-hover:opacity-80 transition-all duration-500 group-hover:scale-110" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M9.75 3.104v5.714a2.25 2.25 0 0 1-.659 1.591L5 14.5M9.75 3.104c-.251.023-.501.05-.75.082m.75-.082a24.301 24.301 0 0 1 4.5 0m0 0v5.714c0 .597.237 1.17.659 1.591L19.8 15.3M14.25 3.104c.251.023.501.05.75.082M19.8 15.3l-1.57.393A9.065 9.065 0 0 1 12 15a9.065 9.065 0 0 1-6.23-.693L5 14.5m14.8.8 1.402 1.402c1 1 .03 2.798-1.442 2.798H4.24c-1.47 0-2.44-1.798-1.442-2.798L4.2 15.3" /></svg>
{% endif %}
</a>
<div class="flex flex-col w-full">
<div class="flex gap-3 mb-2 items-center flex-wrap">
{% for tag in article.tags.all %}
<span class="text-xs font-mono font-bold px-2 py-1 {{ tag|get_tag_css }}">{{ tag.name }}</span>
{% include 'components/article_card.html' with article=article %}
{% endfor %}
<span class="text-sm font-mono text-zinc-500">{{ article.first_published_at|date:"M j" }}</span>
</div>
<a href="{{ article.url }}">
<h4 class="font-display font-bold text-2xl mb-2 group-hover:text-brand-cyan transition-colors">{{ article.title }}</h4>
</a>
<p class="text-zinc-600 dark:text-zinc-400 text-sm line-clamp-2 mb-3">{{ article.summary }}</p>
<a href="{{ article.url }}" class="text-xs font-mono font-bold group-hover:text-brand-cyan transition-colors mt-auto">Read article →</a>
</div>
</article>
{% endfor %}
</div>
{% if more_articles %}
<div class="mt-10">
<div class="flex items-center justify-between mb-6 pb-4 border-b border-zinc-200 dark:border-zinc-800">
<h3 class="font-display font-bold text-2xl">More Articles</h3>
</div>
<div class="space-y-6">
</section>
<section>
{% for article in more_articles %}
{% include 'components/article_card.html' with article=article %}
{% endfor %}
</div>
</div>
{% endif %}
</div>
<!-- Sidebar -->
<aside class="lg:col-span-4 space-y-8">
<!-- Newsletter Widget -->
<div class="bg-brand-surfaceLight dark:bg-brand-surfaceDark border border-zinc-200 dark:border-zinc-800 p-6 relative overflow-hidden">
<div class="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-brand-cyan to-brand-pink"></div>
<h4 class="font-display font-bold text-xl mb-2">Weekly Newsletter</h4>
<p class="text-zinc-600 dark:text-zinc-400 text-sm mb-4">Get our latest articles and coding benchmarks delivered to your inbox every week.</p>
{% include 'components/newsletter_form.html' with source='sidebar' label='Subscribe' %}
</div>
<!-- Popular Articles -->
{% if latest_articles %}
<div>
<h4 class="font-display font-bold mb-4 uppercase tracking-widest text-zinc-500 text-sm">Popular Articles</h4>
<ul class="space-y-4">
{% for article in latest_articles %}
<li class="group">
<a href="{{ article.url }}" class="flex gap-4 items-start">
<span class="font-display font-black text-2xl text-zinc-300 dark:text-zinc-800 group-hover:text-brand-{% cycle 'cyan' 'pink' 'cyan' 'pink' 'cyan' %} transition-colors">0{{ forloop.counter }}</span>
<div>
<h5 class="font-display font-bold text-sm leading-tight group-hover:text-brand-cyan transition-colors">{{ article.title }}</h5>
<div class="text-xs font-mono text-zinc-500 mt-1">{{ article.read_time_mins }} min read</div>
</div>
</a>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% if available_tags %}
<div>
<h4 class="font-display font-bold mb-4 uppercase tracking-widest text-zinc-500 text-sm">Explore Topics</h4>
<div class="flex flex-wrap gap-2">
{% for tag in available_tags %}
<a href="/articles/?tag={{ tag.slug }}" class="px-3 py-1.5 border border-zinc-200 dark:border-zinc-800 text-sm font-mono hover:border-brand-cyan hover:text-brand-cyan transition-colors">#{{ tag.name }}</a>
{% endfor %}
</div>
</div>
{% endif %}
</aside>
</div>
</section>
{% endblock %}

View File

@@ -1,31 +1,8 @@
{% load core_tags wagtailimages_tags %}
<article class="group flex flex-col md:flex-row gap-8 items-center bg-brand-surfaceLight dark:bg-brand-surfaceDark border border-zinc-200 dark:border-zinc-800 p-4 hover:border-brand-cyan dark:hover:border-brand-cyan transition-colors">
<a href="{{ article.url }}" class="w-full md:w-1/3 h-48 md:h-full min-h-[200px] overflow-hidden relative bg-zinc-100 dark:bg-zinc-900 shrink-0 block">
{% if article.hero_image %}
{% image article.hero_image fill-600x400 class="w-full h-full object-cover grayscale group-hover:grayscale-0 transition-all duration-500" %}
{% else %}
<div class="w-full h-full min-h-[200px] flex items-center justify-center bg-zinc-900">
<svg class="w-16 h-16 text-brand-cyan opacity-40 group-hover:opacity-80 transition-opacity" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M9.75 3.104v5.714a2.25 2.25 0 0 1-.659 1.591L5 14.5M9.75 3.104c-.251.023-.501.05-.75.082m.75-.082a24.301 24.301 0 0 1 4.5 0m0 0v5.714c0 .597.237 1.17.659 1.591L19.8 15.3M14.25 3.104c.251.023.501.05.75.082M19.8 15.3l-1.57.393A9.065 9.065 0 0 1 12 15a9.065 9.065 0 0 1-6.23-.693L5 14.5m14.8.8 1.402 1.402c1 1 .03 2.798-1.442 2.798H4.24c-1.47 0-2.44-1.798-1.442-2.798L4.2 15.3" /></svg>
</div>
{% endif %}
</a>
<div class="flex flex-col py-2 w-full">
<div class="flex gap-3 mb-4 items-center flex-wrap">
{% load core_tags %}
<article>
<a href="{{ article.url }}">{{ article.title }}</a>
<p>{{ article.summary|truncatewords:20 }}</p>
{% for tag in article.tags.all %}
<span class="text-xs font-mono font-bold px-2 py-1 {{ tag|get_tag_css }}">{{ tag.name }}</span>
<span class="{{ tag|get_tag_css }}">{{ tag.name }}</span>
{% endfor %}
<span class="text-sm font-mono text-zinc-500">{{ article.first_published_at|date:"M j, Y" }}</span>
</div>
<a href="{{ article.url }}">
<h2 class="font-display font-bold text-2xl md:text-3xl mb-3 group-hover:text-brand-cyan transition-colors">{{ article.title }}</h2>
</a>
<p class="text-zinc-600 dark:text-zinc-400 mb-6 max-w-2xl line-clamp-2">{{ article.summary }}</p>
<div class="flex items-center justify-between mt-auto">
<span class="text-sm font-mono text-zinc-500">{{ article.read_time_mins }} min read</span>
<a href="{{ article.url }}" class="flex items-center gap-2 text-sm font-bold font-mono group-hover:text-brand-cyan transition-colors">
Read Article
<svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="m4.5 19.5 15-15m0 0H8.25m11.25 0v11.25" /></svg>
</a>
</div>
</div>
</article>

View File

@@ -1,43 +1,27 @@
{% if request.consent.requires_prompt %}
<div id="cookie-banner" class="fixed bottom-0 left-0 right-0 z-50 bg-brand-surfaceLight dark:bg-brand-surfaceDark border-t border-zinc-200 dark:border-zinc-800 shadow-lg">
<div class="max-w-7xl mx-auto px-6 py-4 flex flex-col md:flex-row items-start md:items-center justify-between gap-4">
<div class="flex-1">
<p class="font-mono text-sm text-zinc-600 dark:text-zinc-400">
We use cookies to improve your experience.
{% if site_settings and site_settings.privacy_policy_page %}
<a href="{{ site_settings.privacy_policy_page.url }}" class="text-brand-cyan hover:underline">Privacy Policy</a>
{% endif %}
</p>
</div>
<div class="flex items-center gap-3 shrink-0">
<form method="post" action="{% url 'consent' %}" class="inline">
<div id="cookie-banner">
<form method="post" action="{% url 'consent' %}">
{% csrf_token %}
<button type="submit" name="reject_all" value="1"
class="px-4 py-2 border border-zinc-300 dark:border-zinc-700 font-mono text-sm hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-colors">Reject all</button>
<button type="submit" name="accept_all" value="1">Accept all</button>
<button type="submit" name="reject_all" value="1">Reject all</button>
</form>
<form method="post" action="{% url 'consent' %}" class="inline">
<details>
<summary>Manage preferences</summary>
<form method="post" action="{% url 'consent' %}">
{% csrf_token %}
<button type="submit" name="accept_all" value="1"
class="px-4 py-2 bg-brand-dark text-brand-light dark:bg-brand-light dark:text-brand-dark font-display font-bold text-sm hover:bg-brand-cyan transition-colors">Accept all</button>
</form>
<details class="relative">
<summary class="px-4 py-2 border border-zinc-300 dark:border-zinc-700 font-mono text-sm hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-colors cursor-pointer list-none">Manage</summary>
<div class="absolute bottom-full right-0 mb-2 w-72 bg-brand-surfaceLight dark:bg-brand-surfaceDark border border-zinc-200 dark:border-zinc-800 p-4 shadow-lg">
<form method="post" action="{% url 'consent' %}" class="space-y-3">
{% csrf_token %}
<label class="flex items-center gap-3 font-mono text-sm cursor-pointer">
<input type="checkbox" name="analytics" value="1" class="accent-brand-cyan" />
<label>
<input type="checkbox" name="analytics" value="1" />
Analytics cookies
</label>
<label class="flex items-center gap-3 font-mono text-sm cursor-pointer">
<input type="checkbox" name="advertising" value="1" class="accent-brand-pink" />
<label>
<input type="checkbox" name="advertising" value="1" />
Advertising cookies
</label>
<button type="submit" class="w-full px-4 py-2 bg-brand-dark text-brand-light dark:bg-brand-light dark:text-brand-dark font-display font-bold text-sm hover:bg-brand-cyan transition-colors">Save preferences</button>
<button type="submit">Save preferences</button>
</form>
</div>
</details>
</div>
</div>
{% if site_settings and site_settings.privacy_policy_page %}
<a href="{{ site_settings.privacy_policy_page.url }}">Privacy Policy</a>
{% endif %}
</div>
{% endif %}

View File

@@ -1,33 +1,8 @@
{% load core_tags %}
<footer class="border-t border-zinc-200 dark:border-zinc-800 bg-brand-light dark:bg-brand-dark mt-12 py-12 text-center md:text-left">
<div class="max-w-7xl mx-auto px-6 grid grid-cols-1 md:grid-cols-4 gap-8">
<div class="md:col-span-2">
<a href="/" class="font-display font-bold text-2xl tracking-tight mb-4 inline-block">NO HYPE AI</a>
<p class="text-zinc-500 font-mono text-sm max-w-sm mx-auto md:mx-0">
In-depth reviews and benchmarks of the latest AI coding tools.<br>
Honest analysis for developers.
</p>
</div>
<div>
<h4 class="font-display font-bold mb-4 uppercase text-sm tracking-widest text-zinc-400">Navigation</h4>
<ul class="space-y-2 font-mono text-sm text-zinc-500">
<li><a href="/" class="hover:text-brand-cyan transition-colors">Home</a></li>
<li><a href="/articles/" class="hover:text-brand-cyan transition-colors">Articles</a></li>
<li><a href="/about/" class="hover:text-brand-pink transition-colors">About</a></li>
<footer>
{% get_legal_pages as legal_pages %}
{% for page in legal_pages %}
<li><a href="{{ page.url }}" class="hover:text-brand-pink transition-colors">{{ page.title }}</a></li>
{% endfor %}
</ul>
</div>
<div>
<h4 class="font-display font-bold mb-4 uppercase text-sm tracking-widest text-zinc-400">Newsletter</h4>
<p class="text-zinc-500 font-mono text-sm mb-4">Get weekly AI tool reviews.</p>
{% include 'components/newsletter_form.html' with source='footer' label='Newsletter' %}
</div>
</div>
<div class="max-w-7xl mx-auto px-6 mt-12 pt-8 border-t border-zinc-200 dark:border-zinc-800 text-center font-mono text-xs text-zinc-500 flex flex-col md:flex-row justify-between items-center gap-4">
<p>&copy; {% now "Y" %} No Hype AI. All rights reserved.</p>
<p>Honest AI tool reviews for developers.</p>
</div>
{% for page in legal_pages %}
<a href="{{ page.url }}">{{ page.title }}</a>
{% endfor %}
</footer>

View File

@@ -1,71 +1,7 @@
{% load static %}
<nav class="sticky top-0 z-50 backdrop-blur-md bg-brand-light/80 dark:bg-brand-dark/80 border-b border-zinc-200 dark:border-zinc-800 transition-colors">
<div class="max-w-7xl mx-auto px-6 h-20 flex items-center justify-between">
<!-- Logo -->
<a href="/" class="group flex items-center gap-2">
<div class="w-8 h-8 bg-brand-dark dark:bg-brand-light text-brand-light dark:text-brand-dark flex items-center justify-center font-display font-bold text-xl group-hover:rotate-12 transition-transform">
/
</div>
<span class="font-display font-bold text-2xl tracking-tight">NO HYPE AI</span>
</a>
<!-- Desktop Links -->
<div class="hidden md:flex items-center gap-8 font-medium">
<a href="/" class="hover:text-brand-cyan transition-colors">Home</a>
<a href="/articles/" class="hover:text-brand-cyan transition-colors">Articles</a>
<a href="/about/" class="hover:text-brand-pink transition-colors">About</a>
<form method="post" action="/newsletter/subscribe/" data-newsletter-form class="flex items-center gap-2" id="nav-newsletter">
{% csrf_token %}
<input type="hidden" name="source" value="nav" />
<input type="email" name="email" required placeholder="dev@example.com"
class="bg-transparent border border-zinc-300 dark:border-zinc-700 px-3 py-2 text-brand-dark dark:text-brand-light font-mono text-sm focus:outline-none focus:border-brand-pink transition-colors w-44" />
<input type="text" name="honeypot" style="display:none" />
<button type="submit" class="px-5 py-2.5 bg-brand-dark dark:bg-brand-light text-brand-light dark:text-brand-dark font-display font-bold hover:-translate-y-1 hover:shadow-solid-dark dark:hover:shadow-solid-light transition-all border border-transparent dark:border-zinc-700 whitespace-nowrap">Subscribe</button>
<p data-newsletter-message aria-live="polite" class="font-mono text-xs text-brand-cyan min-h-[1rem]"></p>
</form>
</div>
<!-- Theme Toggle + Hamburger -->
<div class="flex items-center gap-4">
<button type="button" data-theme-toggle class="p-2 rounded-full hover:bg-zinc-200 dark:hover:bg-zinc-800 transition-colors" aria-label="Toggle theme">
<svg class="w-5 h-5 hidden dark:block text-yellow-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M12 3v2.25m6.364.386-1.591 1.591M21 12h-2.25m-.386 6.364-1.591-1.591M12 18.75V21m-4.773-4.227-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0Z" /></svg>
<svg class="w-5 h-5 block dark:hidden text-zinc-700" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M21.752 15.002A9.72 9.72 0 0 1 18 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 0 0 3 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 0 0 9.002-5.998Z" /></svg>
</button>
<button type="button" data-mobile-menu-toggle class="md:hidden p-2 hover:bg-zinc-100 dark:hover:bg-zinc-800 rounded transition-colors" aria-label="Open menu" aria-expanded="false" aria-controls="mobile-menu">
<svg class="w-6 h-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" /></svg>
</button>
</div>
</div>
<nav>
<a href="/">Home</a>
<a href="/articles/">Articles</a>
<a href="/about/">About</a>
<button type="button" data-theme-toggle>Toggle theme</button>
{% include 'components/newsletter_form.html' with source='nav' label='Get updates' %}
</nav>
<!-- Mobile Menu (outside <nav> to avoid duplicate form[data-newsletter-form] in nav scope) -->
<div id="mobile-menu" class="md:hidden hidden sticky top-20 z-40 border-b border-zinc-200 dark:border-zinc-800 bg-brand-light/95 dark:bg-brand-dark/95 backdrop-blur-md">
<div class="max-w-7xl mx-auto px-6 py-4 flex flex-col gap-4">
<a href="/" class="font-medium py-2 hover:text-brand-cyan transition-colors">Home</a>
<a href="/articles/" class="font-medium py-2 hover:text-brand-cyan transition-colors">Articles</a>
<a href="/about/" class="font-medium py-2 hover:text-brand-pink transition-colors">About</a>
<form method="post" action="/newsletter/subscribe/" data-newsletter-form class="space-y-2 pt-2 border-t border-zinc-200 dark:border-zinc-800" id="mobile-newsletter">
{% csrf_token %}
<input type="hidden" name="source" value="nav-mobile" />
<input type="email" name="email" required placeholder="dev@example.com"
class="w-full bg-transparent border border-zinc-300 dark:border-zinc-700 px-3 py-2 text-brand-dark dark:text-brand-light font-mono text-sm focus:outline-none focus:border-brand-pink transition-colors" />
<input type="text" name="honeypot" style="display:none" />
<button type="submit" class="w-full px-4 py-2.5 bg-brand-dark dark:bg-brand-light text-brand-light dark:text-brand-dark font-display font-bold transition-colors">Subscribe</button>
<p data-newsletter-message aria-live="polite" class="font-mono text-xs text-brand-cyan min-h-[1rem]"></p>
</form>
</div>
</div>
<script nonce="{{ request.csp_nonce|default:'' }}">
(function(){
var btn = document.querySelector('[data-mobile-menu-toggle]');
var menu = document.getElementById('mobile-menu');
if (btn && menu) {
btn.addEventListener('click', function(){
var isOpen = !menu.classList.contains('hidden');
menu.classList.toggle('hidden');
btn.setAttribute('aria-expanded', String(!isOpen));
});
}
})();
</script>

View File

@@ -1,12 +1,11 @@
<form method="post" action="/newsletter/subscribe/" data-newsletter-form class="space-y-3" id="newsletter">
<form method="post" action="/newsletter/subscribe/" data-newsletter-form>
{% csrf_token %}
<input type="hidden" name="source" value="{{ source|default:'unknown' }}" />
<input type="email" name="email" required placeholder="dev@example.com"
class="w-full bg-transparent border border-zinc-300 dark:border-zinc-700 px-4 py-2 text-brand-dark dark:text-brand-light font-mono text-sm focus:outline-none focus:border-brand-pink transition-colors" />
<label>
<span>{{ label|default:"Newsletter" }}</span>
<input type="email" name="email" required />
</label>
<input type="text" name="honeypot" style="display:none" />
<button type="submit"
class="w-full bg-brand-dark text-brand-light dark:bg-brand-light dark:text-brand-dark font-display font-bold py-2 hover:bg-brand-pink dark:hover:bg-brand-pink hover:text-white transition-colors">
{{ label|default:"Subscribe" }}
</button>
<p data-newsletter-message aria-live="polite" class="font-mono text-xs text-brand-cyan min-h-[1rem]"></p>
<button type="submit">Subscribe</button>
<p data-newsletter-message aria-live="polite"></p>
</form>

View File

@@ -1,18 +1,7 @@
{% extends 'base.html' %}
{% load wagtailcore_tags %}
{% block title %}{{ page.title }} | No Hype AI{% endblock %}
{% block content %}
<!-- Page Header -->
<div class="py-8 md:py-12 border-b border-zinc-200 dark:border-zinc-800 mb-12">
<h1 class="font-display font-black text-4xl md:text-5xl mb-3">{{ page.title }}</h1>
<p class="font-mono text-sm text-zinc-500">Last updated: {{ page.last_updated|date:'F Y' }}</p>
</div>
<!-- Body -->
<div class="max-w-3xl prose prose-lg dark:prose-invert
prose-headings:font-display prose-headings:font-bold
prose-a:text-brand-cyan hover:prose-a:text-brand-pink prose-a:transition-colors prose-a:no-underline hover:prose-a:underline">
{{ page.body|richtext }}
</div>
<h1>{{ page.title }}</h1>
<p>Last updated: {{ page.last_updated|date:'F Y' }}</p>
{{ page.body|richtext }}
{% endblock %}

File diff suppressed because one or more lines are too long

View File

@@ -8,7 +8,6 @@
"name": "nohype-theme",
"version": "1.0.0",
"devDependencies": {
"@tailwindcss/typography": "^0.5.19",
"tailwindcss": "^3.4.17"
}
},
@@ -94,33 +93,6 @@
"node": ">= 8"
}
},
"node_modules/@tailwindcss/typography": {
"version": "0.5.19",
"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.19.tgz",
"integrity": "sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==",
"dev": true,
"license": "MIT",
"dependencies": {
"postcss-selector-parser": "6.0.10"
},
"peerDependencies": {
"tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1"
}
},
"node_modules/@tailwindcss/typography/node_modules/postcss-selector-parser": {
"version": "6.0.10",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz",
"integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==",
"dev": true,
"license": "MIT",
"dependencies": {
"cssesc": "^3.0.0",
"util-deprecate": "^1.0.2"
},
"engines": {
"node": ">=4"
}
},
"node_modules/any-promise": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",

View File

@@ -7,7 +7,6 @@
"dev": "tailwindcss -i ./src/input.css -o ../static/css/styles.css --watch"
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.19",
"tailwindcss": "^3.4.17"
}
}

View File

@@ -1,40 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer utilities {
.bg-grid-pattern {
background-image: radial-gradient(rgba(0, 0, 0, 0.1) 1px, transparent 1px);
background-size: 24px 24px;
}
.dark .bg-grid-pattern {
background-image: radial-gradient(rgba(255, 255, 255, 0.07) 1px, transparent 1px);
}
.text-gradient {
background: linear-gradient(135deg, #06b6d4, #ec4899);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
}
::selection {
background-color: #ec4899;
color: #ffffff;
}
pre {
scrollbar-width: thin;
scrollbar-color: rgba(156, 163, 175, 0.5) transparent;
}
pre::-webkit-scrollbar {
height: 6px;
}
pre::-webkit-scrollbar-thumb {
background-color: rgba(156, 163, 175, 0.5);
border-radius: 3px;
}
body {
transition: background-color 0.3s ease, color 0.3s ease;
}

View File

@@ -1,34 +1,10 @@
module.exports = {
darkMode: 'class',
content: [
"../../templates/**/*.html",
"../../apps/**/templates/**/*.html",
"../../apps/**/templates/**/*.html"
],
theme: {
extend: {
fontFamily: {
sans: ['Inter', 'sans-serif'],
display: ['Space Grotesk', 'sans-serif'],
mono: ['Fira Code', 'monospace'],
extend: {}
},
colors: {
brand: {
cyan: '#06b6d4',
cyanGlow: 'rgba(6,182,212,0.4)',
pink: '#ec4899',
dark: '#09090b',
light: '#fafafa',
surfaceDark: '#18181b',
surfaceLight: '#ffffff',
},
},
boxShadow: {
'neon-cyan': '0 0 20px rgba(6, 182, 212, 0.3)',
'neon-pink': '0 0 20px rgba(236, 72, 153, 0.3)',
'solid-dark': '6px 6px 0px 0px #09090b',
'solid-light': '6px 6px 0px 0px #e4e4e7',
},
},
},
plugins: [require('@tailwindcss/typography')],
plugins: []
};