feat: Implement Phase 1 enhancements - components and animations
Enterprise AI Code Review / ai-review (pull_request) Successful in 19s

Components:
- Extract ServiceItem component with hover effects
- Extract SupportItem component with hover animations
- Extract Card component with scroll animations
- Extract Section component for consistent layout

Enhancements:
- Add Intersection Observer for scroll-triggered animations
- Implement hover effects on cards, services, and support items
- Add PWA manifest (site.webmanifest)
- Create SVG OG image placeholder for social sharing
- Add comprehensive favicon meta tags (16x16, 32x32, apple-touch-icon)
- Respect prefers-reduced-motion for accessibility

Files modified:
- src/pages/index.astro - Refactored to use new components
- src/layouts/BaseLayout.astro - Enhanced favicon and meta tags

Files created:
- src/components/Card.astro
- src/components/Section.astro
- src/components/ServiceItem.astro
- src/components/SupportItem.astro
- public/og-image.svg
- public/site.webmanifest
- public/FAVICON_INSTRUCTIONS.md

Note: PNG favicon variants still need to be generated (see FAVICON_INSTRUCTIONS.md)
This commit is contained in:
2025-12-31 18:12:11 +00:00
parent 51db744cc1
commit 6ed0eee514
9 changed files with 622 additions and 351 deletions
+170 -143
View File
@@ -1,16 +1,16 @@
---
interface Props {
title: string;
description?: string;
ogImage?: string;
canonicalURL?: string;
title: string;
description?: string;
ogImage?: string;
canonicalURL?: string;
}
const {
title,
description = "Hidden Den Cafe - A cozy, self-hosted corner of the internet. Privacy-focused, furry-friendly, and built with love.",
ogImage = "/og-image.png",
canonicalURL = Astro.url.pathname
title,
description = "Hidden Den Cafe - A cozy, self-hosted corner of the internet. Privacy-focused, furry-friendly, and built with love.",
ogImage = "/og-image.png",
canonicalURL = Astro.url.pathname,
} = Astro.props;
const fullCanonicalURL = new URL(canonicalURL, Astro.site).href;
@@ -19,156 +19,183 @@ const fullOgImage = new URL(ogImage, Astro.site).href;
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="generator" content={Astro.generator} />
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="generator" content={Astro.generator} />
<!-- Primary Meta Tags -->
<title>{title}</title>
<meta name="title" content={title} />
<meta name="description" content={description} />
<link rel="canonical" href={fullCanonicalURL} />
<!-- Primary Meta Tags -->
<title>{title}</title>
<meta name="title" content={title} />
<meta name="description" content={description} />
<link rel="canonical" href={fullCanonicalURL} />
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website" />
<meta property="og:url" content={fullCanonicalURL} />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:image" content={fullOgImage} />
<meta property="og:site_name" content="Hidden Den Cafe" />
<meta property="og:locale" content="en_US" />
<!-- Open Graph / Facebook -->
<meta property="og:type" content="website" />
<meta property="og:url" content={fullCanonicalURL} />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="og:image" content={fullOgImage} />
<meta property="og:site_name" content="Hidden Den Cafe" />
<meta property="og:locale" content="en_US" />
<!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:url" content={fullCanonicalURL} />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content={fullOgImage} />
<!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:url" content={fullCanonicalURL} />
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content={fullOgImage} />
<!-- Additional Meta Tags -->
<meta name="author" content="Latte" />
<meta name="keywords" content="self-hosted, privacy, open-source, furry, developer, cozy, hidden den" />
<meta name="theme-color" content="#d4a574" />
<meta name="color-scheme" content="dark" />
<!-- Additional Meta Tags -->
<meta name="author" content="Latte" />
<meta
name="keywords"
content="self-hosted, privacy, open-source, furry, developer, cozy, hidden den"
/>
<meta name="theme-color" content="#d4a574" />
<meta name="color-scheme" content="dark" />
<!-- Favicon -->
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="apple-touch-icon" href="/favicon.svg" />
<!-- Favicons -->
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link
rel="icon"
type="image/png"
sizes="32x32"
href="/favicon-32x32.png"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="/favicon-16x16.png"
/>
<link
rel="apple-touch-icon"
sizes="180x180"
href="/apple-touch-icon.png"
/>
<link rel="manifest" href="/site.webmanifest" />
<!-- Preconnect for performance (if needed for future external resources) -->
<!-- <link rel="preconnect" href="https://example.com" crossorigin /> -->
</head>
<body>
<slot />
</body>
<!-- Preconnect for performance (if needed for future external resources) -->
<!-- <link rel="preconnect" href="https://example.com" crossorigin /> -->
</head>
<body>
<slot />
</body>
</html>
<style is:global>
:root {
/* Cozy Den Color Palette */
--color-bg: #1a1410;
--color-bg-light: #2a1f18;
--color-text: #f4e9d8;
--color-text-dim: #c4b5a0;
--color-accent: #d4a574;
--color-accent-bright: #e8bf8e;
--color-warm: #8b6f47;
:root {
/* Cozy Den Color Palette */
--color-bg: #1a1410;
--color-bg-light: #2a1f18;
--color-text: #f4e9d8;
--color-text-dim: #c4b5a0;
--color-accent: #d4a574;
--color-accent-bright: #e8bf8e;
--color-warm: #8b6f47;
/* Spacing */
--space-xs: 0.5rem;
--space-sm: 1rem;
--space-md: 1.5rem;
--space-lg: 2rem;
--space-xl: 3rem;
/* Spacing */
--space-xs: 0.5rem;
--space-sm: 1rem;
--space-md: 1.5rem;
--space-lg: 2rem;
--space-xl: 3rem;
/* Typography */
--font-body: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html {
font-family: var(--font-body);
background: var(--color-bg);
color: var(--color-text);
scroll-behavior: smooth;
}
body {
min-height: 100vh;
line-height: 1.6;
}
h1, h2, h3, h4, h5, h6 {
color: var(--color-accent-bright);
line-height: 1.2;
}
a {
color: var(--color-accent-bright);
text-decoration: none;
transition: color 0.2s ease;
}
a:hover {
color: var(--color-accent);
}
a:focus-visible {
outline: 2px solid var(--color-accent);
outline-offset: 2px;
border-radius: 2px;
}
/* Smooth animations */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(20px);
/* Typography */
--font-body:
system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
sans-serif;
}
to {
opacity: 1;
transform: translateY(0);
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
}
.fade-in {
animation: fadeIn 0.6s ease-out;
}
/* Respect user's motion preferences */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
html {
font-family: var(--font-body);
background: var(--color-bg);
color: var(--color-text);
scroll-behavior: smooth;
}
}
/* Improve focus visibility for keyboard navigation */
*:focus-visible {
outline: 2px solid var(--color-accent);
outline-offset: 2px;
}
body {
min-height: 100vh;
line-height: 1.6;
}
/* Screen reader only content */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
h1,
h2,
h3,
h4,
h5,
h6 {
color: var(--color-accent-bright);
line-height: 1.2;
}
a {
color: var(--color-accent-bright);
text-decoration: none;
transition: color 0.2s ease;
}
a:hover {
color: var(--color-accent);
}
a:focus-visible {
outline: 2px solid var(--color-accent);
outline-offset: 2px;
border-radius: 2px;
}
/* Smooth animations */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.fade-in {
animation: fadeIn 0.6s ease-out;
}
/* Respect user's motion preferences */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
/* Improve focus visibility for keyboard navigation */
*:focus-visible {
outline: 2px solid var(--color-accent);
outline-offset: 2px;
}
/* Screen reader only content */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
</style>