Core Layout and Styling
The global layout and styling architecture relies on an Astro template (Layout.astro) to enforce semantic HTML structure, SEO metadata, and client-side interactivity for navigation and theming. Visual consistency is achieved through global.css, which utilizes Tailwind CSS to define a responsive grid system, a primary color palette, and CSS variables that adapt to user preferences via a dark-mode class applied to the <body>.
Global HTML Structure
Section titled “Global HTML Structure”The site uses a single global layout template that wraps all pages. This template imports global styles and defines the standard <head> section, which includes Open Graph and Twitter meta tags, canonical URLs, and JSON-LD structured data 1.
The body structure consists of a fixed sidebar for navigation and a main content area. The sidebar contains the site logo, a list of main navigation links, a documentation section, and a theme toggle button. The main content area wraps the page-specific content in a content-wrapper div 2.
Navigation and Interactivity
Section titled “Navigation and Interactivity”The sidebar provides navigation for “Main” and “Documentation” sections. Links are dynamically marked as active based on the current URL path using Astro’s Astro.url.pathname 1.
On mobile devices, the sidebar is hidden by default and can be toggled open using a hamburger menu button (.mobile-nav-toggle). The sidebar’s visibility is controlled by toggling the .is-open class on the .sidebar element 2.
Theme Toggling
Section titled “Theme Toggling”The site supports light and dark modes. To prevent a flash of unstyled content (FOUC), a synchronous inline script applies the dark-mode class to the <body> before the first paint, checking localStorage or the system preference 1.
Users can toggle the theme using the .theme-toggle button. This action toggles the dark-mode class on the body and persists the user’s choice in localStorage 2. CSS transitions for background and text colors are enabled via JavaScript after the first paint to ensure smooth animations.
CSS Theme Definitions
Section titled “CSS Theme Definitions”Global styles are defined in src/styles/global.css, which imports Tailwind CSS 3. The theme system is driven by a custom @custom-variant for dark mode, which targets elements within .dark-mode.
Color Palette
Section titled “Color Palette”A custom primary color palette is defined using CSS variables (e.g., --color-primary-500: #3b82f6). These variables are used throughout the component classes to ensure consistent coloring across light and dark themes.
Body and Backgrounds
Section titled “Body and Backgrounds”The body element has a light gradient background by default. In dark mode, the background switches to a dark gradient, and the text color changes to #ecf0f1. A pseudo-element (body::before) adds a subtle radial gradient overlay that adjusts its opacity in dark mode.
Layout Components
Section titled “Layout Components”The .main-container uses Flexbox to position the fixed .sidebar and the flexible .main-content 4. On screens smaller than 768px, the sidebar is hidden off-screen (-translate-x-full) and slides in when .is-open is applied.
Component Styles
Section titled “Component Styles”Key components like .card, .glass-card, and .btn are styled with Tailwind utility classes that include dark mode variants (e.g., dark:bg-gray-800). These components feature hover effects, shadows, and transitions that adapt to the current theme.
---
import '../styles/global.css';
import { Font } from 'astro:assets';
export interface Props {
title: string;
description: string;
canonicalURL: string;
image?: string;
type?: string;
}
const {
title,
description,
canonicalURL,
image = '/rack-hero.jpeg',
type = 'website'
} = Astro.props;
---
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content={description} />
<meta name="author" content="SDDC.info" />
<meta name="keywords" content="home lab, kubernetes, automation, devops, infrastructure as code, software-defined datacenter, homelab, self-hosted, proxmox, containers, docker" />
<meta name="robots" content="index, follow" />
<meta name="theme-color" content="#3b82f6" />
<meta name="color-scheme" content="light dark" />
<link rel="canonical" href={`https://sddc.info${canonicalURL}`} />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="apple-touch-icon" sizes="180x180" href="/favicon.svg" />
<link rel="manifest" href="/site.webmanifest" />
<!-- Open Graph / Facebook -->
<meta property="og:type" content={type} />
</div>
</main>
</div>
<script>
// Unregister legacy service worker for returning visitors
if ('serviceWorker' in navigator) {
navigator.serviceWorker.getRegistrations().then(rs => rs.forEach(r => r.unregister()));
}
document.addEventListener('DOMContentLoaded', function () {
// Enable theme transitions only after first paint (prevents flash)
requestAnimationFrame(() => {
document.body.style.transition = 'background-color 0.2s, color 0.2s';
});
const sidebar = document.querySelector('.sidebar');
const toggleBtn = document.querySelector('.mobile-nav-toggle');
const themeToggle = document.getElementById('theme-toggle');
if (toggleBtn) {
toggleBtn.addEventListener('click', function () {
sidebar.classList.toggle('is-open');
});
}
if (themeToggle) {
themeToggle.addEventListener('click', () => {
document.body.classList.toggle('dark-mode');
if (document.body.classList.contains('dark-mode')) {
localStorage.setItem('theme', 'dark');
} else {
localStorage.setItem('theme', 'light');
}
});
}
});
</script>
</body>
</html>
@import "tailwindcss";
@custom-variant dark (&:where(.dark-mode, .dark-mode *));
@theme inline {
--font-sans: var(--font-inter), system-ui, sans-serif;
}
@theme {
/* Custom primary color palette */
--color-primary-50: #eff6ff;
--color-primary-100: #dbeafe;
--color-primary-200: #bfdbfe;
--color-primary-300: #93c5fd;
--color-primary-400: #60a5fa;
--color-primary-500: #3b82f6;
--color-primary-600: #2563eb;
--color-primary-700: #1d4ed8;
--color-primary-800: #1e40af;
--color-primary-900: #1e3a8a;
}
@layer base {
html {
scrollbar-gutter: stable;
}
/* Turnstile widget needs explicit height - Tailwind resets collapse it */
.cf-turnstile {
min-height: 65px;
}
.cf-turnstile iframe {
min-height: 65px !important;
height: 65px !important;
}
body {
font-family: var(--font-sans);
line-height: 1.7;
color: #2c3e50;
p-6 overflow-y-auto transition-transform duration-300 z-50;
}
.main-content {
@apply ml-64 flex-1 p-6;
}
.content-wrapper {
@apply max-w-7xl mx-auto;
}
.mobile-nav-toggle {
@apply fixed top-4 left-4 z-50 md:hidden bg-white dark:bg-gray-800
border border-gray-300 dark:border-gray-600 p-2 rounded-lg;
}
.theme-toggle {
@apply w-full mt-8 p-3 bg-gray-700 dark:bg-gray-800 rounded-lg
hover:bg-primary-600 transition-colors duration-200;
}
.sidebar-nav h3 {
@apply text-xs uppercase tracking-wider text-gray-400 mt-6 mb-2;
}
.sidebar-nav a {
@apply block py-2 px-3 rounded-lg transition-colors duration-200
hover:bg-gray-700 dark:hover:bg-gray-700;
}
.sidebar-nav a.active {
@apply bg-primary-600 text-white;
}
.glass-card {
@apply bg-white/95 dark:bg-gray-800/95
backdrop-blur-lg rounded-xl border-2 border-gray-200/60 dark:border-gray-700/60
shadow-[0_8px_32px_-8px_rgba(0,0,0,0.12),0_2px_8px_-2px_rgba(0,0,0,0.08)] dark:shadow-[0_8px_32px_-8px_rgba(0,0,0,0.5),0_2px_8px_-2px_rgba(0,0,0,0.3)]
transition-all duration-300 hover:shadow-[0_16px_48px_-8px_rgba(59,130,246,0.25),0_4px_16px_-4px_rgba(59,130,246,0.15)]
hover:border-primary-400/70 dark:hover:border-primary-600/70