diff --git a/CHANGELOG.md b/CHANGELOG.md index 405b00c..fde0e5c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,62 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). --- +## [0.8.0] — 2026-04-13 + +### Added — Full i18n translation + +- **All pages available in 4 languages** (DE/FR/IT/EN): About, Contact, Blog, Features, Privacy, Legal Notice — 28 locale variants total +- **Language switcher in every navbar** — `PageLayout`, `BlogLayout`, `ProjectLayout` and `LandingLayout` render the language switcher with the correct `currentLocale` +- **Locale-aware nav and footer links** — new `nav.*.href` and `footer.privacy.href` / `footer.imprint.href` keys in `ui.ts` +- **Shared page components** — `AboutPage`, `ContactPage`, `FeaturesIndexPage`, `BlogIndexPage` accept a `locale` prop; each locale page file is a 4-line wrapper with no code duplication +- **Translated privacy pages** — full translated privacy policy on FR (`/fr/privacy`), IT (`/it/privacy`), EN (`/en/privacy`) including Infomaniak certification cards +- **Translated legal notice pages** — Mentions légales (FR), Note legali (IT), Legal Notice (EN) +- **`ui.ts`** — ~40 new translation keys per language: `about.*`, `contact.*`, `blog.*`, `features.cta.*`, `nav.*.href`, `footer.*.href` + +### Changed (2026-04-13) + +- `PageLayout` accepts `locale` prop and passes translated nav items to the header +- `BlogLayout` / `ProjectLayout` show language switcher with translated nav and breadcrumbs +- `LandingLayout` footer links (privacy, legal notice, features) are locale-aware +- Blog index shows all posts across locales (locale filter removed) +- Blog post `armarium-1-0.mdx` locale corrected from `en` to `de` +- `content.config.ts` locale enum extended with `de` and `it` + +--- + +## [0.7.0] — 2026-04-13 — Armarium customization + +### Added + +- **Armarium branding** — horizontal SVG logo in navbar (`logo-horizontal.svg`), `fill="currentColor"` for dark mode support +- **Homepage** (`/`, `/fr/`, `/it/`, `/en/`) — hero ("Armarium Suite — Budget & More"), 6 feature cards, trust bar with Zürich coat of arms, CTA; shared `HomePage.astro` component +- **Swiss identity** — Zürich coat of arms (SVG, #003DA5/white) in trust bar, about page and privacy page +- **Privacy page** (`/datenschutz`) — 6 Infomaniak certification cards (ISO 27001:2022, Swiss Hosting Label, Swiss Made Software, nDSG/GDPR, ISO 14001 + B Corp™, physical security) + 8-section privacy policy +- **Legal notice** (`/impressum`) — operator: Armarium, Zürich; hosting: Infomaniak +- **About page** — mission section, app info cards, 4 value cards (transparency, simplicity, privacy, made in Zürich) +- **Features page** (`/projects`) — 6 feature cards from `projects` collection with icon mapping +- **Blog** — single launch post "Armarium Suite 1.0 ist da" +- **Language switcher** (`LanguageSwitcherDropdown.astro`) — locale code (DE/FR/IT/EN) without flag emojis; desktop + mobile +- **i18n foundation** — `astro.config.mjs` with `prefixDefaultLocale: false`; `ui.ts` for DE/FR/IT/EN (hero, trust bar, features, CTA, footer) + +### Changed + +- Header: Login + Register buttons instead of GitHub / "Get Started" +- Navbar: "Projects" → "Features" +- Footer: columns layout with app and legal link groups, Armarium tagline and copyright +- `contact.astro`, `about.astro`, `404.astro` — updated with Armarium content +- `authors/team.json` — name set to "Armarium" + +### Fixed + +- Unescaped apostrophe in French translation (`d\'épargne`) causing esbuild parse error +- Flag emojis removed from language switcher (no font support on Linux) +- Duplicate `Locale` type import in `LandingLayout.astro` +- Footer copyright HTML rendering (PR #28) +- Stripped `www.` prefix from contact social link display values (PR #27) + +--- + ## [1.0.0] — 2026-04-04 Initial public release of Astro Rocket. diff --git a/astro.config.mjs b/astro.config.mjs index de5a888..7c98287 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -44,6 +44,14 @@ export default defineConfig({ plugins: [tailwindcss()], }, + i18n: { + defaultLocale: 'de', + locales: ['de', 'fr', 'it', 'en'], + routing: { + prefixDefaultLocale: false, + }, + }, + security: { checkOrigin: true, }, diff --git a/src/assets/logo-horizontal.svg b/src/assets/logo-horizontal.svg new file mode 100644 index 0000000..8ab48d9 --- /dev/null +++ b/src/assets/logo-horizontal.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/components/landing/AboutPage.astro b/src/components/landing/AboutPage.astro new file mode 100644 index 0000000..6ab0897 --- /dev/null +++ b/src/components/landing/AboutPage.astro @@ -0,0 +1,167 @@ +--- +import PageLayout from '@/layouts/PageLayout.astro'; +import Icon from '@/components/ui/primitives/Icon/Icon.astro'; +import Badge from '@/components/ui/data-display/Badge/Badge.astro'; +import Card from '@/components/ui/data-display/Card/Card.astro'; +import Button from '@/components/ui/form/Button/Button.astro'; +import { Hero } from '@/components/hero'; +import { useTranslations } from '@/i18n/utils'; +import type { Locale } from '@/i18n/ui'; + +interface Props { + locale: Locale; +} + +const { locale } = Astro.props; +const t = useTranslations(locale); +--- + + + + + + {t('about.badge')} + +

+ {t('about.title.pre')} + {t('about.title.accent')} +

+

{t('about.desc')}

+
+ + +
+
+
+
+
+ {t('about.mission.badge')} +

+ {t('about.mission.title')} +

+
+

{t('about.mission.p1')}

+

{t('about.mission.p2')}

+
+ +
+ +
+
+ +
+
+

{t('about.app.label')}

+

{t('about.app.title')}

+

{t('about.app.desc')}

+
+ Angular + Budget + {t('about.privacy.label')} +
+
+
+
+ + +
+
+ +
+
+

{t('about.privacy.label')}

+

{t('about.privacy.title')}

+

{t('about.privacy.desc')}

+
+
+
+
+
+
+
+ + +
+
+
+
+ {t('about.values.badge')} +

{t('about.values.title')}

+
+
+
+ +
+
+ +
+
+

{t('about.v1.title')}

+

{t('about.v1.desc')}

+
+
+
+ +
+
+ +
+
+

{t('about.v2.title')}

+

{t('about.v2.desc')}

+
+
+
+ +
+
+ +
+
+

{t('about.v3.title')}

+

{t('about.v3.desc')}

+
+
+
+ + +
+
+ + + + + + +
+
+

{t('about.v4.title')}

+

{t('about.v4.desc')}

+
+
+
+
+
+
+ + +
+
+

{t('about.cta.title')}

+

{t('about.cta.desc')}

+
+ + +
+
+
+
diff --git a/src/components/landing/BlogIndexPage.astro b/src/components/landing/BlogIndexPage.astro new file mode 100644 index 0000000..d2d263b --- /dev/null +++ b/src/components/landing/BlogIndexPage.astro @@ -0,0 +1,255 @@ +--- +import PageLayout from '@/layouts/PageLayout.astro'; +import BlogCard from '@/components/blog/BlogCard.astro'; +import Badge from '@/components/ui/data-display/Badge/Badge.astro'; +import Icon from '@/components/ui/primitives/Icon/Icon.astro'; +import CTA from '@/components/ui/marketing/CTA/CTA.astro'; +import { Hero } from '@/components/hero'; +import { getCollection } from 'astro:content'; +import siteConfig from '@/config/site.config'; +import { resolveSocialLinks } from '@/lib/utils'; +import { useTranslations } from '@/i18n/utils'; +import type { Locale } from '@/i18n/ui'; + +interface Props { + locale: Locale; +} + +const { locale } = Astro.props; +const t = useTranslations(locale); +const socialLinks = resolveSocialLinks(siteConfig.socialLinks); + +// Get all published posts (all locales) +const allPosts = await getCollection('blog', ({ data }) => { + return import.meta.env.PROD ? data.draft !== true : true; +}); + +const posts = allPosts.sort((a, b) => b.data.publishedAt.valueOf() - a.data.publishedAt.valueOf()); + +const featuredPosts = posts.filter((post) => post.data.featured); +const nonFeaturedPosts = posts.filter((post) => !post.data.featured); +const regularPosts = nonFeaturedPosts.length > 0 ? nonFeaturedPosts : posts; + +const allTags = [...new Set(posts.flatMap((post) => post.data.tags))].sort(); + +const getPostUrl = (postId: string) => { + // Strip locale folder prefix (e.g., "en/post-slug" → "post-slug") + const slug = postId.replace(/^[a-z]{2}\//, ''); + return `/blog/${slug}`; +}; +--- + + + + + + {t('blog.badge')} + + +

{t('blog.title')}

+ +

{t('blog.desc')}

+
+ + + { + featuredPosts.length > 0 && ( + + ) + } + + +
0 ? 'bg-background' : 'bg-background-secondary', 'py-[var(--space-section-md)] border-t border-border']}> +
+
+ { + featuredPosts.length > 0 && nonFeaturedPosts.length > 0 && ( +

{t('blog.allposts')}

+ ) + } + + {allTags.length > 0 && ( +
+ +
+ + + + + + +
+ +
+ )} +
+ + { + regularPosts.length > 0 ? ( +
+ {regularPosts.map((post) => ( +
+ +
+ ))} +
+ ) : ( +
+
+ +
+

{t('blog.noposts')}

+
+ ) + } + + +
+
+ + + 0 && '!bg-background-secondary']}> +

{t('blog.follow.title')}

+

{t('blog.follow.desc')}

+ +
+ + + RSS Feed + + {socialLinks.map((link) => ( + + + {link.label} + + ))} + + + Email + +
+
+
+ + + + diff --git a/src/components/landing/ContactPage.astro b/src/components/landing/ContactPage.astro new file mode 100644 index 0000000..69f9e4a --- /dev/null +++ b/src/components/landing/ContactPage.astro @@ -0,0 +1,121 @@ +--- +import PageLayout from '@/layouts/PageLayout.astro'; +import Icon from '@/components/ui/primitives/Icon/Icon.astro'; +import Badge from '@/components/ui/data-display/Badge/Badge.astro'; +import Card from '@/components/ui/data-display/Card/Card.astro'; +import { Hero } from '@/components/hero'; +import ContactForm from '@/components/patterns/ContactForm.astro'; +import siteConfig from '@/config/site.config'; +import { resolveSocialLinks } from '@/lib/utils'; +import { useTranslations } from '@/i18n/utils'; +import type { Locale } from '@/i18n/ui'; + +interface Props { + locale: Locale; +} + +const { locale } = Astro.props; +const t = useTranslations(locale); +const socialLinks = resolveSocialLinks(siteConfig.socialLinks); + +const channels = [ + { + icon: 'mail', + label: 'Email', + value: siteConfig.email, + note: t('contact.follow'), + href: `mailto:${siteConfig.email}`, + external: false, + }, + ...socialLinks.map((link) => ({ + icon: link.icon, + label: link.label, + value: link.href.replace(/^https?:\/\/(www\.)?/, ''), + note: t('contact.follow'), + href: link.href, + external: true, + })), +]; +--- + + + + + + {t('contact.badge')} + + +

+ {t('contact.title').replace('.', '')} {t('contact.title').slice(-1)} +

+ +

{t('contact.desc')}

+
+ + +
+
+
+ + +
+ +

{t('contact.form.title')}

+ +
+
+ + +
+
+

{t('contact.direct.title')}

+

{t('contact.direct.desc')}

+
+ + +
+ {channels.map((ch) => ( + + +
+
+ +
+
+

{ch.label}

+

{ch.value}

+
+ +
+
+
+ ))} +
+ + + +
+
+ +
+
+

{siteConfig.address?.city ?? 'Zürich'}

+

{siteConfig.address?.country ?? 'Switzerland'}

+
+
+
+ +
+ +
+
+
+
diff --git a/src/components/landing/FeaturesIndexPage.astro b/src/components/landing/FeaturesIndexPage.astro new file mode 100644 index 0000000..6624bf8 --- /dev/null +++ b/src/components/landing/FeaturesIndexPage.astro @@ -0,0 +1,97 @@ +--- +import PageLayout from '@/layouts/PageLayout.astro'; +import Icon from '@/components/ui/primitives/Icon/Icon.astro'; +import Badge from '@/components/ui/data-display/Badge/Badge.astro'; +import Card from '@/components/ui/data-display/Card/Card.astro'; +import Button from '@/components/ui/form/Button/Button.astro'; +import { Hero } from '@/components/hero'; +import { getCollection } from 'astro:content'; +import { useTranslations } from '@/i18n/utils'; +import type { Locale } from '@/i18n/ui'; + +interface Props { + locale: Locale; +} + +const { locale } = Astro.props; +const t = useTranslations(locale); + +const items = await getCollection('projects', ({ data }) => !data.draft); +const features = items.sort((a, b) => a.data.order - b.data.order); + +const iconMap: Record = { + 'budget-uebersicht': 'layout-dashboard', + 'transaktionen': 'list', + 'kategorien-berichte': 'pie-chart', + 'mehrere-konten': 'wallet', + 'sparziele': 'target', + 'datenschutz-sicherheit': 'shield-check', +}; + +const featuresHref = t('nav.features.href'); +--- + + + + + + {t('features.badge')} + +

+ {t('features.title').split(' ').slice(0, -1).join(' ')} {t('features.title').split(' ').slice(-1)[0]} +

+

{t('features.description')}

+
+ + +
+
+
+ {features.map((feature) => { + const slug = feature.id.replace(/\.mdx?$/, ''); + const icon = iconMap[slug] ?? 'check-circle'; + return ( + +
+
+
+ +
+ +
+

{feature.data.title}

+

{feature.data.description}

+
+ {feature.data.tags.map((tag) => ( + {tag} + ))} +
+
+
+ ); + })} +
+
+
+ + +
+
+

{t('features.cta.title')}

+

{t('features.cta.desc')}

+
+ + +
+
+
+
diff --git a/src/components/landing/HomePage.astro b/src/components/landing/HomePage.astro new file mode 100644 index 0000000..7cdac6a --- /dev/null +++ b/src/components/landing/HomePage.astro @@ -0,0 +1,131 @@ +--- +/** + * Shared home page component — renders in all 4 locales. + */ +import { Hero } from '@/components/hero'; +import Button from '@/components/ui/form/Button/Button.astro'; +import Badge from '@/components/ui/data-display/Badge/Badge.astro'; +import Card from '@/components/ui/data-display/Card/Card.astro'; +import Icon from '@/components/ui/primitives/Icon/Icon.astro'; +import { useTranslations } from '@/i18n/utils'; +import type { Locale } from '@/i18n/ui'; + +interface Props { + locale: Locale; +} + +const { locale } = Astro.props; +const t = useTranslations(locale); + +const features = [ + { key: 'f1', icon: 'layout-dashboard' }, + { key: 'f2', icon: 'list' }, + { key: 'f3', icon: 'pie-chart' }, + { key: 'f4', icon: 'wallet' }, + { key: 'f5', icon: 'target' }, + { key: 'f6', icon: 'shield-check' }, +] as const; +--- + + + + + {t('hero.badge')} + + +

+ Armarium Suite —
+ Budget & More +

+ +

{t('hero.description')}

+ + + + + +
+ + +
+
+
+ + + + + + + + Made in Zürich, Switzerland + + + + {t('trust.privacy')} + + + + {t('trust.free')} + +
+
+
+ + +
+
+
+
+ {t('features.badge')} +

+ {t('features.title')} +

+
+

+ {t('features.description')} +

+
+ +
+ {features.map(({ key, icon }) => ( + +
+
+ +
+
+

{t(`${key}.title` as any)}

+

{t(`${key}.desc` as any)}

+
+
+
+ ))} +
+
+
+ + +
+
+

+ {t('cta.title')} +

+

+ {t('cta.desc')} +

+
+ + +
+
+
diff --git a/src/components/layout/Footer.astro b/src/components/layout/Footer.astro index 88f3c8f..27e5136 100644 --- a/src/components/layout/Footer.astro +++ b/src/components/layout/Footer.astro @@ -140,11 +140,8 @@ function getSocialIcon(platform: string): string { {hasLogoSlot ? ( ) : ( - - - - {siteConfig.name} - + + )} {(hasTaglineSlot || tagline) && ( @@ -207,11 +204,8 @@ function getSocialIcon(platform: string): string { {hasLogoSlot ? ( ) : ( - - - - {siteConfig.name} - + + )} {(hasTaglineSlot || tagline) && ( @@ -310,11 +304,8 @@ function getSocialIcon(platform: string): string { hasLogoSlot ? ( ) : ( - - - - {siteConfig.name} - + + ) )} diff --git a/src/components/layout/Header.astro b/src/components/layout/Header.astro index 86651b9..276996e 100644 --- a/src/components/layout/Header.astro +++ b/src/components/layout/Header.astro @@ -31,7 +31,9 @@ import Logo from '@/components/ui/marketing/Logo/Logo.astro'; import ThemeToggle from '@/components/layout/ThemeToggle.astro'; import ThemeSelector from '@/components/layout/ThemeSelector.astro'; import ThemeSelectorDropdown from '@/components/layout/ThemeSelectorDropdown.astro'; +import LanguageSwitcherDropdown from '@/components/layout/LanguageSwitcherDropdown.astro'; import siteConfig from '@/config/site.config'; +import type { Locale } from '@/i18n/ui'; export interface NavItem { label: string; @@ -83,6 +85,8 @@ interface Props extends HTMLAttributes<'header'> { hideLogo?: boolean; /** Show language switcher */ showLanguageSwitcher?: boolean; + /** Current locale for language switcher */ + currentLocale?: Locale; /** Show social icon links (desktop/tablet only, reads from siteConfig.socialLinks) */ showSocialLinks?: boolean; /** Show scroll progress bar at the bottom of the header */ @@ -110,6 +114,8 @@ const { showActiveState = true, showScrollProgress = false, scrollProgressPosition = 'bottom', + showLanguageSwitcher = false, + currentLocale = 'de', logoText, hideLogo = false, class: className, @@ -190,16 +196,13 @@ const buttonId = `${menuId}-button`; (hasLogoSlot ? ( ) : ( - - - - {logoText || siteConfig.name} - + + )) } @@ -254,6 +257,12 @@ const buttonId = `${menuId}-button`; )} + {showLanguageSwitcher && ( + + )} + {showSocialLinks && siteConfig.socialLinks.length > 0 && ( )} + + {showLanguageSwitcher && ( +
+
+ Language + +
+
+ )} )) diff --git a/src/components/layout/LanguageSwitcherDropdown.astro b/src/components/layout/LanguageSwitcherDropdown.astro new file mode 100644 index 0000000..7324a36 --- /dev/null +++ b/src/components/layout/LanguageSwitcherDropdown.astro @@ -0,0 +1,155 @@ +--- +/** + * LanguageSwitcherDropdown + * Desktop language selector following the ThemeSelectorDropdown pattern. + * Navigates to the equivalent page in the selected locale. + */ +import { cn } from '@/lib/cn'; +import { languages, type Locale } from '@/i18n/ui'; + +interface Props { + currentLocale?: Locale; + class?: string; +} + +const { currentLocale = 'de', class: className } = Astro.props; +const current = languages[currentLocale]; +const allLanguages = Object.values(languages); +--- + +
+ + + + +
+ {allLanguages.map((lang) => ( + + ))} +
+
+ + + + diff --git a/src/components/ui/marketing/Logo/Logo.astro b/src/components/ui/marketing/Logo/Logo.astro index 9687ff3..da0a0e9 100644 --- a/src/components/ui/marketing/Logo/Logo.astro +++ b/src/components/ui/marketing/Logo/Logo.astro @@ -1,28 +1,30 @@ --- /** - * Logo Component — first-letter monogram badge + * Logo Component * - * Renders a brand-coloured square badge with the first letter of the site - * name, used in the navbar, footer, and blog post bylines. + * - variant='logomark' (default): brand-coloured square badge with first letter + * - variant='full': horizontal SVG logo (icon + wordmark) */ import { cn } from '@/lib/cn'; import siteConfig from '@/config/site.config'; +import logoHorizontal from '@/assets/logo-horizontal.svg?raw'; interface Props { /** Size preset */ size?: 'sm' | 'md' | 'lg' | 'xl' | '2xl'; /** Force dark mode logo (for dark backgrounds) — no-op here, kept for API compat */ forceDark?: boolean; - /** Use full logo instead of logomark — no-op here, kept for API compat */ + /** 'logomark': monogram badge | 'full': horizontal SVG logo */ variant?: 'logomark' | 'full'; /** Additional CSS classes */ class?: string; - /** Override the displayed letter (e.g. for author avatars). Defaults to first letter of site name. */ + /** Override the displayed letter (logomark only). Defaults to first letter of site name. */ letter?: string; } const { size = 'md', + variant = 'logomark', class: className, letter: letterProp, } = Astro.props; @@ -37,18 +39,37 @@ const sizeClasses: Record const { box, text, radius } = sizeClasses[size] ?? sizeClasses.md; const letter = letterProp ?? siteConfig.name.charAt(0).toUpperCase(); + +const fullHeights: Record = { + sm: 'h-4', + md: 'h-5', + lg: 'h-7', + xl: 'h-10', + '2xl': 'h-16', +}; +const fullHeight = fullHeights[size] ?? fullHeights.md; --- - - {letter} - +{variant === 'full' ? ( + + + +) : ( + + {letter} + +)} diff --git a/src/config/nav.config.ts b/src/config/nav.config.ts index 264e50e..7154c54 100644 --- a/src/config/nav.config.ts +++ b/src/config/nav.config.ts @@ -13,7 +13,7 @@ export interface NavItem { export const navItems: NavItem[] = [ { label: 'Blog', href: '/blog', order: 1 }, - { label: 'Projects', href: '/projects', order: 2 }, + { label: 'Features', href: '/projects', order: 2 }, { label: 'About', href: '/about', order: 3 }, { label: 'Contact', href: '/contact', order: 4 }, ]; diff --git a/src/content.config.ts b/src/content.config.ts index 96fa89d..5d7c4b8 100644 --- a/src/content.config.ts +++ b/src/content.config.ts @@ -18,7 +18,7 @@ const blog = defineCollection({ svgSlug: z.string().optional(), draft: z.boolean().default(false), featured: z.boolean().default(false), - locale: z.enum(['en', 'es', 'fr']).default('en'), + locale: z.enum(['de', 'en', 'es', 'fr', 'it']).default('de'), }), }); @@ -29,7 +29,7 @@ const pages = defineCollection({ title: z.string(), description: z.string(), updatedAt: z.coerce.date().optional(), - locale: z.enum(['en', 'es', 'fr']).default('en'), + locale: z.enum(['de', 'en', 'es', 'fr', 'it']).default('de'), }), }); diff --git a/src/content/authors/team.json b/src/content/authors/team.json index f5aa622..d79d85f 100644 --- a/src/content/authors/team.json +++ b/src/content/authors/team.json @@ -1,9 +1,7 @@ { - "name": "Astro Rocket", - "bio": "Astro Rocket is a free, open-source Astro 6 starter theme. Built for speed, accessibility, and developer experience — clone it and start shipping.", + "name": "Armarium", + "bio": "Armarium ist dein persönlicher Finanzbegleiter – Budget im Blick, Ziele im Fokus. Entwickelt in Zürich, Switzerland.", "social": { - "github": "https://github.com", - "linkedin": "https://linkedin.com", - "twitter": "https://x.com" + "linkedin": "https://linkedin.com" } } diff --git a/src/content/blog/en/animations-in-astro-rocket.mdx b/src/content/blog/en/animations-in-astro-rocket.mdx deleted file mode 100644 index e86ae11..0000000 --- a/src/content/blog/en/animations-in-astro-rocket.mdx +++ /dev/null @@ -1,194 +0,0 @@ ---- -title: "Animations in Astro Rocket — Every Effect Explained" -description: "A complete breakdown of every animation built into Astro Rocket — page transitions, scroll-triggered counters, the reactive header, card hovers, and the full micro-animation library." -publishedAt: 2026-03-23 -author: "Hans Martens" -tags: ["astro-rocket", "animations", "components", "customization", "css"] -featured: false -locale: en -image: "../../../assets/blog/animations-in-astro-rocket.svg" -imageAlt: "Lightning bolt icon above the word 'animations' on a dark background" -svgSlug: "animations-in-astro-rocket" ---- - -Astro Rocket ships with animations on every page. Not decorative noise — purposeful motion that makes the site feel fast, polished, and alive. This post breaks down every animation in the theme: what it does, where it lives, and how to tune or disable it. - -All animations in Astro Rocket respect the `prefers-reduced-motion` media query. Users who have enabled reduced motion in their operating system preferences will see no micro-animations. The implementation is in `src/styles/global.css` and requires no extra work on your part. - -## Page transitions - -The most noticeable animation is the one between pages. Astro Rocket uses Astro's built-in `` component, which leverages the browser's View Transitions API to animate from one page to the next. - -Instead of a hard reload, the page content slides up and out while the new page slides up into view — a smooth, app-like transition that makes navigation feel immediate. This is enabled globally in `src/layouts/BaseLayout.astro`: - -```astro -import { ClientRouter } from 'astro:transitions'; - - - -``` - -That single line is all it takes. Every internal link in the site benefits from it automatically. No per-page configuration, no JavaScript hydration overhead. - -The default transition is `animate-slide-up`, applied to all pages. Astro also supports `fade` and `none` transitions per element via `transition:animate`, and you can assign persistent elements a `transition:name` so they morph in place rather than slide out and back in — useful for shared headers, logos, or images that appear on multiple pages. - -## Scroll-triggered counter animation - -On the homepage, the stats block — Years Experience, Projects Delivered, Worldwide Clients — doesn't just sit there as static numbers. When the block scrolls into the viewport, each number counts up from zero to its target value. - -The animation is driven by an `IntersectionObserver` in `src/pages/index.astro`: - -- The observer fires once per element, when it first reaches 40% visibility -- Each counter runs for 1.2 seconds with a cubic ease-out curve (`1 - (1 - progress)³`) -- After completing, the observer disconnects — so scrolling back up and down doesn't re-trigger it - -To add a counter to any element, give it a `data-countup` attribute with the target number and an optional `data-suffix`: - -```html -

50+

-``` - -The script picks up any element with `[data-countup]` on the page, so you can place counter elements anywhere. - -## Scroll-triggered Lighthouse scores - -The `LighthouseScores` landing component in `src/components/landing/LighthouseScores.astro` uses its own `IntersectionObserver` to animate the score bars and numbers into place when the section enters the viewport. - -The bars expand from zero width to their final value as the section becomes visible. Like the counter animation, this fires once and cleans itself up. It gives the performance section a satisfying reveal that draws attention to the scores without requiring the user to read static numbers. - -## Scroll-reactive header - -The floating header changes its appearance as the user scrolls. This is driven by a scroll event listener in `src/components/layout/Header.astro`. - -When the page is at the top (within 60px of the scroll origin), the header renders without a background — transparent, with inverted text designed to sit over the hero section. Once the user scrolls past the 60px threshold, the header receives a `data-scrolled` attribute: - -```javascript -if (window.scrollY > SCROLL_THRESHOLD) { - header.setAttribute('data-scrolled', ''); -} else { - header.removeAttribute('data-scrolled'); -} -``` - -CSS transitions on the header element animate the background, border, and text color changes smoothly. There is no layout shift — the header stays in place and only its visual style transitions. - -The threshold is 60px. To adjust it, change the `SCROLL_THRESHOLD` constant in `Header.astro`. - -## Card hover effects - -Every interactive card in the site lifts slightly when hovered. This is a Tailwind utility applied directly in the markup: - -```html -
-``` - -The card moves 4px upward and gains a subtle shadow over 200ms. It creates a tactile feeling that helps users understand which cards are clickable. The transition uses `duration-200` (200ms linear) — fast enough to feel immediate, slow enough to be visible. - -## UI micro-animations - -The full animation library lives in `src/styles/global.css`. These classes are used throughout the component library and are available for use in your own components. - -### Entrance animations - -```css -.animate-fade-in /* fades from transparent to visible — 0.5s ease-out */ -.animate-slide-up /* slides up from 12px below while fading in — 0.5s ease-out */ -.animate-slide-down /* slides down from 12px above while fading in — 0.5s ease-out */ -``` - -Use these to reveal content sections, modals, or any element that appears after interaction. - -### Overlay and menu animations - -```css -.animate-sheet-up /* bottom sheet slides up from off-screen — 0.25s spring */ -.animate-sheet-down /* bottom sheet exits downward — 0.2s */ -.animate-menu-down /* mobile nav drawer opens downward — 0.25s spring */ -.animate-menu-up /* mobile nav drawer closes upward — 0.2s */ -.animate-backdrop /* backdrop fades in — 0.2s ease-out */ -.animate-backdrop-out /* backdrop fades out — 0.2s ease-out */ -``` - -These are used by the `Dialog`, mobile menu sheet, and overlay components. The spring easing (`cubic-bezier(0.32, 0.72, 0, 1)`) gives the open motion a slight overshoot that feels natural and fast. - -### Dropdown animations - -```css -.animate-dropdown-in /* slides down and scales in — 0.2s spring */ -.animate-dropdown-out /* collapses upward and scales out — 0.15s */ -``` - -Used by the `Dropdown` component. The dropdown originates from its trigger point and expands outward, which keeps the motion spatially coherent. - -### Feedback animations - -```css -.animate-tab-enter /* crossfades tab panel content — uses --transition-normal */ -.animate-toast-in /* slides toast in from the edge — 350ms spring */ -.animate-tooltip-in /* fades and scales tooltip into view */ -.animate-shake /* brief shake for error feedback — 400ms */ -``` - -The toast uses `--ease-spring` for a satisfying bounce on entry. The shake animation is useful for form validation — apply it to an input when the user submits an invalid value. - -### Loading states - -```css -.animate-pulse /* breathing opacity pulse for skeleton loaders — 2s infinite */ -.animate-spin /* continuous rotation for loading spinners — 1s linear */ -``` - -These are used by the `Skeleton` and `Progress` components. They loop indefinitely until the element is removed from the DOM. - -### Stagger utilities - -```css -.delay-0 /* 0ms */ -.delay-1 /* 50ms */ -.delay-2 /* 100ms */ -.delay-3 /* 150ms */ -.delay-4 /* 200ms */ -.delay-5 /* 250ms */ -``` - -Combine with any entrance animation to stagger multiple elements into view: - -```html -
First item
-
Second item
-
Third item
-``` - -Each item appears 50ms after the last, creating a cascading reveal that guides the eye down the list. - -## Adding animations to your own content - -All of these classes are available anywhere in the project — in your page content, custom components, or Tailwind HTML. To animate a section heading into view, for example: - -```html -

My heading

-``` - -For scroll-triggered reveals (elements that should only animate when they enter the viewport, not immediately on page load), you can replicate the pattern from the homepage counter — create an `IntersectionObserver` that adds the animation class when the element becomes visible: - -```javascript -const observer = new IntersectionObserver((entries) => { - entries.forEach((entry) => { - if (!entry.isIntersecting) return; - observer.unobserve(entry.target); - entry.target.classList.add('animate-slide-up'); - }); -}, { threshold: 0.2 }); - -document.querySelectorAll('[data-reveal]').forEach((el) => observer.observe(el)); -``` - -Add `data-reveal` to any element you want to reveal on scroll, and the observer handles the rest. - -## Disabling animations - -To disable all micro-animations globally (while keeping page transitions), remove or comment out the animation class definitions in `src/styles/global.css`. The `@media (prefers-reduced-motion: reduce)` block at the bottom of that file already disables the most intensive ones for users with that preference set. - -To disable page transitions, remove `` from `src/layouts/BaseLayout.astro`. - -To disable individual component animations, remove the animation class from the component's markup. diff --git a/src/content/blog/en/armarium-1-0.mdx b/src/content/blog/en/armarium-1-0.mdx new file mode 100644 index 0000000..25c47ad --- /dev/null +++ b/src/content/blog/en/armarium-1-0.mdx @@ -0,0 +1,39 @@ +--- +title: "Armarium Suite 1.0 ist da" +description: "Dein persönlicher Finanzbegleiter ist live. Budget tracken, Transaktionen erfassen, Sparziele setzen – einfach und sicher in der Schweiz gehostet." +publishedAt: 2026-04-13 +author: "Armarium" +tags: ["launch", "budget", "finanzen", "schweiz"] +featured: true +locale: de +--- + +Nach Monaten der Entwicklung ist es so weit: **Armarium Suite 1.0** ist live. + +Armarium ist eine persönliche Finanz-App, die dir hilft, den Überblick über dein Budget zu behalten – ohne Komplexität, ohne Datenmissbrauch, ohne Überraschungen. + +## Was Armarium kann + +**Budget-Übersicht auf einen Blick.** Dein aktueller Kontostand, deine Ausgaben und Einnahmen – übersichtlich und immer aktuell. + +**Transaktionen schnell erfassen.** Ausgaben und Einnahmen eintragen, kategorisieren und filtern. So weisst du jederzeit, wohin dein Geld fliesst. + +**Eigene Kategorien & Berichte.** Erstelle Kategorien, die zu deinem Alltag passen, und verstehe mit klaren Auswertungen dein Ausgabeverhalten. + +**Mehrere Konten verwalten.** Bankkonto, Kreditkarte, Bargeld – alles in einer App zusammengefasst, klar getrennt und übersichtlich. + +**Sparziele verfolgen.** Setze dir finanzielle Ziele und beobachte deinen Fortschritt. Ob Notgroschen, Urlaub oder neues Fahrrad – Schritt für Schritt ans Ziel. + +## Datenschutz ist kein Feature – es ist Grundprinzip + +Deine Finanzdaten sind privat. Deshalb hostet Armarium ausschliesslich auf Servern von **Infomaniak** in der Schweiz – ISO 27001:2022-zertifiziert, Swiss Hosting Label, 100% erneuerbare Energie. + +Kein Tracking. Kein Datenverkauf. Keine Werbung. Deine Daten verlassen die Schweiz nicht. + +## Kostenlos starten + +Armarium ist kostenlos nutzbar. Registriere dich in wenigen Sekunden und lege sofort los – keine Kreditkarte, kein Abo erforderlich. + +**[Jetzt registrieren →](/register)** + +Wir freuen uns auf dein Feedback. Made in Zürich 🇨🇭 diff --git a/src/content/blog/en/astro-rocket-configuration-guide.mdx b/src/content/blog/en/astro-rocket-configuration-guide.mdx deleted file mode 100644 index 327cb09..0000000 --- a/src/content/blog/en/astro-rocket-configuration-guide.mdx +++ /dev/null @@ -1,555 +0,0 @@ ---- -title: "Astro Rocket Configuration — Every Toggle, Theme, and Layout Option Explained" -description: "A complete walkthrough of Astro Rocket's configuration options: 12 colour themes, OKLCH colours, typography, radius and shadow tokens, header styles, dark mode, and more." -publishedAt: 2026-03-24 -author: "Hans Martens" -tags: ["astro-rocket", "configuration", "customization", "themes"] -featured: false -locale: en -image: "../../../assets/blog/astro-rocket-configuration-guide.svg" -svgSlug: "astro-rocket-configuration-guide" -imageAlt: "Feature overview graphic for Astro Rocket on a dark background" ---- - -Astro Rocket ships configured with sensible defaults. This post documents every meaningful option you can change — what it does, where to find it, and what value to set. - - -## The main config file - -Almost everything lives in `src/config/site.config.ts`. Open it and you'll see the full `siteConfig` object. The fields you're most likely to change: - -```ts -name: 'Your Site Name', // Logo, titles, footer copyright -description: 'Short description', // Default meta description -url: 'https://yoursite.com', // Canonical URLs and sitemap -author: 'Your Name', -email: 'hello@yoursite.com', -``` - -The `name` field is not cosmetic only — it feeds into structured data, Open Graph tags, and the auto-generated favicon and logo badge. Set it accurately from the start. - -### Full site config reference - -All fields available in `src/config/site.config.ts`: - -| Field | Type | Required | What it does | -|-------|------|:--------:|--------------| -| `name` | string | ✓ | Site name — appears in logo, page titles, copyright, and structured data | -| `description` | string | ✓ | Default meta description used on pages without their own | -| `url` | string | ✓ | Canonical base URL — used in sitemap, OG tags, and JSON-LD | -| `ogImage` | string | ✓ | Path to the default fallback OG image | -| `author` | string | ✓ | Author name for blog posts and Person schema | -| `email` | string | ✓ | Contact email — used in structured data and the contact form | -| `phone` | string | — | Phone number for structured data (local business schema) | -| `address` | object | — | Physical address for structured data — `street`, `city`, `state`, `zip`, `country` | -| `authorImage` | string | — | Path to author avatar (e.g. `'/avatar.jpg'`) — used in Person schema | -| `socialLinks` | string[] | — | Social profile URLs — icons resolved automatically | -| `twitter.site` | string | — | Twitter site handle — populates `twitter:site` meta tag | -| `twitter.creator` | string | — | Twitter creator handle — populates `twitter:creator` meta tag | -| `verification.google` | string | — | Google Search Console verification code | -| `verification.bing` | string | — | Bing Webmaster verification code | -| `blogImageOverlay` | boolean | — | Brand-colour tint over blog cover images (default: `true`) | -| `branding.logo.alt` | string | ✓ | Accessible alt text for the logo | -| `branding.logo.imageUrl` | string | — | Path to a PNG logo — used in Organization schema for rich results | -| `branding.favicon.svg` | string | ✓ | Path to favicon SVG in `public/` | -| `branding.colors.themeColor` | hex | ✓ | Browser toolbar colour on mobile Chrome/Safari | -| `branding.colors.backgroundColor` | hex | ✓ | PWA splash screen background colour | - -The `phone` and `address` fields are optional but improve local business structured data — relevant if you're building a site for a business with a physical location. Leave them empty or remove them for a personal site. - -## Boolean switches - -### Blog image overlay - -```ts -// src/config/site.config.ts -blogImageOverlay: true, -``` - -When `true`, a translucent brand-colour tint is applied over blog cover images. This helps images blend with your theme if they have neutral or mismatched colours. Set it to `false` if your images are already on-brand or if you prefer photographs to appear unaltered. - -### Dark mode default - -The default mode is set directly in the HTML element in `src/layouts/BaseLayout.astro`: - -```html - -``` - -The `dark` class is what activates dark mode on first load. Remove it to default to light: - -```html - -``` - -The user's preference during their session is stored in `sessionStorage` — so it persists while the tab is open but resets when they open a new tab. This is intentional for a portfolio or marketing site. If you want it to persist across sessions, swap the `sessionStorage` calls for `localStorage` in the same file. - -### Structured data: schema switches - -The landing page (`src/pages/index.astro`) passes three boolean props to `LandingLayout` that control which JSON-LD schemas are injected into the page ``: - -```astro - -``` - -| Prop | Default | What it adds | -|------|:-------:|--------------| -| `includePersonSchema` | `false` | `Person` schema — name, job title, email, social profiles, author image | -| `includeOrgSchema` | `false` | `Organization` schema — name, URL, logo, contact email | -| `includeProfessionalServiceSchema` | `false` | `ProfessionalService` schema — adds address, opening hours, area served | - -Enable `includePersonSchema` for a personal portfolio. Enable `includeOrgSchema` if the site represents a company. Enable `includeProfessionalServiceSchema` only if you have a physical address set in `site.config.ts` — search engines will show it in local results. - -### SEO: noindex and nofollow - -Any page layout accepts `noindex` and `nofollow` props that are passed through to the `` tag: - -```astro - -``` - -| Prop | Default | What it does | -|------|:-------:|--------------| -| `noindex` | `false` | Tells search engines not to index the page | -| `nofollow` | `false` | Tells search engines not to follow outbound links | - -Use `noindex` on pages you don't want in search results (thank-you pages, internal preview pages, staging environments). Leave both at `false` for all public content. - -### Branding: favicon and browser colours - -The favicon is auto-generated from the first letter of `siteConfig.name` and the active `--brand-500` colour — it updates live when the theme changes. No image file is needed. To replace it with a custom SVG, drop your file into `public/` and update the path: - -```ts -// src/config/site.config.ts -branding: { - favicon: { - svg: '/my-logo.svg', // replaces the auto-generated monogram - }, - colors: { - themeColor: '#1d4ed8', // browser toolbar colour on mobile (use your brand hex) - backgroundColor: '#ffffff', // PWA splash screen background - }, -} -``` - -`themeColor` is the colour shown in Chrome's address bar and Safari's status bar on mobile — set it to your brand colour for a polished native-app feel. - -For structured data (Google rich results), you can also supply a static logo image: - -```ts -branding: { - logo: { - alt: 'My Company', - imageUrl: '/logo.png', // add a PNG to public/ and point here - }, -} -``` - -## Colour themes - -Astro Rocket ships with **12 colour themes**. All 12 are shown as coloured swatches in the header via the `ThemeSelector` component. Clicking a swatch switches the active theme instantly — no file edits, no rebuild. The logo badge, blog image gradients, and every brand colour on the page update live together. - -### Changing the active theme - -The default theme is set with `data-theme` on the `` element in `src/layouts/BaseLayout.astro`: - -```html -data-theme="blue" -``` - -Available values — all 12 themes: - -| Value | Hue | Best for | -|-------|-----|----------| -| `orange` | 38° | Bold and warm — the original Velocity/International Orange | -| `amber` | 75° | Editorial, luxury, food, warm creative brands | -| `lime` | 130° | Dev tools, high-energy product sites (Vercel/Linear aesthetic) | -| `emerald` | 160° | All-rounder — works for SaaS, portfolios, and organic brands | -| `teal` | 190° | Developer tooling, modern SaaS, trustworthy services | -| `cyan` | 200° | Fresh, vibrant tech — more energetic than teal | -| `sky` | 222° | Clean and airy — between teal and blue, great for services | -| `blue` | 255° | Studio aesthetic (Linear/Raycast feel), authoritative — the default | -| `indigo` | 264° | Enterprise, productivity tools, serious B2B | -| `violet` | 277° | Creative and premium — sweet spot between indigo and purple | -| `purple` | 292° | Expressive, personality-forward, vivid | -| `magenta` | 330° | Creative agencies, bold personal brands — maximum vivid | - -### The live theme selector - -All 12 themes appear as colour swatches in the header dropdown (desktop) and in the mobile menu when `showThemeSelector` is enabled on the `
` component. The visitor's choice is saved to `localStorage` and persists across sessions. - -The `themes` array in `src/components/layout/ThemeSelector.astro` controls which swatches are shown and in what order. The current set: - -```ts -const themes = [ - { id: 'orange', name: 'Orange', color: 'oklch(62.5% 0.22 38)' }, - { id: 'amber', name: 'Amber', color: 'oklch(68% 0.19 75)' }, - { id: 'lime', name: 'Lime', color: 'oklch(64% 0.27 130)' }, - { id: 'emerald', name: 'Emerald', color: 'oklch(62.5% 0.22 160)' }, - { id: 'teal', name: 'Teal', color: 'oklch(62.5% 0.22 190)' }, - { id: 'cyan', name: 'Cyan', color: 'oklch(65% 0.22 200)' }, - { id: 'sky', name: 'Sky', color: 'oklch(67% 0.21 222)' }, - { id: 'blue', name: 'Blue', color: 'oklch(62.5% 0.22 255)' }, - { id: 'indigo', name: 'Indigo', color: 'oklch(60% 0.24 264)' }, - { id: 'violet', name: 'Violet', color: 'oklch(62.5% 0.26 277)' }, - { id: 'purple', name: 'Purple', color: 'oklch(62.5% 0.25 303)' }, - { id: 'magenta', name: 'Magenta', color: 'oklch(58% 0.28 330)' }, -]; -``` - -Once you've settled on your brand colour, you can remove the selector from the header entirely. Open `src/layouts/LandingLayout.astro` and delete the `showThemeSelector` prop — the swatches disappear without breaking anything else. - -### Understanding OKLCH — the three numbers - -All colour values in Astro Rocket use the OKLCH colour space: `oklch(lightness% chroma hue)`. - -| Parameter | Range | What it means | -|-----------|-------|----------------| -| **Lightness** | 0% (black) → 100% (white) | How light or dark the colour is | -| **Chroma** | 0 (grey) → ~0.37 (maximum vivid) | How saturated the colour is | -| **Hue** | 0–360° | The colour angle on the wheel | - -Common hue landmarks: 0°/360° = red, 38° = orange, 75° = amber, 130° = lime, 155–160° = green, 190–200° = teal/cyan, 222° = sky, 255° = blue, 264° = indigo, 292° = purple, 330° = magenta. - -To shift a theme to a completely different colour, you only need to change the hue number across the `--brand-50` to `--brand-900` scale — keep the lightness and chroma values the same and the whole palette moves together. Use [oklch.com](https://oklch.com) to pick visually. - -### Customising a theme's brand colour - -Every theme is a CSS file in `src/styles/themes/`. Each file defines the full set of colour tokens for both light and dark mode. Open the relevant file and adjust the `--brand-*` scale — all ten steps from 50 to 900: - -```css -html[data-theme="blue"] { - --brand-50: oklch(97.5% 0.02 255); - --brand-100: oklch(94.8% 0.04 255); - --brand-200: oklch(87.5% 0.08 255); - --brand-300: oklch(77.8% 0.14 255); - --brand-400: oklch(68.5% 0.19 255); - --brand-500: oklch(62.5% 0.22 255); /* primary accent in light mode */ - --brand-600: oklch(53.2% 0.19 255); - --brand-700: oklch(45.5% 0.16 255); - --brand-800: oklch(37.2% 0.13 255); - --brand-900: oklch(26.5% 0.09 255); -} -``` - -To shift to a different colour, replace `255` (blue) with your target hue across all ten lines. That's the entire change required. - -You can also edit the shared brand scale in `src/styles/tokens/primitives.css` if you want a single consistent palette that isn't theme-dependent: - -```css -:root { - --brand-500: oklch(62.5% 0.22 255); /* primary brand color */ - /* full --brand-50 to --brand-900 scale */ -} -``` - -### Light mode vs dark mode: which brand step is used - -The theme files use **different brand steps** for light and dark mode: - -| Context | Brand step used | Why | -|---------|----------------|-----| -| Light mode accent (`--accent`) | `--brand-500` | Strong enough on white backgrounds | -| Dark mode accent (`--accent`) | `--brand-400` | One step lighter — needed on dark backgrounds for contrast | -| Light mode input focus / ring | `--brand-500` | | -| Dark mode input focus / ring | `--brand-400` | | - -When you customise the brand colour, adjust **both** steps in each mode's block. If you only change `--brand-500`, dark mode accents will still use the old `--brand-400` value. - -### Typography tokens - -Each theme file defines the font stack and typographic rhythm for the entire site. You can override any of these per theme: - -```css -html[data-theme="blue"] { - /* Font families */ - --theme-font-sans: 'Manrope Variable', ui-sans-serif, system-ui, sans-serif; - --theme-font-display: 'Outfit Variable', var(--theme-font-sans); - --theme-font-mono: 'JetBrains Mono Variable', ui-monospace, monospace; - - /* Typographic rhythm */ - --theme-heading-weight: 700; /* headings font weight */ - --theme-heading-tracking: -0.02em; /* letter-spacing on headings */ - --theme-body-leading: 1.6; /* line-height for body text */ -} -``` - -All 12 themes currently share the same font stack. To use a custom font, install it in `public/fonts/`, add a `@font-face` declaration in `src/styles/global.css`, and update `--theme-font-sans` or `--theme-font-display` in your active theme file. - -### Border radius tokens - -The global roundness of the UI is set per theme with five radius levels: - -```css ---theme-radius-sm: 0.25rem; /* 4px — badges, small buttons */ ---theme-radius-md: 0.375rem; /* 6px — inputs, cards */ ---theme-radius-lg: 0.5rem; /* 8px — panels, larger cards */ ---theme-radius-xl: 0.625rem; /* 10px — large containers */ ---theme-radius-full: 9999px; /* pill shapes, circular avatars */ -``` - -To give your site a sharper, more editorial feel, set all values to `0`. For a rounder, friendlier look, increase them (e.g. `--theme-radius-md: 0.75rem`). All components reference these tokens, so a single change cascades across the entire design. - -### Shadow tokens - -Each theme defines four shadow elevation levels, tinted with the theme's hue: - -```css ---theme-shadow-sm /* subtle depth: cards, list items */ ---theme-shadow-md /* dropdowns, popovers */ ---theme-shadow-lg /* modals, drawers */ ---theme-shadow-xl /* high-emphasis overlays */ -``` - -In dark mode the shadows use higher opacity (35–55%) and are tinted with the brand hue, so the depth reads naturally against dark backgrounds. To make shadows stronger or more neutral, adjust the opacity values or replace the hue-tinted OKLCH colour with a plain `rgba(0,0,0,...)`. - -### Inverted sections - -The `--surface-invert` family of tokens controls sections that have a dark background in light mode — the CTA blocks, some hero variants, and any area where you use `background="invert"` on the Footer or a section component: - -```css ---surface-invert: /* main dark surface background */ ---surface-invert-secondary: /* slightly lighter surface */ ---surface-invert-tertiary: /* even lighter layer */ ---on-invert: /* primary text on dark surface */ ---on-invert-secondary: /* secondary text on dark surface */ ---on-invert-muted: /* muted text on dark surface */ ---border-invert: /* border on dark surface */ ---border-invert-strong: /* stronger border on dark surface */ -``` - -In light mode these default to near-black values (around 10–18% lightness) with a subtle hue tint. In dark mode they become slightly lighter than the main background — providing contrast between sections without the jarring jump of a fully inverted surface. - -### Creating a new theme - -1. Duplicate any file from `src/styles/themes/` as your starting point. -2. Implement all ~35 semantic tokens for both `:root` (light) and `.dark` (dark mode). -3. Add your new theme file's import to `src/styles/tokens/colors.css`. -4. Add an entry to the `themes` array in `ThemeSelector.astro` if you want it in the live picker. - -## Header: floating capsule vs. fixed bar - -Astro Rocket has two header shapes. The landing and marketing layouts use a floating capsule: - -```astro -
-``` - -The blog and standard page layouts use a full-width bar: - -```astro -
-``` - -### Switching the blog header to a floating style - -Open `src/layouts/BlogLayout.astro` and find the `
` line. Change it to: - -```astro -
-``` - -### Switching any page header from floating to a bar - -Find the Header component in the relevant layout file and remove `shape="floating"`: - -```astro - -
- - -
-``` - -### Header prop reference - -All Header props and what they do: - -| Prop | Options | Default | What it controls | -|------|---------|---------|-----------------| -| `position` | `fixed` `sticky` `static` | `fixed` | Whether the header stays at the top while scrolling | -| `shape` | `bar` `floating` | `bar` | Full-width bar or centred floating capsule | -| `size` | `sm` `md` `lg` | `md` | Header height | -| `variant` | `default` `solid` `transparent` | `default` | Background fill | -| `colorScheme` | `default` `invert` | `default` | Use inverted colours — for dark hero backgrounds | -| `layout` | `default` `centered` `minimal` | `default` | Logo and nav arrangement | -| `showThemeToggle` | `true` `false` | `true` | Dark/light mode toggle button | -| `showThemeSelector` | `true` `false` | `false` | Colour theme swatch picker (desktop dropdown + mobile menu) | -| `showSocialLinks` | `true` `false` | `false` | Social icon links (desktop only, reads from `siteConfig.socialLinks`) | -| `showCta` | `true` `false` | `true` | CTA button in the header | -| `showMobileMenu` | `true` `false` | `true` | Hamburger menu on small screens | -| `showActiveState` | `true` `false` | `true` | Highlight for the current page link | -| `hideLogo` | `true` `false` | `false` | Hide the logo entirely | -| `showScrollProgress` | `true` `false` | `false` | Thin brand-coloured scroll progress bar on the header edge | -| `scrollProgressPosition` | `top` `bottom` | `bottom` | Edge of the header where the progress bar sits — `top` suits the floating capsule header, `bottom` suits the solid bar header | - -Set any prop on the `
` component in the layout file for the page type you want to adjust. - -## Footer - -### Changing the copyright text - -The footer copyright line reads from the `copyright` prop. Passing a custom value overrides it: - -```astro -