Compare commits
1 Commits
main
..
ddd0c22311
| Author | SHA1 | Date | |
|---|---|---|---|
| ddd0c22311 |
@@ -1,15 +1,13 @@
|
||||
name: Deploy to Azure Static Web Apps
|
||||
name: Deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, closed]
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
build_and_deploy:
|
||||
if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed')
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
@@ -17,7 +15,9 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup pnpm
|
||||
run: corepack enable pnpm
|
||||
uses: pnpm/action-setup@v3
|
||||
with:
|
||||
version: 9
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
@@ -28,18 +28,46 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Lint
|
||||
run: pnpm run lint
|
||||
|
||||
- name: Type check
|
||||
run: pnpm run check
|
||||
|
||||
- name: Build
|
||||
run: pnpm run build
|
||||
env:
|
||||
SITE_URL: ${{ secrets.SITE_URL }}
|
||||
|
||||
- name: Deploy to Azure Static Web Apps
|
||||
run: npx --yes @azure/static-web-apps-cli@latest deploy dist --deployment-token "${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN }}" --env production
|
||||
|
||||
close_pull_request:
|
||||
if: github.event_name == 'pull_request' && github.event.action == 'closed'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Close preview environment
|
||||
run: npx --yes @azure/static-web-apps-cli@latest deploy --deployment-token "${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN }}" --env close
|
||||
# Uncomment and configure for your deployment platform
|
||||
#
|
||||
# Vercel deployment:
|
||||
# - name: Deploy to Vercel
|
||||
# if: github.ref == 'refs/heads/main'
|
||||
# uses: amondnet/vercel-action@v25
|
||||
# with:
|
||||
# vercel-token: ${{ secrets.VERCEL_TOKEN }}
|
||||
# vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
|
||||
# vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
|
||||
# vercel-args: '--prod'
|
||||
#
|
||||
# Netlify deployment:
|
||||
# - name: Deploy to Netlify
|
||||
# if: github.ref == 'refs/heads/main'
|
||||
# uses: nwtgck/actions-netlify@v2
|
||||
# with:
|
||||
# publish-dir: './dist'
|
||||
# production-branch: main
|
||||
# github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
# env:
|
||||
# NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
|
||||
# NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
|
||||
#
|
||||
# Cloudflare Pages deployment:
|
||||
# - name: Deploy to Cloudflare Pages
|
||||
# if: github.ref == 'refs/heads/main'
|
||||
# uses: cloudflare/wrangler-action@v3
|
||||
# with:
|
||||
# apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
# accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
# command: pages deploy dist --project-name=astro-rocket
|
||||
|
||||
|
Before Width: | Height: | Size: 36 KiB |
@@ -6,112 +6,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
|
||||
---
|
||||
|
||||
## [0.9.1] — 2026-05-21
|
||||
|
||||
### Changed
|
||||
|
||||
- **HomePage** — device tabs section: removed iOS/Android tab buttons and Android panel; content now static; removed tab-switching JS; replaced phone mockup with `armarium_image.jpg` (`rounded-2xl shadow-md`, 75% width)
|
||||
- **HomePage** — bottom split section: replaced Flowbite CDN images with `content_image.jpg` (75% width, centred)
|
||||
- **HomePage** — CTA section: updated description to "Kostenlos mitmachen und sofort loslegen."; removed Login button, register-only
|
||||
- **AboutPage** — full i18n of team and FAQ sections (all 4 locales): new keys `about.team.title`, `about.team.desc1/2`, `about.founder.role`, `about.founder.bio`, `about.faq.title`, `about.faq.q1–q4`, `about.faq.a1–a4`
|
||||
- **AboutPage** — replaced Flowbite placeholder avatar with local `about_photo.jpg`; updated bio text and role to "Founder"; replaced all social icons with Armarium LinkedIn only
|
||||
- **AboutPage** — replaced all Flowbite placeholder text (team description, FAQ) with Armarium-specific content via i18n
|
||||
|
||||
### Added
|
||||
|
||||
- `src/assets/armarium_image.jpg` — app screenshot used in homepage feature section
|
||||
- `src/assets/content_image.jpg` — feature illustration used in homepage bottom split
|
||||
- `src/assets/about_photo.jpg` — founder profile photo used on about page
|
||||
|
||||
---
|
||||
|
||||
## [0.9.0] — 2026-05-20
|
||||
|
||||
### Changed
|
||||
|
||||
- **HomePage** — complete redesign with Flowbite layout:
|
||||
- Hero: Flowbite split-section (text left, `header_img.jpg` right with rounded corners + shadow), removed trust bar and Badge chip
|
||||
- Added device tabs section (iOS/Android, vanilla JS tab switching, phone mockup, feature list + split image)
|
||||
- Added blog preview section (3-column: featured post with image left, 2× 3 posts right, dummy fallback entries)
|
||||
- **FeaturesIndexPage** — replaced old Card grid with Flowbite 4-column feature card grid; removed Badge chip from hero
|
||||
- **AboutPage** — removed "Unsere Mission" and "Unsere Werte" sections; removed hero Badge chip; added Flowbite team section (3 profiles) and native `<details>/<summary>` FAQ
|
||||
- **ContactPage** — replaced custom form with Flowbite contact form + 3 contact info blocks; removed hero Badge chip
|
||||
- **BlogIndexPage** — complete rewrite with Flowbite 3-column card grid; restored hero; added CTA section before footer; removed Badge chip, tag filter, featured/regular split
|
||||
- **Footer** — renamed "App" → "Produkt" (all 4 locales); added LinkedIn round icon link; restructured to 2-column layout (Produkt + Legal)
|
||||
- **URL slug** — `/projects` renamed to `/features` for DE locale (`nav.features.href` in `ui.ts`, `nav.config.ts`)
|
||||
|
||||
### Removed
|
||||
|
||||
- Feature detail pages (`/features/[slug].astro`) and all 6 MDX files in `src/content/projects/`
|
||||
- Trust bar ("Made in Zürich", privacy/free badges) from HomePage
|
||||
- Badge/chip elements from hero sections on HomePage, FeaturesIndexPage, AboutPage, ContactPage
|
||||
|
||||
---
|
||||
|
||||
## [0.8.1] — 2026-04-14
|
||||
|
||||
### Changed
|
||||
|
||||
- Login and register button links now point to `https://app.armarium.ch/login` and `https://app.armarium.ch/register` respectively — affects hero section, CTA section, footer, and all locale variants
|
||||
|
||||
---
|
||||
|
||||
## [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.
|
||||
|
||||
@@ -0,0 +1,707 @@
|
||||
<p align="center">
|
||||
<img src="public/readme-hero.svg" alt="Astro Rocket" width="880" />
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<strong>Astro Rocket</strong> — A production-ready Astro 6 starter theme. Change the text, launch your site.
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://astro.build"><img src="https://img.shields.io/badge/Astro-6.0-bc52ee?logo=astro&logoColor=white" alt="Astro" /></a>
|
||||
<a href="https://tailwindcss.com"><img src="https://img.shields.io/badge/Tailwind-4.0-38bdf8?logo=tailwindcss&logoColor=white" alt="Tailwind CSS" /></a>
|
||||
<a href="https://www.typescriptlang.org"><img src="https://img.shields.io/badge/TypeScript-5.7-3178c6?logo=typescript&logoColor=white" alt="TypeScript" /></a>
|
||||
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-22c55e" alt="License" /></a>
|
||||
<a href="https://github.com/hansmartens68/astro-rocket"><img src="https://img.shields.io/github/stars/hansmartens68/astro-rocket?style=flat&label=%E2%AD%90%20Star%20on%20GitHub&color=f59e0b" alt="Star on GitHub" /></a>
|
||||
<img src="https://visitor-badge.laobi.icu/badge?page_id=hansmartens68.astro-rocket" alt="Visitors" />
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Astro Rocket is a **launch-ready starter theme** for web designers, developers, bloggers, and anyone who needs a portfolio website. Every page is already built and styled — you change the text and content, and your site is ready to go live.
|
||||
|
||||
It ships with a full blog, a complete component library, a built-in SEO layer, dark mode, a contact form, and 12 colour themes you can switch with one click. It's built on Astro 6 and Tailwind CSS v4.
|
||||
|
||||
**[Live demo → astrorocket.dev](https://astrorocket.dev)** · **[Built by Hans Martens → hansmartens.dev](https://hansmartens.dev)**
|
||||
|
||||
> **Astro Rocket is a fork of [Velocity](https://github.com/southwellmedia/velocity) by [Southwell Media](https://southwellmedia.com).** Velocity is the foundation — a powerful Astro boilerplate with a comprehensive design system and component library. Full credit to the Southwell Media team for that work. Astro Rocket builds on it with a different goal: a complete, ready-to-launch website where you only change the text to make it your own.
|
||||
|
||||
---
|
||||
|
||||
## What changed from Velocity
|
||||
|
||||
The following changes were made to the free Velocity theme to create Astro Rocket:
|
||||
|
||||
| Change | Velocity | Astro Rocket |
|
||||
|--------|----------|--------------|
|
||||
| **Theme switching** | Edit a CSS import file and rebuild | 12 colour swatches in the header — click one and the logo badge, blog images, and every brand color update live on screen. No file edits, no rebuilds. Selector can be removed from the header once you've chosen a color. |
|
||||
| **Colour themes** | 1 default theme | 12 Tailwind-based themes — all 12 shown as swatches in the header selector (Orange, Amber, Lime, Emerald, Teal, Cyan, Sky, Blue, Indigo, Violet, Purple, Magenta) |
|
||||
| **Logo badge** | Requires a custom logo file | Auto-generated monogram badge — first letter of your site name on brand color, live-updates with active theme |
|
||||
| **Favicon** | Static file to replace manually | Auto-generated SVG favicon — first letter + brand color, pre-rendered at build time from `site.config.ts`, no design tools needed |
|
||||
| **Blog image gradients** | Plain image containers | Every blog cover and card uses a brand-color gradient background that updates live when the active theme changes |
|
||||
| **Icon system** | Basic SVG `Icon` component | Unified `Icon` component powered by Iconify — 350+ Lucide UI icons + 3000+ Simple Icons brand icons |
|
||||
| **Typing effect** | Not included | Hero section includes an animated typing effect |
|
||||
| **Dark mode storage** | `localStorage` | `sessionStorage` — resets to dark on every new tab/session (see [why](#dark-mode)) |
|
||||
| **Target audience** | Developers & agencies | Web designers, developers, bloggers, and portfolio sites |
|
||||
| **Ready to launch** | Boilerplate starting point | Fully styled pages — replace the text and your site is live |
|
||||
| **Maintained by** | Southwell Media | Hans Martens |
|
||||
|
||||
---
|
||||
|
||||
## Key Features
|
||||
|
||||
| Feature | Description |
|
||||
|---------|-------------|
|
||||
| **Astro 6** | Latest version with Content Layer API, security features, and performance optimizations |
|
||||
| **Tailwind CSS v4** | CSS-first configuration with OKLCH color system and fluid typography |
|
||||
| **12 Colour Themes** | All 12 colour swatches are shown in the header dropdown — click one and the logo badge, blog image gradients, and every brand color update live instantly. No file edits, no rebuilds. The selector can be removed from the header once you've settled on a color. |
|
||||
| **Scroll Progress Bar** | A thin 2px brand-coloured bar on the header edge that fills as you scroll. Enabled on the homepage (above the floating header), blog index, and post pages (below the solid header). Controlled via `showScrollProgress` and `scrollProgressPosition` props on the Header component. |
|
||||
| **Design Tokens** | Three-tier token architecture (reference → semantic → component) |
|
||||
| **57 Components** | 31 UI, 7 patterns, 1 hero, 4 layout, 4 blog, 7 landing, 3 SEO — all accessible with TypeScript |
|
||||
| **Auto Logo & Favicon** | First letter of your site name on brand color — generated automatically from `site.config.ts`, no design tools needed |
|
||||
| **Icon System** | Unified `Icon` component (Astro + React) — 350+ [Lucide](https://lucide.dev) UI icons and 3000+ [Simple Icons](https://simpleicons.org) brand icons via Iconify |
|
||||
| **Typing Effect** | Animated typing effect in the hero section |
|
||||
| **Page Animations** | Smooth page transitions via Astro View Transitions, scroll-triggered counter and score animations, scroll-reactive header, card hover effects, and a full suite of UI micro-animations — all with reduced-motion support |
|
||||
| **SEO Toolkit** | Meta tags, JSON-LD structured data, sitemap, and robots.txt |
|
||||
| **Static OG Image** | A polished default Open Graph image serves as social preview for all pages — no build-time generation required |
|
||||
| **Dark Mode** | Dark-first design with `sessionStorage` persistence |
|
||||
| **Content Collections** | Type-safe blog, pages, authors, and FAQs with Zod validation |
|
||||
| **API Routes** | Contact form and newsletter endpoints with validation |
|
||||
| **React Islands** | Optional client-side interactivity where needed |
|
||||
|
||||
### Internationalization (i18n)
|
||||
|
||||
The base theme is i18n-ready with locale-aware content collection schemas. Full i18n support with language routing and a `LanguageSwitcher` component can be added via the **[create-velocity-astro](https://github.com/southwellmedia/create-velocity-astro)** CLI from Southwell Media.
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- **Node.js 22.12.0+** (required for Astro 6)
|
||||
- **pnpm 9.x** (recommended) or npm/yarn
|
||||
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://github.com/hansmartens68/astro-rocket.git my-project
|
||||
cd my-project
|
||||
|
||||
# Install dependencies
|
||||
pnpm install
|
||||
|
||||
# Copy environment variables
|
||||
cp .env.example .env
|
||||
|
||||
# Start development server
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
Visit `http://localhost:4321` to see your site.
|
||||
|
||||
---
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
astro-rocket/
|
||||
├── public/ # Static assets (fonts, favicon)
|
||||
├── src/
|
||||
│ ├── assets/ # Images and icons (processed by Astro)
|
||||
│ ├── components/
|
||||
│ │ ├── ui/ # UI component library (31 components)
|
||||
│ │ │ ├── form/ # Button, Input, Textarea, Select, Checkbox, Radio, Switch
|
||||
│ │ │ ├── data-display/ # Card, Badge, Avatar, Table, Pagination, Progress, Skeleton
|
||||
│ │ │ ├── feedback/ # Alert, Toast, Tooltip
|
||||
│ │ │ ├── overlay/ # Dialog, Dropdown, Tabs, VerticalTabs, Accordion
|
||||
│ │ │ ├── layout/ # Separator
|
||||
│ │ │ ├── primitives/ # Icon
|
||||
│ │ │ ├── content/ # CodeBlock
|
||||
│ │ │ └── marketing/ # Logo, CTA, NpmCopyButton, SocialProof, TerminalDemo
|
||||
│ │ ├── patterns/ # Composed patterns (ContactForm, SearchInput, StatCard, etc.)
|
||||
│ │ ├── layout/ # Header, Footer, Navigation, ThemeToggle, ThemeSelector
|
||||
│ │ ├── seo/ # SEO, JsonLd, Breadcrumbs
|
||||
│ │ ├── blog/ # Blog-specific components
|
||||
│ │ └── landing/ # Landing page components
|
||||
│ ├── content/ # Content collections
|
||||
│ │ ├── blog/ # Blog posts (en/, es/, fr/)
|
||||
│ │ ├── projects/ # Portfolio project pages
|
||||
│ │ ├── authors/ # Author profiles
|
||||
│ │ └── faqs/ # FAQ entries
|
||||
│ ├── layouts/ # Page layouts
|
||||
│ ├── lib/ # Utilities (schema, cn)
|
||||
│ ├── pages/ # Routes and API endpoints
|
||||
│ │ ├── api/ # Contact, newsletter endpoints
|
||||
│ │ └── blog/ # Blog routes
|
||||
│ ├── styles/ # Global CSS and design tokens
|
||||
│ │ ├── tokens/ # colors.css, typography.css, spacing.css
|
||||
│ │ └── themes/ # 12 colour theme files
|
||||
│ └── config/ # Site and navigation configuration
|
||||
├── astro.config.mjs # Astro configuration
|
||||
├── package.json
|
||||
└── tsconfig.json
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `pnpm dev` | Start development server with hot reload |
|
||||
| `pnpm build` | Build for production |
|
||||
| `pnpm preview` | Preview production build locally |
|
||||
| `pnpm check` | Run Astro type checker |
|
||||
| `pnpm lint` | Run ESLint |
|
||||
| `pnpm lint:fix` | Fix ESLint issues |
|
||||
| `pnpm format` | Format code with Prettier |
|
||||
| `pnpm format:check` | Check code formatting |
|
||||
| `pnpm test` | Run Vitest tests |
|
||||
| `pnpm test:e2e` | Run Playwright E2E tests |
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
### Site Configuration
|
||||
|
||||
Edit `src/config/site.config.ts`:
|
||||
|
||||
```typescript
|
||||
const siteConfig = {
|
||||
name: 'Your Site Name',
|
||||
description: 'Your site description for SEO',
|
||||
url: 'https://yoursite.com',
|
||||
ogImage: '/og-default.svg',
|
||||
author: 'Your Name',
|
||||
email: 'hello@yoursite.com',
|
||||
twitter: {
|
||||
site: '@yourhandle',
|
||||
creator: '@yourhandle',
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
|
||||
Create a `.env` file from `.env.example`:
|
||||
|
||||
```bash
|
||||
# Required
|
||||
SITE_URL=https://yoursite.com
|
||||
|
||||
# Optional - Analytics
|
||||
PUBLIC_GA_MEASUREMENT_ID=G-XXXXXXXXXX
|
||||
PUBLIC_GTM_ID=GTM-XXXXXXX
|
||||
|
||||
# Optional - Verification
|
||||
GOOGLE_SITE_VERIFICATION=your-code
|
||||
BING_SITE_VERIFICATION=your-code
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Design System
|
||||
|
||||
Astro Rocket uses a three-tier design token system with OKLCH colors for perceptual uniformity:
|
||||
|
||||
1. **Primitives** (`src/styles/tokens/primitives.css`) — raw color scales (gray, brand, status)
|
||||
2. **Semantic tokens** (`src/styles/themes/*.css`) — purpose-based mappings (background, foreground, border, etc.)
|
||||
3. **Tailwind** (`src/styles/global.css`) — `@theme` directives that expose tokens as utility classes
|
||||
|
||||
### Switching Themes
|
||||
|
||||
Astro Rocket ships with 12 colour themes, all based on Tailwind's color palette. All 12 are shown as colour swatches in the header dropdown (`ThemeSelectorDropdown`) on desktop and in the mobile menu (`ThemeSelector`). Clicking a swatch applies the theme instantly — the logo badge, blog image gradients, and every brand color on the page update live. No file edits, no rebuilds. This is the key difference from Velocity, where switching theme requires editing a CSS import file and rebuilding.
|
||||
|
||||
The 12 themes in order: Orange, Amber, Lime, Emerald, Teal, Cyan, Sky, Blue (default), Indigo, Violet, Purple, and Magenta. The `themes` array in `src/components/layout/ThemeSelector.astro` controls which swatches are shown and in what order. You can also **remove the selector from the header entirely** once you've settled on a color — just remove `showThemeSelector` from the layout file.
|
||||
|
||||
The theme files live in `src/styles/themes/`:
|
||||
|
||||
```
|
||||
amber.css blue.css cyan.css emerald.css
|
||||
green.css indigo.css lime.css magenta.css
|
||||
orange.css purple.css sky.css teal.css
|
||||
violet.css
|
||||
```
|
||||
|
||||
### Customizing Brand Colors
|
||||
|
||||
Edit `src/styles/tokens/primitives.css` and update the `--brand-*` OKLCH values:
|
||||
|
||||
```css
|
||||
:root {
|
||||
--brand-50: oklch(97.5% 0.02 45); /* lightest tint */
|
||||
--brand-100: oklch(94.8% 0.04 45);
|
||||
--brand-200: oklch(87.5% 0.08 45);
|
||||
--brand-300: oklch(77.8% 0.14 45);
|
||||
--brand-400: oklch(68.5% 0.19 40);
|
||||
--brand-500: oklch(62.5% 0.22 38); /* primary brand color */
|
||||
--brand-600: oklch(53.2% 0.19 38);
|
||||
--brand-700: oklch(45.5% 0.16 38);
|
||||
--brand-800: oklch(37.2% 0.13 38);
|
||||
--brand-900: oklch(26.5% 0.09 38);
|
||||
}
|
||||
```
|
||||
|
||||
OKLCH values are `oklch(lightness chroma hue)`. To shift your brand to blue, change the hue from `38-45` to `~260`. Use [oklch.com](https://oklch.com) to pick colors visually.
|
||||
|
||||
### Creating a New Theme
|
||||
|
||||
1. Duplicate `src/styles/themes/default.css` as your starting point
|
||||
2. Implement all ~35 semantic tokens for both `:root` (light) and `.dark` (dark):
|
||||
|
||||
**Backgrounds**: `--background`, `--background-secondary`, `--background-tertiary`, `--background-elevated`
|
||||
|
||||
**Foregrounds**: `--foreground`, `--foreground-secondary`, `--foreground-muted`, `--foreground-subtle`
|
||||
|
||||
**Borders**: `--border`, `--border-strong`, `--border-subtle`
|
||||
|
||||
**Interactive**: `--primary`, `--primary-hover`, `--primary-foreground`, `--secondary`, `--secondary-hover`, `--secondary-foreground`, `--accent`, `--accent-hover`, `--accent-light`
|
||||
|
||||
**Surfaces**: `--muted`, `--muted-foreground`, `--card`, `--card-border`, `--input-bg`, `--input-border`, `--input-focus`, `--ring`
|
||||
|
||||
**Destructive**: `--destructive`, `--destructive-foreground`
|
||||
|
||||
**Gradients**: `--gradient-start`, `--gradient-end`
|
||||
|
||||
**Invert sections**: `--surface-invert`, `--surface-invert-secondary`, `--surface-invert-tertiary`, `--on-invert`, `--on-invert-secondary`, `--on-invert-muted`, `--border-invert`, `--border-invert-strong`
|
||||
|
||||
3. Update the import in `src/styles/tokens/colors.css` to point to your new theme file
|
||||
|
||||
### Dark Mode
|
||||
|
||||
Dark mode toggles via the `.dark` class on `<html>`. The default is **dark** — the design was built dark-first and it looks great for portfolios and creative sites.
|
||||
|
||||
FOUC is prevented by an inline script that reads `sessionStorage` before first paint. Use the included `ThemeToggle` component:
|
||||
|
||||
```astro
|
||||
---
|
||||
import ThemeToggle from '@/components/layout/ThemeToggle.astro';
|
||||
---
|
||||
|
||||
<ThemeToggle />
|
||||
```
|
||||
|
||||
To opt out of dark mode, remove the `.dark { ... }` block from your theme file.
|
||||
|
||||
> **Why `sessionStorage` instead of `localStorage`?** This is a deliberate choice. `sessionStorage` persists the user's preference during their visit but resets when the tab is closed — so every new visit starts with the intended dark design. For a portfolio or marketing site this is the right call. For a product users return to daily (a SaaS dashboard, editor, etc.), switch to `localStorage` so the preference survives across sessions. Read the full reasoning in [this blog post](https://hansmartens.dev/blog/dark-mode-sessionstorage).
|
||||
|
||||
### WCAG Contrast Requirements
|
||||
|
||||
Foreground tokens are documented with their contrast ratios inline. When customizing, maintain these minimums:
|
||||
|
||||
| Token | Minimum ratio | Standard |
|
||||
|-------|:---:|:---:|
|
||||
| `--foreground` | 7:1 | WCAG AAA |
|
||||
| `--foreground-secondary` | 7:1 | WCAG AAA |
|
||||
| `--foreground-muted` | 4.5:1 | WCAG AA |
|
||||
| `--foreground-subtle` | 4.5:1 | WCAG AA |
|
||||
| Status `-foreground` tokens | 4.5:1 | WCAG AA (on their `-light` bg) |
|
||||
|
||||
### Using Design Tokens
|
||||
|
||||
```astro
|
||||
<!-- Tailwind utilities (recommended) -->
|
||||
<div class="bg-background text-foreground">
|
||||
<h1 class="text-primary font-display">Hello</h1>
|
||||
</div>
|
||||
|
||||
<!-- CSS custom properties -->
|
||||
<style>
|
||||
.custom {
|
||||
background: var(--background-secondary);
|
||||
color: var(--foreground);
|
||||
}
|
||||
</style>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Components
|
||||
|
||||
Astro Rocket includes 57 components across 7 categories. All UI components use [class-variance-authority (CVA)](https://cva.style) for type-safe variant management.
|
||||
|
||||
### UI Components (31)
|
||||
|
||||
#### Form (`ui/form/`)
|
||||
|
||||
| Component | Description |
|
||||
|-----------|-------------|
|
||||
| Button | Interactive button with primary, secondary, outline, ghost, destructive variants and loading state |
|
||||
| Input | Text input with label, hint, and error states |
|
||||
| Textarea | Multi-line text input |
|
||||
| Select | Dropdown selection |
|
||||
| Checkbox | Boolean toggle with indeterminate state |
|
||||
| Radio | Single selection from group |
|
||||
| Switch | Toggle switch input |
|
||||
|
||||
#### Data Display (`ui/data-display/`)
|
||||
|
||||
| Component | Description |
|
||||
|-----------|-------------|
|
||||
| Card | Content container with variant, padding, and hover options |
|
||||
| Badge | Status labels and tags with contextual variants |
|
||||
| Avatar | User images with fallback |
|
||||
| AvatarGroup | Grouped avatar display with overlap |
|
||||
| Table | Styled data table |
|
||||
| Pagination | Page navigation controls |
|
||||
| Progress | Progress bar indicator |
|
||||
| Skeleton | Loading placeholders |
|
||||
|
||||
#### Feedback (`ui/feedback/`)
|
||||
|
||||
| Component | Description |
|
||||
|-----------|-------------|
|
||||
| Alert | Contextual feedback messages (info, success, warning, error) |
|
||||
| Toast | Temporary notification messages |
|
||||
| Tooltip | Hover tooltips with positioning |
|
||||
|
||||
#### Overlay (`ui/overlay/`)
|
||||
|
||||
| Component | Description |
|
||||
|-----------|-------------|
|
||||
| Dialog | Modal overlay |
|
||||
| Dropdown | Menu with trigger |
|
||||
| Tabs | Horizontal tabbed content panels |
|
||||
| VerticalTabs | Vertical tab navigation |
|
||||
| Accordion | Collapsible content sections |
|
||||
|
||||
#### Layout (`ui/layout/`)
|
||||
|
||||
| Component | Description |
|
||||
|-----------|-------------|
|
||||
| Separator | Visual divider between sections |
|
||||
|
||||
#### Primitives (`ui/primitives/`)
|
||||
|
||||
| Component | Description |
|
||||
|-----------|-------------|
|
||||
| Icon | Unified icon component (Astro + React) powered by Iconify. Supports all [Lucide](https://lucide.dev) icons (`lucide:*`) and all [Simple Icons](https://simpleicons.org) brand icons (`simple-icons:*`). Includes shorthand names for common social and brand icons. Five size variants: `xs`, `sm`, `md`, `lg`, `xl`. |
|
||||
|
||||
#### Content (`ui/content/`)
|
||||
|
||||
| Component | Description |
|
||||
|-----------|-------------|
|
||||
| CodeBlock | Syntax-highlighted code display |
|
||||
|
||||
#### Marketing (`ui/marketing/`)
|
||||
|
||||
| Component | Description |
|
||||
|-----------|-------------|
|
||||
| Logo | Auto-generated monogram badge — renders the first letter of `siteConfig.name` on the active brand color. Five sizes: `sm`, `md`, `lg`, `xl`, `2xl`. No logo file required. |
|
||||
| CTA | Call-to-action sections with slot-based composition |
|
||||
| NpmCopyButton | NPM install command with copy-to-clipboard |
|
||||
| SocialProof | Testimonial and trust indicator cards |
|
||||
| TerminalDemo | Animated terminal demonstration (React) |
|
||||
|
||||
### Pattern Components (7)
|
||||
|
||||
| Component | Description |
|
||||
|-----------|-------------|
|
||||
| ContactForm | Complete contact form with validation |
|
||||
| NewsletterForm | Email subscription form |
|
||||
| FormField | Reusable form field wrapper |
|
||||
| SearchInput | Search input with icon |
|
||||
| PasswordInput | Password input with visibility toggle |
|
||||
| StatCard | Statistics display card |
|
||||
| EmptyState | Empty state placeholder with icon and action |
|
||||
|
||||
### Other Categories
|
||||
|
||||
| Category | Count | Components |
|
||||
|----------|-------|------------|
|
||||
| Hero | 1 | Hero section with centered/split layouts, grid pattern, and typing effect |
|
||||
| Layout | 6 | Header (with scroll progress bar), Footer, ThemeToggle, ThemeSelector, ThemeSelectorDropdown, Analytics |
|
||||
| Blog | 4 | ArticleHero, BlogCard, ShareButtons, RelatedPosts |
|
||||
| Landing | 5 | Credibility, LighthouseScores, TechStack, FeatureTabs, and more |
|
||||
| SEO | 3 | SEO, JsonLd, Breadcrumbs |
|
||||
|
||||
### Usage Example
|
||||
|
||||
```astro
|
||||
---
|
||||
import { Button, Input, Card } from '@/components/ui';
|
||||
---
|
||||
|
||||
<Card>
|
||||
<Input label="Email" type="email" name="email" required />
|
||||
<Button variant="primary">Submit</Button>
|
||||
</Card>
|
||||
```
|
||||
|
||||
### Icon Usage
|
||||
|
||||
```astro
|
||||
---
|
||||
import Icon from '@/components/ui/primitives/Icon/Icon.astro';
|
||||
---
|
||||
|
||||
<!-- Lucide UI icons — use any icon name from lucide.dev -->
|
||||
<Icon name="arrow-right" size="md" />
|
||||
<Icon name="mail" size="sm" />
|
||||
<Icon name="layers" size="lg" />
|
||||
|
||||
<!-- Simple Icons brand icons — shorthand names available -->
|
||||
<Icon name="github" size="md" />
|
||||
<Icon name="x-twitter" size="md" />
|
||||
<Icon name="brand-astro" size="md" />
|
||||
<Icon name="brand-tailwind" size="md" />
|
||||
|
||||
<!-- Or use the full Iconify name directly -->
|
||||
<Icon name="simple-icons:vercel" size="md" />
|
||||
<Icon name="lucide:rocket" size="xl" />
|
||||
```
|
||||
|
||||
All UI components are imported via barrel exports from `@/components/ui`. View all components at `/components` in development.
|
||||
|
||||
---
|
||||
|
||||
## Content Management
|
||||
|
||||
### Blog Posts
|
||||
|
||||
Create posts in `src/content/blog/[locale]/`:
|
||||
|
||||
```markdown
|
||||
---
|
||||
title: "Your Post Title"
|
||||
description: "Brief description for SEO"
|
||||
publishedAt: 2026-01-30
|
||||
author: "Author Name"
|
||||
tags: ["astro", "tutorial"]
|
||||
locale: en
|
||||
---
|
||||
|
||||
Your content here...
|
||||
```
|
||||
|
||||
### Querying Content
|
||||
|
||||
```astro
|
||||
---
|
||||
import { getCollection } from 'astro:content';
|
||||
|
||||
const posts = await getCollection('blog', ({ data }) => {
|
||||
return import.meta.env.PROD ? !data.draft : true;
|
||||
});
|
||||
---
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## SEO
|
||||
|
||||
### Automatic Features
|
||||
|
||||
- **Meta tags**: Title, description, canonical URL
|
||||
- **Open Graph**: Complete OG tags for social sharing
|
||||
- **Twitter Cards**: Large image cards
|
||||
- **JSON-LD**: WebSite, Organization, BlogPosting, Breadcrumb, FAQ schemas
|
||||
- **Sitemap**: Auto-generated at `/sitemap-index.xml`
|
||||
- **robots.txt**: Dynamic generation with sitemap reference
|
||||
- **OG Images**: A static default OG image serves all pages and blog posts
|
||||
|
||||
### Using the SEO Component
|
||||
|
||||
```astro
|
||||
---
|
||||
import SEO from '@/components/seo/SEO.astro';
|
||||
---
|
||||
|
||||
<head>
|
||||
<SEO
|
||||
title="Page Title"
|
||||
description="Page description"
|
||||
/>
|
||||
</head>
|
||||
```
|
||||
|
||||
### OG Image
|
||||
|
||||
A static default OG image (`public/og-default.svg`) serves as the social preview for all pages. The path is set via `ogImage` in `src/config/site.config.ts`. To use a custom image for a specific page, pass it as the `image` prop to the layout component.
|
||||
|
||||
---
|
||||
|
||||
## API Routes
|
||||
|
||||
### Contact Form
|
||||
|
||||
**POST** `/api/contact`
|
||||
|
||||
```typescript
|
||||
// Request (FormData)
|
||||
{
|
||||
name: string, // 2-100 chars
|
||||
email: string, // Valid email
|
||||
subject: string, // Required
|
||||
message: string, // 10-5000 chars
|
||||
honeypot: string // Must be empty (spam check)
|
||||
}
|
||||
|
||||
// Response
|
||||
{ success: true }
|
||||
// or
|
||||
{ success: false, errors: { field: ["message"] } }
|
||||
```
|
||||
|
||||
### Newsletter
|
||||
|
||||
**POST** `/api/newsletter`
|
||||
|
||||
```typescript
|
||||
// Request (FormData)
|
||||
{ email: string }
|
||||
|
||||
// Response
|
||||
{ success: true }
|
||||
// or
|
||||
{ success: false, error: "message" }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Deployment
|
||||
|
||||
Configuration files included for major platforms.
|
||||
|
||||
### Vercel (Recommended)
|
||||
|
||||
```bash
|
||||
vercel
|
||||
```
|
||||
|
||||
### Netlify
|
||||
|
||||
```bash
|
||||
netlify deploy --prod
|
||||
```
|
||||
|
||||
### Cloudflare Pages
|
||||
|
||||
```bash
|
||||
wrangler pages deploy dist
|
||||
```
|
||||
|
||||
### Static Export
|
||||
|
||||
Build outputs to `dist/` for any static host:
|
||||
|
||||
```bash
|
||||
pnpm build
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Browser Support
|
||||
|
||||
- Chrome (last 2 versions)
|
||||
- Firefox (last 2 versions)
|
||||
- Safari (last 2 versions)
|
||||
- Edge (last 2 versions)
|
||||
|
||||
---
|
||||
|
||||
## Performance
|
||||
|
||||
Astro Rocket is optimized for Core Web Vitals:
|
||||
|
||||
- **Lighthouse Score**: 95+ across all categories
|
||||
- **Zero JavaScript** by default (islands architecture)
|
||||
- **Optimized fonts** with `font-display: swap`
|
||||
- **Image optimization** via Astro's built-in processing
|
||||
- **Prefetching** for instant page transitions
|
||||
|
||||
---
|
||||
|
||||
## Animations
|
||||
|
||||
Every page in Astro Rocket includes purposeful animations that make the site feel polished and alive. All animations respect the user's `prefers-reduced-motion` setting — they are disabled automatically for users who prefer less motion.
|
||||
|
||||
### Page transitions
|
||||
|
||||
Astro Rocket uses Astro's built-in `<ClientRouter />` (View Transitions API) to animate between pages. Instead of a full browser reload, content fades smoothly from one page to the next. This is enabled globally in `BaseLayout.astro` and requires no per-page configuration.
|
||||
|
||||
### Scroll-triggered animations
|
||||
|
||||
Two components use an `IntersectionObserver` to trigger animations when elements enter the viewport:
|
||||
|
||||
- **Counter animation** — the stats block on the homepage (Years Experience, Projects Delivered, etc.) counts up from zero when it scrolls into view. Each number animates with a cubic ease-out over 1.2 seconds.
|
||||
- **Lighthouse score bars** — the `LighthouseScores` landing component animates its score bars into place as the section becomes visible.
|
||||
|
||||
### Scroll-reactive header
|
||||
|
||||
The floating header changes its appearance as the user scrolls. When the page is at the top, the header is transparent with inverted text. Once the user scrolls past 60px, the header gains a solid background and the text flips to normal colors — all driven by CSS transitions via a `data-scrolled` attribute.
|
||||
|
||||
### Scroll progress bar
|
||||
|
||||
A thin 2px brand-coloured bar on the header edge that grows from left to right as the user scrolls, showing reading progress at a glance. Enable it with two props on the `<Header>` component:
|
||||
|
||||
| Prop | Type | Default | What it does |
|
||||
|------|------|:-------:|--------------|
|
||||
| `showScrollProgress` | `boolean` | `false` | Renders the progress bar |
|
||||
| `scrollProgressPosition` | `'top'` \| `'bottom'` | `'bottom'` | Edge of the header where the bar sits |
|
||||
|
||||
The bar is enabled by default on three page types: the **homepage** (above the floating header), the **blog index**, and **individual blog posts** (both below the solid bar header). Use `scrollProgressPosition="top"` on a floating capsule header and `'bottom'` on a solid bar header. The bar colour always matches `--color-brand-500` and updates instantly when the visitor switches themes.
|
||||
|
||||
### Card hover effects
|
||||
|
||||
Cards throughout the site lift slightly on hover (`-translate-y-1`) and gain a subtle shadow. This is a Tailwind utility applied consistently to all interactive cards.
|
||||
|
||||
### UI micro-animations
|
||||
|
||||
The full animation library is defined in `src/styles/global.css`. These classes are used by components throughout the site:
|
||||
|
||||
| Class | What it does |
|
||||
|-------|-------------|
|
||||
| `animate-fade-in` | Fades an element from transparent to visible (0.5s ease-out) |
|
||||
| `animate-slide-up` | Slides an element up from 12px below while fading in (0.5s ease-out) |
|
||||
| `animate-slide-down` | Slides an element down from 12px above while fading in (0.5s ease-out) |
|
||||
| `animate-dropdown-in` | Slides and scales a dropdown menu into view (0.2s spring) |
|
||||
| `animate-dropdown-out` | Collapses a dropdown menu out of view (0.15s) |
|
||||
| `animate-sheet-up` | Slides a bottom sheet up from off-screen (0.25s spring) |
|
||||
| `animate-menu-down` | Slides the mobile navigation drawer open (0.25s spring) |
|
||||
| `animate-tab-enter` | Crossfades tab panel content when switching tabs |
|
||||
| `animate-toast-in` | Slides a toast notification in from the right (350ms spring) |
|
||||
| `animate-tooltip-in` | Fades and scales a tooltip into view |
|
||||
| `animate-pulse` | Breathing pulse for skeleton loading states |
|
||||
| `animate-spin` | Continuous rotation for loading spinners |
|
||||
| `animate-shake` | Brief shake for error feedback (400ms) |
|
||||
|
||||
Animation delay utilities (`.delay-0` through `.delay-5`, in 50ms steps) let you stagger multiple elements into view.
|
||||
|
||||
---
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome!
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
|
||||
3. Commit your changes (`git commit -m 'Add amazing feature'`)
|
||||
4. Push to the branch (`git push origin feature/amazing-feature`)
|
||||
5. Open a Pull Request
|
||||
|
||||
Please ensure your code passes linting (`pnpm lint`) and type checking (`pnpm check`) before submitting.
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
MIT License — see [LICENSE](LICENSE) for details.
|
||||
|
||||
---
|
||||
|
||||
## Links
|
||||
|
||||
- [Astro Rocket on GitHub](https://github.com/hansmartens68/astro-rocket)
|
||||
- [Velocity — the original theme](https://github.com/southwellmedia/velocity) by [Southwell Media](https://southwellmedia.com)
|
||||
- [Astro Documentation](https://docs.astro.build)
|
||||
- [Tailwind CSS v4](https://tailwindcss.com/docs)
|
||||
|
||||
---
|
||||
|
||||
**Astro Rocket** is designed and maintained by [Hans Martens](https://hansmartens.dev).
|
||||
Built on [Velocity](https://github.com/southwellmedia/velocity) — the original theme by [Southwell Media](https://southwellmedia.com).
|
||||
@@ -1,4 +0,0 @@
|
||||
[ZoneTransfer]
|
||||
ZoneId=3
|
||||
ReferrerUrl=https://pixabay.com/de/photos/z%c3%bcrich-bahngeleise-schienenverkehr-5267550/
|
||||
HostUrl=https://pixabay.com/get/gfaa32ac532b3c750d56712bfe236c8f846493d84e77ff4cc0010cfb183c48b9bee255e9f41145c9ffa3f9e69e65038441a2a75fe27c1bbd2cf145b7c4f6c9fbd64a1160c6356ea324db1047e1d6254ff.jpg?attachment=
|
||||
|
Before Width: | Height: | Size: 4.7 MiB |
@@ -4,9 +4,13 @@ import sitemap from '@astrojs/sitemap';
|
||||
import react from '@astrojs/react';
|
||||
import icon from 'astro-icon';
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
import vercel from '@astrojs/vercel';
|
||||
import netlify from '@astrojs/netlify';
|
||||
|
||||
const isNetlify = process.env.DEPLOY_TARGET === 'netlify';
|
||||
|
||||
export default defineConfig({
|
||||
output: 'static',
|
||||
adapter: isNetlify ? netlify() : vercel(),
|
||||
site: process.env.SITE_URL || 'https://example.com',
|
||||
|
||||
env: {
|
||||
@@ -40,12 +44,8 @@ export default defineConfig({
|
||||
plugins: [tailwindcss()],
|
||||
},
|
||||
|
||||
i18n: {
|
||||
defaultLocale: 'de',
|
||||
locales: ['de', 'fr', 'it', 'en'],
|
||||
routing: {
|
||||
prefixDefaultLocale: false,
|
||||
},
|
||||
security: {
|
||||
checkOrigin: true,
|
||||
},
|
||||
|
||||
markdown: {
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
[ZoneTransfer]
|
||||
ZoneId=3
|
||||
ReferrerUrl=https://pixabay.com/de/photos/uhr-standuhr-pendeluhr-tischuhr-2663147/
|
||||
HostUrl=https://pixabay.com/get/g6392b6a09c1ae47ca7f6a894668ad94b74db35cc46485fb9326e1e7f69905ea3070c907fc9387c66bc70e717311e7a027a9ce9c740dc52c78b5374e9e85f951b2898e1c5087a2b2bdf69e302349d008e.jpg?attachment=
|
||||
@@ -0,0 +1,638 @@
|
||||
{
|
||||
"$schema": "./component-registry.schema.json",
|
||||
"version": "2.0.0",
|
||||
"categories": {
|
||||
"ui": {
|
||||
"name": "UI Components",
|
||||
"description": "Core building blocks - buttons, forms, cards, dialogs",
|
||||
"subcategories": {
|
||||
"form": {
|
||||
"name": "Form",
|
||||
"description": "Form input components - buttons, inputs, selects"
|
||||
},
|
||||
"data-display": {
|
||||
"name": "Data Display",
|
||||
"description": "Components for displaying data - cards, badges, tables"
|
||||
},
|
||||
"feedback": {
|
||||
"name": "Feedback",
|
||||
"description": "User feedback components - alerts, toasts, tooltips"
|
||||
},
|
||||
"overlay": {
|
||||
"name": "Overlay",
|
||||
"description": "Overlay components - dialogs, dropdowns, tabs"
|
||||
},
|
||||
"layout": {
|
||||
"name": "Layout",
|
||||
"description": "Layout components - separators, dividers"
|
||||
},
|
||||
"primitives": {
|
||||
"name": "Primitives",
|
||||
"description": "Fundamental components used across the system"
|
||||
},
|
||||
"content": {
|
||||
"name": "Content",
|
||||
"description": "Content display components"
|
||||
},
|
||||
"marketing": {
|
||||
"name": "Marketing",
|
||||
"description": "Marketing and landing page components"
|
||||
}
|
||||
}
|
||||
},
|
||||
"layout": {
|
||||
"name": "Layout Components",
|
||||
"description": "Page structure components - header, footer, navigation"
|
||||
},
|
||||
"patterns": {
|
||||
"name": "Patterns",
|
||||
"description": "Reusable form and UI patterns"
|
||||
},
|
||||
"hero": {
|
||||
"name": "Hero",
|
||||
"description": "Flexible hero section component"
|
||||
}
|
||||
},
|
||||
"utilities": {
|
||||
"cn": {
|
||||
"name": "cn utility",
|
||||
"description": "Tailwind CSS class merging utility",
|
||||
"files": ["src/lib/cn.ts"],
|
||||
"npm": ["clsx", "tailwind-merge"]
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"button": {
|
||||
"name": "Button",
|
||||
"category": "ui",
|
||||
"subcategory": "form",
|
||||
"files": [
|
||||
"src/components/ui/form/Button/Button.astro",
|
||||
"src/components/ui/form/Button/Button.tsx",
|
||||
"src/components/ui/form/Button/button.variants.ts",
|
||||
"src/components/ui/form/Button/index.ts"
|
||||
],
|
||||
"dependencies": {
|
||||
"utilities": ["cn"],
|
||||
"components": []
|
||||
},
|
||||
"premium": false
|
||||
},
|
||||
"input": {
|
||||
"name": "Input",
|
||||
"category": "ui",
|
||||
"subcategory": "form",
|
||||
"files": [
|
||||
"src/components/ui/form/Input/Input.astro",
|
||||
"src/components/ui/form/Input/Input.tsx",
|
||||
"src/components/ui/form/Input/input.variants.ts",
|
||||
"src/components/ui/form/Input/index.ts"
|
||||
],
|
||||
"dependencies": {
|
||||
"utilities": ["cn"],
|
||||
"components": []
|
||||
},
|
||||
"premium": false
|
||||
},
|
||||
"textarea": {
|
||||
"name": "Textarea",
|
||||
"category": "ui",
|
||||
"subcategory": "form",
|
||||
"files": [
|
||||
"src/components/ui/form/Textarea/Textarea.astro",
|
||||
"src/components/ui/form/Textarea/Textarea.tsx",
|
||||
"src/components/ui/form/Textarea/textarea.variants.ts",
|
||||
"src/components/ui/form/Textarea/index.ts"
|
||||
],
|
||||
"dependencies": {
|
||||
"utilities": ["cn"],
|
||||
"components": []
|
||||
},
|
||||
"premium": false
|
||||
},
|
||||
"select": {
|
||||
"name": "Select",
|
||||
"category": "ui",
|
||||
"subcategory": "form",
|
||||
"files": [
|
||||
"src/components/ui/form/Select/Select.astro",
|
||||
"src/components/ui/form/Select/Select.tsx",
|
||||
"src/components/ui/form/Select/select.variants.ts",
|
||||
"src/components/ui/form/Select/index.ts"
|
||||
],
|
||||
"dependencies": {
|
||||
"utilities": ["cn"],
|
||||
"components": []
|
||||
},
|
||||
"premium": false
|
||||
},
|
||||
"checkbox": {
|
||||
"name": "Checkbox",
|
||||
"category": "ui",
|
||||
"subcategory": "form",
|
||||
"files": [
|
||||
"src/components/ui/form/Checkbox/Checkbox.astro",
|
||||
"src/components/ui/form/Checkbox/Checkbox.tsx",
|
||||
"src/components/ui/form/Checkbox/checkbox.variants.ts",
|
||||
"src/components/ui/form/Checkbox/index.ts"
|
||||
],
|
||||
"dependencies": {
|
||||
"utilities": ["cn"],
|
||||
"components": []
|
||||
},
|
||||
"premium": false
|
||||
},
|
||||
"radio": {
|
||||
"name": "Radio",
|
||||
"category": "ui",
|
||||
"subcategory": "form",
|
||||
"files": [
|
||||
"src/components/ui/form/Radio/Radio.astro",
|
||||
"src/components/ui/form/Radio/Radio.tsx",
|
||||
"src/components/ui/form/Radio/radio.variants.ts",
|
||||
"src/components/ui/form/Radio/index.ts"
|
||||
],
|
||||
"dependencies": {
|
||||
"utilities": ["cn"],
|
||||
"components": []
|
||||
},
|
||||
"premium": false
|
||||
},
|
||||
"switch": {
|
||||
"name": "Switch",
|
||||
"category": "ui",
|
||||
"subcategory": "form",
|
||||
"files": [
|
||||
"src/components/ui/form/Switch/Switch.astro",
|
||||
"src/components/ui/form/Switch/Switch.tsx",
|
||||
"src/components/ui/form/Switch/switch.variants.ts",
|
||||
"src/components/ui/form/Switch/index.ts"
|
||||
],
|
||||
"dependencies": {
|
||||
"utilities": ["cn"],
|
||||
"components": []
|
||||
},
|
||||
"premium": false
|
||||
},
|
||||
"card": {
|
||||
"name": "Card",
|
||||
"category": "ui",
|
||||
"subcategory": "data-display",
|
||||
"files": [
|
||||
"src/components/ui/data-display/Card/Card.astro",
|
||||
"src/components/ui/data-display/Card/Card.tsx",
|
||||
"src/components/ui/data-display/Card/card.variants.ts",
|
||||
"src/components/ui/data-display/Card/index.ts"
|
||||
],
|
||||
"dependencies": {
|
||||
"utilities": ["cn"],
|
||||
"components": []
|
||||
},
|
||||
"premium": false
|
||||
},
|
||||
"badge": {
|
||||
"name": "Badge",
|
||||
"category": "ui",
|
||||
"subcategory": "data-display",
|
||||
"files": [
|
||||
"src/components/ui/data-display/Badge/Badge.astro",
|
||||
"src/components/ui/data-display/Badge/badge.variants.ts",
|
||||
"src/components/ui/data-display/Badge/index.ts"
|
||||
],
|
||||
"dependencies": {
|
||||
"utilities": ["cn"],
|
||||
"components": []
|
||||
},
|
||||
"premium": false
|
||||
},
|
||||
"avatar": {
|
||||
"name": "Avatar",
|
||||
"category": "ui",
|
||||
"subcategory": "data-display",
|
||||
"files": [
|
||||
"src/components/ui/data-display/Avatar/Avatar.astro",
|
||||
"src/components/ui/data-display/Avatar/avatar.variants.ts",
|
||||
"src/components/ui/data-display/Avatar/index.ts"
|
||||
],
|
||||
"dependencies": {
|
||||
"utilities": ["cn"],
|
||||
"components": []
|
||||
},
|
||||
"premium": false
|
||||
},
|
||||
"avatar-group": {
|
||||
"name": "AvatarGroup",
|
||||
"category": "ui",
|
||||
"subcategory": "data-display",
|
||||
"files": [
|
||||
"src/components/ui/data-display/AvatarGroup/AvatarGroup.astro",
|
||||
"src/components/ui/data-display/AvatarGroup/index.ts"
|
||||
],
|
||||
"dependencies": {
|
||||
"utilities": ["cn"],
|
||||
"components": ["avatar"]
|
||||
},
|
||||
"premium": false
|
||||
},
|
||||
"table": {
|
||||
"name": "Table",
|
||||
"category": "ui",
|
||||
"subcategory": "data-display",
|
||||
"files": [
|
||||
"src/components/ui/data-display/Table/Table.astro",
|
||||
"src/components/ui/data-display/Table/index.ts"
|
||||
],
|
||||
"dependencies": {
|
||||
"utilities": ["cn"],
|
||||
"components": []
|
||||
},
|
||||
"premium": false
|
||||
},
|
||||
"pagination": {
|
||||
"name": "Pagination",
|
||||
"category": "ui",
|
||||
"subcategory": "data-display",
|
||||
"files": [
|
||||
"src/components/ui/data-display/Pagination/Pagination.astro",
|
||||
"src/components/ui/data-display/Pagination/pagination.variants.ts",
|
||||
"src/components/ui/data-display/Pagination/index.ts"
|
||||
],
|
||||
"dependencies": {
|
||||
"utilities": ["cn"],
|
||||
"components": ["icon"]
|
||||
},
|
||||
"premium": false
|
||||
},
|
||||
"progress": {
|
||||
"name": "Progress",
|
||||
"category": "ui",
|
||||
"subcategory": "data-display",
|
||||
"files": [
|
||||
"src/components/ui/data-display/Progress/Progress.astro",
|
||||
"src/components/ui/data-display/Progress/progress.variants.ts",
|
||||
"src/components/ui/data-display/Progress/index.ts"
|
||||
],
|
||||
"dependencies": {
|
||||
"utilities": ["cn"],
|
||||
"components": []
|
||||
},
|
||||
"premium": false
|
||||
},
|
||||
"skeleton": {
|
||||
"name": "Skeleton",
|
||||
"category": "ui",
|
||||
"subcategory": "data-display",
|
||||
"files": [
|
||||
"src/components/ui/data-display/Skeleton/Skeleton.astro",
|
||||
"src/components/ui/data-display/Skeleton/skeleton.variants.ts",
|
||||
"src/components/ui/data-display/Skeleton/index.ts"
|
||||
],
|
||||
"dependencies": {
|
||||
"utilities": ["cn"],
|
||||
"components": []
|
||||
},
|
||||
"premium": false
|
||||
},
|
||||
"alert": {
|
||||
"name": "Alert",
|
||||
"category": "ui",
|
||||
"subcategory": "feedback",
|
||||
"files": [
|
||||
"src/components/ui/feedback/Alert/Alert.astro",
|
||||
"src/components/ui/feedback/Alert/alert.variants.ts",
|
||||
"src/components/ui/feedback/Alert/index.ts"
|
||||
],
|
||||
"dependencies": {
|
||||
"utilities": ["cn"],
|
||||
"components": []
|
||||
},
|
||||
"premium": false
|
||||
},
|
||||
"toast": {
|
||||
"name": "Toast",
|
||||
"category": "ui",
|
||||
"subcategory": "feedback",
|
||||
"files": [
|
||||
"src/components/ui/feedback/Toast/Toast.astro",
|
||||
"src/components/ui/feedback/Toast/Toast.tsx",
|
||||
"src/components/ui/feedback/Toast/ToastDemo.tsx",
|
||||
"src/components/ui/feedback/Toast/toast.variants.ts",
|
||||
"src/components/ui/feedback/Toast/index.ts"
|
||||
],
|
||||
"dependencies": {
|
||||
"utilities": ["cn"],
|
||||
"components": []
|
||||
},
|
||||
"premium": false
|
||||
},
|
||||
"tooltip": {
|
||||
"name": "Tooltip",
|
||||
"category": "ui",
|
||||
"subcategory": "feedback",
|
||||
"files": [
|
||||
"src/components/ui/feedback/Tooltip/Tooltip.astro",
|
||||
"src/components/ui/feedback/Tooltip/index.ts"
|
||||
],
|
||||
"dependencies": {
|
||||
"utilities": ["cn"],
|
||||
"components": []
|
||||
},
|
||||
"premium": false
|
||||
},
|
||||
"dialog": {
|
||||
"name": "Dialog",
|
||||
"category": "ui",
|
||||
"subcategory": "overlay",
|
||||
"files": [
|
||||
"src/components/ui/overlay/Dialog/Dialog.astro",
|
||||
"src/components/ui/overlay/Dialog/index.ts"
|
||||
],
|
||||
"dependencies": {
|
||||
"utilities": ["cn"],
|
||||
"components": []
|
||||
},
|
||||
"premium": false
|
||||
},
|
||||
"dropdown": {
|
||||
"name": "Dropdown",
|
||||
"category": "ui",
|
||||
"subcategory": "overlay",
|
||||
"files": [
|
||||
"src/components/ui/overlay/Dropdown/Dropdown.astro",
|
||||
"src/components/ui/overlay/Dropdown/index.ts"
|
||||
],
|
||||
"dependencies": {
|
||||
"utilities": ["cn"],
|
||||
"components": []
|
||||
},
|
||||
"premium": false
|
||||
},
|
||||
"tabs": {
|
||||
"name": "Tabs",
|
||||
"category": "ui",
|
||||
"subcategory": "overlay",
|
||||
"files": [
|
||||
"src/components/ui/overlay/Tabs/Tabs.astro",
|
||||
"src/components/ui/overlay/Tabs/index.ts"
|
||||
],
|
||||
"dependencies": {
|
||||
"utilities": ["cn"],
|
||||
"components": []
|
||||
},
|
||||
"premium": false
|
||||
},
|
||||
"vertical-tabs": {
|
||||
"name": "VerticalTabs",
|
||||
"category": "ui",
|
||||
"subcategory": "overlay",
|
||||
"files": [
|
||||
"src/components/ui/overlay/VerticalTabs/VerticalTabs.astro",
|
||||
"src/components/ui/overlay/VerticalTabs/VerticalTabs.tsx",
|
||||
"src/components/ui/overlay/VerticalTabs/index.ts"
|
||||
],
|
||||
"dependencies": {
|
||||
"utilities": ["cn"],
|
||||
"components": []
|
||||
},
|
||||
"premium": false
|
||||
},
|
||||
"accordion": {
|
||||
"name": "Accordion",
|
||||
"category": "ui",
|
||||
"subcategory": "overlay",
|
||||
"files": [
|
||||
"src/components/ui/overlay/Accordion/Accordion.astro",
|
||||
"src/components/ui/overlay/Accordion/accordion.variants.ts",
|
||||
"src/components/ui/overlay/Accordion/index.ts"
|
||||
],
|
||||
"dependencies": {
|
||||
"utilities": ["cn"],
|
||||
"components": ["icon"]
|
||||
},
|
||||
"premium": false
|
||||
},
|
||||
"separator": {
|
||||
"name": "Separator",
|
||||
"category": "ui",
|
||||
"subcategory": "layout",
|
||||
"files": [
|
||||
"src/components/ui/layout/Separator/Separator.astro",
|
||||
"src/components/ui/layout/Separator/separator.variants.ts",
|
||||
"src/components/ui/layout/Separator/index.ts"
|
||||
],
|
||||
"dependencies": {
|
||||
"utilities": ["cn"],
|
||||
"components": []
|
||||
},
|
||||
"premium": false
|
||||
},
|
||||
"icon": {
|
||||
"name": "Icon",
|
||||
"category": "ui",
|
||||
"subcategory": "primitives",
|
||||
"files": [
|
||||
"src/components/ui/primitives/Icon/Icon.astro",
|
||||
"src/components/ui/primitives/Icon/index.ts"
|
||||
],
|
||||
"dependencies": {
|
||||
"utilities": ["cn"],
|
||||
"components": []
|
||||
},
|
||||
"premium": false
|
||||
},
|
||||
"code-block": {
|
||||
"name": "CodeBlock",
|
||||
"category": "ui",
|
||||
"subcategory": "content",
|
||||
"files": [
|
||||
"src/components/ui/content/CodeBlock/CodeBlock.astro",
|
||||
"src/components/ui/content/CodeBlock/index.ts"
|
||||
],
|
||||
"dependencies": {
|
||||
"utilities": ["cn"],
|
||||
"components": []
|
||||
},
|
||||
"premium": false
|
||||
},
|
||||
"logo": {
|
||||
"name": "Logo",
|
||||
"category": "ui",
|
||||
"subcategory": "marketing",
|
||||
"files": [
|
||||
"src/components/ui/marketing/Logo/Logo.astro",
|
||||
"src/components/ui/marketing/Logo/index.ts"
|
||||
],
|
||||
"dependencies": {
|
||||
"utilities": ["cn"],
|
||||
"components": []
|
||||
},
|
||||
"premium": false
|
||||
},
|
||||
"cta": {
|
||||
"name": "CTA",
|
||||
"category": "ui",
|
||||
"subcategory": "marketing",
|
||||
"files": [
|
||||
"src/components/ui/marketing/CTA/CTA.astro",
|
||||
"src/components/ui/marketing/CTA/index.ts"
|
||||
],
|
||||
"dependencies": {
|
||||
"utilities": ["cn"],
|
||||
"components": ["logo"]
|
||||
},
|
||||
"premium": false
|
||||
},
|
||||
"npm-copy-button": {
|
||||
"name": "NpmCopyButton",
|
||||
"category": "ui",
|
||||
"subcategory": "marketing",
|
||||
"files": [
|
||||
"src/components/ui/marketing/NpmCopyButton/NpmCopyButton.astro",
|
||||
"src/components/ui/marketing/NpmCopyButton/index.ts"
|
||||
],
|
||||
"dependencies": {
|
||||
"utilities": ["cn"],
|
||||
"components": ["icon"]
|
||||
},
|
||||
"premium": false
|
||||
},
|
||||
"social-proof": {
|
||||
"name": "SocialProof",
|
||||
"category": "ui",
|
||||
"subcategory": "marketing",
|
||||
"files": [
|
||||
"src/components/ui/marketing/SocialProof/SocialProof.astro",
|
||||
"src/components/ui/marketing/SocialProof/index.ts"
|
||||
],
|
||||
"dependencies": {
|
||||
"utilities": ["cn"],
|
||||
"components": []
|
||||
},
|
||||
"premium": false
|
||||
},
|
||||
"terminal-demo": {
|
||||
"name": "TerminalDemo",
|
||||
"category": "ui",
|
||||
"subcategory": "marketing",
|
||||
"files": [
|
||||
"src/components/ui/marketing/TerminalDemo/TerminalDemo.tsx",
|
||||
"src/components/ui/marketing/TerminalDemo/index.ts"
|
||||
],
|
||||
"dependencies": {
|
||||
"utilities": ["cn"],
|
||||
"components": []
|
||||
},
|
||||
"premium": false
|
||||
},
|
||||
"contact-form": {
|
||||
"name": "ContactForm",
|
||||
"category": "patterns",
|
||||
"files": ["src/components/patterns/ContactForm.astro"],
|
||||
"dependencies": {
|
||||
"utilities": ["cn"],
|
||||
"components": ["input", "textarea", "button"]
|
||||
},
|
||||
"premium": false
|
||||
},
|
||||
"newsletter-form": {
|
||||
"name": "NewsletterForm",
|
||||
"category": "patterns",
|
||||
"files": ["src/components/patterns/NewsletterForm.astro"],
|
||||
"dependencies": {
|
||||
"utilities": ["cn"],
|
||||
"components": ["input", "button"]
|
||||
},
|
||||
"premium": false
|
||||
},
|
||||
"form-field": {
|
||||
"name": "FormField",
|
||||
"category": "patterns",
|
||||
"files": ["src/components/patterns/FormField.astro"],
|
||||
"dependencies": {
|
||||
"utilities": ["cn"],
|
||||
"components": []
|
||||
},
|
||||
"premium": false
|
||||
},
|
||||
"search-input": {
|
||||
"name": "SearchInput",
|
||||
"category": "patterns",
|
||||
"files": ["src/components/patterns/SearchInput.astro"],
|
||||
"dependencies": {
|
||||
"utilities": ["cn"],
|
||||
"components": ["input"]
|
||||
},
|
||||
"premium": false
|
||||
},
|
||||
"password-input": {
|
||||
"name": "PasswordInput",
|
||||
"category": "patterns",
|
||||
"files": ["src/components/patterns/PasswordInput.astro"],
|
||||
"dependencies": {
|
||||
"utilities": ["cn"],
|
||||
"components": ["input", "icon"]
|
||||
},
|
||||
"premium": false
|
||||
},
|
||||
"stat-card": {
|
||||
"name": "StatCard",
|
||||
"category": "patterns",
|
||||
"files": ["src/components/patterns/StatCard.astro"],
|
||||
"dependencies": {
|
||||
"utilities": ["cn"],
|
||||
"components": ["card", "icon"]
|
||||
},
|
||||
"premium": false
|
||||
},
|
||||
"empty-state": {
|
||||
"name": "EmptyState",
|
||||
"category": "patterns",
|
||||
"files": ["src/components/patterns/EmptyState.astro"],
|
||||
"dependencies": {
|
||||
"utilities": ["cn"],
|
||||
"components": ["icon", "button"]
|
||||
},
|
||||
"premium": false
|
||||
},
|
||||
"hero": {
|
||||
"name": "Hero",
|
||||
"category": "hero",
|
||||
"files": [
|
||||
"src/components/hero/Hero.astro",
|
||||
"src/components/hero/hero.variants.ts",
|
||||
"src/components/hero/index.ts"
|
||||
],
|
||||
"dependencies": {
|
||||
"utilities": ["cn"],
|
||||
"components": []
|
||||
},
|
||||
"premium": false
|
||||
},
|
||||
"header": {
|
||||
"name": "Header",
|
||||
"category": "layout",
|
||||
"files": [
|
||||
"src/components/layout/Header.astro",
|
||||
"src/components/layout/header.variants.ts"
|
||||
],
|
||||
"dependencies": {
|
||||
"utilities": ["cn"],
|
||||
"components": ["button", "icon", "logo"]
|
||||
},
|
||||
"premium": false
|
||||
},
|
||||
"footer": {
|
||||
"name": "Footer",
|
||||
"category": "layout",
|
||||
"files": [
|
||||
"src/components/layout/Footer.astro",
|
||||
"src/components/layout/footer.variants.ts"
|
||||
],
|
||||
"dependencies": {
|
||||
"utilities": ["cn"],
|
||||
"components": ["icon", "logo"]
|
||||
},
|
||||
"premium": false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://github.com/hansmartens68/astro--rocket/component-registry.schema.json",
|
||||
"title": "Astro Rocket Component Registry",
|
||||
"description": "Schema for the Astro Rocket component registry",
|
||||
"type": "object",
|
||||
"required": ["version", "categories", "utilities", "components"],
|
||||
"properties": {
|
||||
"$schema": {
|
||||
"type": "string",
|
||||
"description": "JSON schema reference"
|
||||
},
|
||||
"version": {
|
||||
"type": "string",
|
||||
"pattern": "^\\d+\\.\\d+\\.\\d+$",
|
||||
"description": "Registry version (semver)"
|
||||
},
|
||||
"categories": {
|
||||
"type": "object",
|
||||
"description": "Component categories",
|
||||
"additionalProperties": {
|
||||
"$ref": "#/$defs/category"
|
||||
}
|
||||
},
|
||||
"utilities": {
|
||||
"type": "object",
|
||||
"description": "Shared utility files",
|
||||
"additionalProperties": {
|
||||
"$ref": "#/$defs/utility"
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
"type": "object",
|
||||
"description": "Component definitions",
|
||||
"additionalProperties": {
|
||||
"$ref": "#/$defs/component"
|
||||
}
|
||||
}
|
||||
},
|
||||
"$defs": {
|
||||
"category": {
|
||||
"type": "object",
|
||||
"required": ["name", "description"],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Display name of the category"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "Brief description of the category"
|
||||
}
|
||||
}
|
||||
},
|
||||
"utility": {
|
||||
"type": "object",
|
||||
"required": ["name", "description", "files", "npm"],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Display name of the utility"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "Brief description of the utility"
|
||||
},
|
||||
"files": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "File paths for this utility"
|
||||
},
|
||||
"npm": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "NPM packages required by this utility"
|
||||
}
|
||||
}
|
||||
},
|
||||
"component": {
|
||||
"type": "object",
|
||||
"required": ["name", "category", "files", "dependencies", "premium"],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Display name of the component"
|
||||
},
|
||||
"category": {
|
||||
"type": "string",
|
||||
"description": "Category ID this component belongs to"
|
||||
},
|
||||
"subcategory": {
|
||||
"type": "string",
|
||||
"description": "Subcategory within the main category (e.g., form, data-display, feedback)"
|
||||
},
|
||||
"files": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "File paths for this component"
|
||||
},
|
||||
"dependencies": {
|
||||
"type": "object",
|
||||
"required": ["utilities", "components"],
|
||||
"properties": {
|
||||
"utilities": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Utility IDs this component depends on"
|
||||
},
|
||||
"components": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Component IDs this component depends on"
|
||||
}
|
||||
}
|
||||
},
|
||||
"premium": {
|
||||
"type": "boolean",
|
||||
"description": "Whether this is a premium component"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 1.6 MiB |
|
Before Width: | Height: | Size: 1.7 MiB |
@@ -0,0 +1,30 @@
|
||||
[build]
|
||||
command = "pnpm run build"
|
||||
publish = "dist"
|
||||
|
||||
[build.environment]
|
||||
NODE_VERSION = "22"
|
||||
|
||||
[[headers]]
|
||||
for = "/*"
|
||||
[headers.values]
|
||||
X-Frame-Options = "DENY"
|
||||
X-Content-Type-Options = "nosniff"
|
||||
Referrer-Policy = "strict-origin-when-cross-origin"
|
||||
Strict-Transport-Security = "max-age=31536000; includeSubDomains"
|
||||
|
||||
[[headers]]
|
||||
for = "/fonts/*"
|
||||
[headers.values]
|
||||
Cache-Control = "public, max-age=31536000, immutable"
|
||||
|
||||
[[headers]]
|
||||
for = "/_astro/*"
|
||||
[headers.values]
|
||||
Cache-Control = "public, max-age=31536000, immutable"
|
||||
|
||||
# Redirect rules
|
||||
[[redirects]]
|
||||
from = "/api/*"
|
||||
to = "/.netlify/functions/:splat"
|
||||
status = 200
|
||||
@@ -40,8 +40,10 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/mdx": "5.0.0",
|
||||
"@astrojs/netlify": "^7.0.2",
|
||||
"@astrojs/react": "5.0.0",
|
||||
"@astrojs/sitemap": "^3.7.1",
|
||||
"@astrojs/vercel": "^10.0.0",
|
||||
"@fontsource-variable/jetbrains-mono": "^5.2.8",
|
||||
"@fontsource-variable/manrope": "^5.2.8",
|
||||
"@fontsource-variable/outfit": "^5.2.8",
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"navigationFallback": {
|
||||
"rewrite": "/404.html",
|
||||
"exclude": ["/assets/*", "/fonts/*", "/_astro/*", "*.ico", "*.png", "*.jpg", "*.svg", "*.webp", "*.webmanifest", "*.xml", "*.txt"]
|
||||
},
|
||||
"trailingSlash": "auto",
|
||||
"mimeTypes": {
|
||||
".json": "application/json"
|
||||
},
|
||||
"responseOverrides": {
|
||||
"404": {
|
||||
"rewrite": "/404.html"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M32 16C32 24.8366 24.8366 32 16 32C7.16344 32 0 24.8366 0 16C0 7.16344 7.16344 0 16 0C24.8366 0 32 7.16344 32 16ZM4.5578 16C4.5578 22.3194 9.68065 27.4422 16 27.4422C22.3194 27.4422 27.4422 22.3194 27.4422 16C27.4422 9.68065 22.3194 4.5578 16 4.5578C9.68065 4.5578 4.5578 9.68065 4.5578 16Z" fill="#6200EA"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 421 B |
@@ -1,4 +0,0 @@
|
||||
<svg width="208" height="71" viewBox="0 0 208 71" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M113 16C113 24.8366 105.837 32 97 32C88.1634 32 81 24.8366 81 16C81 7.16344 88.1634 0 97 0C105.837 0 113 7.16344 113 16ZM85.5578 16C85.5578 22.3194 90.6806 27.4422 97 27.4422C103.319 27.4422 108.442 22.3194 108.442 16C108.442 9.68065 103.319 4.5578 97 4.5578C90.6806 4.5578 85.5578 9.68065 85.5578 16Z" fill="#6200EA"/>
|
||||
<path d="M14.5801 37.793L24.3008 63H20.6973L18.4648 57.1992H7.0918L4.85938 63H1.25586L10.9766 37.793H14.5801ZM8.46289 53.5957H17.0762L12.7871 42.4863L8.46289 53.5957ZM48.541 46.793C48.541 50.3086 47.041 52.3125 44.041 52.8047L48.7871 63H44.7969L40.1211 52.9277H32.2285V63H28.625V37.793H42.4238C46.502 37.793 48.541 39.8379 48.541 43.9277V46.793ZM32.2285 49.3242H42.2305C43.1914 49.3242 43.8828 49.1133 44.3047 48.6914C44.7266 48.2695 44.9375 47.5781 44.9375 46.6172V44.1035C44.9375 43.1426 44.7266 42.4512 44.3047 42.0293C43.8828 41.6074 43.1914 41.3965 42.2305 41.3965H32.2285V49.3242ZM67.5781 63L58.1738 44.5254V63H54.5703V37.793H58.5078L68.1055 56.7773L77.7207 37.793H81.6406V63H78.0547V44.5254L68.6504 63H67.5781ZM99.3066 37.793L109.027 63H105.424L103.191 57.1992H91.8184L89.5859 63H85.9824L95.7031 37.793H99.3066ZM93.1895 53.5957H101.803L97.5137 42.4863L93.1895 53.5957ZM133.268 46.793C133.268 50.3086 131.768 52.3125 128.768 52.8047L133.514 63H129.523L124.848 52.9277H116.955V63H113.352V37.793H127.15C131.229 37.793 133.268 39.8379 133.268 43.9277V46.793ZM116.955 49.3242H126.957C127.918 49.3242 128.609 49.1133 129.031 48.6914C129.453 48.2695 129.664 47.5781 129.664 46.6172V44.1035C129.664 43.1426 129.453 42.4512 129.031 42.0293C128.609 41.6074 127.918 41.3965 126.957 41.3965H116.955V49.3242ZM142.9 37.793V63H139.297V37.793H142.9ZM149.387 37.793H152.99V59.3965H167.387V37.793H170.99V59.3965C170.99 59.8887 170.896 60.3574 170.709 60.8027C170.521 61.2363 170.264 61.6172 169.936 61.9453C169.607 62.2734 169.221 62.5312 168.775 62.7188C168.342 62.9062 167.879 63 167.387 63H152.99C152.498 63 152.029 62.9062 151.584 62.7188C151.15 62.5312 150.77 62.2734 150.441 61.9453C150.113 61.6172 149.855 61.2363 149.668 60.8027C149.48 60.3574 149.387 59.8887 149.387 59.3965V37.793ZM190.484 63L181.08 44.5254V63H177.477V37.793H181.414L191.012 56.7773L200.627 37.793H204.547V63H200.961V44.5254L191.557 63H190.484Z" fill="black"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 4.7 MiB |
|
Before Width: | Height: | Size: 1.6 MiB |
|
Before Width: | Height: | Size: 1.7 MiB |
@@ -1,4 +0,0 @@
|
||||
<svg width="248" height="39" viewBox="0 0 248 39" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M32 20C32 28.8366 24.8366 36 16 36C7.16344 36 0 28.8366 0 20C0 11.1634 7.16344 4 16 4C24.8366 4 32 11.1634 32 20ZM4.5578 20C4.5578 26.3194 9.68065 31.4422 16 31.4422C22.3194 31.4422 27.4422 26.3194 27.4422 20C27.4422 13.6806 22.3194 8.5578 16 8.5578C9.68065 8.5578 4.5578 13.6806 4.5578 20Z" fill="#6200EA"/>
|
||||
<path d="M54.5801 5.79297L64.3008 31H60.6973L58.4648 25.1992H47.0918L44.8594 31H41.2559L50.9766 5.79297H54.5801ZM48.4629 21.5957H57.0762L52.7871 10.4863L48.4629 21.5957ZM88.541 14.793C88.541 18.3086 87.041 20.3125 84.041 20.8047L88.7871 31H84.7969L80.1211 20.9277H72.2285V31H68.625V5.79297H82.4238C86.502 5.79297 88.541 7.83789 88.541 11.9277V14.793ZM72.2285 17.3242H82.2305C83.1914 17.3242 83.8828 17.1133 84.3047 16.6914C84.7266 16.2695 84.9375 15.5781 84.9375 14.6172V12.1035C84.9375 11.1426 84.7266 10.4512 84.3047 10.0293C83.8828 9.60742 83.1914 9.39648 82.2305 9.39648H72.2285V17.3242ZM107.578 31L98.1738 12.5254V31H94.5703V5.79297H98.5078L108.105 24.7773L117.721 5.79297H121.641V31H118.055V12.5254L108.65 31H107.578ZM139.307 5.79297L149.027 31H145.424L143.191 25.1992H131.818L129.586 31H125.982L135.703 5.79297H139.307ZM133.189 21.5957H141.803L137.514 10.4863L133.189 21.5957ZM173.268 14.793C173.268 18.3086 171.768 20.3125 168.768 20.8047L173.514 31H169.523L164.848 20.9277H156.955V31H153.352V5.79297H167.15C171.229 5.79297 173.268 7.83789 173.268 11.9277V14.793ZM156.955 17.3242H166.957C167.918 17.3242 168.609 17.1133 169.031 16.6914C169.453 16.2695 169.664 15.5781 169.664 14.6172V12.1035C169.664 11.1426 169.453 10.4512 169.031 10.0293C168.609 9.60742 167.918 9.39648 166.957 9.39648H156.955V17.3242ZM182.9 5.79297V31H179.297V5.79297H182.9ZM189.387 5.79297H192.99V27.3965H207.387V5.79297H210.99V27.3965C210.99 27.8887 210.896 28.3574 210.709 28.8027C210.521 29.2363 210.264 29.6172 209.936 29.9453C209.607 30.2734 209.221 30.5312 208.775 30.7188C208.342 30.9062 207.879 31 207.387 31H192.99C192.498 31 192.029 30.9062 191.584 30.7188C191.15 30.5312 190.77 30.2734 190.441 29.9453C190.113 29.6172 189.855 29.2363 189.668 28.8027C189.48 28.3574 189.387 27.8887 189.387 27.3965V5.79297ZM230.484 31L221.08 12.5254V31H217.477V5.79297H221.414L231.012 24.7773L240.627 5.79297H244.547V31H240.961V12.5254L231.557 31H230.484Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.3 KiB |
@@ -0,0 +1,38 @@
|
||||
<svg width="880" height="260" viewBox="0 0 880 260" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<radialGradient id="glow" cx="440" cy="90" r="280" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0%" stop-color="#10b981" stop-opacity="0.18"/>
|
||||
<stop offset="100%" stop-color="#10b981" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
</defs>
|
||||
|
||||
<!-- Background -->
|
||||
<rect width="880" height="260" fill="#0d1117"/>
|
||||
<rect width="880" height="260" fill="url(#glow)"/>
|
||||
|
||||
<!-- Top accent stripe -->
|
||||
<rect width="880" height="4" fill="#10b981"/>
|
||||
|
||||
<!-- Corner marks -->
|
||||
<path d="M 30 50 L 30 30 L 50 30" stroke="#10b981" stroke-width="1.5" fill="none" stroke-opacity="0.5"/>
|
||||
<path d="M 830 30 L 850 30 L 850 50" stroke="#10b981" stroke-width="1.5" fill="none" stroke-opacity="0.5"/>
|
||||
<path d="M 30 210 L 30 230 L 50 230" stroke="#10b981" stroke-width="1.5" fill="none" stroke-opacity="0.5"/>
|
||||
<path d="M 830 230 L 850 230 L 850 210" stroke="#10b981" stroke-width="1.5" fill="none" stroke-opacity="0.5"/>
|
||||
|
||||
<!-- Lucide Rocket — scale(5), visual centre at (440, 90) -->
|
||||
<!-- Icon x≈2.5–22, y≈2–21 in 24×24; at scale 5: translate(379, 35) -->
|
||||
<g transform="translate(379, 35) scale(5)"
|
||||
stroke="#10b981" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" fill="none">
|
||||
<path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5"/>
|
||||
<path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09"/>
|
||||
<path d="M9 12a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.4 22.4 0 0 1-4 2z"/>
|
||||
<path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 .05 5 .05"/>
|
||||
</g>
|
||||
|
||||
<!-- Wordmark — baseline y=218, icon bottom ≈ y=140, gap ≈ 33 px -->
|
||||
<text x="440" y="218"
|
||||
text-anchor="middle"
|
||||
font-family="system-ui, -apple-system, 'Segoe UI', Helvetica, Arial, sans-serif"
|
||||
font-weight="800" font-size="62" letter-spacing="-1.5"
|
||||
fill="white">Astro <tspan fill="#10b981">Rocket</tspan></text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
@@ -109,7 +109,7 @@ const readingTime = Math.max(1, Math.ceil(estimatedWords / wordsPerMinute));
|
||||
{svgSlug ? (
|
||||
<div
|
||||
class="relative overflow-hidden rounded-xl border border-border shadow-2xl
|
||||
bg-gradient-to-br from-brand-100/50 to-brand-50/30 dark:from-brand-900/50 dark:to-brand-600/30"
|
||||
bg-gradient-to-br from-brand-100/50 to-brand-50/30 dark:from-brand-900/50 dark:to-brand-800/30"
|
||||
style="color: var(--brand-500);"
|
||||
>
|
||||
<BlogImageSVG slug={svgSlug} title={imageAlt || title} />
|
||||
|
||||
@@ -39,7 +39,7 @@ const readingTime = Math.max(1, Math.ceil(estimatedWords / wordsPerMinute));
|
||||
<a href={href} class="block">
|
||||
<div
|
||||
class="relative mb-4 overflow-hidden rounded-md
|
||||
bg-background-secondary bg-gradient-to-br from-brand-100/65 to-transparent dark:from-brand-900/60 dark:to-brand-600/25"
|
||||
bg-background-secondary bg-gradient-to-br from-brand-100/65 to-transparent dark:from-brand-900/60 dark:to-brand-800/25"
|
||||
style="color: var(--brand-500);"
|
||||
>
|
||||
{svgSlug ? (
|
||||
|
||||
@@ -44,7 +44,7 @@ const svgContent = svgs[`/src/assets/blog/${slug}.svg`] ?? '';
|
||||
/* ── Dark mode ───────────────────────────────────────────────────────────
|
||||
Deep background with all brand colors at full opacity — vivid, not faded.
|
||||
── */
|
||||
:global(html.dark) .svg-host :global(.bg) { fill: var(--brand-600); }
|
||||
:global(html.dark) .svg-host :global(.bg) { fill: var(--brand-800); }
|
||||
:global(html.dark) .svg-host :global(.ico) { stroke: var(--brand-200); }
|
||||
:global(html.dark) .svg-host :global(.txt) { fill: var(--brand-50); }
|
||||
:global(html.dark) .svg-host :global(.ln) { stroke: var(--brand-300); stroke-opacity: 0.5; }
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
---
|
||||
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 { Image } from 'astro:assets';
|
||||
import aboutPhoto from '@/assets/about_photo.jpg';
|
||||
import { useTranslations } from '@/i18n/utils';
|
||||
import type { Locale } from '@/i18n/ui';
|
||||
|
||||
interface Props {
|
||||
locale: Locale;
|
||||
}
|
||||
|
||||
const { locale } = Astro.props;
|
||||
const t = useTranslations(locale);
|
||||
---
|
||||
|
||||
<PageLayout
|
||||
title={`${t('about.title.pre')}${t('about.title.accent')} — Armarium`}
|
||||
description={t('about.desc')}
|
||||
locale={locale}
|
||||
>
|
||||
<Hero layout="centered" size="sm">
|
||||
<h1 slot="title">
|
||||
<span class="text-foreground [-webkit-text-fill-color:currentColor]">{t('about.title.pre')}</span>
|
||||
<span class="text-brand-500 [-webkit-text-fill-color:var(--color-brand-500)]">{t('about.title.accent')}</span>
|
||||
</h1>
|
||||
<p slot="description">{t('about.desc')}</p>
|
||||
</Hero>
|
||||
|
||||
<!-- Team -->
|
||||
<section class="bg-white dark:bg-gray-900 border-t border-border">
|
||||
<div class="grid gap-16 py-8 px-4 mx-auto max-w-screen-xl lg:grid-cols-2 lg:py-16 lg:px-6">
|
||||
<div class="text-foreground-muted sm:text-lg">
|
||||
<h2 class="mb-4 text-4xl tracking-tight font-extrabold text-foreground dark:text-white">{t('about.team.title')}</h2>
|
||||
<p class="mb-2 md:text-lg">{t('about.team.desc1')}</p>
|
||||
<p class="font-light md:text-lg">{t('about.team.desc2')}</p>
|
||||
</div>
|
||||
<div class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<div class="flex flex-col items-center pb-8 sm:flex-row">
|
||||
<Image src={aboutPhoto} alt="Daniel Krähenbühl" class="mx-auto mb-4 w-36 h-36 rounded-full object-cover sm:ml-0 sm:mr-6" />
|
||||
<div class="text-center sm:text-left">
|
||||
<h3 class="text-xl font-bold tracking-tight text-foreground dark:text-white">Daniel Krähenbühl</h3>
|
||||
<span class="text-foreground-muted dark:text-gray-400">{t('about.founder.role')}</span>
|
||||
<p class="mt-3 mb-4 max-w-sm font-light text-foreground-muted dark:text-gray-400">{t('about.founder.bio')}</p>
|
||||
<ul class="flex justify-center space-x-4 sm:justify-start">
|
||||
<li>
|
||||
<a href="https://www.linkedin.com/company/armarium-suite" target="_blank" rel="noopener noreferrer" aria-label="LinkedIn" class="text-foreground-muted hover:text-foreground dark:hover:text-white">
|
||||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true"><path d="M19 0h-14c-2.761 0-5 2.239-5 5v14c0 2.761 2.239 5 5 5h14c2.762 0 5-2.239 5-5v-14c0-2.761-2.238-5-5-5zm-11 19h-3v-11h3v11zm-1.5-12.268c-.966 0-1.75-.79-1.75-1.764s.784-1.764 1.75-1.764 1.75.79 1.75 1.764-.783 1.764-1.75 1.764zm13.5 12.268h-3v-5.604c0-3.368-4-3.113-4 0v5.604h-3v-11h3v1.765c1.396-2.586 7-2.777 7 2.476v6.759z"/></svg>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- FAQ -->
|
||||
<section class="bg-white dark:bg-gray-900 border-t border-border">
|
||||
<div class="py-8 px-4 mx-auto max-w-screen-xl sm:py-16 lg:px-6">
|
||||
<h2 class="mb-6 lg:mb-8 text-3xl lg:text-4xl tracking-tight font-extrabold text-center text-foreground dark:text-white">{t('about.faq.title')}</h2>
|
||||
<div class="mx-auto max-w-screen-md divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{([1,2,3,4] as const).map((n) => (
|
||||
<details class="group">
|
||||
<summary class="flex justify-between items-center py-5 w-full font-medium text-left text-foreground cursor-pointer list-none">
|
||||
<span>{t(`about.faq.q${n}` as any)}</span>
|
||||
<svg class="w-6 h-6 shrink-0 transition-transform group-open:rotate-180" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd"/></svg>
|
||||
</summary>
|
||||
<div class="py-5 border-b border-gray-200 dark:border-gray-700">
|
||||
<p class="text-foreground-muted dark:text-gray-400">{t(`about.faq.a${n}` as any)}</p>
|
||||
</div>
|
||||
</details>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- CTA -->
|
||||
<section class="py-[var(--space-section-md)] bg-background">
|
||||
<div class="mx-auto max-w-2xl px-6 text-center" data-reveal>
|
||||
<h2 class="font-display text-4xl font-bold text-foreground mb-4 text-balance">{t('about.cta.title')}</h2>
|
||||
<p class="text-lg text-foreground-muted mb-8 text-balance">{t('about.cta.desc')}</p>
|
||||
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<Button size="lg" href="https://app.armarium.ch/register">
|
||||
{t('cta.register')}
|
||||
<Icon name="arrow-right" size="sm" />
|
||||
</Button>
|
||||
<Button size="lg" variant="outline" href={locale === 'de' ? '/' : `/${locale}/`}>
|
||||
{t('about.cta.back')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</PageLayout>
|
||||
@@ -1,94 +0,0 @@
|
||||
---
|
||||
import PageLayout from '@/layouts/PageLayout.astro';
|
||||
import { Hero } from '@/components/hero';
|
||||
import Button from '@/components/ui/form/Button/Button.astro';
|
||||
import Icon from '@/components/ui/primitives/Icon/Icon.astro';
|
||||
import BlogImageSVG from '@/components/blog/BlogImageSVG.astro';
|
||||
import { Image } from 'astro:assets';
|
||||
import { getCollection } from 'astro:content';
|
||||
import { formatDate } 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 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 getPostUrl = (postId: string) => {
|
||||
const slug = postId.replace(/^[a-z]{2}\//, '');
|
||||
return `/blog/${slug}`;
|
||||
};
|
||||
---
|
||||
|
||||
<PageLayout
|
||||
title={`${t('blog.title')} — Armarium`}
|
||||
description={t('blog.desc')}
|
||||
showScrollProgress
|
||||
locale={locale}
|
||||
>
|
||||
<Hero layout="centered" size="sm">
|
||||
<h1 slot="title">{t('blog.title')}</h1>
|
||||
<p slot="description">{t('blog.desc')}</p>
|
||||
</Hero>
|
||||
|
||||
<section class="bg-white dark:bg-gray-900 border-t border-border">
|
||||
<div class="py-8 px-4 mx-auto max-w-screen-xl lg:py-16 lg:px-6">
|
||||
<div class="grid gap-8 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{posts.map((post) => (
|
||||
<article class="p-4 bg-white rounded-lg border border-gray-200 shadow-md dark:bg-gray-800 dark:border-gray-700">
|
||||
<a href={getPostUrl(post.id)} class="block mb-5 rounded-lg overflow-hidden">
|
||||
{post.data.svgSlug ? (
|
||||
<BlogImageSVG slug={post.data.svgSlug} title={post.data.title} />
|
||||
) : post.data.image ? (
|
||||
<Image src={post.data.image} alt={post.data.imageAlt ?? post.data.title} class="w-full h-48 object-cover rounded-lg" />
|
||||
) : (
|
||||
<div class="w-full h-48 bg-gradient-to-br from-brand-500/20 to-brand-500/5 rounded-lg" />
|
||||
)}
|
||||
</a>
|
||||
{post.data.tags[0] && (
|
||||
<span class="bg-brand-500/10 text-brand-500 text-xs font-semibold mr-2 px-2.5 py-0.5 rounded">
|
||||
{post.data.tags[0]}
|
||||
</span>
|
||||
)}
|
||||
<h2 class="my-2 text-2xl font-bold tracking-tight text-foreground dark:text-white">
|
||||
<a href={getPostUrl(post.id)} class="hover:text-brand-500 transition-colors">{post.data.title}</a>
|
||||
</h2>
|
||||
<p class="mb-4 font-light text-foreground-muted dark:text-gray-400">{post.data.description}</p>
|
||||
<div class="flex items-center space-x-4">
|
||||
<div class="w-10 h-10 rounded-full bg-brand-500/15 flex items-center justify-center text-brand-500 font-bold text-sm shrink-0">
|
||||
{post.data.author?.charAt(0) ?? 'A'}
|
||||
</div>
|
||||
<div class="font-medium text-foreground dark:text-white">
|
||||
<div>{post.data.author ?? 'Team'}</div>
|
||||
<div class="text-sm font-normal text-foreground-muted dark:text-gray-400">{formatDate(post.data.publishedAt)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- CTA -->
|
||||
<section class="py-[var(--space-section-md)] bg-background border-t border-border">
|
||||
<div class="mx-auto max-w-2xl px-6 text-center" data-reveal>
|
||||
<h2 class="font-display text-4xl font-bold text-foreground mb-4 text-balance">{t('cta.title')}</h2>
|
||||
<p class="text-lg text-foreground-muted mb-8 text-balance">{t('cta.desc')}</p>
|
||||
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<Button size="lg" href="https://app.armarium.ch/register">
|
||||
{t('cta.register')}
|
||||
<Icon name="arrow-right" size="sm" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</PageLayout>
|
||||
@@ -1,66 +0,0 @@
|
||||
---
|
||||
import PageLayout from '@/layouts/PageLayout.astro';
|
||||
import { Hero } from '@/components/hero';
|
||||
import siteConfig from '@/config/site.config';
|
||||
import { useTranslations } from '@/i18n/utils';
|
||||
import type { Locale } from '@/i18n/ui';
|
||||
|
||||
interface Props {
|
||||
locale: Locale;
|
||||
}
|
||||
|
||||
const { locale } = Astro.props;
|
||||
const t = useTranslations(locale);
|
||||
---
|
||||
|
||||
<PageLayout
|
||||
title={`${t('contact.badge')} — Armarium`}
|
||||
description={t('contact.desc')}
|
||||
locale={locale}
|
||||
>
|
||||
<Hero layout="centered" size="sm">
|
||||
<h1 slot="title">
|
||||
{t('contact.title').replace('.', '')} <span class="text-brand-500">{t('contact.title').slice(-1)}</span>
|
||||
</h1>
|
||||
<p slot="description">{t('contact.desc')}</p>
|
||||
</Hero>
|
||||
|
||||
<section class="bg-white dark:bg-gray-900 border-t border-border">
|
||||
<div class="py-16 px-4 mx-auto max-w-screen-xl sm:py-24 lg:px-6">
|
||||
<form action="#" class="grid grid-cols-1 gap-8 p-6 mx-auto mb-16 max-w-screen-md bg-white rounded-lg border border-gray-200 shadow-sm lg:mb-28 dark:bg-gray-800 dark:border-gray-700 sm:grid-cols-2">
|
||||
<div>
|
||||
<label for="first-name" class="block mb-2 text-sm font-medium text-foreground dark:text-gray-300">First Name</label>
|
||||
<input type="text" id="first-name" class="block p-3 w-full text-sm text-foreground bg-gray-50 rounded-lg border border-gray-300 shadow-sm focus:ring-brand-500 focus:border-brand-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white" placeholder="Bonnie" required>
|
||||
</div>
|
||||
<div>
|
||||
<label for="last-name" class="block mb-2 text-sm font-medium text-foreground dark:text-gray-300">Last Name</label>
|
||||
<input type="text" id="last-name" class="block p-3 w-full text-sm text-foreground bg-gray-50 rounded-lg border border-gray-300 shadow-sm focus:ring-brand-500 focus:border-brand-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white" placeholder="Green" required>
|
||||
</div>
|
||||
<div>
|
||||
<label for="email" class="block mb-2 text-sm font-medium text-foreground dark:text-gray-300">Your email</label>
|
||||
<input type="email" id="email" class="shadow-sm bg-gray-50 border border-gray-300 text-foreground text-sm rounded-lg focus:ring-brand-500 focus:border-brand-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white" placeholder={`name@${siteConfig.url?.replace(/^https?:\/\//, '') ?? 'example.com'}`} required>
|
||||
</div>
|
||||
<div>
|
||||
<label for="phone-number" class="block mb-2 text-sm font-medium text-foreground dark:text-gray-300">Phone Number</label>
|
||||
<input type="tel" id="phone-number" class="block p-3 w-full text-sm text-foreground bg-gray-50 rounded-lg border border-gray-300 shadow-sm focus:ring-brand-500 focus:border-brand-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white" placeholder="+41 79 123 45 67">
|
||||
</div>
|
||||
<div class="sm:col-span-2">
|
||||
<label for="message" class="block mb-2 text-sm font-medium text-foreground dark:text-gray-400">Your message</label>
|
||||
<textarea id="message" rows="6" class="block p-2.5 w-full text-sm text-foreground bg-gray-50 rounded-lg shadow-sm border border-gray-300 focus:ring-brand-500 focus:border-brand-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white" placeholder="Leave a comment..."></textarea>
|
||||
<p class="mt-4 text-sm text-foreground-muted">By submitting this form you agree to our <a href={t('footer.privacy.href')} class="text-brand-500 hover:underline">terms and conditions</a> and our <a href={t('footer.privacy.href')} class="text-brand-500 hover:underline">privacy policy</a>.</p>
|
||||
</div>
|
||||
<button type="submit" class="py-3 px-5 text-sm font-medium text-center text-white rounded-lg bg-brand-500 sm:w-fit hover:bg-brand-600 focus:ring-4 focus:outline-none focus:ring-brand-300 transition-colors">Send message</button>
|
||||
</form>
|
||||
|
||||
<div class="flex justify-center">
|
||||
<div class="text-center">
|
||||
<div class="flex justify-center items-center mx-auto mb-4 w-10 h-10 bg-gray-100 rounded-lg dark:bg-gray-800 lg:h-16 lg:w-16">
|
||||
<svg class="w-5 h-5 text-foreground-muted lg:w-8 lg:h-8" fill="currentColor" viewBox="0 0 20 20"><path d="M2.003 5.884L10 9.882l7.997-3.998A2 2 0 0016 4H4a2 2 0 00-1.997 1.884z"/><path d="M18 8.118l-8 4-8-4V14a2 2 0 002 2h12a2 2 0 002-2V8.118z"/></svg>
|
||||
</div>
|
||||
<p class="mb-2 text-xl font-bold text-foreground dark:text-white">Email us:</p>
|
||||
<a href="mailto:info@armarium.ch" class="font-semibold text-brand-500 hover:underline">info@armarium.ch</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</PageLayout>
|
||||
@@ -1,128 +0,0 @@
|
||||
---
|
||||
import PageLayout from '@/layouts/PageLayout.astro';
|
||||
import Button from '@/components/ui/form/Button/Button.astro';
|
||||
import Icon from '@/components/ui/primitives/Icon/Icon.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<string, string> = {
|
||||
'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');
|
||||
---
|
||||
|
||||
<PageLayout
|
||||
title={`${t('nav.features')} — Armarium`}
|
||||
description={t('features.description')}
|
||||
locale={locale}
|
||||
>
|
||||
<Hero layout="centered" size="sm">
|
||||
<h1 slot="title">
|
||||
{t('features.title').split(' ').slice(0, -1).join(' ')} <span class="text-brand-500 [-webkit-text-fill-color:var(--color-brand-500)]">{t('features.title').split(' ').slice(-1)[0]}</span>
|
||||
</h1>
|
||||
<p slot="description">{t('features.description')}</p>
|
||||
</Hero>
|
||||
|
||||
<!-- Feature cards -->
|
||||
<section class="bg-white dark:bg-gray-900 border-t border-border">
|
||||
<div class="py-8 px-4 mx-auto max-w-screen-xl sm:py-16 lg:px-6">
|
||||
<div class="mx-auto max-w-screen-md text-center mb-8 lg:mb-16">
|
||||
<h2 class="mb-4 text-4xl tracking-tight font-extrabold text-foreground dark:text-white">Secure platform, secure data</h2>
|
||||
<p class="font-light text-foreground-muted dark:text-gray-400 sm:text-xl">Here at Flowbite we focus on markets where technology, innovation, and capital can unlock long-term value and drive economic growth.</p>
|
||||
</div>
|
||||
<div class="space-y-8 md:grid md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 md:gap-8 xl:gap-8 md:space-y-0">
|
||||
<div class="p-6 bg-white rounded shadow dark:bg-gray-800">
|
||||
<div class="flex justify-center items-center mb-4 w-10 h-10 rounded bg-brand-500/15 lg:h-12 lg:w-12 dark:bg-brand-500/20">
|
||||
<svg class="w-5 h-5 text-brand-500 lg:w-6 lg:h-6 dark:text-brand-400" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M3 3a1 1 0 000 2v8a2 2 0 002 2h2.586l-1.293 1.293a1 1 0 101.414 1.414L10 15.414l2.293 2.293a1 1 0 001.414-1.414L12.414 15H15a2 2 0 002-2V5a1 1 0 100-2H3zm11.707 4.707a1 1 0 00-1.414-1.414L10 9.586 8.707 8.293a1 1 0 00-1.414 0l-2 2a1 1 0 101.414 1.414L8 10.414l1.293 1.293a1 1 0 001.414 0l4-4z" clip-rule="evenodd"></path></svg>
|
||||
</div>
|
||||
<h3 class="mb-2 text-xl font-bold dark:text-white">Marketing</h3>
|
||||
<p class="font-light text-foreground-muted dark:text-gray-400">Plan it, create it, launch it. Collaborate seamlessly with all the organization and hit your marketing goals every month with our marketing plan.</p>
|
||||
</div>
|
||||
<div class="p-6 bg-white rounded shadow dark:bg-gray-800">
|
||||
<div class="flex justify-center items-center mb-4 w-10 h-10 rounded bg-brand-500/15 lg:h-12 lg:w-12 dark:bg-brand-500/20">
|
||||
<svg class="w-5 h-5 text-brand-500 lg:w-6 lg:h-6 dark:text-brand-400" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M10.394 2.08a1 1 0 00-.788 0l-7 3a1 1 0 000 1.84L5.25 8.051a.999.999 0 01.356-.257l4-1.714a1 1 0 11.788 1.838L7.667 9.088l1.94.831a1 1 0 00.787 0l7-3a1 1 0 000-1.838l-7-3zM3.31 9.397L5 10.12v4.102a8.969 8.969 0 00-1.05-.174 1 1 0 01-.89-.89 11.115 11.115 0 01.25-3.762zM9.3 16.573A9.026 9.026 0 007 14.935v-3.957l1.818.78a3 3 0 002.364 0l5.508-2.361a11.026 11.026 0 01.25 3.762 1 1 0 01-.89.89 8.968 8.968 0 00-5.35 2.524 1 1 0 01-1.4 0zM6 18a1 1 0 001-1v-2.065a8.935 8.935 0 00-2-.712V17a1 1 0 001 1z"></path></svg>
|
||||
</div>
|
||||
<h3 class="mb-2 text-xl font-bold dark:text-white">Legal</h3>
|
||||
<p class="font-light text-foreground-muted dark:text-gray-400">Protect your organization, devices and stay compliant with our structured workflows and custom permissions made for you.</p>
|
||||
</div>
|
||||
<div class="p-6 bg-white rounded shadow dark:bg-gray-800">
|
||||
<div class="flex justify-center items-center mb-4 w-10 h-10 rounded bg-brand-500/15 lg:h-12 lg:w-12 dark:bg-brand-500/20">
|
||||
<svg class="w-5 h-5 text-brand-500 lg:w-6 lg:h-6 dark:text-brand-400" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M6 6V5a3 3 0 013-3h2a3 3 0 013 3v1h2a2 2 0 012 2v3.57A22.952 22.952 0 0110 13a22.95 22.95 0 01-8-1.43V8a2 2 0 012-2h2zm2-1a1 1 0 011-1h2a1 1 0 011 1v1H8V5zm1 5a1 1 0 011-1h.01a1 1 0 110 2H10a1 1 0 01-1-1z" clip-rule="evenodd"></path><path d="M2 13.692V16a2 2 0 002 2h12a2 2 0 002-2v-2.308A24.974 24.974 0 0110 15c-2.796 0-5.487-.46-8-1.308z"></path></svg>
|
||||
</div>
|
||||
<h3 class="mb-2 text-xl font-bold dark:text-white">Business Automation</h3>
|
||||
<p class="font-light text-foreground-muted dark:text-gray-400">Auto-assign tasks, send Slack messages, and much more. Now power up with hundreds of new templates to help you get started.</p>
|
||||
</div>
|
||||
<div class="p-6 bg-white rounded shadow dark:bg-gray-800">
|
||||
<div class="flex justify-center items-center mb-4 w-10 h-10 rounded bg-brand-500/15 lg:h-12 lg:w-12 dark:bg-brand-500/20">
|
||||
<svg class="w-5 h-5 text-brand-500 lg:w-6 lg:h-6 dark:text-brand-400" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M8.433 7.418c.155-.103.346-.196.567-.267v1.698a2.305 2.305 0 01-.567-.267C8.07 8.34 8 8.114 8 8c0-.114.07-.34.433-.582zM11 12.849v-1.698c.22.071.412.164.567.267.364.243.433.468.433.582 0 .114-.07.34-.433.582a2.305 2.305 0 01-.567.267z"></path><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-13a1 1 0 10-2 0v.092a4.535 4.535 0 00-1.676.662C6.602 6.234 6 7.009 6 8c0 .99.602 1.765 1.324 2.246.48.32 1.054.545 1.676.662v1.941c-.391-.127-.68-.317-.843-.504a1 1 0 10-1.51 1.31c.562.649 1.413 1.076 2.353 1.253V15a1 1 0 102 0v-.092a4.535 4.535 0 001.676-.662C13.398 13.766 14 12.991 14 12c0-.99-.602-1.765-1.324-2.246A4.535 4.535 0 0011 9.092V7.151c.391.127.68.317.843.504a1 1 0 101.511-1.31c-.563-.649-1.413-1.076-2.354-1.253V5z" clip-rule="evenodd"></path></svg>
|
||||
</div>
|
||||
<h3 class="mb-2 text-xl font-bold dark:text-white">Finance</h3>
|
||||
<p class="font-light text-foreground-muted dark:text-gray-400">Audit-proof software built for critical financial operations like month-end close and quarterly budgeting.</p>
|
||||
</div>
|
||||
<div class="p-6 bg-white rounded shadow dark:bg-gray-800">
|
||||
<div class="flex justify-center items-center mb-4 w-10 h-10 rounded bg-brand-500/15 lg:h-12 lg:w-12 dark:bg-brand-500/20">
|
||||
<svg class="w-5 h-5 text-brand-500 lg:w-6 lg:h-6 dark:text-brand-400" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M7 3a1 1 0 000 2h6a1 1 0 100-2H7zM4 7a1 1 0 011-1h10a1 1 0 110 2H5a1 1 0 01-1-1zM2 11a2 2 0 012-2h12a2 2 0 012 2v4a2 2 0 01-2 2H4a2 2 0 01-2-2v-4z"></path></svg>
|
||||
</div>
|
||||
<h3 class="mb-2 text-xl font-bold dark:text-white">Enterprise Design</h3>
|
||||
<p class="font-light text-foreground-muted dark:text-gray-400">Craft beautiful, delightful experiences for both marketing and product with real cross-company collaboration.</p>
|
||||
</div>
|
||||
<div class="p-6 bg-white rounded shadow dark:bg-gray-800">
|
||||
<div class="flex justify-center items-center mb-4 w-10 h-10 rounded bg-brand-500/15 lg:h-12 lg:w-12 dark:bg-brand-500/20">
|
||||
<svg class="w-5 h-5 text-brand-500 lg:w-6 lg:h-6 dark:text-brand-400" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M11.49 3.17c-.38-1.56-2.6-1.56-2.98 0a1.532 1.532 0 01-2.286.948c-1.372-.836-2.942.734-2.106 2.106.54.886.061 2.042-.947 2.287-1.561.379-1.561 2.6 0 2.978a1.532 1.532 0 01.947 2.287c-.836 1.372.734 2.942 2.106 2.106a1.532 1.532 0 012.287.947c.379 1.561 2.6 1.561 2.978 0a1.533 1.533 0 012.287-.947c1.372.836 2.942-.734 2.106-2.106a1.533 1.533 0 01.947-2.287c1.561-.379 1.561-2.6 0-2.978a1.532 1.532 0 01-.947-2.287c.836-1.372-.734-2.942-2.106-2.106a1.532 1.532 0 01-2.287-.947zM10 13a3 3 0 100-6 3 3 0 000 6z" clip-rule="evenodd"></path></svg>
|
||||
</div>
|
||||
<h3 class="mb-2 text-xl font-bold dark:text-white">Operations</h3>
|
||||
<p class="font-light text-foreground-muted dark:text-gray-400">Keep your company's lights on with customizable, iterative, and structured workflows built for all efficient teams and individual.</p>
|
||||
</div>
|
||||
<div class="p-6 bg-white rounded shadow dark:bg-gray-800">
|
||||
<div class="flex justify-center items-center mb-4 w-10 h-10 rounded bg-brand-500/15 lg:h-12 lg:w-12 dark:bg-brand-500/20">
|
||||
<svg class="w-5 h-5 text-brand-500 lg:w-6 lg:h-6 dark:text-brand-400" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path d="M7 3a1 1 0 000 2h6a1 1 0 100-2H7zM4 7a1 1 0 011-1h10a1 1 0 110 2H5a1 1 0 01-1-1zM2 11a2 2 0 012-2h12a2 2 0 012 2v4a2 2 0 01-2 2H4a2 2 0 01-2-2v-4z"></path></svg>
|
||||
</div>
|
||||
<h3 class="mb-2 text-xl font-bold dark:text-white">Enterprise Design</h3>
|
||||
<p class="font-light text-foreground-muted dark:text-gray-400">Craft beautiful, delightful experiences for both marketing and product with real cross-company collaboration.</p>
|
||||
</div>
|
||||
<div class="p-6 bg-white rounded shadow dark:bg-gray-800">
|
||||
<div class="flex justify-center items-center mb-4 w-10 h-10 rounded bg-brand-500/15 lg:h-12 lg:w-12 dark:bg-brand-500/20">
|
||||
<svg class="w-5 h-5 text-brand-500 lg:w-6 lg:h-6 dark:text-brand-400" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M11.49 3.17c-.38-1.56-2.6-1.56-2.98 0a1.532 1.532 0 01-2.286.948c-1.372-.836-2.942.734-2.106 2.106.54.886.061 2.042-.947 2.287-1.561.379-1.561 2.6 0 2.978a1.532 1.532 0 01.947 2.287c-.836 1.372.734 2.942 2.106 2.106a1.532 1.532 0 012.287.947c.379 1.561 2.6 1.561 2.978 0a1.533 1.533 0 012.287-.947c1.372.836 2.942-.734 2.106-2.106a1.533 1.533 0 01.947-2.287c1.561-.379 1.561-2.6 0-2.978a1.532 1.532 0 01-.947-2.287c.836-1.372-.734-2.942-2.106-2.106a1.532 1.532 0 01-2.287-.947zM10 13a3 3 0 100-6 3 3 0 000 6z" clip-rule="evenodd"></path></svg>
|
||||
</div>
|
||||
<h3 class="mb-2 text-xl font-bold dark:text-white">Operations</h3>
|
||||
<p class="font-light text-foreground-muted dark:text-gray-400">Keep your company's lights on with customizable, iterative, and structured workflows built for all efficient teams and individual.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- CTA -->
|
||||
<section class="py-[var(--space-section-md)] bg-background border-t border-border">
|
||||
<div class="mx-auto max-w-2xl px-6 text-center" data-reveal>
|
||||
<h2 class="font-display text-4xl font-bold text-foreground mb-4 text-balance">{t('features.cta.title')}</h2>
|
||||
<p class="text-lg text-foreground-muted mb-8 text-balance">{t('features.cta.desc')}</p>
|
||||
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<Button size="lg" href="https://app.armarium.ch/register">
|
||||
{t('cta.register')}
|
||||
<Icon name="arrow-right" size="sm" />
|
||||
</Button>
|
||||
<Button size="lg" variant="outline" href="https://app.armarium.ch/login">
|
||||
{t('features.cta.login')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</PageLayout>
|
||||
@@ -1,242 +0,0 @@
|
||||
---
|
||||
import Icon from '@/components/ui/primitives/Icon/Icon.astro';
|
||||
import { Image } from 'astro:assets';
|
||||
import headerImg from '@/assets/header_img.jpg';
|
||||
import contentImg from '@/assets/content_image.jpg';
|
||||
import armariumImg from '@/assets/armarium_image.jpg';
|
||||
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 allPosts = await getCollection('blog', ({ data }) => !data.draft && data.locale === locale);
|
||||
const posts = allPosts.sort((a, b) => b.data.publishedAt.getTime() - a.data.publishedAt.getTime()).slice(0, 7);
|
||||
const [featured, ...restPosts] = posts;
|
||||
|
||||
type BlogEntry = { href: string; title: string; description: string };
|
||||
|
||||
function toEntry(p: (typeof restPosts)[number]): BlogEntry {
|
||||
return { href: `/blog/${p.id}`, title: p.data.title, description: p.data.description };
|
||||
}
|
||||
|
||||
const dummies: BlogEntry[] = [
|
||||
{ href: '#', title: 'So behältst du den Überblick über deine Finanzen', description: 'Mit einfachen Tricks und der richtigen App kannst du dein Budget im Griff behalten — ohne Stress.' },
|
||||
{ href: '#', title: '5 Spartipps für den Alltag', description: 'Kleine Änderungen im Alltag können langfristig einen grossen Unterschied machen.' },
|
||||
{ href: '#', title: 'Warum ein Haushaltsbuch sinnvoll ist', description: 'Wer seine Ausgaben kennt, kann gezielt sparen und Sparziele schneller erreichen.' },
|
||||
];
|
||||
|
||||
function fillTo3(items: (typeof restPosts), offset = 0): BlogEntry[] {
|
||||
const real = items.map(toEntry);
|
||||
return [...real, ...dummies].slice(offset, offset + 3);
|
||||
}
|
||||
|
||||
const colA = fillTo3(restPosts.slice(0, 3));
|
||||
const colB = fillTo3(restPosts.slice(3, 6), restPosts.slice(3, 6).length === 0 ? 0 : 0);
|
||||
|
||||
---
|
||||
|
||||
<!-- Hero -->
|
||||
<section class="bg-white dark:bg-gray-900">
|
||||
<div class="grid max-w-screen-xl px-4 py-16 mx-auto lg:gap-8 xl:gap-0 lg:py-28 lg:grid-cols-12">
|
||||
<div class="mr-auto place-self-center lg:col-span-7">
|
||||
<h1 class="max-w-2xl mb-4 text-4xl font-extrabold tracking-tight leading-none md:text-5xl xl:text-6xl dark:text-white">
|
||||
Armarium Suite<br />
|
||||
<span class="text-brand-500">Budget</span> & More
|
||||
</h1>
|
||||
<p class="max-w-2xl mb-6 font-light text-gray-500 lg:mb-8 md:text-lg lg:text-xl dark:text-gray-400">
|
||||
{t('hero.description')}
|
||||
</p>
|
||||
<a href="https://app.armarium.ch/register" class="inline-flex items-center justify-center px-5 py-3 mr-3 text-base font-medium text-center text-white rounded-lg bg-brand-500 hover:bg-brand-600 focus:ring-4 focus:ring-brand-300 dark:focus:ring-brand-900">
|
||||
{t('hero.register')}
|
||||
<Icon name="arrow-right" size="sm" class="ml-2 -mr-1" />
|
||||
</a>
|
||||
</div>
|
||||
<div class="hidden lg:mt-0 lg:col-span-5 lg:flex items-center">
|
||||
<div class="w-full overflow-hidden rounded-2xl shadow-md">
|
||||
<Image src={headerImg} alt="Armarium Suite" class="w-full h-full object-contain" widths={[480, 720]} sizes="(max-width: 1024px) 0px, 40vw" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
|
||||
<!-- Device tabs section -->
|
||||
<section class="bg-white dark:bg-gray-900 antialiased border-t border-border">
|
||||
<div class="max-w-screen-xl px-4 py-8 mx-auto sm:py-16 lg:py-24">
|
||||
<div class="grid grid-cols-1 gap-8 lg:gap-16 lg:grid-cols-2">
|
||||
<div>
|
||||
<div class="space-y-4 sm:space-y-6 lg:space-y-8">
|
||||
<div>
|
||||
<h2 class="text-3xl font-extrabold leading-tight text-gray-900 sm:text-4xl dark:text-white">
|
||||
{t('f1.title' as any)} — {t('f2.title' as any)}
|
||||
</h2>
|
||||
<p class="mt-4 text-base font-normal text-gray-500 dark:text-gray-400 sm:text-xl">
|
||||
{t('hero.description')}
|
||||
</p>
|
||||
</div>
|
||||
<div class="pt-4 border-t border-gray-200 sm:pt-6 lg:pt-8 dark:border-gray-800">
|
||||
<ul class="space-y-4">
|
||||
{(['f1','f2','f3'] as const).map((key) => (
|
||||
<li class="flex items-center gap-2.5">
|
||||
<div class="inline-flex items-center justify-center w-5 h-5 rounded-full bg-brand-500/10 text-brand-500 shrink-0">
|
||||
<svg aria-hidden="true" class="w-3.5 h-3.5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="text-base font-medium text-gray-900 dark:text-white">{t(`${key}.title` as any)}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<a href={t('nav.features.href')} class="inline-flex items-center text-base font-medium text-brand-500 hover:underline">
|
||||
{t('nav.features')}
|
||||
<svg aria-hidden="true" class="w-5 h-5 ml-1.5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- App image -->
|
||||
<div class="hidden lg:flex items-center">
|
||||
<Image src={armariumImg} alt="Armarium Suite" class="w-3/4 mx-auto rounded-2xl shadow-md object-contain" widths={[480, 720]} sizes="30vw" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bottom split: image + feature list -->
|
||||
<div class="grid grid-cols-1 gap-8 mt-8 lg:mt-20 lg:gap-16 lg:grid-cols-2">
|
||||
<div class="hidden lg:block">
|
||||
<Image src={contentImg} alt="Armarium Suite Features" class="w-3/4 mx-auto rounded-2xl shadow-md object-cover" widths={[640, 960]} sizes="40vw" />
|
||||
</div>
|
||||
|
||||
<div class="space-y-4 sm:space-y-6 lg:space-y-8">
|
||||
<div>
|
||||
<h2 class="text-3xl font-extrabold leading-tight text-gray-900 sm:text-4xl dark:text-white">
|
||||
{t('features.title')}
|
||||
</h2>
|
||||
<p class="mt-4 text-base font-normal text-gray-500 dark:text-gray-400 sm:text-xl">
|
||||
{t('features.description')}
|
||||
</p>
|
||||
</div>
|
||||
<div class="pt-4 border-t border-gray-200 sm:pt-6 lg:pt-8 dark:border-gray-800">
|
||||
<ul class="space-y-4">
|
||||
{(['f1','f2','f3','f4','f5','f6'] as const).map((key) => (
|
||||
<li class="flex items-center gap-2.5">
|
||||
<div class="inline-flex items-center justify-center w-5 h-5 rounded-full bg-brand-500/10 text-brand-500 shrink-0">
|
||||
<svg aria-hidden="true" class="w-3.5 h-3.5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
</div>
|
||||
<span class="text-base font-medium text-gray-900 dark:text-white">{t(`${key}.title` as any)}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<a href="https://app.armarium.ch/register"
|
||||
class="text-white bg-brand-500 justify-center hover:bg-brand-600 inline-flex items-center focus:ring-4 focus:outline-none focus:ring-brand-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:bg-brand-600 dark:hover:bg-brand-700 dark:focus:ring-brand-800">
|
||||
{t('hero.register')}
|
||||
<svg aria-hidden="true" class="w-5 h-5 ml-2 -mr-1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
</a>
|
||||
<a href={t('nav.features.href')}
|
||||
class="px-5 py-2.5 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded-lg border border-gray-200 hover:bg-gray-100 hover:text-brand-600 focus:z-10 focus:ring-4 focus:ring-gray-200 dark:focus:ring-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:border-gray-600 dark:hover:text-white dark:hover:bg-gray-700">
|
||||
{t('nav.features')}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
<!-- Blog section -->
|
||||
<section class="bg-white dark:bg-gray-900 border-t border-border">
|
||||
<div class="py-8 px-6 mx-auto max-w-screen-xl sm:py-16 lg:px-12">
|
||||
<div class="mx-auto max-w-screen-sm text-center">
|
||||
<h2 class="mb-4 text-3xl lg:text-4xl tracking-tight font-extrabold text-gray-900 dark:text-white">
|
||||
{t('nav.blog')}
|
||||
</h2>
|
||||
<p class="mb-8 lg:mb-16 font-light text-gray-500 dark:text-gray-400 sm:text-xl">
|
||||
{t('features.description')}
|
||||
</p>
|
||||
</div>
|
||||
<div class="grid gap-8 mb-16 lg:grid-cols-3 [&>*]:min-w-0">
|
||||
<!-- Featured post with image -->
|
||||
{featured && (
|
||||
<article class="min-w-0">
|
||||
<a href={`/blog/${featured.id}`} class="block mb-5">
|
||||
<Image src={headerImg} alt={featured.data.title} class="rounded-lg w-full h-48 object-cover" />
|
||||
</a>
|
||||
<h2 class="my-2 text-2xl font-bold tracking-tight text-gray-900 dark:text-white">
|
||||
<a href={`/blog/${featured.id}`}>{featured.data.title}</a>
|
||||
</h2>
|
||||
<p class="mb-4 font-light text-gray-500 dark:text-gray-400">{featured.data.description}</p>
|
||||
<a href={`/blog/${featured.id}`} class="inline-flex items-center font-medium text-brand-500 hover:underline">
|
||||
{t('blog.readmore' as any) || 'Weiterlesen'}
|
||||
<svg class="ml-2 w-4 h-4" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z" clip-rule="evenodd"/></svg>
|
||||
</a>
|
||||
</article>
|
||||
)}
|
||||
<!-- Column A -->
|
||||
<div class="space-y-8 lg:pl-10 lg:border-l lg:border-gray-200 dark:lg:border-gray-700">
|
||||
{colA.map((post) => (
|
||||
<article>
|
||||
<h2 class="mb-2 text-2xl font-bold tracking-tight text-gray-900 dark:text-white">
|
||||
<a href={post.href}>{post.title}</a>
|
||||
</h2>
|
||||
<p class="mb-4 font-light text-gray-500 dark:text-gray-400">{post.description}</p>
|
||||
<a href={post.href} class="inline-flex items-center font-medium text-brand-500 hover:underline">
|
||||
Weiterlesen
|
||||
<svg class="ml-2 w-4 h-4" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z" clip-rule="evenodd"/></svg>
|
||||
</a>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
<!-- Column B -->
|
||||
<div class="space-y-8 lg:pl-10 lg:border-l lg:border-gray-200 dark:lg:border-gray-700">
|
||||
{colB.map((post) => (
|
||||
<article>
|
||||
<h2 class="mb-2 text-2xl font-bold tracking-tight text-gray-900 dark:text-white">
|
||||
<a href={post.href}>{post.title}</a>
|
||||
</h2>
|
||||
<p class="mb-4 font-light text-gray-500 dark:text-gray-400">{post.description}</p>
|
||||
<a href={post.href} class="inline-flex items-center font-medium text-brand-500 hover:underline">
|
||||
Weiterlesen
|
||||
<svg class="ml-2 w-4 h-4" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z" clip-rule="evenodd"/></svg>
|
||||
</a>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- CTA -->
|
||||
<section class="relative z-10 py-[var(--space-section-md)] bg-background border-t border-border">
|
||||
<div class="mx-auto max-w-2xl px-6 text-center" data-reveal>
|
||||
<h2 class="font-display text-4xl font-bold text-foreground mb-4 text-balance">
|
||||
{t('cta.title')}
|
||||
</h2>
|
||||
<p class="text-lg text-foreground-muted mb-8 text-balance">
|
||||
Kostenlos mitmachen und sofort loslegen.
|
||||
</p>
|
||||
<div class="flex justify-center">
|
||||
<a href="https://app.armarium.ch/register" class="inline-flex items-center justify-center px-5 py-3 text-base font-medium text-center text-white rounded-lg bg-brand-500 hover:bg-brand-600 focus:ring-4 focus:ring-brand-300 dark:focus:ring-brand-900">
|
||||
{t('cta.register')}
|
||||
<Icon name="arrow-right" size="sm" class="ml-2 -mr-1" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -1,58 +0,0 @@
|
||||
---
|
||||
import Footer from './Footer.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;
|
||||
background?: 'default' | 'secondary' | 'invert';
|
||||
}
|
||||
|
||||
const { locale = 'de', background = 'secondary' } = Astro.props;
|
||||
const t = useTranslations(locale);
|
||||
---
|
||||
|
||||
<Footer
|
||||
layout="columns"
|
||||
columns={2}
|
||||
{background}
|
||||
copyright={t('footer.copyright')}
|
||||
showSocial={false}
|
||||
>
|
||||
<div slot="tagline" class="space-y-3">
|
||||
<p class="text-sm max-w-xs text-foreground-muted">{t('footer.tagline')}</p>
|
||||
<a
|
||||
href={t('nav.contact.href')}
|
||||
class="inline-flex items-center gap-2 px-4 py-2 rounded-md text-sm font-medium border border-border text-foreground-muted hover:text-foreground hover:border-border-strong transition-colors"
|
||||
>
|
||||
{t('nav.contact')}
|
||||
</a>
|
||||
<a
|
||||
href="https://www.linkedin.com/company/armarium-suite"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="LinkedIn"
|
||||
class="inline-flex items-center justify-center w-9 h-9 rounded-full border border-border text-foreground-muted hover:text-foreground hover:border-border-strong transition-colors"
|
||||
>
|
||||
<Icon name="linkedin" size="sm" />
|
||||
</a>
|
||||
</div>
|
||||
<div slot="columns" class="grid grid-cols-2 gap-8">
|
||||
<div class="space-y-[var(--space-stack-md)]">
|
||||
<h3 class="font-semibold text-sm text-foreground">{t('footer.app')}</h3>
|
||||
<ul class="space-y-2">
|
||||
<li><a href={t('nav.blog.href')} class="text-sm transition-colors text-foreground-muted hover:text-foreground">{t('nav.blog')}</a></li>
|
||||
<li><a href={t('nav.features.href')} class="text-sm transition-colors text-foreground-muted hover:text-foreground">{t('nav.features')}</a></li>
|
||||
<li><a href={t('nav.about.href')} class="text-sm transition-colors text-foreground-muted hover:text-foreground">{t('nav.about')}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="space-y-[var(--space-stack-md)]">
|
||||
<h3 class="font-semibold text-sm text-foreground">{t('footer.legal')}</h3>
|
||||
<ul class="space-y-2">
|
||||
<li><a href={t('footer.privacy.href')} class="text-sm transition-colors text-foreground-muted hover:text-foreground">{t('footer.privacy')}</a></li>
|
||||
<li><a href={t('footer.imprint.href')} class="text-sm transition-colors text-foreground-muted hover:text-foreground">{t('footer.imprint')}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</Footer>
|
||||
@@ -140,8 +140,11 @@ function getSocialIcon(platform: string): string {
|
||||
{hasLogoSlot ? (
|
||||
<slot name="logo" />
|
||||
) : (
|
||||
<a href="/" class="flex items-center">
|
||||
<Logo variant="full" size="md" />
|
||||
<a href="/" class="flex items-center gap-2">
|
||||
<Logo size="md" />
|
||||
<span class="font-display text-base font-bold text-brand-500">
|
||||
{siteConfig.name}
|
||||
</span>
|
||||
</a>
|
||||
)}
|
||||
{(hasTaglineSlot || tagline) && (
|
||||
@@ -204,8 +207,11 @@ function getSocialIcon(platform: string): string {
|
||||
{hasLogoSlot ? (
|
||||
<slot name="logo" />
|
||||
) : (
|
||||
<a href="/" class="flex items-center">
|
||||
<Logo variant="full" size="md" />
|
||||
<a href="/" class="flex items-center gap-2">
|
||||
<Logo size="md" />
|
||||
<span class="font-display text-base font-bold text-brand-500">
|
||||
{siteConfig.name}
|
||||
</span>
|
||||
</a>
|
||||
)}
|
||||
{(hasTaglineSlot || tagline) && (
|
||||
@@ -268,7 +274,9 @@ function getSocialIcon(platform: string): string {
|
||||
) : (
|
||||
<>
|
||||
{showCopyright && (
|
||||
<p class="text-sm text-foreground-muted" set:html={processedCopyright} />
|
||||
<p class="text-sm text-foreground-muted">
|
||||
{processedCopyright}
|
||||
</p>
|
||||
)}
|
||||
{legalLinks.length > 0 && (
|
||||
<div class="flex items-center gap-[var(--space-inline-lg)]">
|
||||
@@ -292,7 +300,9 @@ function getSocialIcon(platform: string): string {
|
||||
{layout === 'minimal' && (
|
||||
<div class="text-center">
|
||||
{showCopyright && (
|
||||
<p class="text-sm text-foreground-muted" set:html={processedCopyright} />
|
||||
<p class="text-sm text-foreground-muted">
|
||||
{processedCopyright}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
@@ -304,8 +314,11 @@ function getSocialIcon(platform: string): string {
|
||||
hasLogoSlot ? (
|
||||
<slot name="logo" />
|
||||
) : (
|
||||
<a href="/" class="flex items-center">
|
||||
<Logo variant="full" size="md" />
|
||||
<a href="/" class="flex items-center gap-2">
|
||||
<Logo size="md" />
|
||||
<span class="font-display text-xl font-bold text-brand-500">
|
||||
{siteConfig.name}
|
||||
</span>
|
||||
</a>
|
||||
)
|
||||
)}
|
||||
@@ -352,7 +365,9 @@ function getSocialIcon(platform: string): string {
|
||||
{(showCopyright || legalLinks.length > 0) && (
|
||||
<div class="pt-[var(--space-stack-lg)] border-t border-border w-full flex flex-col items-center gap-[var(--space-stack-md)]">
|
||||
{showCopyright && (
|
||||
<p class="text-sm text-foreground-muted" set:html={processedCopyright} />
|
||||
<p class="text-sm text-foreground-muted">
|
||||
{processedCopyright}
|
||||
</p>
|
||||
)}
|
||||
{legalLinks.length > 0 && (
|
||||
<div class="flex items-center gap-[var(--space-inline-lg)]">
|
||||
|
||||
@@ -31,9 +31,7 @@ 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;
|
||||
@@ -85,8 +83,6 @@ 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 */
|
||||
@@ -114,8 +110,6 @@ const {
|
||||
showActiveState = true,
|
||||
showScrollProgress = false,
|
||||
scrollProgressPosition = 'bottom',
|
||||
showLanguageSwitcher = false,
|
||||
currentLocale = 'de',
|
||||
logoText,
|
||||
hideLogo = false,
|
||||
class: className,
|
||||
@@ -196,13 +190,16 @@ const buttonId = `${menuId}-button`;
|
||||
(hasLogoSlot ? (
|
||||
<slot name="logo" />
|
||||
) : (
|
||||
<a href="/" class="flex items-center">
|
||||
<Logo
|
||||
variant="full"
|
||||
size={size === 'lg' ? 'lg' : 'md'}
|
||||
forceDark={isInvert}
|
||||
class={cn(isFloating ? 'hdr-logo-text' : (isInvert ? 'text-on-invert' : 'text-foreground'))}
|
||||
/>
|
||||
<a href="/" class="flex items-center gap-2">
|
||||
<Logo size={size === 'lg' ? 'lg' : 'md'} forceDark={isInvert} />
|
||||
<span
|
||||
class={cn(
|
||||
'font-display text-xl font-bold tracking-tight',
|
||||
isFloating ? 'hdr-logo-text' : (isInvert ? 'text-on-invert' : 'text-brand-500')
|
||||
)}
|
||||
>
|
||||
{logoText || siteConfig.name}
|
||||
</span>
|
||||
</a>
|
||||
))
|
||||
}
|
||||
@@ -257,12 +254,6 @@ const buttonId = `${menuId}-button`;
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showLanguageSwitcher && (
|
||||
<div class="hidden md:flex">
|
||||
<LanguageSwitcherDropdown currentLocale={currentLocale as Locale} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showSocialLinks && siteConfig.socialLinks.length > 0 && (
|
||||
<div class="hidden md:flex items-center gap-0.5">
|
||||
{siteConfig.socialLinks.map((url) => {
|
||||
@@ -425,15 +416,6 @@ const buttonId = `${menuId}-button`;
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showLanguageSwitcher && (
|
||||
<div class="border-border mt-3 border-t pt-3">
|
||||
<div class="flex items-center justify-between px-1">
|
||||
<span class="text-sm text-foreground-muted">Language</span>
|
||||
<LanguageSwitcherDropdown currentLocale={currentLocale as Locale} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
|
||||
@@ -1,155 +0,0 @@
|
||||
---
|
||||
/**
|
||||
* 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);
|
||||
---
|
||||
|
||||
<div class={cn('relative lang-dropdown-wrapper', className)} data-current-locale={currentLocale}>
|
||||
<!-- Trigger -->
|
||||
<button
|
||||
type="button"
|
||||
id="lang-dropdown-trigger"
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
aria-label="Select language"
|
||||
class={cn(
|
||||
'inline-flex items-center gap-1.5 rounded-full border border-border-strong px-2.5 py-1.5',
|
||||
'text-foreground-muted hover:text-foreground hover:bg-secondary',
|
||||
'transition-colors duration-(--transition-fast)',
|
||||
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2'
|
||||
)}
|
||||
>
|
||||
<span class="text-xs font-semibold tracking-wide uppercase">{current.code}</span>
|
||||
<svg
|
||||
class="lang-chevron h-3 w-3 shrink-0 transition-transform duration-200"
|
||||
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"
|
||||
fill="none" stroke="currentColor" stroke-width="2.5"
|
||||
stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"
|
||||
>
|
||||
<path d="m6 9 6 6 6-6"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- Dropdown panel -->
|
||||
<div
|
||||
id="lang-dropdown-panel"
|
||||
role="listbox"
|
||||
aria-label="Language"
|
||||
class={cn(
|
||||
'absolute right-0 top-full mt-2 z-50',
|
||||
'w-40 rounded-xl border border-border bg-background shadow-lg',
|
||||
'p-1.5',
|
||||
'hidden'
|
||||
)}
|
||||
>
|
||||
{allLanguages.map((lang) => (
|
||||
<button
|
||||
type="button"
|
||||
role="option"
|
||||
class="lang-option w-full flex items-center gap-2.5 rounded-lg px-3 py-2 text-sm text-left
|
||||
hover:bg-secondary transition-colors duration-(--transition-fast)
|
||||
focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
data-locale={lang.code}
|
||||
aria-selected={lang.code === currentLocale ? 'true' : 'false'}
|
||||
>
|
||||
<span class="text-xs font-bold tracking-wide uppercase text-foreground-muted">{lang.code}</span>
|
||||
<span class="font-medium text-foreground">{lang.name}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.lang-option[aria-selected="true"] {
|
||||
background-color: var(--color-secondary);
|
||||
}
|
||||
.lang-option[aria-selected="true"] span:last-child {
|
||||
color: var(--color-brand-500);
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
function getLocalePath(targetLocale: string, currentPath: string): string {
|
||||
const nonDefault = ['fr', 'it', 'en'];
|
||||
let basePath = currentPath;
|
||||
for (const loc of nonDefault) {
|
||||
if (currentPath.startsWith(`/${loc}/`)) {
|
||||
basePath = currentPath.slice(loc.length + 1);
|
||||
break;
|
||||
} else if (currentPath === `/${loc}`) {
|
||||
basePath = '/';
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (targetLocale === 'de') return basePath || '/';
|
||||
return basePath === '/' ? `/${targetLocale}/` : `/${targetLocale}${basePath}`;
|
||||
}
|
||||
|
||||
function closeLangDropdown() {
|
||||
const panel = document.getElementById('lang-dropdown-panel');
|
||||
const trigger = document.getElementById('lang-dropdown-trigger');
|
||||
panel?.classList.add('hidden');
|
||||
trigger?.setAttribute('aria-expanded', 'false');
|
||||
trigger?.querySelector('.lang-chevron')?.classList.remove('rotate-180');
|
||||
}
|
||||
|
||||
function initLangDropdown() {
|
||||
const trigger = document.getElementById('lang-dropdown-trigger');
|
||||
const panel = document.getElementById('lang-dropdown-panel');
|
||||
const wrapper = trigger?.closest('.lang-dropdown-wrapper') as HTMLElement | null;
|
||||
if (!trigger || !panel || !wrapper) return;
|
||||
if (trigger.dataset.langInit) return;
|
||||
trigger.dataset.langInit = 'true';
|
||||
|
||||
// Bind language option clicks
|
||||
panel.querySelectorAll<HTMLButtonElement>('.lang-option').forEach((btn) => {
|
||||
btn.addEventListener('click', () => {
|
||||
const targetLocale = btn.dataset.locale!;
|
||||
const path = getLocalePath(targetLocale, window.location.pathname);
|
||||
closeLangDropdown();
|
||||
window.location.href = path;
|
||||
});
|
||||
});
|
||||
|
||||
// Toggle dropdown
|
||||
trigger.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const isOpen = !panel.classList.contains('hidden');
|
||||
if (isOpen) {
|
||||
closeLangDropdown();
|
||||
} else {
|
||||
panel.classList.remove('hidden');
|
||||
trigger.setAttribute('aria-expanded', 'true');
|
||||
trigger.querySelector('.lang-chevron')?.classList.add('rotate-180');
|
||||
}
|
||||
});
|
||||
|
||||
// Close on outside click
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!wrapper.contains(e.target as Node)) closeLangDropdown();
|
||||
});
|
||||
|
||||
// Close on Escape
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') closeLangDropdown();
|
||||
});
|
||||
}
|
||||
|
||||
initLangDropdown();
|
||||
document.addEventListener('astro:page-load', initLangDropdown);
|
||||
document.addEventListener('astro:after-swap', initLangDropdown);
|
||||
</script>
|
||||
@@ -1,30 +1,28 @@
|
||||
---
|
||||
/**
|
||||
* Logo Component
|
||||
* Logo Component — first-letter monogram badge
|
||||
*
|
||||
* - variant='logomark' (default): brand-coloured square badge with first letter
|
||||
* - variant='full': horizontal SVG logo (icon + wordmark)
|
||||
* Renders a brand-coloured square badge with the first letter of the site
|
||||
* name, used in the navbar, footer, and blog post bylines.
|
||||
*/
|
||||
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;
|
||||
/** 'logomark': monogram badge | 'full': horizontal SVG logo */
|
||||
/** Use full logo instead of logomark — no-op here, kept for API compat */
|
||||
variant?: 'logomark' | 'full';
|
||||
/** Additional CSS classes */
|
||||
class?: string;
|
||||
/** Override the displayed letter (logomark only). Defaults to first letter of site name. */
|
||||
/** Override the displayed letter (e.g. for author avatars). Defaults to first letter of site name. */
|
||||
letter?: string;
|
||||
}
|
||||
|
||||
const {
|
||||
size = 'md',
|
||||
variant = 'logomark',
|
||||
class: className,
|
||||
letter: letterProp,
|
||||
} = Astro.props;
|
||||
@@ -39,37 +37,18 @@ const sizeClasses: Record<string, { box: string; text: string; radius: string }>
|
||||
|
||||
const { box, text, radius } = sizeClasses[size] ?? sizeClasses.md;
|
||||
const letter = letterProp ?? siteConfig.name.charAt(0).toUpperCase();
|
||||
|
||||
const fullHeights: Record<string, string> = {
|
||||
sm: 'h-4',
|
||||
md: 'h-5',
|
||||
lg: 'h-7',
|
||||
xl: 'h-10',
|
||||
'2xl': 'h-16',
|
||||
};
|
||||
const fullHeight = fullHeights[size] ?? fullHeights.md;
|
||||
---
|
||||
|
||||
{variant === 'full' ? (
|
||||
<span
|
||||
class={cn('inline-flex items-center shrink-0 text-foreground', fullHeight, className)}
|
||||
aria-label={siteConfig.branding.logo.alt}
|
||||
role="img"
|
||||
>
|
||||
<Fragment set:html={logoHorizontal} />
|
||||
</span>
|
||||
) : (
|
||||
<span
|
||||
class={cn(
|
||||
'inline-flex items-center justify-center shrink-0',
|
||||
'bg-brand-500 text-white',
|
||||
'font-display font-bold leading-none select-none',
|
||||
box, text, radius,
|
||||
className
|
||||
)}
|
||||
aria-label={siteConfig.branding.logo.alt}
|
||||
role="img"
|
||||
>
|
||||
{letter}
|
||||
</span>
|
||||
)}
|
||||
<span
|
||||
class={cn(
|
||||
'inline-flex items-center justify-center shrink-0',
|
||||
'bg-brand-500 text-white',
|
||||
'font-display font-bold leading-none select-none',
|
||||
box, text, radius,
|
||||
className
|
||||
)}
|
||||
aria-label={siteConfig.branding.logo.alt}
|
||||
role="img"
|
||||
>
|
||||
{letter}
|
||||
</span>
|
||||
|
||||
@@ -13,7 +13,7 @@ export interface NavItem {
|
||||
|
||||
export const navItems: NavItem[] = [
|
||||
{ label: 'Blog', href: '/blog', order: 1 },
|
||||
{ label: 'Features', href: '/features', order: 2 },
|
||||
{ label: 'Projects', href: '/projects', order: 2 },
|
||||
{ label: 'About', href: '/about', order: 3 },
|
||||
{ label: 'Contact', href: '/contact', order: 4 },
|
||||
];
|
||||
|
||||
@@ -75,7 +75,6 @@ const siteConfig: SiteConfig = {
|
||||
socialLinks: [
|
||||
'https://github.com/hansmartens68/Astro-Rocket',
|
||||
'https://x.com/hansmartens_dev',
|
||||
'https://www.linkedin.com',
|
||||
],
|
||||
twitter: {
|
||||
site: 'https://x.com/hansmartens_dev',
|
||||
|
||||
@@ -18,7 +18,7 @@ const blog = defineCollection({
|
||||
svgSlug: z.string().optional(),
|
||||
draft: z.boolean().default(false),
|
||||
featured: z.boolean().default(false),
|
||||
locale: z.enum(['de', 'en', 'es', 'fr', 'it']).default('de'),
|
||||
locale: z.enum(['en', 'es', 'fr']).default('en'),
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -29,7 +29,7 @@ const pages = defineCollection({
|
||||
title: z.string(),
|
||||
description: z.string(),
|
||||
updatedAt: z.coerce.date().optional(),
|
||||
locale: z.enum(['de', 'en', 'es', 'fr', 'it']).default('de'),
|
||||
locale: z.enum(['en', 'es', 'fr']).default('en'),
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
{
|
||||
"name": "Armarium",
|
||||
"bio": "Armarium ist dein persönlicher Finanzbegleiter – Budget im Blick, Ziele im Fokus. Entwickelt in Zürich, Switzerland.",
|
||||
"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.",
|
||||
"social": {
|
||||
"linkedin": "https://linkedin.com"
|
||||
"github": "https://github.com",
|
||||
"linkedin": "https://linkedin.com",
|
||||
"twitter": "https://x.com"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
---
|
||||
title: "5 Tipps für ein besseres Budget"
|
||||
description: "Mit diesen fünf einfachen Strategien behältst du deine Finanzen im Griff – ohne auf Lebensqualität verzichten zu müssen."
|
||||
publishedAt: 2026-05-01
|
||||
author: "Armarium"
|
||||
tags: ["budget", "tipps"]
|
||||
svgSlug: "astro-rocket-features"
|
||||
featured: false
|
||||
locale: de
|
||||
---
|
||||
|
||||
Ein gutes Budget ist keine Einschränkung – es ist Freiheit. Wer weiss, wohin sein Geld fliesst, kann bewusster entscheiden.
|
||||
|
||||
## 1. Fixkosten zuerst
|
||||
|
||||
Erfasse alle monatlich wiederkehrenden Ausgaben: Miete, Versicherungen, Abonnements. Was übrig bleibt, ist dein frei verfügbares Budget.
|
||||
|
||||
## 2. Die 50/30/20-Regel
|
||||
|
||||
50 % für Notwendigkeiten, 30 % für persönliche Wünsche, 20 % für Sparen. Ein einfaches Prinzip mit grosser Wirkung.
|
||||
|
||||
## 3. Kategorien konsequent nutzen
|
||||
|
||||
Weise jede Ausgabe einer Kategorie zu. So erkennst du schnell, wo du mehr ausgibst als geplant.
|
||||
|
||||
## 4. Wöchentlich überprüfen
|
||||
|
||||
Einmal pro Woche einen kurzen Blick auf die Ausgaben – das reicht, um den Überblick zu behalten.
|
||||
|
||||
## 5. Sparziele setzen
|
||||
|
||||
Ein konkretes Ziel motiviert. Ob Urlaub, Notfallreserve oder neues Gerät – trag es in Armarium ein und verfolge deinen Fortschritt.
|
||||
@@ -1,24 +0,0 @@
|
||||
---
|
||||
title: "Mehrere Konten clever verwalten"
|
||||
description: "Bankkonto, Kreditkarte, Sparkonto – so behältst du den Überblick über all deine Konten in einer einzigen App."
|
||||
publishedAt: 2026-04-20
|
||||
author: "Armarium"
|
||||
tags: ["konten", "übersicht"]
|
||||
svgSlug: "hero-typing-effect"
|
||||
featured: false
|
||||
locale: de
|
||||
---
|
||||
|
||||
Wer mehrere Konten hat, verliert leicht den Überblick. Armarium bringt alles an einem Ort zusammen.
|
||||
|
||||
## Warum mehrere Konten sinnvoll sind
|
||||
|
||||
Viele Finanzprofis empfehlen getrennte Konten für verschiedene Zwecke: ein Konto für fixe Ausgaben, eines für variable Kosten, eines zum Sparen.
|
||||
|
||||
## Alle Konten in Armarium
|
||||
|
||||
In Armarium legst du beliebig viele Konten an – Bankkonto, Kreditkarte, Bargeld oder digitale Wallets. Alle Transaktionen laufen zentral zusammen.
|
||||
|
||||
## Gesamtbild auf einen Blick
|
||||
|
||||
Das Dashboard zeigt dir jederzeit den Gesamtsaldo über alle Konten – ohne zwischen verschiedenen Apps wechseln zu müssen.
|
||||
@@ -1,27 +0,0 @@
|
||||
---
|
||||
title: "Sparziele richtig setzen und erreichen"
|
||||
description: "Wie du realistische Sparziele definierst, sie in Armarium verfolgst und Monat für Monat näher ans Ziel kommst."
|
||||
publishedAt: 2026-04-25
|
||||
author: "Armarium"
|
||||
tags: ["sparziele", "finanzen"]
|
||||
svgSlug: "design-system-color-tokens"
|
||||
featured: false
|
||||
locale: de
|
||||
---
|
||||
|
||||
Sparen funktioniert besser mit einem konkreten Ziel. Wer weiss, wofür er spart, bleibt motivierter und gibt weniger unbedacht aus.
|
||||
|
||||
## Was macht ein gutes Sparziel aus?
|
||||
|
||||
Ein gutes Sparziel ist **spezifisch**, **realistisch** und **zeitgebunden**. Statt "ich will mehr sparen" lieber: "Ich spare bis Dezember 2026 CHF 2'000 für einen Urlaub."
|
||||
|
||||
## In Armarium ein Ziel anlegen
|
||||
|
||||
1. Neues Sparziel erstellen
|
||||
2. Zielbetrag und Deadline festlegen
|
||||
3. Monatlichen Sparbeitrag definieren
|
||||
4. Fortschritt verfolgen
|
||||
|
||||
## Kleine Beträge, grosse Wirkung
|
||||
|
||||
Schon CHF 5 täglich ergeben CHF 1'825 im Jahr. Regelmässigkeit schlägt Betragsgrösse.
|
||||
@@ -0,0 +1,194 @@
|
||||
---
|
||||
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 `<ClientRouter />` 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';
|
||||
|
||||
<!-- inside <head> -->
|
||||
<ClientRouter />
|
||||
```
|
||||
|
||||
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
|
||||
<p data-countup="50" data-suffix="+">50+</p>
|
||||
```
|
||||
|
||||
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
|
||||
<div class="transition-all duration-200 hover:-translate-y-1 hover:shadow-md">
|
||||
```
|
||||
|
||||
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
|
||||
<div class="animate-slide-up delay-0">First item</div>
|
||||
<div class="animate-slide-up delay-1">Second item</div>
|
||||
<div class="animate-slide-up delay-2">Third item</div>
|
||||
```
|
||||
|
||||
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
|
||||
<h2 class="animate-slide-up">My heading</h2>
|
||||
```
|
||||
|
||||
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 `<ClientRouter />` from `src/layouts/BaseLayout.astro`.
|
||||
|
||||
To disable individual component animations, remove the animation class from the component's markup.
|
||||
@@ -1,40 +0,0 @@
|
||||
---
|
||||
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"]
|
||||
svgSlug: "astro-rocket-is-live"
|
||||
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 🇨🇭
|
||||
@@ -0,0 +1,555 @@
|
||||
---
|
||||
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
|
||||
<html lang="en" class="scroll-smooth dark" data-theme="blue">
|
||||
```
|
||||
|
||||
The `dark` class is what activates dark mode on first load. Remove it to default to light:
|
||||
|
||||
```html
|
||||
<html lang="en" class="scroll-smooth" data-theme="blue">
|
||||
```
|
||||
|
||||
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 `<head>`:
|
||||
|
||||
```astro
|
||||
<LandingLayout
|
||||
includePersonSchema={true}
|
||||
includeOrgSchema={false}
|
||||
includeProfessionalServiceSchema={false}
|
||||
>
|
||||
```
|
||||
|
||||
| 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 `<meta name="robots">` tag:
|
||||
|
||||
```astro
|
||||
<PageLayout noindex={true} nofollow={false}>
|
||||
```
|
||||
|
||||
| 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 `<html>` 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 `<Header>` 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
|
||||
<Header shape="floating" variant="transparent" colorScheme="invert" position="fixed" />
|
||||
```
|
||||
|
||||
The blog and standard page layouts use a full-width bar:
|
||||
|
||||
```astro
|
||||
<Header position="fixed" size="lg" />
|
||||
```
|
||||
|
||||
### Switching the blog header to a floating style
|
||||
|
||||
Open `src/layouts/BlogLayout.astro` and find the `<Header>` line. Change it to:
|
||||
|
||||
```astro
|
||||
<Header shape="floating" variant="transparent" position="fixed" />
|
||||
```
|
||||
|
||||
### Switching any page header from floating to a bar
|
||||
|
||||
Find the Header component in the relevant layout file and remove `shape="floating"`:
|
||||
|
||||
```astro
|
||||
<!-- Before -->
|
||||
<Header shape="floating" variant="transparent" position="fixed" />
|
||||
|
||||
<!-- After -->
|
||||
<Header position="fixed" size="lg" />
|
||||
```
|
||||
|
||||
### 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 `<Header>` 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
|
||||
<Footer copyright="© {year} Your Name. All rights reserved." />
|
||||
```
|
||||
|
||||
The `{year}` and `{siteName}` placeholders are replaced automatically at build time. Without a `copyright` prop it falls back to the site name from `site.config.ts`.
|
||||
|
||||
The Footer is used in three layout files:
|
||||
|
||||
| Layout file | Pages it covers |
|
||||
|-------------|----------------|
|
||||
| `src/layouts/PageLayout.astro` | Blog index, about, contact, any standard page |
|
||||
| `src/layouts/BlogLayout.astro` | Individual blog posts |
|
||||
| `src/layouts/LandingLayout.astro` | The landing page |
|
||||
|
||||
Edit the `<Footer>` line in whichever file covers the pages you want to change.
|
||||
|
||||
### Footer layout options
|
||||
|
||||
```astro
|
||||
<Footer layout="simple" /> <!-- Single row: logo, nav, social, copyright -->
|
||||
<Footer layout="stacked" /> <!-- Vertically stacked sections -->
|
||||
<Footer layout="columns" /> <!-- Multi-column link groups -->
|
||||
<Footer layout="minimal" /> <!-- Copyright line only -->
|
||||
```
|
||||
|
||||
### Footer prop reference
|
||||
|
||||
| Prop | Type | Default | What it controls |
|
||||
|------|------|:-------:|-----------------|
|
||||
| `layout` | `simple` `stacked` `columns` `minimal` | `simple` | Overall footer structure |
|
||||
| `background` | `default` `secondary` `invert` | `default` | Footer background colour |
|
||||
| `columns` | `2` `3` `4` | `3` | Number of link columns (only applies with `layout="columns"`) |
|
||||
| `showSocial` | boolean | `true` | Social media icons |
|
||||
| `showCopyright` | boolean | `true` | Copyright line |
|
||||
| `hideLogo` | boolean | `false` | Hide the footer logo |
|
||||
| `tagline` | string | — | Short tagline shown under the logo |
|
||||
| `copyright` | string | — | Custom copyright text — supports `{year}` and `{siteName}` placeholders |
|
||||
|
||||
### Legal links
|
||||
|
||||
The `legalLinks` prop adds a row of small links (Privacy Policy, Terms of Service, etc.) alongside the copyright line:
|
||||
|
||||
```astro
|
||||
<Footer
|
||||
legalLinks={[
|
||||
{ label: 'Privacy Policy', href: '/privacy' },
|
||||
{ label: 'Terms of Service', href: '/terms' },
|
||||
]}
|
||||
/>
|
||||
```
|
||||
|
||||
### Columns layout with link groups
|
||||
|
||||
The `columns` layout renders grouped link sections — useful for a site with many pages:
|
||||
|
||||
```astro
|
||||
<Footer
|
||||
layout="columns"
|
||||
columns={3}
|
||||
linkGroups={[
|
||||
{
|
||||
title: 'Product',
|
||||
links: [
|
||||
{ label: 'Features', href: '/features' },
|
||||
{ label: 'Pricing', href: '/pricing' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Company',
|
||||
links: [
|
||||
{ label: 'About', href: '/about' },
|
||||
{ label: 'Contact', href: '/contact' },
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
```
|
||||
|
||||
## Navigation
|
||||
|
||||
Edit `src/config/nav.config.ts` to change the header and footer navigation in one place:
|
||||
|
||||
```ts
|
||||
export const navItems: NavItem[] = [
|
||||
{ label: 'Blog', href: '/blog', order: 1 },
|
||||
{ label: 'About', href: '/about', order: 2 },
|
||||
{ label: 'Contact', href: '/contact', order: 3 },
|
||||
];
|
||||
```
|
||||
|
||||
Add, remove, or reorder items freely. Both the header and footer read from this array.
|
||||
|
||||
## Analytics and verification
|
||||
|
||||
Set these in your `.env` file (copy from `.env.example`):
|
||||
|
||||
```bash
|
||||
# Analytics
|
||||
PUBLIC_GA_MEASUREMENT_ID=G-XXXXXXXXXX # Google Analytics 4
|
||||
PUBLIC_GTM_ID=GTM-XXXXXXX # Google Tag Manager
|
||||
|
||||
# Cookie consent
|
||||
PUBLIC_CONSENT_ENABLED=true # Show cookie consent banner
|
||||
PUBLIC_PRIVACY_POLICY_URL=/privacy # Link in the consent banner
|
||||
|
||||
# Search console verification
|
||||
GOOGLE_SITE_VERIFICATION=your-code
|
||||
BING_SITE_VERIFICATION=your-code
|
||||
```
|
||||
|
||||
None of these are required during development. The analytics components simply render nothing if the IDs are absent.
|
||||
|
||||
## Social links
|
||||
|
||||
Social links in the footer come from the `socialLinks` array in `src/config/site.config.ts`:
|
||||
|
||||
```ts
|
||||
socialLinks: [
|
||||
'https://github.com/yourname',
|
||||
'https://x.com/yourhandle',
|
||||
'https://instagram.com/yourname',
|
||||
],
|
||||
```
|
||||
|
||||
Remove any URL you don't use. The footer won't render an empty social section. Icons are resolved automatically from the URL — GitHub, X, Instagram, LinkedIn, and Bluesky are all recognised out of the box.
|
||||
|
||||
## Quick reference
|
||||
|
||||
| What | Where | Key |
|
||||
|------|-------|-----|
|
||||
| Site name, author, email | `src/config/site.config.ts` | `name`, `author`, `email` |
|
||||
| Phone, address | `src/config/site.config.ts` | `phone`, `address` |
|
||||
| Author avatar (Person schema) | `src/config/site.config.ts` | `authorImage` |
|
||||
| Browser toolbar colour | `src/config/site.config.ts` | `branding.colors.themeColor` |
|
||||
| Blog image colour overlay | `src/config/site.config.ts` | `blogImageOverlay: true/false` |
|
||||
| Social links | `src/config/site.config.ts` | `socialLinks` array |
|
||||
| Twitter card handles | `src/config/site.config.ts` | `twitter.site`, `twitter.creator` |
|
||||
| Search console verification | `.env` or `site.config.ts` | `verification.google/bing` |
|
||||
| Navigation items | `src/config/nav.config.ts` | `navItems` array |
|
||||
| Default colour theme | `src/layouts/BaseLayout.astro` | `data-theme="blue"` |
|
||||
| Default dark/light mode | `src/layouts/BaseLayout.astro` | `class="dark"` on `<html>` |
|
||||
| JSON-LD schema switches | `src/pages/index.astro` | `includePersonSchema`, `includeOrgSchema` |
|
||||
| noindex / nofollow | any layout | `noindex={true}`, `nofollow={true}` |
|
||||
| Live theme selector (12 swatches) | `src/components/layout/ThemeSelector.astro` | `themes` array |
|
||||
| All 12 theme files | `src/styles/themes/` | One CSS file per theme |
|
||||
| Brand colour scale (light & dark) | `src/styles/themes/*.css` | `--brand-50` → `--brand-900` |
|
||||
| Typography (fonts, weight, tracking) | `src/styles/themes/*.css` | `--theme-font-*`, `--theme-heading-*` |
|
||||
| Border radius (global roundness) | `src/styles/themes/*.css` | `--theme-radius-sm/md/lg/xl/full` |
|
||||
| Shadow elevation levels | `src/styles/themes/*.css` | `--theme-shadow-sm/md/lg/xl` |
|
||||
| Inverted section colours | `src/styles/themes/*.css` | `--surface-invert`, `--on-invert` |
|
||||
| Header shape, position | `src/layouts/*.astro` | `shape`, `position` props |
|
||||
| Header show/hide toggles | `src/layouts/*.astro` | `showThemeToggle`, `showCta`, `showSocialLinks`, etc. |
|
||||
| Footer copyright text | `src/layouts/*.astro` | `copyright` prop |
|
||||
| Footer layout & columns | `src/layouts/*.astro` | `layout`, `columns` props |
|
||||
| Footer legal links | `src/layouts/*.astro` | `legalLinks` array |
|
||||
| Analytics IDs | `.env` | `PUBLIC_GA_MEASUREMENT_ID` etc. |
|
||||
|
||||
If you like Astro Rocket, a [star on GitHub](https://github.com/hansmartens68/astro-rocket) helps other developers find it. Takes two seconds.
|
||||
@@ -0,0 +1,172 @@
|
||||
---
|
||||
title: "Getting Started with Astro Rocket — From Install to Live in Minutes"
|
||||
description: "How to install, configure, and deploy Astro Rocket — covering site.config.ts, brand colours, navigation, writing posts, and deploying to Vercel."
|
||||
publishedAt: 2026-03-27
|
||||
author: "Hans Martens"
|
||||
tags: ["astro-rocket", "getting-started", "configuration", "vercel"]
|
||||
featured: true
|
||||
locale: en
|
||||
image: "../../../assets/blog/astro-rocket-getting-started.svg"
|
||||
svgSlug: "astro-rocket-getting-started"
|
||||
imageAlt: "Rocket icon above the words 'get started' on a dark background"
|
||||
---
|
||||
|
||||
This guide walks you through everything needed to get Astro Rocket running locally, configured to your brand, and deployed to the web. No previous Astro experience required.
|
||||
|
||||
## What you need before you start
|
||||
|
||||
- [Node.js](https://nodejs.org) version 22.12.0 or later
|
||||
- A package manager: `npm`, `pnpm`, or `yarn`
|
||||
- A [Vercel account](https://vercel.com) (free) for deployment
|
||||
- A code editor — [VS Code](https://code.visualstudio.com) with the [Astro extension](https://marketplace.visualstudio.com/items?itemName=astro-build.astro-vscode) is recommended
|
||||
|
||||
## Installation
|
||||
|
||||
Astro Rocket is free and open source. Clone the repository from GitHub and install dependencies:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/hansmartens68/astro-rocket my-site
|
||||
cd my-site
|
||||
pnpm install
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
Open `http://localhost:4321` and you'll see the full site running locally. Every change you make updates instantly without a full page reload.
|
||||
|
||||
## Configuring your site
|
||||
|
||||
Almost everything about the site is controlled from a single file: `src/config/site.config.ts`.
|
||||
|
||||
```ts
|
||||
const siteConfig: SiteConfig = {
|
||||
name: 'Your Site Name',
|
||||
description: 'A short description for search engines.',
|
||||
url: 'https://yoursite.com',
|
||||
author: 'Your Name',
|
||||
email: 'hello@yoursite.com',
|
||||
```
|
||||
|
||||
Start here. The `name` field populates the logo badge, page titles, footer copyright, and structured data. The `description` becomes the default meta description on any page that doesn't provide its own.
|
||||
|
||||
### Brand colour
|
||||
|
||||
Set your brand colour in the `branding.colors` section:
|
||||
|
||||
```ts
|
||||
branding: {
|
||||
colors: {
|
||||
themeColor: '#F94C10', // Your primary brand colour
|
||||
backgroundColor: '#ffffff', // Used in the web app manifest
|
||||
},
|
||||
},
|
||||
```
|
||||
|
||||
The `themeColor` drives every brand-coloured element across the site — buttons, links, the logo badge, highlights, and the favicon — through a single CSS custom property. Change it once and the entire site updates.
|
||||
|
||||
### Navigation
|
||||
|
||||
Open `src/config/nav.config.ts`. This is the only file you need to touch to change the site navigation:
|
||||
|
||||
```ts
|
||||
export const navItems: NavItem[] = [
|
||||
{ label: 'Home', href: '/' },
|
||||
{ label: 'Blog', href: '/blog' },
|
||||
{ label: 'About', href: '/about' },
|
||||
{ label: 'Contact', href: '/contact' },
|
||||
];
|
||||
```
|
||||
|
||||
Add, remove, or reorder items. The header and footer both read from this config automatically.
|
||||
|
||||
## Writing blog posts
|
||||
|
||||
Create a new `.mdx` file in `src/content/blog/en/`. Use this frontmatter template:
|
||||
|
||||
```mdx
|
||||
---
|
||||
title: "Your Post Title"
|
||||
description: "A short summary under 200 characters."
|
||||
publishedAt: 2026-03-15
|
||||
author: "Your Name"
|
||||
tags: ["tag-one", "tag-two"]
|
||||
featured: false
|
||||
locale: en
|
||||
image: "../../../assets/blog/your-image.svg"
|
||||
imageAlt: "Describe the image for screen readers."
|
||||
---
|
||||
|
||||
Your post content starts here. Standard Markdown works — headings,
|
||||
lists, links, bold, italic — plus any MDX components you need.
|
||||
```
|
||||
|
||||
Save the file and the post appears in the blog index immediately. No rebuilds, no cache clearing required.
|
||||
|
||||
### Cover images
|
||||
|
||||
Blog post cover images live in `src/assets/blog/`. The recommended format is SVG at 1200×630 pixels — the standard Open Graph image size. SVGs keep file sizes minimal and scale perfectly to every screen. PNG and JPG work equally well if you prefer photographs; Astro's `<Image>` component handles optimisation automatically.
|
||||
|
||||
## Setting up the contact form
|
||||
|
||||
The contact form uses [Resend](https://resend.com) for email delivery. To activate it:
|
||||
|
||||
1. Create a free Resend account and get your API key
|
||||
2. Verify your sending domain in Resend
|
||||
3. Set your `email` in `src/config/site.config.ts` — this is where form submissions are delivered
|
||||
4. Add your API key to `.env`:
|
||||
|
||||
```
|
||||
RESEND_API_KEY=re_xxxxxxxxxxxx
|
||||
```
|
||||
|
||||
Optionally, set `RESEND_FROM_EMAIL` to a verified sender address on your domain. If omitted, the form uses the `email` value from `site.config.ts` as the sender address.
|
||||
|
||||
The form works without these variables — it simply disables submission — so you can develop and deploy the rest of the site before setting up email.
|
||||
|
||||
## Deploying to Vercel
|
||||
|
||||
1. Create a new repository on GitHub (or GitLab / Bitbucket) and push your local project to it
|
||||
2. Go to [vercel.com](https://vercel.com) and click **Add New Project**
|
||||
3. Import your repository — Vercel detects Astro automatically
|
||||
4. Add your environment variables under **Settings → Environment Variables**
|
||||
5. Click **Deploy**
|
||||
|
||||
Your site is live. Every subsequent push to `main` triggers a new deployment automatically. Pull request previews are created for every branch.
|
||||
|
||||
Set your `SITE_URL` environment variable to your production domain:
|
||||
|
||||
```
|
||||
SITE_URL=https://yoursite.com
|
||||
```
|
||||
|
||||
This ensures canonical URLs, the sitemap, and structured data all use the correct domain.
|
||||
|
||||
## Deploying to Netlify
|
||||
|
||||
Astro Rocket also supports [Netlify](https://netlify.com) out of the box. A `netlify.toml` is included with the correct build settings and security headers pre-configured.
|
||||
|
||||
1. Create a new repository on GitHub (or GitLab / Bitbucket) and push your local project to it
|
||||
2. Go to [netlify.com](https://netlify.com), click **Add new site → Import an existing project**, and connect your repository
|
||||
3. Under **Site configuration → Environment variables**, add your variables including:
|
||||
|
||||
```
|
||||
SITE_URL=https://yoursite.com
|
||||
DEPLOY_TARGET=netlify
|
||||
```
|
||||
|
||||
4. Click **Deploy site**
|
||||
|
||||
Setting `DEPLOY_TARGET=netlify` tells the build to use the Netlify adapter instead of the default Vercel one. Everything else — the contact form, API routes, image optimisation — works identically on both platforms.
|
||||
|
||||
## What to do next
|
||||
|
||||
With the site live, here's a sensible order for next steps:
|
||||
|
||||
1. **Replace the placeholder content** — update the hero text, the about section, and landing page copy to reflect your actual work
|
||||
2. **Write your first real post** — publishing regularly is the highest-value thing you can do for search visibility
|
||||
3. **Set up Google Search Console** — add `GOOGLE_SITE_VERIFICATION` to your `.env` file and submit your sitemap
|
||||
4. **Add your social links** — the `socialLinks` array in `site.config.ts` populates the footer social icons
|
||||
5. **Set up analytics** — add `PUBLIC_GA_MEASUREMENT_ID` or `PUBLIC_GTM_ID` to activate the built-in analytics integration
|
||||
|
||||
The site is yours. Everything is documented, everything is changeable, and nothing is hidden behind abstractions you can't read.
|
||||
|
||||
If Astro Rocket saved you time, a [star on GitHub](https://github.com/hansmartens68/astro-rocket) helps other developers find it. Takes two seconds.
|
||||
@@ -0,0 +1,73 @@
|
||||
---
|
||||
title: "Astro Rocket Is Live — My New Open Source Astro Theme"
|
||||
description: "Astro Rocket is a production-ready Astro 6 theme with a full blog, 57 components, 12 colour themes, dark mode, SEO, and a contact form. Free and open source."
|
||||
publishedAt: 2026-03-28
|
||||
author: "Hans Martens"
|
||||
tags: ["astro-rocket", "open-source", "astro", "launch"]
|
||||
featured: true
|
||||
locale: en
|
||||
svgSlug: "astro-rocket-is-live"
|
||||
---
|
||||
|
||||
I'm so happy to share this: Astro Rocket is live. It's a fully built website theme for Astro 6 and Tailwind CSS v4, and it's free for anyone to use.
|
||||
|
||||
Astro Rocket is a fork of [Velocity](https://github.com/southwellmedia/velocity) by [Southwell Media](https://southwellmedia.com) — a powerful Astro boilerplate with a comprehensive design system and component library. All credit to the Southwell Media team for that work. That was the foundation. I built Astro Rocket on top of it with a different goal: a complete website you can launch immediately — you change the text, and your site is ready.
|
||||
|
||||
## Who is it for?
|
||||
|
||||
Astro Rocket is made for web designers, developers, bloggers, and anyone who needs a portfolio or business website. Every page is already built and styled — you update the text and your site is live.
|
||||
|
||||
## What I added to Velocity
|
||||
|
||||
Velocity is the foundation. Here's everything I built on top of it:
|
||||
|
||||
**Live theme switching with 12 colour themes** — Velocity required editing a CSS import file and rebuilding every time you wanted to change your brand colour. In Astro Rocket, you click one of the 12 colour swatches in the header and everything updates on screen instantly: the logo badge, the blog image gradients, and every brand colour across the site. No file edits, no rebuilds. Once you've settled on a colour, you remove the selector from the header with a single line deletion.
|
||||
|
||||
All 12 themes are shown as swatches in the header: Orange, Amber, Lime, Emerald, Teal, Cyan, Sky, Blue, Indigo, Violet, Purple, and Magenta — Blue is the default. Every theme is based on the official Tailwind CSS colour palette.
|
||||
|
||||
**Auto-generated logo badge** — Velocity required a custom logo file. Astro Rocket generates the logo automatically: the first letter of your site name on the active brand colour. It updates live when you switch themes.
|
||||
|
||||
**Auto-generated favicon** — no design tools needed. The favicon is an SVG pre-rendered at build time from `site.config.ts` — the same letter, the same colour as your logo badge.
|
||||
|
||||
**Blog image gradients** — every blog cover and card uses a brand-colour gradient background that updates live when the active theme changes.
|
||||
|
||||
**Unified icon system via Iconify** — Velocity had a basic SVG Icon component. Astro Rocket has a unified `Icon` component with 350+ Lucide UI icons and 3000+ Simple Icons brand icons, all via one component.
|
||||
|
||||
**Animated typing effect** — the hero section includes an animated typing effect that cycles through words, with configurable typing and delete speed.
|
||||
|
||||
**Full animation library** — smooth page transitions via Astro View Transitions, scroll-triggered counter and score animations, a scroll-reactive header, card hover effects, and a complete set of UI micro-animations for dropdowns, toasts, modals, tab switches, and more. All with full `prefers-reduced-motion` support.
|
||||
|
||||
**Scroll progress bar** — a thin 2px brand-coloured line in the header that fills from left to right as you scroll down the page. It's enabled on the homepage (sitting on top of the floating capsule header), the blog index, and individual post pages (both underneath the solid header). Each position is independently configurable via two `Header` props: `showScrollProgress` and `scrollProgressPosition`. [Read the full post](/blog/scroll-progress-bar).
|
||||
|
||||
**Dark mode hero gradient** — the homepage hero fades from your active brand colour at the top to pure black at the bottom in dark mode. One prop on the `Hero` component (`gradient`), zero JavaScript, zero impact on light mode. The homepage header is a floating capsule that lets the brand colour bleed through behind it. On all other pages the header is a full-width solid bar. [Read the full post](/blog/dark-mode-hero-gradient).
|
||||
|
||||
**sessionStorage for dark mode** — Velocity uses `localStorage`, which stores the user's preference permanently. I deliberately switched this to `sessionStorage`, so every new visitor sees the site the way it was designed: dark. Dark mode is a design choice here, not a user setting. I wrote a [full post explaining why](/blog/dark-mode-sessionstorage).
|
||||
|
||||
## What Astro Rocket ships with
|
||||
|
||||
Everything from Velocity is still there, and it's all fully integrated:
|
||||
|
||||
**A full blog** — built on Astro Content Collections with MDX support. Every post has a typed frontmatter schema, tag filtering, a related posts section, a reading time indicator, and automatically generated Open Graph metadata.
|
||||
|
||||
**57 components** — 31 UI components (Button, Input, Card, Badge, Avatar, Table, Tabs, Dialog, Accordion, and more), 7 pattern components (ContactForm, NewsletterForm, StatCard, and more), plus Hero, Header, Footer, BlogCard, ShareButtons, and SEO components.
|
||||
|
||||
**A complete SEO layer** — meta tags, Open Graph, Twitter Cards, JSON-LD structured data (WebSite, Organization, BlogPosting, Breadcrumb, FAQ), an auto-generated sitemap, and robots.txt.
|
||||
|
||||
**Static OG image** — a single polished default Open Graph image serves as the social preview for all pages and blog posts. No build-time generation required.
|
||||
|
||||
**Dark mode without a flash** — dark-first design with sessionStorage (see above), so every new visitor sees the site as intended.
|
||||
|
||||
**Contact form and newsletter form** — both connected to Resend for email delivery, with server-side Zod validation and honeypot spam protection.
|
||||
|
||||
**One-click deployment** — configuration files for Vercel, Netlify, and Cloudflare Pages are all included.
|
||||
|
||||
**Lighthouse 95+** across Performance, Accessibility, Best Practices, and SEO.
|
||||
|
||||
## Give it a star on GitHub
|
||||
|
||||
It's open source, MIT-licensed, and ready to use.
|
||||
|
||||
- **[Astro Rocket on GitHub](https://github.com/hansmartens68/astro-rocket)** — if you find it useful, a star on GitHub is hugely appreciated. It costs you one click and helps other developers discover the theme. Thank you!
|
||||
- **[Live demo → astrorocket.dev](https://astrorocket.dev)**
|
||||
|
||||
I'm looking forward to seeing what you build with it.
|
||||
@@ -0,0 +1,286 @@
|
||||
---
|
||||
title: "57 Components Ready to Use — Astro Rocket's Full UI Library"
|
||||
description: "Astro Rocket ships with 57 production-ready components from the Velocity library — buttons, cards, dialogs, forms, data display, and full page-structure components. All accessible, all themed."
|
||||
publishedAt: 2026-03-28
|
||||
author: "Hans Martens"
|
||||
tags: ["astro-rocket", "components", "ui", "velocity"]
|
||||
featured: false
|
||||
locale: en
|
||||
image: "../../../assets/blog/component-library.svg"
|
||||
svgSlug: "component-library"
|
||||
imageAlt: "Dashboard layout icon above the word 'components' with a large faint '57' in the background"
|
||||
---
|
||||
|
||||
Astro Rocket inherits a complete UI component library from [Velocity](https://github.com/southwellmedia/velocity) by Southwell Media. That means 57 production-ready components are available the moment you install the theme — no npm packages to install, no extra setup. Every component is already styled with the design system's color tokens, so they adapt automatically when you switch themes or toggle dark mode.
|
||||
|
||||
**See every component live:** [/components](/components)
|
||||
|
||||
The library is organized in three layers: 31 UI primitives that form the building blocks, 7 higher-level pattern components, and a set of page-structure components for the Hero, Header, Footer, Blog, and SEO layers.
|
||||
|
||||
---
|
||||
|
||||
## UI primitives
|
||||
|
||||
These 31 components cover every common UI need. They accept variants and sizes through props and are built with accessibility in mind — keyboard navigation, ARIA attributes, and focus management are handled for you.
|
||||
|
||||
### Form (7 components)
|
||||
|
||||
The form layer covers every standard input type.
|
||||
|
||||
| Component | What it does |
|
||||
|-----------|--------------|
|
||||
| `Button` | Primary interactive element. Supports `variant` (brand, secondary, outline, ghost, destructive) and `size` (sm, md, lg). Works as a `<button>` or renders as an `<a>` when an `href` is provided. |
|
||||
| `Input` | Single-line text input with label, helper text, and error state support. |
|
||||
| `Textarea` | Multi-line text input with the same label/error pattern as Input. |
|
||||
| `Select` | Styled native select element with the same form field pattern. |
|
||||
| `Checkbox` | Accessible checkbox with custom styling that follows the active theme. |
|
||||
| `Radio` | Radio button with the same theming as Checkbox. |
|
||||
| `Switch` | Toggle switch for on/off states, with smooth CSS transition. |
|
||||
|
||||
```astro
|
||||
---
|
||||
import Button from '@/components/ui/form/Button/Button.astro';
|
||||
import Input from '@/components/ui/form/Input/Input.astro';
|
||||
---
|
||||
|
||||
<Button variant="brand" size="md">Get started</Button>
|
||||
<Button variant="outline" size="md">Learn more</Button>
|
||||
|
||||
<Input label="Email address" type="email" placeholder="you@example.com" />
|
||||
```
|
||||
|
||||
### Data display (8 components)
|
||||
|
||||
| Component | What it does |
|
||||
|-----------|--------------|
|
||||
| `Card` | Container with border and rounded corners. Use it for any grouped content block. |
|
||||
| `Badge` | Small inline label. Supports `variant` (brand, secondary, success, warning, destructive) and `pill` prop for rounded style. |
|
||||
| `Avatar` | Circular image or initials fallback. |
|
||||
| `AvatarGroup` | Stacked row of Avatars for team or contributor lists. |
|
||||
| `Table` | Styled HTML table with header, body, and striped row support. |
|
||||
| `Pagination` | Page navigation with prev/next and numbered page buttons. |
|
||||
| `Progress` | Horizontal progress bar that fills to a percentage value. |
|
||||
| `Skeleton` | Loading placeholder that pulses in the shape of the content it replaces. |
|
||||
|
||||
```astro
|
||||
---
|
||||
import Badge from '@/components/ui/data-display/Badge/Badge.astro';
|
||||
import Card from '@/components/ui/data-display/Card/Card.astro';
|
||||
---
|
||||
|
||||
<Badge variant="brand" pill>New</Badge>
|
||||
<Badge variant="success">Active</Badge>
|
||||
|
||||
<Card>
|
||||
<p>Any content goes here.</p>
|
||||
</Card>
|
||||
```
|
||||
|
||||
### Feedback (3 components)
|
||||
|
||||
| Component | What it does |
|
||||
|-----------|--------------|
|
||||
| `Alert` | Inline message for info, success, warning, or error states. |
|
||||
| `Toast` | Temporary notification that appears in the corner of the screen and auto-dismisses. |
|
||||
| `Tooltip` | Text label that appears on hover or focus above any element. |
|
||||
|
||||
### Overlay (6 components)
|
||||
|
||||
| Component | What it does |
|
||||
|-----------|--------------|
|
||||
| `Accordion` | Collapsible content panels — any number of items, only one open at a time. |
|
||||
| `Dialog` | Modal window with backdrop, focus trapping, and keyboard close. |
|
||||
| `Dropdown` | Floating menu that opens below a trigger button. |
|
||||
| `Tabs` | Horizontal tab bar that switches between content panels. |
|
||||
| `VerticalTabs` | Same as Tabs but with the tab list on the left. Good for settings panels. |
|
||||
| `ConsentBanner` | Cookie consent banner with accept/decline actions. |
|
||||
|
||||
```astro
|
||||
---
|
||||
import Accordion from '@/components/ui/overlay/Accordion/Accordion.astro';
|
||||
---
|
||||
|
||||
<Accordion items={[
|
||||
{ title: 'What is Astro Rocket?', content: 'A production-ready Astro 6 theme.' },
|
||||
{ title: 'Is it free?', content: 'Yes. MIT licensed.' },
|
||||
]} />
|
||||
```
|
||||
|
||||
### Layout & content (3 components)
|
||||
|
||||
| Component | What it does |
|
||||
|-----------|--------------|
|
||||
| `Icon` | Unified icon component. Covers 350+ Lucide UI icons and 3,000+ Simple Icons brand icons via Iconify. |
|
||||
| `Separator` | Horizontal or vertical divider line. |
|
||||
| `CodeBlock` | Syntax-highlighted code block with copy button and language label. |
|
||||
|
||||
The `Icon` component is used throughout the theme. You reference any Lucide icon by name, or any Simple Icons brand icon by its shorthand name (e.g. `github`, `x-twitter`, `instagram`) or with the full `simple-icons:` prefix:
|
||||
|
||||
```astro
|
||||
---
|
||||
import Icon from '@/components/ui/primitives/Icon/Icon.astro';
|
||||
---
|
||||
|
||||
<Icon name="arrow-right" size="sm" />
|
||||
<Icon name="github" size="md" />
|
||||
<Icon name="simple-icons:vercel" size="md" />
|
||||
```
|
||||
|
||||
### Marketing UI (5 components)
|
||||
|
||||
These are UI-level marketing components, distinct from the full landing-page section components below.
|
||||
|
||||
| Component | What it does |
|
||||
|-----------|--------------|
|
||||
| `CTA` | Call-to-action block with heading, description, and button. |
|
||||
| `Logo` | Auto-generated logo badge — the first letter of your site name on the active brand color. Updates live on theme switch. |
|
||||
| `SocialProof` | Avatar stack plus a short proof statement (e.g., "Join 2,000+ developers"). |
|
||||
| `NpmCopyButton` | One-click npm install command with copy icon — useful for open-source project pages. |
|
||||
| `TerminalDemo` | Animated terminal window showing a sequence of commands. |
|
||||
|
||||
---
|
||||
|
||||
## Pattern components
|
||||
|
||||
Pattern components compose the UI primitives into specific real-world use cases. They are higher-level and more opinionated.
|
||||
|
||||
| Component | What it does |
|
||||
|-----------|--------------|
|
||||
| `ContactForm` | Full contact form connected to Resend. Server-side Zod validation and honeypot spam protection included. |
|
||||
| `NewsletterForm` | Email capture form, also connected to Resend. |
|
||||
| `StatCard` | Metric display card with a large number, label, and optional trend indicator. Used on dashboard or about pages. |
|
||||
| `EmptyState` | Placeholder for empty lists or zero-result searches. Accepts icon, heading, description, and action button. |
|
||||
| `FormField` | Shared label + input + error wrapper used internally by all form components. |
|
||||
| `SearchInput` | Input with a search icon and clear button. |
|
||||
| `PasswordInput` | Input with a show/hide password toggle. |
|
||||
|
||||
---
|
||||
|
||||
## Page-structure components
|
||||
|
||||
These components are fixed parts of the site architecture. They are used once per page type and are not generally composed with other components — they define the page.
|
||||
|
||||
**Hero** — the `Hero` component supports four layouts (`centered`, `split`, `minimal`, `card`) and three sizes. All hero content uses named slots: `badge`, `title`, `description`, `actions`, and optionally `image`.
|
||||
|
||||
**Header** — fixed top navigation bar with logo, nav links, dark mode toggle, theme selector, and optional CTA button. Three rendering variants: `solid` (solid bar), `transparent` (transparent, used on blog posts), and `floating` (the capsule header on the homepage). Includes the scroll progress bar when enabled.
|
||||
|
||||
**Footer** — bottom navigation with site links, social icons, and copyright text.
|
||||
|
||||
**Breadcrumbs** — auto-generated from the current URL path. Used at the top of blog posts and inner pages.
|
||||
|
||||
**ThemeToggle / ThemeSelector** — dark/light mode toggle and the live theme switcher with 12-colour swatch picker.
|
||||
|
||||
**Blog components** — `BlogCard` (post listing card with cover image, tags, and reading time), `ArticleHero` (full-width post header with cover image and metadata), `RelatedPosts` (three-card section at the bottom of every post), `ShareButtons` (Twitter/X, LinkedIn, clipboard copy), and `BlogImageSVG` (the brand-color SVG backgrounds for post covers).
|
||||
|
||||
**Landing page sections** — `Credibility` (logo bar), `FeatureTabs` (interactive tabbed feature showcase), `LighthouseScores` (animated score dials), and `TechStack` (technology badge strip).
|
||||
|
||||
**SEO** — `SEO` (all meta tags, Open Graph, Twitter Cards), `JsonLd` (structured data for WebSite, Organization, BlogPosting, Breadcrumb, FAQ), and `Analytics` (analytics script slot).
|
||||
|
||||
---
|
||||
|
||||
## Everything responds to the active theme
|
||||
|
||||
Every component uses the design system's CSS color tokens — `--color-brand-500`, `--color-foreground`, `--color-background`, and so on. Switch from Emerald to Purple and every button, badge, progress bar, and focus ring updates instantly. No component has a hardcoded brand color.
|
||||
|
||||
The same applies to dark mode: all token values change when the `dark` class is toggled on `<html>`, and every component updates without any extra code.
|
||||
|
||||
---
|
||||
|
||||
## Adding your own components
|
||||
|
||||
When you need something that isn't in the library, follow the same patterns used throughout the codebase to create new components that fit the existing system.
|
||||
|
||||
### Where to put it
|
||||
|
||||
Place new components in the matching subdirectory under `src/components/`:
|
||||
|
||||
```
|
||||
src/components/
|
||||
├── ui/
|
||||
│ ├── form/ # Button, Input, Select, Checkbox, Radio, Switch, Textarea
|
||||
│ ├── data-display/ # Card, Badge, Avatar, Table, Pagination, Progress, Skeleton
|
||||
│ ├── feedback/ # Alert, Toast, Tooltip
|
||||
│ ├── overlay/ # Dialog, Dropdown, Tabs, VerticalTabs, Accordion
|
||||
│ ├── layout/ # Separator
|
||||
│ ├── primitives/ # Icon
|
||||
│ ├── content/ # CodeBlock
|
||||
│ └── marketing/ # Logo, CTA, NpmCopyButton, SocialProof, TerminalDemo
|
||||
├── patterns/ # Composed patterns (ContactForm, SearchInput, etc.)
|
||||
├── layout/ # Page structure (Header, Footer)
|
||||
├── blog/ # Blog-specific
|
||||
└── landing/ # Marketing pages
|
||||
```
|
||||
|
||||
### Basic component structure
|
||||
|
||||
A typical UI component defines its props in the frontmatter, maps variants to class strings, and uses `class:list` to compose the final class:
|
||||
|
||||
```astro
|
||||
---
|
||||
interface Props {
|
||||
variant?: 'default' | 'primary' | 'success';
|
||||
size?: 'sm' | 'md';
|
||||
class?: string;
|
||||
}
|
||||
|
||||
const {
|
||||
variant = 'default',
|
||||
size = 'md',
|
||||
class: className = '',
|
||||
} = Astro.props;
|
||||
|
||||
const variants = {
|
||||
default: 'bg-background-secondary text-foreground',
|
||||
primary: 'bg-primary text-primary-foreground',
|
||||
success: 'bg-success-light text-success-foreground',
|
||||
};
|
||||
|
||||
const sizes = {
|
||||
sm: 'px-2 py-0.5 text-xs',
|
||||
md: 'px-3 py-1 text-sm',
|
||||
};
|
||||
---
|
||||
<span
|
||||
class:list={[
|
||||
'inline-flex items-center rounded-full font-medium',
|
||||
variants[variant],
|
||||
sizes[size],
|
||||
className,
|
||||
]}
|
||||
>
|
||||
<slot />
|
||||
</span>
|
||||
```
|
||||
|
||||
### Three rules to follow
|
||||
|
||||
**Use design tokens, not hardcoded values.** Every built-in component uses `bg-background`, `text-foreground`, `border-border`, and the brand tokens. If you hardcode `bg-white` or `text-gray-900`, your component will break in dark mode and ignore the active colour theme.
|
||||
|
||||
**Support named slots for flexible content.** Slots let the consumer pass in icons, actions, or footer content without the component needing to know about them:
|
||||
|
||||
```astro
|
||||
<FeatureCard title="Fast">
|
||||
<Icon name="zap" slot="icon" />
|
||||
Lightning quick builds.
|
||||
<Button slot="footer">Learn more</Button>
|
||||
</FeatureCard>
|
||||
```
|
||||
|
||||
**Use React for interactivity.** Astro components are server-rendered and have no client-side state. For anything that needs `useState`, `onClick`, or real-time updates, create a `.tsx` file and use the `client:visible` directive when you include it:
|
||||
|
||||
```astro
|
||||
---
|
||||
import { Counter } from '@/components/ui';
|
||||
---
|
||||
<Counter client:visible />
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Browse the live demo
|
||||
|
||||
The fastest way to get a feel for all 57 components is the live component demo. Every component is rendered with live props you can inspect:
|
||||
|
||||
**[/components](/components)**
|
||||
|
||||
This showcase is built into Astro Rocket itself. All 57 components are rendered there — same markup, same variants, same props you will use in your own project.
|
||||
@@ -0,0 +1,123 @@
|
||||
---
|
||||
title: "Dark Mode Hero Gradient — Brand Colour at the Top, Black at the Bottom"
|
||||
description: "Astro Rocket's homepage hero fades from your active brand colour at the top to pure black at the bottom in dark mode. One prop on the Hero component, zero JavaScript, zero impact on light mode."
|
||||
publishedAt: 2026-03-28
|
||||
author: "Hans Martens"
|
||||
tags: ["astro-rocket", "dark-mode", "css", "design", "features"]
|
||||
featured: false
|
||||
locale: en
|
||||
image: "../../../assets/blog/dark-mode-hero-gradient.svg"
|
||||
svgSlug: "dark-mode-hero-gradient"
|
||||
imageAlt: "Stacked gradient bands fading from brand colour to dark above the word 'gradient' on a brand background"
|
||||
---
|
||||
|
||||
The homepage hero in Astro Rocket carries a dark mode gradient: your active brand colour at the top, fading into pure black at the bottom. Switch to light mode and nothing changes — the effect is fully scoped to `.dark`. On every other page, the hero uses its standard background with no gradient.
|
||||
|
||||
## What it looks like
|
||||
|
||||
In dark mode, the homepage hero opens with your theme's `--brand-700` colour at the very top and transitions smoothly down to pure black — `oklch(0% 0 0)`. The brand colour shows through behind the floating header, creating a seamless visual connection between the navigation and the hero content below.
|
||||
|
||||
In light mode, the hero uses its normal background unchanged. The gradient is a pure dark-mode enhancement — your light-mode design is untouched.
|
||||
|
||||
## The CSS
|
||||
|
||||
A single utility class in `src/styles/global.css` does the work:
|
||||
|
||||
```css
|
||||
/* Dark mode hero gradient: brand to black */
|
||||
.dark .hero-dark-gradient {
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
var(--brand-700) 0%,
|
||||
oklch(0% 0 0) 100%
|
||||
) !important;
|
||||
}
|
||||
```
|
||||
|
||||
The `!important` is safe here: the rule is fully scoped to `.dark`, so it only applies in dark mode. It overrides the `bg-background` Tailwind utility on the hero element without leaking into light mode.
|
||||
|
||||
A second rule keeps brand-coloured heading text at its original shade inside the gradient. In dark mode, `text-brand-500` on a `<h1>` or `<h2>` inside a gradient section stays at `--brand-500` — readable against the brand-coloured top of the gradient:
|
||||
|
||||
```css
|
||||
.dark .hero-dark-gradient :is(h1, h2) .text-brand-500 {
|
||||
color: var(--brand-500);
|
||||
-webkit-text-fill-color: var(--brand-500);
|
||||
}
|
||||
```
|
||||
|
||||
Both rules live in the "Dark mode hero gradient" section at the bottom of `global.css`. No images, no JavaScript, no build step required.
|
||||
|
||||
## Where it is applied
|
||||
|
||||
### Homepage hero
|
||||
|
||||
The `Hero` component accepts a `gradient` boolean prop. When `gradient` is set, the component adds `hero-dark-gradient` to its section element:
|
||||
|
||||
```ts
|
||||
// src/components/hero/hero.variants.ts (simplified)
|
||||
const sectionClasses = cn(
|
||||
heroSectionVariants({ size }),
|
||||
gradient && 'hero-dark-gradient',
|
||||
className
|
||||
);
|
||||
```
|
||||
|
||||
On the homepage, the Hero is called with `gradient`:
|
||||
|
||||
```astro
|
||||
<!-- src/pages/index.astro -->
|
||||
<Hero layout="centered" size="xl" gradient class="sticky top-0 z-0 overflow-clip">
|
||||
```
|
||||
|
||||
No other page passes `gradient`, so the effect stays exclusive to the homepage.
|
||||
|
||||
## Adding it to your own sections
|
||||
|
||||
Any section or Hero that should carry the gradient in dark mode needs just one change. For a `<Hero>` component, add the prop:
|
||||
|
||||
```astro
|
||||
<Hero gradient>
|
||||
```
|
||||
|
||||
For a plain section element:
|
||||
|
||||
```html
|
||||
<section class="your-existing-classes hero-dark-gradient">
|
||||
```
|
||||
|
||||
In dark mode the gradient overrides the background. In light mode the class has no visual effect whatsoever — you can add it safely without touching your light-mode design.
|
||||
|
||||
To remove the gradient from the homepage, delete the `gradient` prop from the `<Hero>` in `src/pages/index.astro`. The normal `bg-background` immediately takes over.
|
||||
|
||||
## The floating header
|
||||
|
||||
The homepage header is a floating capsule — it sits above the hero content and lets the gradient show through behind it. The `LandingLayout` uses:
|
||||
|
||||
```astro
|
||||
<Header
|
||||
shape="floating"
|
||||
variant="default"
|
||||
colorScheme="default"
|
||||
position="fixed"
|
||||
showThemeSelector
|
||||
showScrollProgress={isHomePage}
|
||||
scrollProgressPosition="top"
|
||||
/>
|
||||
```
|
||||
|
||||
The `variant="default"` gives the header a semi-transparent background and backdrop blur. At the top of the page the gradient colour bleeds through; once you scroll past 60 px, the header gains a solid fill via the `data-scrolled` attribute. On all other pages the header is a full-width solid bar — the floating style is exclusive to the landing/homepage layout.
|
||||
|
||||
## How the colours work
|
||||
|
||||
The gradient endpoints use the active theme's tokens:
|
||||
|
||||
| Stop | Value | What it does |
|
||||
|------|-------|--------------|
|
||||
| `0%` | `var(--brand-700)` | A deep, saturated shade of your active brand colour — vivid at the very top |
|
||||
| `100%` | `oklch(0% 0 0)` | Pure black — theme-agnostic, always dark at the bottom |
|
||||
|
||||
`--brand-700` sits two steps darker than the primary brand accent (`--brand-500`). When you switch themes in the header selector, the gradient top colour updates instantly — no rebuild, no page reload.
|
||||
|
||||
## Light mode behaviour
|
||||
|
||||
In light mode there is nothing to disable or special-case. The `.dark .hero-dark-gradient` rule does not match when `.dark` is absent, so the `gradient` prop on the Hero has no visual effect in light mode. You can leave it in place without affecting light-mode visitors at all.
|
||||
@@ -0,0 +1,136 @@
|
||||
---
|
||||
title: "Why Astro Rocket Uses sessionStorage for Dark Mode (Not localStorage)"
|
||||
description: "Dark mode is the default experience in Astro Rocket — and that's a deliberate design decision. Here's the reasoning, the code, and exactly how to change it."
|
||||
publishedAt: 2026-03-15
|
||||
author: "Hans Martens"
|
||||
tags: ["dark-mode", "astro-rocket", "design", "tutorial"]
|
||||
featured: false
|
||||
locale: en
|
||||
image: "../../../assets/blog/dark-mode-sessionstorage.svg"
|
||||
svgSlug: "dark-mode-sessionstorage"
|
||||
imageAlt: "Split panel showing dark mode on the left with a moon and sessionStorage badge, and light mode on the right with a sun and localStorage badge"
|
||||
---
|
||||
|
||||
Most dark mode implementations ask: "what did the user last choose?" Astro Rocket asks a different question: "what is the right default for this site?" The answer is dark — always — and `sessionStorage` is the technical expression of that decision.
|
||||
|
||||
This post explains the reasoning, shows exactly how the implementation works, and gives you the precise code to change it if your own site calls for something different.
|
||||
|
||||
|
||||
## The design decision
|
||||
|
||||
Dark mode is not a fallback for Astro Rocket. It is the primary visual experience. The typography, colour palette, and contrast ratios were all designed and tested in dark mode first. Light mode works and is fully supported, but the site is at its best in the dark.
|
||||
|
||||
`sessionStorage` encodes this intent precisely. The site always loads in dark mode. If a visitor switches to light during a session, that preference is respected for the duration of that visit. When they open a new tab or return the next day, they are back in the dark — the designed state.
|
||||
|
||||
`localStorage` would say "your last choice is always right." `sessionStorage` says "dark is the default; light is available when you need it." For this site, that distinction matters.
|
||||
|
||||
## How it works
|
||||
|
||||
The implementation has three parts.
|
||||
|
||||
### 1. The HTML default
|
||||
|
||||
`BaseLayout.astro` renders the `<html>` element with `class="dark"` baked in:
|
||||
|
||||
```html
|
||||
<html lang="en" class="scroll-smooth dark" data-theme="blue">
|
||||
```
|
||||
|
||||
This means the server always sends dark markup. There is no flash of an incorrect theme on load, regardless of what any script does next.
|
||||
|
||||
### 2. The inline script (before first paint)
|
||||
|
||||
An inline `<script is:inline>` runs immediately in `<head>`, before the browser paints anything. It checks `sessionStorage` and removes the `dark` class only if the visitor explicitly chose light in this session:
|
||||
|
||||
```js
|
||||
if (sessionStorage.getItem('theme') === 'light') {
|
||||
el.classList.remove('dark');
|
||||
} else {
|
||||
el.classList.add('dark'); // dark is the default — always
|
||||
}
|
||||
```
|
||||
|
||||
Because `dark` is the HTML default and the script only removes it, there is no flash in either direction. Dark-mode visitors see dark immediately. Light-mode visitors see light immediately, with a single class removal so fast it is invisible.
|
||||
|
||||
### 3. The toggle
|
||||
|
||||
`ThemeToggle.astro` writes to `sessionStorage` on each click:
|
||||
|
||||
```js
|
||||
toggle.addEventListener('click', () => {
|
||||
const isDark = document.documentElement.classList.contains('dark');
|
||||
|
||||
if (isDark) {
|
||||
document.documentElement.classList.remove('dark');
|
||||
sessionStorage.setItem('theme', 'light');
|
||||
} else {
|
||||
document.documentElement.classList.add('dark');
|
||||
sessionStorage.removeItem('theme'); // dark needs no storage — it is the default
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
Note that switching back to dark removes the key entirely rather than storing `'dark'`. Dark does not need to be remembered — it is what the site is.
|
||||
|
||||
## How to switch to localStorage
|
||||
|
||||
If you are building a site for a general audience and want the user's preference to persist across sessions, two small changes are all it takes.
|
||||
|
||||
**In `BaseLayout.astro`**, change the inline script's storage check:
|
||||
|
||||
```js
|
||||
// Before
|
||||
if (sessionStorage.getItem('theme') === 'light') {
|
||||
|
||||
// After
|
||||
if (localStorage.getItem('theme') === 'light') {
|
||||
```
|
||||
|
||||
**In `ThemeToggle.astro`**, update both reads and writes in the click handler:
|
||||
|
||||
```js
|
||||
// Before
|
||||
sessionStorage.setItem('theme', 'light');
|
||||
sessionStorage.removeItem('theme');
|
||||
|
||||
// After
|
||||
localStorage.setItem('theme', 'light');
|
||||
localStorage.removeItem('theme');
|
||||
```
|
||||
|
||||
That is the complete change. The toggle now remembers the user's choice permanently — across tabs, across sessions, across visits.
|
||||
|
||||
## The third option: respect the OS preference
|
||||
|
||||
If you want to defer entirely to the user's operating system setting — the most widely recommended approach for general-purpose sites — replace the `sessionStorage` check in `BaseLayout.astro` with a `prefers-color-scheme` media query:
|
||||
|
||||
```js
|
||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
const stored = localStorage.getItem('theme');
|
||||
|
||||
if (stored === 'light' || (!stored && !prefersDark)) {
|
||||
el.classList.remove('dark');
|
||||
} else {
|
||||
el.classList.add('dark');
|
||||
}
|
||||
```
|
||||
|
||||
This gives you a layered fallback: a stored preference wins if it exists; otherwise the OS setting is used. A visitor who has set their system to light mode gets light mode automatically. One who has set it to dark gets dark. The toggle still works and overrides both.
|
||||
|
||||
This is the right pattern for a site with a broad, unknown audience. It is more work to maintain, but it is the most respectful of user intent.
|
||||
|
||||
## Which approach is right for your site?
|
||||
|
||||
| | sessionStorage | localStorage | OS preference |
|
||||
|---|---|---|---|
|
||||
| Dark is always the default | ✓ | only if set first | depends on system |
|
||||
| No flash on load | ✓ | ✓ | ✓ |
|
||||
| Preference survives new tab | ✗ | ✓ | n/a |
|
||||
| Preference survives next visit | ✗ | ✓ | n/a |
|
||||
| Respects OS dark/light setting | ✗ | ✗ | ✓ |
|
||||
| Best for opinionated design sites | ✓ | — | — |
|
||||
| Best for general-audience sites | — | ✓ | ✓ |
|
||||
|
||||
Astro Rocket uses `sessionStorage` because the site has a clear visual identity and dark mode is part of it. If you are building a business site, a documentation portal, or anything serving a broad audience, `localStorage` or OS preference is the more considerate default.
|
||||
|
||||
The code is yours. Change two lines and it behaves exactly the way your users expect.
|
||||
@@ -0,0 +1,141 @@
|
||||
---
|
||||
title: "How Astro Rocket's Design System Works — Tokens, Colors, and Dark Mode"
|
||||
description: "Astro Rocket uses a three-tier token architecture with OKLCH colors. Change one value and the entire site updates. Here's how it works and how to make it yours."
|
||||
publishedAt: 2026-03-16
|
||||
author: "Hans Martens"
|
||||
tags: ["astro-rocket", "design-system", "tailwind", "customization", "tutorial"]
|
||||
featured: false
|
||||
locale: en
|
||||
image: "../../../assets/blog/design-system-color-tokens.svg"
|
||||
svgSlug: "design-system-color-tokens"
|
||||
imageAlt: "Palette icon above the words 'design system' on a dark background"
|
||||
---
|
||||
|
||||
Most themes give you a stylesheet full of hardcoded values. Change the brand colour and you are hunting through dozens of files. Astro Rocket works differently — one change propagates everywhere, instantly, in both light and dark mode.
|
||||
|
||||
The reason is a three-tier token architecture. Understanding it takes ten minutes. Using it makes every customisation faster and more consistent.
|
||||
|
||||
## The three-tier architecture
|
||||
|
||||
Tokens are organised in three layers, each building on the one below.
|
||||
|
||||
### Tier 1 — Reference tokens
|
||||
|
||||
Reference tokens are raw values: specific colors, sizes, radii. They have no semantic meaning — they just define the palette.
|
||||
|
||||
```css
|
||||
--color-lime-500: oklch(0.768 0.233 130.85);
|
||||
--color-lime-600: oklch(0.702 0.213 130.85);
|
||||
```
|
||||
|
||||
You rarely touch these directly. They are the raw material.
|
||||
|
||||
### Tier 2 — Semantic tokens
|
||||
|
||||
Semantic tokens map intent to reference values. `--color-brand-500` does not know what hex value it holds — it knows what role it plays.
|
||||
|
||||
```css
|
||||
:root {
|
||||
--color-brand-500: var(--color-lime-500);
|
||||
--color-brand-600: var(--color-lime-600);
|
||||
}
|
||||
```
|
||||
|
||||
This is the layer you edit when switching themes. Change which reference token feeds into `--color-brand-500` and every button, link, badge, and highlight on the entire site updates in one move.
|
||||
|
||||
### Tier 3 — Component tokens
|
||||
|
||||
Component tokens are scoped to specific UI elements. The button does not reach for `--color-brand-500` directly — it uses `--btn-primary-bg`, which itself points to `--color-brand-500`.
|
||||
|
||||
```css
|
||||
--btn-primary-bg: var(--color-brand-500);
|
||||
--btn-primary-bg-hover: var(--color-brand-600);
|
||||
```
|
||||
|
||||
This means you can restyle a single component without disturbing anything else. Change `--btn-primary-bg` and only buttons change. The badge, the logo highlight, the link colour — all untouched.
|
||||
|
||||
## Why OKLCH
|
||||
|
||||
Astro Rocket uses [OKLCH](https://oklch.com/) for all colour values instead of hex or HSL.
|
||||
|
||||
OKLCH describes colour in three dimensions: lightness (`L`), chroma (`C`), and hue (`H`). The key advantage is perceptual uniformity — two colours with the same `L` value look equally bright to the human eye. Hex and HSL do not guarantee this, which is why manually constructed palettes often look uneven.
|
||||
|
||||
In practice, this means the design token palette stays visually consistent across the full range of shades without manual correction. Dark mode colours remain readable. Brand accents stay vivid without looking garish.
|
||||
|
||||
```css
|
||||
/* The same hue (130.85°) across lightness levels stays visually balanced */
|
||||
--color-lime-300: oklch(0.897 0.181 130.85);
|
||||
--color-lime-500: oklch(0.768 0.233 130.85);
|
||||
--color-lime-700: oklch(0.593 0.174 130.85);
|
||||
```
|
||||
|
||||
## Dark mode through tokens
|
||||
|
||||
Dark mode in Astro Rocket is not a separate stylesheet. It is the same token system with different values applied under the `.dark` selector.
|
||||
|
||||
```css
|
||||
:root {
|
||||
--color-background: oklch(0.98 0 0);
|
||||
--color-foreground: oklch(0.15 0 0);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--color-background: oklch(0.10 0.02 265);
|
||||
--color-foreground: oklch(0.95 0 0);
|
||||
}
|
||||
```
|
||||
|
||||
When the `dark` class is toggled on `<html>`, every semantic token remaps simultaneously. No JavaScript. No style recalculation cascade. One class change, entire site repaints.
|
||||
|
||||
Component tokens inherit this automatically because they point to semantic tokens. `--btn-primary-bg` → `--color-brand-500` → correct value for the active mode. Nothing in the component layer needs to know about dark mode at all.
|
||||
|
||||
## Changing the brand colour
|
||||
|
||||
The base brand colour scale lives in `src/styles/tokens/primitives.css`. To switch from lime to, say, violet:
|
||||
|
||||
**1. Add or verify your reference tokens exist:**
|
||||
|
||||
```css
|
||||
--color-violet-500: oklch(0.606 0.258 292.72);
|
||||
--color-violet-600: oklch(0.541 0.229 292.72);
|
||||
```
|
||||
|
||||
**2. Remap the semantic brand tokens:**
|
||||
|
||||
```css
|
||||
:root {
|
||||
--color-brand-500: var(--color-violet-500);
|
||||
--color-brand-600: var(--color-violet-600);
|
||||
}
|
||||
```
|
||||
|
||||
That is the complete change. Every button, link, badge, logo badge, favicon letter, and highlighted text across the entire site now uses violet — in both light and dark mode — without touching a single component file.
|
||||
|
||||
The `themeColor` value in `site.config.ts` controls the browser's `<meta name="theme-color">` tag (the mobile browser chrome colour) and should be updated to match:
|
||||
|
||||
```ts
|
||||
themeColor: '#7c3aed', // violet-600 as hex for browser chrome
|
||||
```
|
||||
|
||||
## The Tailwind connection
|
||||
|
||||
Astro Rocket uses Tailwind CSS v4, which reads CSS custom properties directly. The `brand-500` utility class maps to `--color-brand-500` — no `tailwind.config.js` entry required.
|
||||
|
||||
```html
|
||||
<!-- This reads --color-brand-500 automatically -->
|
||||
<span class="text-brand-500">highlighted text</span>
|
||||
```
|
||||
|
||||
When you update the semantic token, the Tailwind utility updates with it. You do not need to touch the templates.
|
||||
|
||||
## What this means in practice
|
||||
|
||||
The token system is not an abstraction for its own sake. It has three concrete benefits:
|
||||
|
||||
**Single source of truth** — brand colour, spacing scale, border radius, and typography sizes are all defined once. Inconsistencies cannot creep in because there is only one value to change.
|
||||
|
||||
**Safe component customisation** — you can restyle a specific component at the component token level without risking side effects elsewhere. The blast radius of any change is exactly as large as you intend.
|
||||
|
||||
**Dark mode for free** — because all colours are semantic, every new component you build inherits dark mode automatically as long as you use token-based utilities rather than hardcoded values.
|
||||
|
||||
The next time you want to adjust the site's look — a slightly different brand hue, a tighter border radius, a different heading weight — start in `src/styles/tokens/primitives.css` or the relevant theme file in `src/styles/themes/`. The change will be smaller than you expect.
|
||||
@@ -0,0 +1,345 @@
|
||||
---
|
||||
title: "The Hero Typing Effect in Astro Rocket — How It Works and How to Tune It"
|
||||
description: "Astro Rocket's hero headline cycles through words with a typing animation. Learn how it works, how to tune every speed and pause, and how to disable it entirely."
|
||||
publishedAt: 2026-03-16
|
||||
author: "Hans Martens"
|
||||
tags: ["astro-rocket", "components", "customization", "tutorial", "javascript"]
|
||||
featured: false
|
||||
locale: en
|
||||
image: "../../../assets/blog/hero-typing-effect.svg"
|
||||
svgSlug: "hero-typing-effect"
|
||||
imageAlt: "Rocket icon above the words 'typing effect' on a dark background"
|
||||
---
|
||||
|
||||
Open the Astro Rocket About page and the headline does not sit still. It types one word, pauses, deletes it, and types the next — looping forever. This post explains exactly how that effect is built, what each value controls, and how to make it faster, slower, or gone entirely.
|
||||
|
||||
## Where the component lives
|
||||
|
||||
The entire effect is self-contained in one file:
|
||||
|
||||
```
|
||||
src/components/ui/TypingEffect.astro
|
||||
```
|
||||
|
||||
It is a standard Astro component with a scoped `<style>` block for the cursor blink and a `<script>` block for the typing logic. No third-party library, no external dependency.
|
||||
|
||||
## Where it is used
|
||||
|
||||
The component currently lives in the About page hero, inside a brand-coloured `<span>`:
|
||||
|
||||
```astro
|
||||
<h1 slot="title">
|
||||
<span class="text-foreground [-webkit-text-fill-color:currentColor]">Astro Rocket —</span><br />
|
||||
<span class="text-brand-500 [-webkit-text-fill-color:var(--color-brand-500)]">
|
||||
<TypingEffect words={["Web Designer", "Web Developer", "Astro Developer", "Blogger", "Coffee lover"]} />
|
||||
</span>
|
||||
</h1>
|
||||
```
|
||||
|
||||
The homepage hero uses static text. The typing effect was kept on the About page where it acts as a personal "who am I" cycling statement rather than a product tagline.
|
||||
|
||||
You can place `<TypingEffect>` inside any heading or inline context. The `words` prop is the only required value.
|
||||
|
||||
## How the animation works
|
||||
|
||||
The component uses a single recursive `setTimeout` loop — no `setInterval`, no `requestAnimationFrame`. Each call to `tick()` decides whether to add or remove one character, then schedules the next call after the appropriate delay.
|
||||
|
||||
```
|
||||
Start
|
||||
└─ wait 600 ms (initial settle delay)
|
||||
└─ tick()
|
||||
├─ typing: add one character, wait typeSpeed ms
|
||||
│ └─ when word is complete: wait pauseAfterType ms, then switch to deleting
|
||||
└─ deleting: remove one character, wait deleteSpeed ms
|
||||
└─ when empty: wait pauseAfterDelete ms, advance to next word, switch to typing
|
||||
```
|
||||
|
||||
The 600 ms initial delay exists so the animation does not start mid-paint on a slow connection.
|
||||
|
||||
The full script, exactly as it runs today:
|
||||
|
||||
```js
|
||||
function startTyping() {
|
||||
const root = document.getElementById(id);
|
||||
if (!root) return;
|
||||
const textEl = root.querySelector('.typing-text');
|
||||
|
||||
// Lock the element width to the widest word so the layout never shifts
|
||||
const measurer = document.createElement('span');
|
||||
measurer.setAttribute('aria-hidden', 'true');
|
||||
measurer.style.cssText = 'visibility:hidden;position:absolute;white-space:nowrap;pointer-events:none;';
|
||||
const cs = getComputedStyle(root);
|
||||
measurer.style.font = cs.font;
|
||||
measurer.style.letterSpacing = cs.letterSpacing;
|
||||
document.body.appendChild(measurer);
|
||||
|
||||
let maxWidth = 0;
|
||||
for (const word of words) {
|
||||
measurer.textContent = word + '|'; // include cursor character in measurement
|
||||
maxWidth = Math.max(maxWidth, measurer.offsetWidth);
|
||||
}
|
||||
document.body.removeChild(measurer);
|
||||
root.style.minWidth = maxWidth + 'px';
|
||||
|
||||
let wordIndex = 0;
|
||||
let charIndex = 0;
|
||||
let isDeleting = false;
|
||||
let timer;
|
||||
|
||||
function tick() {
|
||||
const current = words[wordIndex];
|
||||
|
||||
if (isDeleting) {
|
||||
charIndex--;
|
||||
textEl.textContent = current.slice(0, charIndex);
|
||||
|
||||
if (charIndex === 0) {
|
||||
isDeleting = false;
|
||||
wordIndex = (wordIndex + 1) % words.length;
|
||||
timer = setTimeout(tick, pauseAfterDelete);
|
||||
return;
|
||||
}
|
||||
timer = setTimeout(tick, deleteSpeed);
|
||||
} else {
|
||||
charIndex++;
|
||||
textEl.textContent = current.slice(0, charIndex);
|
||||
|
||||
if (charIndex === current.length) {
|
||||
isDeleting = true;
|
||||
timer = setTimeout(tick, pauseAfterType);
|
||||
return;
|
||||
}
|
||||
timer = setTimeout(tick, typeSpeed);
|
||||
}
|
||||
}
|
||||
|
||||
// Start after a short initial delay so the page paint settles
|
||||
timer = setTimeout(tick, 600);
|
||||
|
||||
// Clean up pending timer when navigating away
|
||||
document.addEventListener('astro:before-swap', () => clearTimeout(timer), { once: true });
|
||||
}
|
||||
|
||||
// Run on initial load and on every client-side navigation back to this page
|
||||
document.addEventListener('astro:page-load', startTyping);
|
||||
```
|
||||
|
||||
## Why this component forks from the obvious implementation
|
||||
|
||||
A naive typing effect takes about twenty lines: set an interval, increment a character index, write to a DOM node. That works fine in isolation. Astro Rocket runs Astro's `ClientRouter` for client-side navigation, lives in a heading (where descenders are visible), and cycles through words of different lengths — each of those facts breaks the naive version in a different way. Here is what was added and why.
|
||||
|
||||
### Fix 1 — Client-side navigation (astro:page-load)
|
||||
|
||||
Astro's `ClientRouter` swaps pages by replacing DOM nodes without a full browser reload. A plain top-level script runs once when the browser first parses the page. When the user clicks a link and then hits back, the DOM is swapped back in but the script does not re-run — the animation stays frozen.
|
||||
|
||||
The fix wraps the entire animation in a `startTyping()` function and registers it on `astro:page-load`, which Astro fires on both the initial load *and* every subsequent client-side navigation:
|
||||
|
||||
```js
|
||||
document.addEventListener('astro:page-load', startTyping);
|
||||
```
|
||||
|
||||
The companion cleanup is equally important. If a pending `setTimeout` from a previous visit is still in flight when the user navigates away, it can fire against a DOM element that no longer exists. `astro:before-swap` fires just before Astro tears down the current page, so clearing the timer there prevents stale callbacks:
|
||||
|
||||
```js
|
||||
document.addEventListener('astro:before-swap', () => clearTimeout(timer), { once: true });
|
||||
```
|
||||
|
||||
`{ once: true }` ensures the listener removes itself after the first navigation so it does not accumulate across repeated visits.
|
||||
|
||||
### Fix 2 — Layout shift (width locking)
|
||||
|
||||
When the animation cycles through words of different lengths — "Web Designer" is longer than "Blogger" — the element changes width on every word transition. Everything to the right of it (or below it on a wrapped line) shifts. This is a jarring visual jump and a real Core Web Vitals hit.
|
||||
|
||||
The fix measures every word before the animation starts, using a hidden off-screen `<span>` that inherits the same font and letter-spacing as the real element. The widest measurement (including the cursor character `|`) is applied as `minWidth`:
|
||||
|
||||
```js
|
||||
const measurer = document.createElement('span');
|
||||
measurer.style.cssText = 'visibility:hidden;position:absolute;white-space:nowrap;pointer-events:none;';
|
||||
measurer.style.font = getComputedStyle(root).font;
|
||||
measurer.style.letterSpacing = getComputedStyle(root).letterSpacing;
|
||||
document.body.appendChild(measurer);
|
||||
|
||||
let maxWidth = 0;
|
||||
for (const word of words) {
|
||||
measurer.textContent = word + '|';
|
||||
maxWidth = Math.max(maxWidth, measurer.offsetWidth);
|
||||
}
|
||||
document.body.removeChild(measurer);
|
||||
root.style.minWidth = maxWidth + 'px';
|
||||
```
|
||||
|
||||
The measurer is appended to `<body>` (not inserted inline) so it does not inherit any overflow clipping from ancestor elements. It is removed immediately after measurement.
|
||||
|
||||
### Fix 3 — Descender clipping (overflow hidden removed)
|
||||
|
||||
The `.typing-effect` span originally had `overflow: hidden` — a common guard when animating text to prevent runaway characters from bleeding outside the box. The problem is that `overflow: hidden` clips the descenders of letters like `g`, `j`, `p`, `q`, and `y`. In a heading at large font sizes this is very visible: the bottom of those letters looks cut off.
|
||||
|
||||
The fix is simply to remove `overflow: hidden`. Width is already controlled by `minWidth` from Fix 2, so there is nothing to clip. The remaining styles are:
|
||||
|
||||
```css
|
||||
.typing-effect {
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
```
|
||||
|
||||
`vertical-align: bottom` aligns the inline-block to the text baseline of the surrounding line, which keeps the heading vertically stable as words change length.
|
||||
|
||||
## The props and their defaults
|
||||
|
||||
| Prop | Default | What it controls |
|
||||
|---|---|---|
|
||||
| `words` | *(required)* | Array of strings to cycle through |
|
||||
| `typeSpeed` | `120` | Milliseconds between each character typed |
|
||||
| `deleteSpeed` | `70` | Milliseconds between each character deleted |
|
||||
| `pauseAfterType` | `1800` | Pause in ms after the word is fully typed |
|
||||
| `pauseAfterDelete` | `400` | Pause in ms after the word is fully deleted |
|
||||
|
||||
## How to adjust the speed
|
||||
|
||||
Pass any combination of props directly on the component. You only need to set the values you want to override:
|
||||
|
||||
```astro
|
||||
<TypingEffect
|
||||
words={["Web Designer", "Web Developer", "Astro Developer"]}
|
||||
typeSpeed={80}
|
||||
deleteSpeed={40}
|
||||
pauseAfterType={2500}
|
||||
pauseAfterDelete={200}
|
||||
/>
|
||||
```
|
||||
|
||||
**Faster, snappier feel** — lower `typeSpeed` and `deleteSpeed`, shorten both pauses:
|
||||
|
||||
```astro
|
||||
<TypingEffect
|
||||
words={["Designer", "Developer", "Builder"]}
|
||||
typeSpeed={60}
|
||||
deleteSpeed={30}
|
||||
pauseAfterType={1200}
|
||||
pauseAfterDelete={200}
|
||||
/>
|
||||
```
|
||||
|
||||
**Slower, more deliberate feel** — raise `typeSpeed` and extend `pauseAfterType` so readers have time to absorb each word:
|
||||
|
||||
```astro
|
||||
<TypingEffect
|
||||
words={["Designer", "Developer", "Builder"]}
|
||||
typeSpeed={160}
|
||||
deleteSpeed={80}
|
||||
pauseAfterType={3000}
|
||||
pauseAfterDelete={600}
|
||||
/>
|
||||
```
|
||||
|
||||
## How to change the words
|
||||
|
||||
Edit the `words` array wherever you use the component. You can have as many strings as you like — the component loops back to the first word when it reaches the end:
|
||||
|
||||
```astro
|
||||
<TypingEffect
|
||||
words={[
|
||||
"Web Designer",
|
||||
"Web Developer",
|
||||
"Astro Developer",
|
||||
"UI/UX Enthusiast",
|
||||
"Performance Nerd",
|
||||
]}
|
||||
/>
|
||||
```
|
||||
|
||||
Keep words at a similar length if possible. The width-locking logic sets `minWidth` to the widest word, so very short words will have visible empty space to their right while the longer words are deleted.
|
||||
|
||||
## The cursor
|
||||
|
||||
The blinking cursor is a `<span>` rendered immediately after the text span:
|
||||
|
||||
```html
|
||||
<span class="typing-text"></span><span class="typing-cursor" aria-hidden="true">|</span>
|
||||
```
|
||||
|
||||
It is styled with a 0.75 s `step-end` blink animation and coloured with the active theme's brand colour:
|
||||
|
||||
```css
|
||||
.typing-cursor {
|
||||
display: inline-block;
|
||||
margin-left: 1px;
|
||||
animation: blink 0.75s step-end infinite;
|
||||
color: var(--color-brand-500, currentColor);
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0; }
|
||||
}
|
||||
```
|
||||
|
||||
To change the cursor character, open `TypingEffect.astro` and replace the `|` inside the cursor span. Common alternatives are `▌` (block cursor) or `_` (underscore).
|
||||
|
||||
To change the blink speed, adjust the `0.75s` value. Faster blinking (`0.5s`) reads as more urgent; slower (`1.2s`) is more relaxed.
|
||||
|
||||
## Accessibility
|
||||
|
||||
The component wraps everything in a `<span>` with an `aria-label` set to all words joined by a comma:
|
||||
|
||||
```html
|
||||
<span id="typing-abc123" class="typing-effect" aria-label="Web Designer, Web Developer, Astro Developer, Blogger, Coffee lover">
|
||||
```
|
||||
|
||||
Screen readers announce the full list of words from the `aria-label` and ignore the animated content inside (the cursor has `aria-hidden="true"`). The text is therefore both readable and not disruptive to assistive technology.
|
||||
|
||||
## SEO impact
|
||||
|
||||
Because Astro renders the heading on the server, the full element — including the `<span aria-label="…">` with all words — is present in the HTML source before any JavaScript runs. Google's crawler reads the static HTML and indexes all words from the `aria-label`. The visual animation is a progressive enhancement on top of that static foundation.
|
||||
|
||||
## How to disable the typing effect
|
||||
|
||||
**Option 1 — Replace with static text**
|
||||
|
||||
Remove the `<TypingEffect>` component and its import, then put your static heading copy directly in the slot:
|
||||
|
||||
```astro
|
||||
---
|
||||
// Remove this line:
|
||||
// import TypingEffect from '@/components/ui/TypingEffect.astro';
|
||||
---
|
||||
|
||||
<h1 slot="title">
|
||||
<span class="text-foreground [-webkit-text-fill-color:currentColor]">Astro Rocket —</span><br />
|
||||
Web Developer
|
||||
</h1>
|
||||
```
|
||||
|
||||
**Option 2 — Single static word without the cursor**
|
||||
|
||||
If you want the styled text container but no animation and no cursor, just drop the text inside a plain `<span>`:
|
||||
|
||||
```astro
|
||||
<h1 slot="title">
|
||||
<span class="text-foreground [-webkit-text-fill-color:currentColor]">Astro Rocket —</span><br />
|
||||
<span>Web Developer</span>
|
||||
</h1>
|
||||
```
|
||||
|
||||
**Option 3 — Keep one word with the cursor but stop cycling**
|
||||
|
||||
Pass an array with a single string. The component will type it once, pause, delete it, and retype it — giving you a "hello, I am typing" feel without ever changing the word. If you also want it to stop after the first type, that requires editing the component logic directly.
|
||||
|
||||
## Cheat sheet
|
||||
|
||||
| Goal | What to change |
|
||||
|---|---|
|
||||
| Faster typing | Lower `typeSpeed` (e.g. `120` → `60`) |
|
||||
| Slower typing | Raise `typeSpeed` (e.g. `120` → `180`) |
|
||||
| Faster deleting | Lower `deleteSpeed` (e.g. `70` → `35`) |
|
||||
| Longer pause after typing | Raise `pauseAfterType` (e.g. `1800` → `3000`) |
|
||||
| Shorter pause between words | Lower `pauseAfterDelete` (e.g. `400` → `150`) |
|
||||
| Different words | Edit the `words` array |
|
||||
| Different cursor character | Replace `\|` in `TypingEffect.astro` |
|
||||
| Different cursor colour | Override `--color-brand-500` in your theme |
|
||||
| Remove the effect entirely | Replace `<TypingEffect>` with a plain `<span>` |
|
||||
|
||||
Five props, one file, zero dependencies — the typing effect is deliberately simple so it stays easy to own. The three forks above are the minimum needed to make it work correctly in a real Astro project with client-side navigation, variable-length words, and a heading with descenders.
|
||||
@@ -0,0 +1,88 @@
|
||||
---
|
||||
title: "Scroll Progress Bar — Reading Progress at a Glance"
|
||||
description: "Astro Rocket now has a scroll progress bar: a thin brand-coloured line that fills as you scroll. Here's how it works, where it lives, and how to enable it on any page."
|
||||
publishedAt: 2026-03-25
|
||||
author: "Hans Martens"
|
||||
tags: ["astro-rocket", "features", "header", "ux"]
|
||||
featured: false
|
||||
locale: en
|
||||
image: "../../../assets/blog/scroll-progress-bar.svg"
|
||||
svgSlug: "scroll-progress-bar"
|
||||
imageAlt: "A brand-coloured horizontal progress bar, partially filled, on a brand background"
|
||||
---
|
||||
|
||||
Astro Rocket now has a scroll progress bar. It's the thin 2px line at the top or bottom of the header that fills from left to right as you scroll down the page — a quiet signal to the reader of how far through the content they are.
|
||||
|
||||
It's off by default. When you enable it, it tracks the page scroll position in real time using `requestAnimationFrame`, so it stays smooth even on long pages.
|
||||
|
||||
## Where it shows up
|
||||
|
||||
The bar is enabled on three page types, each with a different position:
|
||||
|
||||
**Homepage** — the bar sits on top of the floating capsule header. On the homepage the header is transparent and floating, so placing the bar above it keeps it visible and out of the way of the header content. As the page scrolls and the header gains its solid background, the bar sits cleanly on the very top edge.
|
||||
|
||||
**Blog index** — the bar sits underneath the solid bar header, exactly at the bottom border of the header. This is the standard position for a reading progress indicator on a content listing page.
|
||||
|
||||
**Individual blog posts** — same as the blog index: the bar runs along the bottom of the header. On a long-form post this is where it matters most — a reader can glance at the header and know at a glance how far through the article they are.
|
||||
|
||||
## How it works
|
||||
|
||||
The bar is an absolutely positioned `div` inside the `<header>` element. A small script updates its `width` on every scroll event, capped with `requestAnimationFrame` to avoid layout thrashing:
|
||||
|
||||
```ts
|
||||
function update() {
|
||||
const scrollTop = window.scrollY;
|
||||
const docHeight = document.documentElement.scrollHeight - window.innerHeight;
|
||||
const pct = docHeight > 0 ? (scrollTop / docHeight) * 100 : 0;
|
||||
bar.style.width = `${pct}%`;
|
||||
}
|
||||
|
||||
window.addEventListener('scroll', () => {
|
||||
if (!ticking) {
|
||||
ticking = true;
|
||||
requestAnimationFrame(update);
|
||||
}
|
||||
}, { passive: true });
|
||||
```
|
||||
|
||||
The bar colour is `var(--color-brand-500)` — it automatically matches your active colour theme and updates instantly when the visitor switches themes.
|
||||
|
||||
## The two props
|
||||
|
||||
The `Header` component exposes two props that control the scroll progress bar:
|
||||
|
||||
| Prop | Type | Default | What it does |
|
||||
|------|------|:-------:|--------------|
|
||||
| `showScrollProgress` | `boolean` | `false` | Renders the progress bar |
|
||||
| `scrollProgressPosition` | `'top'` \| `'bottom'` | `'bottom'` | Places the bar on the top or bottom edge of the header |
|
||||
|
||||
## Enabling it on any page
|
||||
|
||||
To add the scroll progress bar to a page, open the layout file that the page uses and add `showScrollProgress` to the `<Header>` component. For the standard page layout (`PageLayout.astro`) the layout accepts a `showScrollProgress` prop that passes through automatically:
|
||||
|
||||
```astro
|
||||
<!-- src/pages/your-page.astro -->
|
||||
<PageLayout title="Your Page" showScrollProgress>
|
||||
...
|
||||
</PageLayout>
|
||||
```
|
||||
|
||||
To control which edge of the header the bar sits on, pass `scrollProgressPosition` directly to the `<Header>` component in the layout file:
|
||||
|
||||
```astro
|
||||
<!-- Bottom of header (default) -->
|
||||
<Header showScrollProgress />
|
||||
|
||||
<!-- Top of header -->
|
||||
<Header showScrollProgress scrollProgressPosition="top" />
|
||||
```
|
||||
|
||||
The homepage uses `scrollProgressPosition="top"` because the floating capsule header looks better with the bar above it. All other pages use the default `'bottom'` position.
|
||||
|
||||
## Turning it off
|
||||
|
||||
Each page layout controls the bar independently. To disable it on a specific page, remove `showScrollProgress` from the layout call or set it to `false`. The blog index, for example, can have the bar removed by opening `src/pages/blog/index.astro` and deleting the `showScrollProgress` prop from `<PageLayout>`.
|
||||
|
||||
## Performance note
|
||||
|
||||
The scroll listener uses `{ passive: true }` and `requestAnimationFrame` throttling, so it adds no measurable overhead to scrolling performance. The bar has `transition-none` applied so there is no CSS transition lag — the fill tracks the scroll position directly with no animation delay.
|
||||
@@ -0,0 +1,168 @@
|
||||
---
|
||||
title: "SEO in Astro Rocket: What's Built In and How to Configure It"
|
||||
description: "Astro Rocket handles structured data, Open Graph, canonical URLs, sitemaps, and more out of the box. Here's exactly what ships and where to configure it."
|
||||
publishedAt: 2026-03-17
|
||||
author: "Hans Martens"
|
||||
tags: ["astro-rocket", "seo", "structured-data", "tutorial", "configuration"]
|
||||
featured: false
|
||||
locale: en
|
||||
image: "../../../assets/blog/seo-in-astro-rocket.svg"
|
||||
svgSlug: "seo-in-astro-rocket"
|
||||
imageAlt: "Search icon above the word 'seo' on a dark navy background"
|
||||
---
|
||||
|
||||
SEO is one of those areas where "we've handled it" can mean anything from three meta tags to a complete implementation. Astro Rocket ships a complete one. Here is exactly what is included and where each piece lives.
|
||||
|
||||
## What ships out of the box
|
||||
|
||||
The SEO layer covers six distinct concerns:
|
||||
|
||||
1. **Structured data (JSON-LD)** — schema.org markup for the home page and every blog post
|
||||
2. **Open Graph and Twitter Cards** — social sharing metadata for every page
|
||||
3. **Canonical URLs** — preventing duplicate content penalties
|
||||
4. **Sitemap** — auto-generated and kept in sync with your pages
|
||||
5. **Robots meta** — per-page `noindex` / `nofollow` control
|
||||
6. **Verification tags** — Google Search Console and Bing Webmaster
|
||||
|
||||
None of these require per-page setup. They are wired into the base layout and run automatically.
|
||||
|
||||
## Structured data
|
||||
|
||||
Structured data is the part most themes skip. Astro Rocket outputs [JSON-LD](https://json-ld.org/) schema.org markup on every page.
|
||||
|
||||
**Home page** outputs three schemas:
|
||||
|
||||
- `WebSite` — site name, URL, and search action
|
||||
- `Organization` — your organisation name, logo, and contact details
|
||||
- `Person` — the site author
|
||||
|
||||
**Blog posts** output `BlogPosting` with full metadata:
|
||||
|
||||
```json
|
||||
{
|
||||
"@type": "BlogPosting",
|
||||
"headline": "Post title",
|
||||
"description": "Post description",
|
||||
"datePublished": "2026-03-17",
|
||||
"dateModified": "2026-03-17",
|
||||
"author": { "@type": "Person", "name": "Hans Martens" },
|
||||
"image": "https://yoursite.com/og-image.jpg"
|
||||
}
|
||||
```
|
||||
|
||||
Structured data does not guarantee rich results in Google Search, but it is a prerequisite for them. Posts with `BlogPosting` markup are eligible for article rich results and knowledge panel entries. Pages without it are not.
|
||||
|
||||
All values are pulled from `site.config.ts` and the post frontmatter — nothing is hardcoded.
|
||||
|
||||
## Open Graph and Twitter Cards
|
||||
|
||||
Every page generates a complete set of social metadata. For regular pages:
|
||||
|
||||
```html
|
||||
<meta property="og:title" content="Page Title" />
|
||||
<meta property="og:description" content="Page description" />
|
||||
<meta property="og:image" content="https://yoursite.com/og-default.jpg" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
```
|
||||
|
||||
Blog posts use their cover image automatically:
|
||||
|
||||
```html
|
||||
<meta property="og:image" content="https://yoursite.com/blog/post-cover.jpg" />
|
||||
<meta property="og:type" content="article" />
|
||||
```
|
||||
|
||||
The default OG image for non-blog pages is configured in `site.config.ts`:
|
||||
|
||||
```ts
|
||||
ogImage: '/og-default.svg',
|
||||
```
|
||||
|
||||
Drop your 1200×630 image into `/public/` and update the path. Every page that does not have its own image will use it.
|
||||
|
||||
## Canonical URLs
|
||||
|
||||
Every page outputs a canonical URL tag pointing to the primary URL of that page:
|
||||
|
||||
```html
|
||||
<link rel="canonical" href="https://yoursite.com/blog/my-post" />
|
||||
```
|
||||
|
||||
This runs automatically — no frontmatter field required. The canonical URL is always constructed from the production domain set in `site.config.ts`, so it stays correct regardless of staging environments or preview deployments.
|
||||
|
||||
## Sitemap
|
||||
|
||||
The sitemap is generated by `@astrojs/sitemap` and includes every page that Astro renders at build time. Blog posts, landing pages, the contact page — all included automatically.
|
||||
|
||||
The sitemap URL is `https://yoursite.com/sitemap-index.xml`. Submit it to [Google Search Console](https://search.google.com/search-console) once after deployment and Google will pick up new posts as they are published.
|
||||
|
||||
To exclude a page from the sitemap, mark it with `noindex` — the integration respects the same pages you would not want indexed.
|
||||
|
||||
## Robots meta
|
||||
|
||||
Pages can be excluded from search engine indexing via the `SEO` component's props:
|
||||
|
||||
```astro
|
||||
<SEO noindex nofollow />
|
||||
```
|
||||
|
||||
This renders:
|
||||
|
||||
```html
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
```
|
||||
|
||||
Use this for admin pages, thank-you pages after form submissions, staging pages, or any content you want crawlers to skip. The base layout applies `index, follow` by default to all other pages.
|
||||
|
||||
## Verification tags
|
||||
|
||||
Search console verification codes go in your `.env` file — no template editing:
|
||||
|
||||
```bash
|
||||
GOOGLE_SITE_VERIFICATION=your-code-here
|
||||
BING_SITE_VERIFICATION=your-code-here
|
||||
```
|
||||
|
||||
These are read by `site.config.ts` via `astro:env/server` and rendered in `<head>` on every page through the `verification.google` and `verification.bing` fields. Leave them empty and nothing is rendered.
|
||||
|
||||
## Configuring the site identity
|
||||
|
||||
All structured data pulls from `siteConfig` in `src/config/site.config.ts`. The fields that feed directly into SEO output:
|
||||
|
||||
```ts
|
||||
const siteConfig = {
|
||||
name: 'Your Site Name',
|
||||
url: 'https://yoursite.com',
|
||||
description: 'Your site description for meta tags',
|
||||
author: 'Your Name',
|
||||
email: 'hello@yoursite.com',
|
||||
authorImage: '/avatar.jpg', // used in Person schema
|
||||
branding: {
|
||||
logo: {
|
||||
alt: 'Your Site Name',
|
||||
imageUrl: '/logo.png', // used in Organization schema
|
||||
},
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
Fill these in accurately. The `url` field in particular must be the production URL — it feeds into canonical tags, OG image URLs, and structured data. An incorrect URL there breaks your canonical implementation and can cause indexing issues.
|
||||
|
||||
## Blog post SEO
|
||||
|
||||
Each blog post has two frontmatter fields that feed directly into SEO:
|
||||
|
||||
- **`title`** — becomes the page `<title>`, `og:title`, and `BlogPosting.headline`. Keep it under 60 characters for clean display in search results.
|
||||
- **`description`** — becomes the meta description and `og:description`. Keep it under 155 characters. Write it as a plain-language summary of the post — search engines use it as the snippet in results pages.
|
||||
|
||||
These two fields are the highest-leverage SEO work you will do for each post. Everything else is automatic.
|
||||
|
||||
## Checking your SEO output
|
||||
|
||||
After deploying, verify with two tools:
|
||||
|
||||
- [Google Rich Results Test](https://search.google.com/test/rich-results) — paste your URL and confirm structured data is parsed correctly
|
||||
- [Open Graph Debugger](https://developers.facebook.com/tools/debug/) — verify social sharing metadata and force-refresh Facebook's cache for updated images
|
||||
|
||||
Both are free and give you the ground truth on what search engines and social platforms actually see when they crawl your pages.
|
||||
@@ -0,0 +1,46 @@
|
||||
---
|
||||
title: "Astro Rocket"
|
||||
description: "A production-ready Astro 6 starter for designers and developers — 12 beautiful themes, 57+ components, built-in i18n, dark mode and a fast, modern foundation to build anything on."
|
||||
url: "https://astrorocket.dev"
|
||||
repo: "https://github.com/hansmartens68/Astro-Rocket"
|
||||
tags: ["Astro", "TypeScript", "Tailwind CSS", "Open Source"]
|
||||
featured: true
|
||||
order: 1
|
||||
year: 2026
|
||||
role: "Designer & Developer"
|
||||
services: ["Design System", "Component Library", "Open Source"]
|
||||
---
|
||||
|
||||
## The Brief
|
||||
|
||||
I wanted a starter theme that was genuinely ready to launch — not a boilerplate that still requires hours of setup. The goal was simple: clone it, change the text, go live. Everything else should already be done.
|
||||
|
||||
Most Astro starters are developer-focused scaffolding. Astro Rocket is different — it ships as a complete, styled website aimed at designers, freelancers, and anyone who needs a portfolio or marketing site without starting from scratch.
|
||||
|
||||
## What I Built
|
||||
|
||||
Astro Rocket is built on top of [Velocity](https://github.com/southwellmedia/velocity) by Southwell Media, which provided a strong foundation — a well-structured Astro boilerplate with a solid design system. I extended it substantially:
|
||||
|
||||
- **12 colour themes** switchable live in the header with no rebuild required
|
||||
- **57+ components** across UI, patterns, layout, blog, landing, and SEO categories
|
||||
- **Auto-generated logo and favicon** from the site name — no design tools needed
|
||||
- **Typing effect** in the hero section
|
||||
- **Dark mode** with `sessionStorage` persistence (resets on new tab by design)
|
||||
- **Scroll progress bar**, parallax grid, and full animation suite
|
||||
- **Content collections** for blog, projects, authors, FAQs, and tech stack
|
||||
- **RSS feed**, sitemap, and full JSON-LD structured data
|
||||
|
||||
## Design Decisions
|
||||
|
||||
**Theme switching without rebuilds.** Velocity required editing a CSS import file and rebuilding to change themes. Astro Rocket uses CSS custom properties scoped to `data-theme` attributes, so switching is instant — no server round-trip, no rebuild.
|
||||
|
||||
**`sessionStorage` over `localStorage` for dark mode.** A portfolio site should always show its best face. With `localStorage`, a returning visitor might see the wrong mode if they changed it on a whim. `sessionStorage` resets on each new tab, so every first impression is intentional.
|
||||
|
||||
**Auto-generated branding.** Logo badges and favicons are generated at runtime from the site name and active brand colour. Cloners don't need Figma or Illustrator to get a polished result.
|
||||
|
||||
## Results
|
||||
|
||||
- Lighthouse scores of 95+ across all categories
|
||||
- Zero JavaScript shipped by default (Astro islands architecture)
|
||||
- Submitted to the official Astro themes directory
|
||||
- Open source under the MIT licence
|
||||
@@ -0,0 +1,33 @@
|
||||
---
|
||||
title: "Blog Starter"
|
||||
description: "A minimal, opinionated blog starter built on Astro — fast by default, with MDX support, RSS, dark mode, and a clean reading experience."
|
||||
tags: ["Astro", "MDX", "Tailwind CSS", "Open Source"]
|
||||
featured: false
|
||||
order: 5
|
||||
year: 2025
|
||||
role: "Designer & Developer"
|
||||
services: ["Design", "Development", "Open Source"]
|
||||
---
|
||||
|
||||
## The Brief
|
||||
|
||||
Most blog starters come with too much — heavy frameworks, complex setups, and styling opinions baked so deep they're impossible to change. I wanted something that got out of the way. A starting point that was genuinely minimal, genuinely fast, and opinionated only where it counts: the reading experience.
|
||||
|
||||
## What I Built
|
||||
|
||||
A lean Astro-based blog starter with everything needed to write and nothing extra.
|
||||
|
||||
- MDX support with component embedding
|
||||
- RSS feed included by default
|
||||
- Dark mode with no flash on load
|
||||
- Responsive, well-spaced typography tuned for reading
|
||||
- Sitemap and SEO meta tags out of the box
|
||||
- Zero client-side JavaScript on static pages
|
||||
|
||||
## Typography & Reading Experience
|
||||
|
||||
The reading experience was the primary constraint. Every decision — from line length to spacing to code block contrast — was made to keep the reader focused on the content. Body text is set at a comfortable measure, headings are restrained, and the layout collapses cleanly on small screens without losing hierarchy.
|
||||
|
||||
## Results
|
||||
|
||||
Used as a starting point for several client projects and available as a free open-source template. Feedback from developers has been consistent: it's easy to understand, easy to extend, and doesn't fight back.
|
||||
@@ -0,0 +1,33 @@
|
||||
---
|
||||
title: "Component Library"
|
||||
description: "An open-source set of accessible, themeable UI components — built with Astro and Tailwind CSS, documented with live examples."
|
||||
tags: ["Astro", "TypeScript", "Tailwind CSS", "Open Source"]
|
||||
featured: false
|
||||
order: 4
|
||||
year: 2025
|
||||
role: "Designer & Developer"
|
||||
services: ["Design System", "Component Library", "Documentation", "Open Source"]
|
||||
---
|
||||
|
||||
## The Brief
|
||||
|
||||
Replace this with your actual motivation for building this. Why did you start it? What problem were you solving — for yourself or for others?
|
||||
|
||||
*Example: Every project I built was repeating the same components from scratch. I needed a library that was opinionated enough to be useful but flexible enough to adapt to any brand.*
|
||||
|
||||
## What I Built
|
||||
|
||||
Describe the scope of the library. How many components? What categories? What design decisions underpinned the whole system?
|
||||
|
||||
- Component categories and count
|
||||
- Theming system
|
||||
- Accessibility approach
|
||||
- Documentation approach
|
||||
|
||||
## Design System Approach
|
||||
|
||||
Walk through how the design tokens, variants, and documentation were structured. This is where designers and developers reading your portfolio will pay attention.
|
||||
|
||||
## Results
|
||||
|
||||
Downloads, GitHub stars, community adoption, or how it's saved time across your own projects.
|
||||
@@ -0,0 +1,34 @@
|
||||
---
|
||||
title: "Documentation Site"
|
||||
description: "Developer docs for an open-source CLI tool — versioned content, full-text search, and a clean reading experience across all screen sizes."
|
||||
tags: ["Astro", "MDX", "Tailwind CSS", "Open Source"]
|
||||
featured: false
|
||||
order: 7
|
||||
year: 2025
|
||||
role: "Designer & Developer"
|
||||
services: ["Design", "Development", "Technical Writing Support"]
|
||||
---
|
||||
|
||||
## The Brief
|
||||
|
||||
Replace this with the actual context. What was being documented? Who were the readers? What did the old docs look like?
|
||||
|
||||
*Example: An open-source CLI tool had docs scattered across a README and a GitHub wiki. Contributors were confused, new users dropped off during setup, and there was no search. It needed a real documentation site.*
|
||||
|
||||
## What I Built
|
||||
|
||||
Describe the structure and features of the documentation site.
|
||||
|
||||
- Versioned content structure
|
||||
- Full-text search
|
||||
- Code blocks with syntax highlighting and copy button
|
||||
- Navigation that scales across a large content tree
|
||||
- Mobile-friendly reading experience
|
||||
|
||||
## Content Architecture
|
||||
|
||||
Good documentation is as much about structure as it is about writing. Walk through how you organised the content — guides vs. reference vs. tutorials, versioning strategy, URL structure.
|
||||
|
||||
## Results
|
||||
|
||||
Time-to-first-success for new users, contribution rate, search traffic, or maintainer feedback. Replace with your real outcome.
|
||||
@@ -0,0 +1,34 @@
|
||||
---
|
||||
title: "E-Commerce Store"
|
||||
description: "Custom storefront for an independent fashion brand — product pages, cart, and checkout built for performance and a smooth mobile experience."
|
||||
tags: ["Astro", "TypeScript", "Tailwind CSS", "Client Work"]
|
||||
featured: false
|
||||
order: 6
|
||||
year: 2025
|
||||
client: "Your Client Name"
|
||||
role: "Designer & Developer"
|
||||
services: ["Web Design", "Web Development", "E-Commerce"]
|
||||
---
|
||||
|
||||
## The Brief
|
||||
|
||||
Replace this with the actual client brief. What was the brand? What did their old shop look like? What were the conversion problems they were trying to solve?
|
||||
|
||||
*Example: An independent fashion label was losing sales to a slow, poorly structured Shopify theme. They wanted a custom storefront that reflected their aesthetic and loaded instantly on mobile.*
|
||||
|
||||
## What I Built
|
||||
|
||||
Describe the store's structure and what made it technically interesting.
|
||||
|
||||
- Product listing and detail pages
|
||||
- Cart and checkout flow
|
||||
- Mobile-first design
|
||||
- Performance optimisation strategy
|
||||
|
||||
## Mobile-First Approach
|
||||
|
||||
E-commerce lives on mobile. Describe the specific decisions you made to serve mobile shoppers — touch targets, image loading, checkout simplification.
|
||||
|
||||
## Results
|
||||
|
||||
Conversion rate change, page speed improvements, revenue impact, or client feedback. Replace with your real outcome.
|
||||
@@ -0,0 +1,35 @@
|
||||
---
|
||||
title: "SaaS Landing Page"
|
||||
description: "Marketing site for a B2B analytics platform — clear messaging, fast load times, and a conversion-focused layout."
|
||||
tags: ["Astro", "TypeScript", "Tailwind CSS", "Client Work"]
|
||||
featured: false
|
||||
order: 3
|
||||
year: 2025
|
||||
client: "Your Client Name"
|
||||
role: "Designer & Developer"
|
||||
services: ["Web Design", "Web Development", "Copywriting Support"]
|
||||
---
|
||||
|
||||
## The Brief
|
||||
|
||||
Replace this with your actual project brief. What was the client trying to achieve? What did the old site lack? What did a successful launch look like?
|
||||
|
||||
*Example: A B2B analytics startup had a product that was ahead of competitors but a site that didn't reflect it. They needed a marketing site that could explain a complex product simply, rank on search, and convert trial sign-ups.*
|
||||
|
||||
## What I Built
|
||||
|
||||
Describe the pages, sections, and components you built. What made this project interesting from a design or development perspective?
|
||||
|
||||
- Hero section with clear value proposition
|
||||
- Feature sections with interactive demos
|
||||
- Pricing table
|
||||
- Trust signals and social proof
|
||||
- Optimised for conversion
|
||||
|
||||
## Key Decisions
|
||||
|
||||
Walk through the decisions that shaped the project. Copywriting approach, layout choices, performance trade-offs, animation decisions.
|
||||
|
||||
## Results
|
||||
|
||||
Share what happened after launch — trial sign-ups, bounce rate, page speed scores, or client feedback. Replace with your actual numbers.
|
||||
@@ -0,0 +1,33 @@
|
||||
---
|
||||
title: "Studio Portfolio"
|
||||
description: "Complete redesign of a creative studio's portfolio — fast, focused, and built to convert visitors into clients."
|
||||
tags: ["Astro", "Tailwind CSS", "Design", "Client Work"]
|
||||
featured: false
|
||||
order: 2
|
||||
year: 2025
|
||||
client: "Your Client Name"
|
||||
role: "Designer & Developer"
|
||||
services: ["Web Design", "Web Development", "Performance Optimisation"]
|
||||
---
|
||||
|
||||
## The Brief
|
||||
|
||||
Replace this with your actual project brief. Describe what the client came to you with — their goals, their existing site's problems, and what success would look like.
|
||||
|
||||
*Example: The studio had been running on a WordPress theme for six years. It was slow, hard to maintain, and no longer reflected the quality of their work. They needed a site that felt as considered as the projects they put on it.*
|
||||
|
||||
## What I Built
|
||||
|
||||
Describe what you built. Walk through the key decisions — layout, structure, technology choices, and anything that makes this project worth showing.
|
||||
|
||||
- What pages were built?
|
||||
- What was technically interesting about it?
|
||||
- What design decisions did you make and why?
|
||||
|
||||
## The Process
|
||||
|
||||
Walk through how you worked. Discovery, wireframes, design, build, feedback, launch. This is where you show how you think.
|
||||
|
||||
## Results
|
||||
|
||||
Share the outcome. Metrics, client feedback, what changed after launch.
|
||||
@@ -1,509 +0,0 @@
|
||||
export const languages = {
|
||||
de: { code: 'de', name: 'Deutsch', flag: '🇩🇪' },
|
||||
fr: { code: 'fr', name: 'Français', flag: '🇫🇷' },
|
||||
it: { code: 'it', name: 'Italiano', flag: '🇮🇹' },
|
||||
en: { code: 'en', name: 'English', flag: '🇬🇧' },
|
||||
} as const;
|
||||
|
||||
export type Locale = keyof typeof languages;
|
||||
export const defaultLocale: Locale = 'de';
|
||||
|
||||
export const ui = {
|
||||
de: {
|
||||
// Nav labels
|
||||
'nav.blog': 'Blog',
|
||||
'nav.features': 'Features',
|
||||
'nav.about': 'Über uns',
|
||||
'nav.contact': 'Kontakt',
|
||||
// Nav hrefs (locale-aware)
|
||||
'nav.blog.href': '/blog',
|
||||
'nav.features.href': '/features',
|
||||
'nav.about.href': '/about',
|
||||
'nav.contact.href': '/contact',
|
||||
// Hero
|
||||
'hero.description': 'Behalte den Überblick über deine Finanzen. Erfasse Ausgaben, verwalte Budgets und erreiche deine Sparziele – einfach und übersichtlich.',
|
||||
'hero.register': 'Registrieren',
|
||||
'hero.login': 'Login',
|
||||
// Features section
|
||||
'features.badge': 'Features',
|
||||
'features.title': 'Alles was du brauchst',
|
||||
'features.description': 'Armarium vereint alle wichtigen Funktionen für ein solides Finanzmanagement in einer übersichtlichen App.',
|
||||
// Feature cards
|
||||
'f1.title': 'Budget-Übersicht',
|
||||
'f1.desc': 'Alle Einnahmen und Ausgaben auf einen Blick. Dein aktueller Kontostand immer im Griff.',
|
||||
'f2.title': 'Transaktionen erfassen',
|
||||
'f2.desc': 'Ausgaben und Einnahmen schnell erfassen, kategorisieren und filtern – für lückenlosen Überblick.',
|
||||
'f3.title': 'Kategorien & Berichte',
|
||||
'f3.desc': 'Eigene Kategorien definieren und Ausgaben analysieren. Verstehe, wohin dein Geld fliesst.',
|
||||
'f4.title': 'Mehrere Konten',
|
||||
'f4.desc': 'Verwalte Bankkonto, Kreditkarte und Bargeld getrennt – alles in einer App zusammengefasst.',
|
||||
'f5.title': 'Sparziele',
|
||||
'f5.desc': 'Setze dir finanzielle Ziele und verfolge deinen Fortschritt – Schritt für Schritt zum Ziel.',
|
||||
'f6.title': 'Sicher & Privat',
|
||||
'f6.desc': 'Deine Daten gehören dir. Kein Datenverkauf, kein Tracking – volle Kontrolle über deine Finanzdaten.',
|
||||
// Blog section
|
||||
'blog.section.title': 'Neuigkeiten & Updates',
|
||||
'blog.readmore': 'Weiterlesen',
|
||||
'blog.allposts': 'Alle Beiträge',
|
||||
// Why Armarium section
|
||||
'why.title': 'Finanzmanagement neu gedacht',
|
||||
'why.intro': 'Armarium vereinfacht dein Finanzleben – egal ob du Ausgaben verfolgst, Budgets planst oder Sparziele setzt.',
|
||||
'why.item1.title': 'Daten in der Schweiz',
|
||||
'why.item1.desc': 'Deine Finanzdaten werden ausschliesslich auf Schweizer Servern gespeichert. Kein Datenverkauf, kein Tracking – volle Kontrolle bleibt bei dir.',
|
||||
'why.item2.title': 'Clevere Budgetverwaltung',
|
||||
'why.item2.desc': 'Behalte alle Einnahmen und Ausgaben im Blick. Automatische Kategorisierung und übersichtliche Grafiken zeigen dir genau, wo dein Geld hinfliesst.',
|
||||
'why.item3.title': 'Individuell anpassbar',
|
||||
'why.item3.desc': 'Erstelle eigene Kategorien, verwalte mehrere Konten und passe die App genau an deinen Alltag an.',
|
||||
'why.item4.title': 'Mehrere Konten',
|
||||
'why.item4.desc': 'Verwalte Bankkonto, Kreditkarte und Bargeld getrennt – alles übersichtlich in einer einzigen App.',
|
||||
'why.learnmore': 'Mehr erfahren',
|
||||
'why.footer': 'Kostenlos starten – keine Kreditkarte erforderlich.',
|
||||
// CTA
|
||||
'cta.title': 'Bereit, dein Budget im Griff zu haben?',
|
||||
'cta.desc': 'Kostenlos registrieren und sofort loslegen. Keine Kreditkarte erforderlich.',
|
||||
'cta.register': 'Kostenlos registrieren',
|
||||
'cta.login': 'Bereits registriert? Login',
|
||||
// Footer
|
||||
'footer.tagline': 'Dein persönlicher Finanzbegleiter – Budget im Blick, Ziele im Fokus.',
|
||||
'footer.copyright': '© {year} Armarium Suite',
|
||||
'footer.app': 'Produkt',
|
||||
'footer.start': 'Jetzt starten',
|
||||
'footer.register': 'Registrieren',
|
||||
'footer.legal': 'Rechtliches',
|
||||
'footer.privacy': 'Datenschutz',
|
||||
'footer.privacy.href': '/datenschutz',
|
||||
'footer.imprint': 'Impressum',
|
||||
'footer.imprint.href': '/impressum',
|
||||
// About page
|
||||
'about.badge': 'Über die App',
|
||||
'about.title.pre': 'Über ',
|
||||
'about.title.accent': 'Armarium',
|
||||
'about.desc': 'Armarium ist ein persönlicher Finanzbegleiter, der dir hilft, den Überblick über deine Finanzen zu behalten – einfach, sicher und ohne Ablenkung.',
|
||||
'about.mission.badge': 'Unsere Mission',
|
||||
'about.mission.title': 'Finanzen einfach im Griff',
|
||||
'about.mission.p1': 'Armarium entstand aus dem Wunsch, Finanzen ohne unnötige Komplexität zu verwalten. Keine überladenen Dashboards, kein Datendschungel – nur das Wesentliche, klar und übersichtlich.',
|
||||
'about.mission.p2': 'Deine Daten gehören dir. Kein Datenverkauf, kein Tracking, keine versteckten Kosten. Armarium ist ein Werkzeug, das für dich arbeitet – nicht umgekehrt.',
|
||||
'about.app.label': 'Finanz-App',
|
||||
'about.app.title': 'Armarium Suite',
|
||||
'about.app.desc': 'Budget, Transaktionen, Kategorien, Sparziele – alles in einer App. Verfügbar als Web-App, optimiert für Desktop und Mobile.',
|
||||
'about.privacy.label': 'Datenschutz',
|
||||
'about.privacy.title': 'Deine Daten, deine Kontrolle',
|
||||
'about.privacy.desc': 'Armarium speichert deine Finanzdaten sicher. Kein Verkauf an Dritte, keine Werbung, kein Tracking.',
|
||||
'about.values.badge': 'Unsere Werte',
|
||||
'about.values.title': 'Wofür Armarium steht',
|
||||
'about.v1.title': 'Transparenz',
|
||||
'about.v1.desc': 'Klare Übersichten ohne versteckte Kosten oder komplizierte Strukturen.',
|
||||
'about.v2.title': 'Einfachheit',
|
||||
'about.v2.desc': 'Funktionen, die du wirklich brauchst – ohne unnötigen Ballast.',
|
||||
'about.v3.title': 'Datenschutz',
|
||||
'about.v3.desc': 'Deine Finanzdaten sind privat. Punkt.',
|
||||
'about.v4.title': 'Made in Zürich',
|
||||
'about.v4.desc': 'Entwickelt in Zürich, Switzerland. Schweizer Qualität und Präzision.',
|
||||
'about.cta.title': 'Bereit loszulegen?',
|
||||
'about.cta.desc': 'Kostenlos registrieren und Armarium sofort nutzen.',
|
||||
'about.cta.back': 'Zurück zur Startseite',
|
||||
'about.team.title': 'Das Team hinter Armarium',
|
||||
'about.team.desc1': 'Armarium Suite ist ein Ein-Mann-Projekt, das aus einem echten persönlichen Bedürfnis entstanden ist: eine einfache, datenschutzfreundliche App zur Budgetverwaltung zu haben – ohne Kompromisse.',
|
||||
'about.team.desc2': 'Als Entwickler und Nutzer in einem stecke ich mein ganzes Herzblut in die App. Jede Funktion ist bewusst gewählt, jede Entscheidung zielt auf Einfachheit und Vertrauen.',
|
||||
'about.founder.role': 'Founder',
|
||||
'about.founder.bio': 'Ich habe Armarium Suite gegründet, weil ich selbst eine einfache, datenschutzfreundliche App zur Budgetverwaltung gesucht habe — und keine gefunden habe, die wirklich überzeugt. Armarium ist meine Antwort darauf: klar, lokal und ohne Kompromisse beim Datenschutz.',
|
||||
'about.faq.title': 'Häufig gestellte Fragen',
|
||||
'about.faq.q1': 'Ist Armarium Suite kostenlos?',
|
||||
'about.faq.a1': 'Ja, Armarium Suite ist vollständig kostenlos nutzbar. Es gibt keine versteckten Kosten, keine Abonnements und keine Premium-Funktionen hinter einer Paywall.',
|
||||
'about.faq.q2': 'Wo werden meine Daten gespeichert?',
|
||||
'about.faq.a2': 'Deine Finanzdaten werden ausschliesslich lokal in deinem Browser gespeichert. Wir haben keinen Zugriff auf deine Transaktionen, Budgets oder Sparziele. Deine Daten verlassen dein Gerät nicht.',
|
||||
'about.faq.q3': 'Funktioniert Armarium auf meinem Gerät?',
|
||||
'about.faq.a3': 'Armarium Suite ist eine Web-App und läuft in jedem modernen Browser – auf Desktop, Tablet und Smartphone. Du kannst sie auch als PWA auf deinem Startbildschirm installieren.',
|
||||
'about.faq.q4': 'Kann ich meine Daten exportieren?',
|
||||
'about.faq.a4': 'Ja, du kannst alle deine Daten jederzeit als CSV oder JSON exportieren. Gehe dazu unter Einstellungen → Daten exportieren.',
|
||||
// Contact page
|
||||
'contact.badge': 'Kontakt',
|
||||
'contact.title': 'Schreib uns.',
|
||||
'contact.desc': 'Nutze das Formular oder kontaktiere uns direkt. Wir melden uns innerhalb eines Werktages.',
|
||||
'contact.form.title': 'Nachricht senden',
|
||||
'contact.direct.title': 'Direkt erreichen',
|
||||
'contact.direct.desc': 'Bevorzugst du einen direkten Kanal? Wähle was am besten passt.',
|
||||
'contact.follow': 'Drop us a line',
|
||||
// Blog index
|
||||
'blog.badge': 'Alle Beiträge',
|
||||
'blog.title': 'Blog',
|
||||
'blog.desc': 'Neuigkeiten, Tipps und Updates rund um Armarium und persönliche Finanzen.',
|
||||
'blog.featured': 'Hervorgehoben',
|
||||
'blog.allposts': 'Alle Beiträge',
|
||||
'blog.filter.label': 'Filter',
|
||||
'blog.filter.all': 'Alle Beiträge',
|
||||
'blog.noposts': 'Keine Beiträge gefunden',
|
||||
'blog.noposts.tag': 'Keine Beiträge für diesen Tag',
|
||||
'blog.follow.title': 'Auf dem Laufenden bleiben',
|
||||
'blog.follow.desc': 'Neue Beiträge, Tipps und Updates direkt von uns.',
|
||||
'blog.back': 'Zurück zum Blog',
|
||||
// Features index
|
||||
'features.cta.title': 'Bereit loszulegen?',
|
||||
'features.cta.desc': 'Kostenlos registrieren und alle Features sofort nutzen.',
|
||||
'features.cta.login': 'Login',
|
||||
'features.back': 'Zurück zu Features',
|
||||
},
|
||||
fr: {
|
||||
'nav.blog': 'Blog',
|
||||
'nav.features': 'Fonctionnalités',
|
||||
'nav.about': 'À propos',
|
||||
'nav.contact': 'Contact',
|
||||
'nav.blog.href': '/fr/blog',
|
||||
'nav.features.href': '/fr/features',
|
||||
'nav.about.href': '/fr/about',
|
||||
'nav.contact.href': '/fr/contact',
|
||||
'hero.description': 'Gardez le contrôle de vos finances. Enregistrez vos dépenses, gérez vos budgets et atteignez vos objectifs d\'épargne – simplement et clairement.',
|
||||
'hero.register': 'S\'inscrire',
|
||||
'hero.login': 'Connexion',
|
||||
'features.badge': 'Fonctionnalités',
|
||||
'features.title': 'Tout ce qu\'il vous faut',
|
||||
'features.description': 'Armarium réunit toutes les fonctions essentielles pour une gestion financière solide dans une application claire.',
|
||||
'f1.title': 'Vue d\'ensemble',
|
||||
'f1.desc': 'Toutes les recettes et dépenses en un coup d\'œil. Votre solde actuel toujours sous contrôle.',
|
||||
'f2.title': 'Saisir des transactions',
|
||||
'f2.desc': 'Enregistrez rapidement dépenses et recettes, catégorisez et filtrez – pour une vue complète.',
|
||||
'f3.title': 'Catégories & rapports',
|
||||
'f3.desc': 'Définissez vos propres catégories et analysez vos dépenses. Comprenez où va votre argent.',
|
||||
'f4.title': 'Plusieurs comptes',
|
||||
'f4.desc': 'Gérez compte bancaire, carte de crédit et espèces séparément – tout regroupé dans une seule app.',
|
||||
'f5.title': 'Objectifs d\'épargne',
|
||||
'f5.desc': 'Fixez-vous des objectifs financiers et suivez votre progression – pas à pas vers le but.',
|
||||
'f6.title': 'Sûr & privé',
|
||||
'f6.desc': 'Vos données vous appartiennent. Pas de vente, pas de tracking – contrôle total sur vos données financières.',
|
||||
'blog.section.title': 'Actualités & mises à jour',
|
||||
'blog.readmore': 'Lire la suite',
|
||||
'blog.allposts': 'Tous les articles',
|
||||
'why.title': 'Repensez votre gestion financière',
|
||||
'why.intro': 'Armarium simplifie votre vie financière – que vous suiviez des dépenses, planifiiez des budgets ou fixiez des objectifs d\'épargne.',
|
||||
'why.item1.title': 'Données en Suisse',
|
||||
'why.item1.desc': 'Vos données financières sont stockées exclusivement sur des serveurs suisses. Pas de vente de données, pas de tracking – vous gardez le contrôle.',
|
||||
'why.item2.title': 'Gestion budgétaire intelligente',
|
||||
'why.item2.desc': 'Gardez une vue d\'ensemble sur vos revenus et dépenses. La catégorisation automatique et les graphiques clairs montrent exactement où va votre argent.',
|
||||
'why.item3.title': 'Entièrement personnalisable',
|
||||
'why.item3.desc': 'Créez vos propres catégories, gérez plusieurs comptes et adaptez l\'app à votre quotidien.',
|
||||
'why.item4.title': 'Plusieurs comptes',
|
||||
'why.item4.desc': 'Gérez compte bancaire, carte de crédit et espèces séparément – tout regroupé dans une seule app.',
|
||||
'why.learnmore': 'En savoir plus',
|
||||
'why.footer': 'Commencer gratuitement – aucune carte de crédit requise.',
|
||||
'cta.title': 'Prêt à maîtriser votre budget?',
|
||||
'cta.desc': 'Inscrivez-vous gratuitement et commencez immédiatement. Aucune carte de crédit requise.',
|
||||
'cta.register': 'S\'inscrire gratuitement',
|
||||
'cta.login': 'Déjà inscrit? Connexion',
|
||||
'footer.tagline': 'Votre assistant financier personnel – budget maîtrisé, objectifs en vue.',
|
||||
'footer.copyright': '© {year} Armarium Suite',
|
||||
'footer.app': 'Produit',
|
||||
'footer.start': 'Commencer',
|
||||
'footer.register': 'S\'inscrire',
|
||||
'footer.legal': 'Légal',
|
||||
'footer.privacy': 'Confidentialité',
|
||||
'footer.privacy.href': '/fr/privacy',
|
||||
'footer.imprint': 'Mentions légales',
|
||||
'footer.imprint.href': '/fr/impressum',
|
||||
'about.badge': 'À propos de l\'app',
|
||||
'about.title.pre': 'À propos d\'',
|
||||
'about.title.accent': 'Armarium',
|
||||
'about.desc': 'Armarium est votre assistant financier personnel – simple, sécurisé et sans distraction.',
|
||||
'about.mission.badge': 'Notre mission',
|
||||
'about.mission.title': 'Vos finances simplement maîtrisées',
|
||||
'about.mission.p1': 'Armarium est né du souhait de gérer les finances sans complexité inutile. Pas de tableaux de bord surchargés, pas de jungle de données – seulement l\'essentiel, clair et structuré.',
|
||||
'about.mission.p2': 'Vos données vous appartiennent. Pas de vente de données, pas de tracking, pas de frais cachés. Armarium est un outil qui travaille pour vous – pas l\'inverse.',
|
||||
'about.app.label': 'Application financière',
|
||||
'about.app.title': 'Armarium Suite',
|
||||
'about.app.desc': 'Budget, transactions, catégories, objectifs d\'épargne – tout en une app. Disponible en web, optimisé pour desktop et mobile.',
|
||||
'about.privacy.label': 'Protection des données',
|
||||
'about.privacy.title': 'Vos données, votre contrôle',
|
||||
'about.privacy.desc': 'Armarium stocke vos données financières en toute sécurité. Pas de vente à des tiers, pas de publicité, pas de tracking.',
|
||||
'about.values.badge': 'Nos valeurs',
|
||||
'about.values.title': 'Ce que représente Armarium',
|
||||
'about.v1.title': 'Transparence',
|
||||
'about.v1.desc': 'Des aperçus clairs sans frais cachés ni structures compliquées.',
|
||||
'about.v2.title': 'Simplicité',
|
||||
'about.v2.desc': 'Les fonctionnalités dont vous avez vraiment besoin – sans superflu.',
|
||||
'about.v3.title': 'Protection des données',
|
||||
'about.v3.desc': 'Vos données financières sont privées. Point final.',
|
||||
'about.v4.title': 'Made in Zürich',
|
||||
'about.v4.desc': 'Développé à Zürich, Suisse. Qualité et précision helvétiques.',
|
||||
'about.cta.title': 'Prêt à commencer?',
|
||||
'about.cta.desc': 'Inscrivez-vous gratuitement et utilisez Armarium immédiatement.',
|
||||
'about.cta.back': 'Retour à l\'accueil',
|
||||
'about.team.title': 'L\'équipe derrière Armarium',
|
||||
'about.team.desc1': 'Armarium Suite est un projet solo né d\'un besoin personnel réel : disposer d\'une app de gestion budgétaire simple et respectueuse de la vie privée – sans compromis.',
|
||||
'about.team.desc2': 'En tant que développeur et utilisateur à la fois, je mets tout mon cœur dans l\'app. Chaque fonctionnalité est choisie délibérément, chaque décision vise la simplicité et la confiance.',
|
||||
'about.founder.role': 'Founder',
|
||||
'about.founder.bio': 'J\'ai fondé Armarium Suite parce que je cherchais moi-même une app simple et respectueuse de la vie privée pour gérer mon budget — et je n\'en ai pas trouvé qui soit vraiment convaincante. Armarium est ma réponse : claire, locale et sans compromis sur la confidentialité.',
|
||||
'about.faq.title': 'Questions fréquentes',
|
||||
'about.faq.q1': 'Armarium Suite est-il gratuit ?',
|
||||
'about.faq.a1': 'Oui, Armarium Suite est entièrement gratuit. Pas de frais cachés, pas d\'abonnement, pas de fonctionnalités premium derrière un paywall.',
|
||||
'about.faq.q2': 'Où sont stockées mes données ?',
|
||||
'about.faq.a2': 'Vos données financières sont stockées uniquement en local dans votre navigateur. Nous n\'avons pas accès à vos transactions, budgets ou objectifs d\'épargne. Vos données ne quittent pas votre appareil.',
|
||||
'about.faq.q3': 'Armarium fonctionne-t-il sur mon appareil ?',
|
||||
'about.faq.a3': 'Armarium Suite est une web app qui fonctionne dans tout navigateur moderne – sur desktop, tablette et smartphone. Vous pouvez aussi l\'installer comme PWA sur votre écran d\'accueil.',
|
||||
'about.faq.q4': 'Puis-je exporter mes données ?',
|
||||
'about.faq.a4': 'Oui, vous pouvez exporter toutes vos données à tout moment en CSV ou JSON. Allez dans Paramètres → Exporter les données.',
|
||||
'contact.badge': 'Contact',
|
||||
'contact.title': 'Écrivez-nous.',
|
||||
'contact.desc': 'Utilisez le formulaire ou contactez-nous directement. Nous répondons dans un jour ouvrable.',
|
||||
'contact.form.title': 'Envoyer un message',
|
||||
'contact.direct.title': 'Nous rejoindre directement',
|
||||
'contact.direct.desc': 'Vous préférez un canal direct? Choisissez ce qui vous convient le mieux.',
|
||||
'contact.follow': 'Envoyez-nous un message',
|
||||
'blog.badge': 'Tous les articles',
|
||||
'blog.title': 'Blog',
|
||||
'blog.desc': 'Actualités, conseils et mises à jour sur Armarium et les finances personnelles.',
|
||||
'blog.featured': 'En vedette',
|
||||
'blog.allposts': 'Tous les articles',
|
||||
'blog.filter.label': 'Filtrer',
|
||||
'blog.filter.all': 'Tous les articles',
|
||||
'blog.noposts': 'Aucun article trouvé',
|
||||
'blog.noposts.tag': 'Aucun article pour ce tag',
|
||||
'blog.follow.title': 'Restez informé',
|
||||
'blog.follow.desc': 'Nouveaux articles, conseils et mises à jour directement de nous.',
|
||||
'blog.back': 'Retour au blog',
|
||||
'features.cta.title': 'Prêt à commencer?',
|
||||
'features.cta.desc': 'Inscrivez-vous gratuitement et utilisez toutes les fonctionnalités immédiatement.',
|
||||
'features.cta.login': 'Connexion',
|
||||
'features.back': 'Retour aux fonctionnalités',
|
||||
},
|
||||
it: {
|
||||
'nav.blog': 'Blog',
|
||||
'nav.features': 'Funzionalità',
|
||||
'nav.about': 'Chi siamo',
|
||||
'nav.contact': 'Contatto',
|
||||
'nav.blog.href': '/it/blog',
|
||||
'nav.features.href': '/it/features',
|
||||
'nav.about.href': '/it/about',
|
||||
'nav.contact.href': '/it/contact',
|
||||
'hero.description': 'Tieni sotto controllo le tue finanze. Registra le spese, gestisci i budget e raggiungi i tuoi obiettivi di risparmio – in modo semplice e chiaro.',
|
||||
'hero.register': 'Registrarsi',
|
||||
'hero.login': 'Accedi',
|
||||
'features.badge': 'Funzionalità',
|
||||
'features.title': 'Tutto ciò di cui hai bisogno',
|
||||
'features.description': 'Armarium riunisce tutte le funzioni essenziali per una gestione finanziaria solida in un\'app chiara.',
|
||||
'f1.title': 'Panoramica del budget',
|
||||
'f1.desc': 'Tutte le entrate e le uscite in un colpo d\'occhio. Il tuo saldo attuale sempre sotto controllo.',
|
||||
'f2.title': 'Registrare transazioni',
|
||||
'f2.desc': 'Registra rapidamente spese ed entrate, categorizza e filtra – per una panoramica completa.',
|
||||
'f3.title': 'Categorie & report',
|
||||
'f3.desc': 'Definisci le tue categorie e analizza le spese. Capisce dove vanno i tuoi soldi.',
|
||||
'f4.title': 'Più conti',
|
||||
'f4.desc': 'Gestisci conto bancario, carta di credito e contanti separatamente – tutto in un\'unica app.',
|
||||
'f5.title': 'Obiettivi di risparmio',
|
||||
'f5.desc': 'Fissa obiettivi finanziari e monitora i tuoi progressi – passo dopo passo verso il traguardo.',
|
||||
'f6.title': 'Sicuro & privato',
|
||||
'f6.desc': 'I tuoi dati sono tuoi. Nessuna vendita, nessun tracking – pieno controllo sui tuoi dati finanziari.',
|
||||
'blog.section.title': 'Novità & aggiornamenti',
|
||||
'blog.readmore': 'Leggi di più',
|
||||
'blog.allposts': 'Tutti gli articoli',
|
||||
'why.title': 'Ripensa la tua gestione finanziaria',
|
||||
'why.intro': 'Armarium semplifica la tua vita finanziaria – che tu voglia tracciare spese, pianificare budget o fissare obiettivi di risparmio.',
|
||||
'why.item1.title': 'Dati in Svizzera',
|
||||
'why.item1.desc': 'I tuoi dati finanziari sono archiviati esclusivamente su server svizzeri. Nessuna vendita di dati, nessun tracking – il controllo rimane tuo.',
|
||||
'why.item2.title': 'Gestione intelligente del budget',
|
||||
'why.item2.desc': 'Tieni d\'occhio tutte le entrate e le uscite. La categorizzazione automatica e i grafici chiari mostrano esattamente dove va il tuo denaro.',
|
||||
'why.item3.title': 'Completamente personalizzabile',
|
||||
'why.item3.desc': 'Crea categorie personalizzate, gestisci più conti e adatta l\'app alla tua vita quotidiana.',
|
||||
'why.item4.title': 'Più conti',
|
||||
'why.item4.desc': 'Gestisci conto bancario, carta di credito e contanti separatamente – tutto in un\'unica app.',
|
||||
'why.learnmore': 'Scopri di più',
|
||||
'why.footer': 'Inizia gratuitamente – nessuna carta di credito richiesta.',
|
||||
'cta.title': 'Pronto a gestire il tuo budget?',
|
||||
'cta.desc': 'Registrati gratuitamente e inizia subito. Nessuna carta di credito richiesta.',
|
||||
'cta.register': 'Registrarsi gratuitamente',
|
||||
'cta.login': 'Già registrato? Accedi',
|
||||
'footer.tagline': 'Il tuo assistente finanziario personale – budget sotto controllo, obiettivi in vista.',
|
||||
'footer.copyright': '© {year} Armarium Suite',
|
||||
'footer.app': 'Prodotto',
|
||||
'footer.start': 'Inizia',
|
||||
'footer.register': 'Registrarsi',
|
||||
'footer.legal': 'Legale',
|
||||
'footer.privacy': 'Privacy',
|
||||
'footer.privacy.href': '/it/privacy',
|
||||
'footer.imprint': 'Note legali',
|
||||
'footer.imprint.href': '/it/impressum',
|
||||
'about.badge': 'Sull\'app',
|
||||
'about.title.pre': 'Chi siamo — ',
|
||||
'about.title.accent': 'Armarium',
|
||||
'about.desc': 'Armarium è il tuo assistente finanziario personale – semplice, sicuro e senza distrazioni.',
|
||||
'about.mission.badge': 'La nostra missione',
|
||||
'about.mission.title': 'Finanze semplici e chiare',
|
||||
'about.mission.p1': 'Armarium è nato dal desiderio di gestire le finanze senza complessità inutili. Niente dashboard sovraccariche, niente giungla di dati – solo l\'essenziale, chiaro e ordinato.',
|
||||
'about.mission.p2': 'I tuoi dati sono tuoi. Nessuna vendita di dati, nessun tracking, nessun costo nascosto. Armarium è uno strumento che lavora per te – non il contrario.',
|
||||
'about.app.label': 'App finanziaria',
|
||||
'about.app.title': 'Armarium Suite',
|
||||
'about.app.desc': 'Budget, transazioni, categorie, obiettivi di risparmio – tutto in un\'unica app. Disponibile come web app, ottimizzata per desktop e mobile.',
|
||||
'about.privacy.label': 'Privacy',
|
||||
'about.privacy.title': 'I tuoi dati, il tuo controllo',
|
||||
'about.privacy.desc': 'Armarium archivia i tuoi dati finanziari in modo sicuro. Nessuna vendita a terzi, nessuna pubblicità, nessun tracking.',
|
||||
'about.values.badge': 'I nostri valori',
|
||||
'about.values.title': 'I valori di Armarium',
|
||||
'about.v1.title': 'Trasparenza',
|
||||
'about.v1.desc': 'Panoramiche chiare senza costi nascosti o strutture complicate.',
|
||||
'about.v2.title': 'Semplicità',
|
||||
'about.v2.desc': 'Le funzionalità di cui hai davvero bisogno – senza inutile complessità.',
|
||||
'about.v3.title': 'Privacy',
|
||||
'about.v3.desc': 'I tuoi dati finanziari sono privati. Punto.',
|
||||
'about.v4.title': 'Made in Zürich',
|
||||
'about.v4.desc': 'Sviluppato a Zurigo, Svizzera. Qualità e precisione svizzere.',
|
||||
'about.cta.title': 'Pronto per iniziare?',
|
||||
'about.cta.desc': 'Registrati gratuitamente e usa Armarium subito.',
|
||||
'about.cta.back': 'Torna alla home',
|
||||
'about.team.title': 'Il team dietro Armarium',
|
||||
'about.team.desc1': 'Armarium Suite è un progetto individuale nato da un bisogno personale reale: avere un\'app semplice e rispettosa della privacy per gestire il budget – senza compromessi.',
|
||||
'about.team.desc2': 'Come sviluppatore e utente allo stesso tempo, metto tutto il mio impegno nell\'app. Ogni funzionalità è scelta deliberatamente, ogni decisione punta alla semplicità e alla fiducia.',
|
||||
'about.founder.role': 'Founder',
|
||||
'about.founder.bio': 'Ho fondato Armarium Suite perché cercavo io stesso un\'app semplice e rispettosa della privacy per gestire il budget — e non ne ho trovata una davvero convincente. Armarium è la mia risposta: chiara, locale e senza compromessi sulla privacy.',
|
||||
'about.faq.title': 'Domande frequenti',
|
||||
'about.faq.q1': 'Armarium Suite è gratuito?',
|
||||
'about.faq.a1': 'Sì, Armarium Suite è completamente gratuito. Nessun costo nascosto, nessun abbonamento, nessuna funzionalità premium a pagamento.',
|
||||
'about.faq.q2': 'Dove vengono archiviati i miei dati?',
|
||||
'about.faq.a2': 'I tuoi dati finanziari vengono archiviati esclusivamente in locale nel tuo browser. Non abbiamo accesso alle tue transazioni, budget o obiettivi di risparmio. I tuoi dati non lasciano il tuo dispositivo.',
|
||||
'about.faq.q3': 'Armarium funziona sul mio dispositivo?',
|
||||
'about.faq.a3': 'Armarium Suite è una web app che funziona in qualsiasi browser moderno – su desktop, tablet e smartphone. Puoi anche installarla come PWA sulla schermata iniziale.',
|
||||
'about.faq.q4': 'Posso esportare i miei dati?',
|
||||
'about.faq.a4': 'Sì, puoi esportare tutti i tuoi dati in qualsiasi momento come CSV o JSON. Vai su Impostazioni → Esporta dati.',
|
||||
'contact.badge': 'Contatto',
|
||||
'contact.title': 'Scrivici.',
|
||||
'contact.desc': 'Usa il modulo o contattaci direttamente. Rispondiamo entro un giorno lavorativo.',
|
||||
'contact.form.title': 'Invia un messaggio',
|
||||
'contact.direct.title': 'Contattaci direttamente',
|
||||
'contact.direct.desc': 'Preferisci un canale diretto? Scegli quello che ti conviene di più.',
|
||||
'contact.follow': 'Scrivici',
|
||||
'blog.badge': 'Tutti gli articoli',
|
||||
'blog.title': 'Blog',
|
||||
'blog.desc': 'Notizie, suggerimenti e aggiornamenti su Armarium e le finanze personali.',
|
||||
'blog.featured': 'In evidenza',
|
||||
'blog.allposts': 'Tutti gli articoli',
|
||||
'blog.filter.label': 'Filtra',
|
||||
'blog.filter.all': 'Tutti gli articoli',
|
||||
'blog.noposts': 'Nessun articolo trovato',
|
||||
'blog.noposts.tag': 'Nessun articolo per questo tag',
|
||||
'blog.follow.title': 'Rimani aggiornato',
|
||||
'blog.follow.desc': 'Nuovi articoli, suggerimenti e aggiornamenti direttamente da noi.',
|
||||
'blog.back': 'Torna al blog',
|
||||
'features.cta.title': 'Pronto per iniziare?',
|
||||
'features.cta.desc': 'Registrati gratuitamente e usa tutte le funzionalità subito.',
|
||||
'features.cta.login': 'Accedi',
|
||||
'features.back': 'Torna alle funzionalità',
|
||||
},
|
||||
en: {
|
||||
'nav.blog': 'Blog',
|
||||
'nav.features': 'Features',
|
||||
'nav.about': 'About',
|
||||
'nav.contact': 'Contact',
|
||||
'nav.blog.href': '/en/blog',
|
||||
'nav.features.href': '/en/features',
|
||||
'nav.about.href': '/en/about',
|
||||
'nav.contact.href': '/en/contact',
|
||||
'hero.description': 'Keep track of your finances. Record expenses, manage budgets and reach your savings goals – simply and clearly.',
|
||||
'hero.register': 'Sign up',
|
||||
'hero.login': 'Login',
|
||||
'features.badge': 'Features',
|
||||
'features.title': 'Everything you need',
|
||||
'features.description': 'Armarium brings together all the essential functions for solid financial management in one clear app.',
|
||||
'f1.title': 'Budget Overview',
|
||||
'f1.desc': 'All income and expenses at a glance. Your current balance always under control.',
|
||||
'f2.title': 'Record Transactions',
|
||||
'f2.desc': 'Quickly record income and expenses, categorize and filter – for a complete overview.',
|
||||
'f3.title': 'Categories & Reports',
|
||||
'f3.desc': 'Define your own categories and analyze spending. Understand where your money goes.',
|
||||
'f4.title': 'Multiple Accounts',
|
||||
'f4.desc': 'Manage bank account, credit card and cash separately – all combined in one app.',
|
||||
'f5.title': 'Savings Goals',
|
||||
'f5.desc': 'Set financial goals and track your progress – step by step towards the target.',
|
||||
'f6.title': 'Secure & Private',
|
||||
'f6.desc': 'Your data belongs to you. No data selling, no tracking – full control over your financial data.',
|
||||
'blog.section.title': 'News & updates',
|
||||
'blog.readmore': 'Read more',
|
||||
'blog.allposts': 'All posts',
|
||||
'why.title': 'Financial management reimagined',
|
||||
'why.intro': 'Armarium simplifies your financial life – whether you\'re tracking expenses, planning budgets, or setting savings goals.',
|
||||
'why.item1.title': 'Data stored in Switzerland',
|
||||
'why.item1.desc': 'Your financial data is stored exclusively on Swiss servers. No data selling, no tracking – full control stays with you.',
|
||||
'why.item2.title': 'Smart budget management',
|
||||
'why.item2.desc': 'Keep track of all income and expenses. Automatic categorisation and clear charts show you exactly where your money goes.',
|
||||
'why.item3.title': 'Fully customisable',
|
||||
'why.item3.desc': 'Create your own categories, manage multiple accounts and tailor the app to your everyday life.',
|
||||
'why.item4.title': 'Multiple Accounts',
|
||||
'why.item4.desc': 'Manage bank account, credit card and cash separately – all combined in one app.',
|
||||
'why.learnmore': 'Learn more',
|
||||
'why.footer': 'Start for free – no credit card required.',
|
||||
'cta.title': 'Ready to take control of your budget?',
|
||||
'cta.desc': 'Sign up for free and get started immediately. No credit card required.',
|
||||
'cta.register': 'Sign up for free',
|
||||
'cta.login': 'Already registered? Login',
|
||||
'footer.tagline': 'Your personal finance companion – budget in check, goals in focus.',
|
||||
'footer.copyright': '© {year} Armarium Suite',
|
||||
'footer.app': 'Product',
|
||||
'footer.start': 'Get started',
|
||||
'footer.register': 'Sign up',
|
||||
'footer.legal': 'Legal',
|
||||
'footer.privacy': 'Privacy Policy',
|
||||
'footer.privacy.href': '/en/privacy',
|
||||
'footer.imprint': 'Legal Notice',
|
||||
'footer.imprint.href': '/en/impressum',
|
||||
'about.badge': 'About the app',
|
||||
'about.title.pre': 'About ',
|
||||
'about.title.accent': 'Armarium',
|
||||
'about.desc': 'Armarium is your personal finance companion – simple, secure and distraction-free.',
|
||||
'about.mission.badge': 'Our mission',
|
||||
'about.mission.title': 'Finances made simple',
|
||||
'about.mission.p1': 'Armarium was born from the desire to manage finances without unnecessary complexity. No overloaded dashboards, no data jungle – just the essentials, clear and structured.',
|
||||
'about.mission.p2': 'Your data belongs to you. No data selling, no tracking, no hidden costs. Armarium is a tool that works for you – not the other way around.',
|
||||
'about.app.label': 'Finance App',
|
||||
'about.app.title': 'Armarium Suite',
|
||||
'about.app.desc': 'Budget, transactions, categories, savings goals – all in one app. Available as a web app, optimized for desktop and mobile.',
|
||||
'about.privacy.label': 'Privacy',
|
||||
'about.privacy.title': 'Your data, your control',
|
||||
'about.privacy.desc': 'Armarium stores your financial data securely. No selling to third parties, no advertising, no tracking.',
|
||||
'about.values.badge': 'Our values',
|
||||
'about.values.title': 'What Armarium stands for',
|
||||
'about.v1.title': 'Transparency',
|
||||
'about.v1.desc': 'Clear overviews without hidden costs or complicated structures.',
|
||||
'about.v2.title': 'Simplicity',
|
||||
'about.v2.desc': 'The features you actually need – without unnecessary ballast.',
|
||||
'about.v3.title': 'Privacy',
|
||||
'about.v3.desc': 'Your financial data is private. Period.',
|
||||
'about.v4.title': 'Made in Zürich',
|
||||
'about.v4.desc': 'Developed in Zürich, Switzerland. Swiss quality and precision.',
|
||||
'about.cta.title': 'Ready to get started?',
|
||||
'about.cta.desc': 'Sign up for free and start using Armarium right away.',
|
||||
'about.cta.back': 'Back to home',
|
||||
'about.team.title': 'The team behind Armarium',
|
||||
'about.team.desc1': 'Armarium Suite is a solo project born from a real personal need: having a simple, privacy-friendly app for budget management – without compromise.',
|
||||
'about.team.desc2': 'As both developer and user, I put my heart into the app. Every feature is deliberately chosen, every decision aimed at simplicity and trust.',
|
||||
'about.founder.role': 'Founder',
|
||||
'about.founder.bio': 'I founded Armarium Suite because I was looking for a simple, privacy-friendly app to manage my budget — and couldn\'t find one that truly convinced me. Armarium is my answer: clear, local and uncompromising on privacy.',
|
||||
'about.faq.title': 'Frequently asked questions',
|
||||
'about.faq.q1': 'Is Armarium Suite free?',
|
||||
'about.faq.a1': 'Yes, Armarium Suite is completely free. No hidden costs, no subscriptions, no premium features behind a paywall.',
|
||||
'about.faq.q2': 'Where is my data stored?',
|
||||
'about.faq.a2': 'Your financial data is stored exclusively locally in your browser. We have no access to your transactions, budgets or savings goals. Your data never leaves your device.',
|
||||
'about.faq.q3': 'Does Armarium work on my device?',
|
||||
'about.faq.a3': 'Armarium Suite is a web app that works in any modern browser – on desktop, tablet and smartphone. You can also install it as a PWA on your home screen.',
|
||||
'about.faq.q4': 'Can I export my data?',
|
||||
'about.faq.a4': 'Yes, you can export all your data at any time as CSV or JSON. Go to Settings → Export data.',
|
||||
'contact.badge': 'Contact',
|
||||
'contact.title': 'Write to us.',
|
||||
'contact.desc': 'Use the form or contact us directly. We respond within one business day.',
|
||||
'contact.form.title': 'Send a message',
|
||||
'contact.direct.title': 'Reach us directly',
|
||||
'contact.direct.desc': 'Prefer a direct channel? Choose what works best for you.',
|
||||
'contact.follow': 'Drop us a line',
|
||||
'blog.badge': 'All posts',
|
||||
'blog.title': 'Blog',
|
||||
'blog.desc': 'News, tips and updates about Armarium and personal finance.',
|
||||
'blog.featured': 'Featured',
|
||||
'blog.allposts': 'All posts',
|
||||
'blog.filter.label': 'Filter',
|
||||
'blog.filter.all': 'All posts',
|
||||
'blog.noposts': 'No posts found',
|
||||
'blog.noposts.tag': 'No posts found for this tag',
|
||||
'blog.follow.title': 'Stay in the loop',
|
||||
'blog.follow.desc': 'New articles, tips and updates directly from us.',
|
||||
'blog.back': 'Back to Blog',
|
||||
'features.cta.title': 'Ready to get started?',
|
||||
'features.cta.desc': 'Sign up for free and use all features right away.',
|
||||
'features.cta.login': 'Login',
|
||||
'features.back': 'Back to Features',
|
||||
},
|
||||
} as const satisfies Record<Locale, Record<string, string>>;
|
||||
@@ -1,28 +0,0 @@
|
||||
import { ui, defaultLocale, type Locale } from './ui';
|
||||
|
||||
export function useTranslations(locale: Locale) {
|
||||
return function t(key: keyof typeof ui[typeof defaultLocale]): string {
|
||||
const localeUi = ui[locale] as Record<string, string>;
|
||||
const defaultUi = ui[defaultLocale] as Record<string, string>;
|
||||
return localeUi[key] ?? defaultUi[key] ?? key;
|
||||
};
|
||||
}
|
||||
|
||||
/** Returns the path for the given locale, stripping/adding the locale prefix. */
|
||||
export function getLocalePath(targetLocale: Locale, currentPath: string): string {
|
||||
const nonDefaultLocales: Locale[] = ['fr', 'it', 'en'];
|
||||
|
||||
let basePath = currentPath;
|
||||
for (const locale of nonDefaultLocales) {
|
||||
if (currentPath.startsWith(`/${locale}/`)) {
|
||||
basePath = currentPath.slice(locale.length + 1);
|
||||
break;
|
||||
} else if (currentPath === `/${locale}`) {
|
||||
basePath = '/';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (targetLocale === defaultLocale) return basePath || '/';
|
||||
return basePath === '/' ? `/${targetLocale}/` : `/${targetLocale}${basePath}`;
|
||||
}
|
||||
@@ -88,21 +88,6 @@ if (includeProfessionalServiceSchema) {
|
||||
<!-- View Transitions (client-side routing with animated page transitions) -->
|
||||
<ClientRouter />
|
||||
|
||||
<!-- Random theme per session — runs synchronously to avoid flash -->
|
||||
<script is:inline>
|
||||
(function () {
|
||||
var THEMES = ['orange','amber','lime','emerald','teal','cyan','sky','blue','indigo','violet','purple','magenta'];
|
||||
var KEY = 'color-theme';
|
||||
var theme;
|
||||
try { theme = sessionStorage.getItem(KEY); } catch (e) {}
|
||||
if (!theme) {
|
||||
theme = THEMES[Math.floor(Math.random() * THEMES.length)];
|
||||
try { sessionStorage.setItem(KEY, theme); } catch (e) {}
|
||||
}
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
})();
|
||||
</script>
|
||||
|
||||
<!-- Dynamic favicon: syncs letter (first letter of site name) and color (brand-500) with active theme -->
|
||||
<script define:vars={{ _faviconLetter: siteConfig.name.charAt(0).toUpperCase() }}>
|
||||
(function () {
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { ImageMetadata } from 'astro';
|
||||
import siteConfig from '@/config/site.config';
|
||||
import BaseLayout from './BaseLayout.astro';
|
||||
import Header from '@/components/layout/Header.astro';
|
||||
import AppFooter from '@/components/layout/AppFooter.astro';
|
||||
import Footer from '@/components/layout/Footer.astro';
|
||||
import Breadcrumbs from '@/components/seo/Breadcrumbs.astro';
|
||||
import ArticleHero from '@/components/blog/ArticleHero.astro';
|
||||
import ShareButtons from '@/components/blog/ShareButtons.astro';
|
||||
@@ -11,8 +11,6 @@ import RelatedPosts from '@/components/blog/RelatedPosts.astro';
|
||||
import CTA from '@/components/ui/marketing/CTA/CTA.astro';
|
||||
import Icon from '@/components/ui/primitives/Icon/Icon.astro';
|
||||
import { resolveSocialLinks } from '@/lib/utils';
|
||||
import { useTranslations } from '@/i18n/utils';
|
||||
import type { Locale } from '@/i18n/ui';
|
||||
|
||||
const socialLinks = resolveSocialLinks(siteConfig.socialLinks);
|
||||
|
||||
@@ -27,7 +25,6 @@ interface Props {
|
||||
svgSlug?: string;
|
||||
tags?: string[];
|
||||
slug?: string;
|
||||
locale?: Locale;
|
||||
}
|
||||
|
||||
const {
|
||||
@@ -41,15 +38,13 @@ const {
|
||||
svgSlug,
|
||||
tags = [],
|
||||
slug = '',
|
||||
locale = (Astro.currentLocale ?? 'de') as Locale,
|
||||
} = Astro.props;
|
||||
|
||||
const t = useTranslations(locale);
|
||||
const ogImage = image?.src || siteConfig.ogImage;
|
||||
|
||||
const breadcrumbs = [
|
||||
{ label: 'Home', href: locale === 'de' ? '/' : `/${locale}/` },
|
||||
{ label: t('nav.blog'), href: t('nav.blog.href') },
|
||||
{ label: 'Home', href: '/' },
|
||||
{ label: 'Blog', href: '/blog' },
|
||||
{ label: title },
|
||||
];
|
||||
|
||||
@@ -75,17 +70,10 @@ const fullUrl = new URL(Astro.url.pathname, Astro.site).toString();
|
||||
shape="bar"
|
||||
variant="solid"
|
||||
size="lg"
|
||||
showCta
|
||||
cta={{ label: t('hero.login'), href: 'https://app.armarium.ch/login' }}
|
||||
showThemeSelector
|
||||
showCta={false}
|
||||
cta={{ label: 'Get in touch', href: '/contact' }}
|
||||
showScrollProgress
|
||||
showLanguageSwitcher
|
||||
currentLocale={locale}
|
||||
nav={[
|
||||
{ label: t('nav.blog'), href: t('nav.blog.href') },
|
||||
{ label: t('nav.features'), href: t('nav.features.href') },
|
||||
{ label: t('nav.about'), href: t('nav.about.href') },
|
||||
{ label: t('nav.contact'), href: t('nav.contact.href') },
|
||||
]}
|
||||
/>
|
||||
|
||||
<div class="min-h-screen pt-16">
|
||||
@@ -119,7 +107,7 @@ const fullUrl = new URL(Astro.url.pathname, Astro.site).toString();
|
||||
<div class="flex flex-col gap-6 sm:flex-row sm:items-center sm:justify-between">
|
||||
<!-- Back link -->
|
||||
<a
|
||||
href={t('nav.blog.href')}
|
||||
href="/blog"
|
||||
class="text-brand-600 hover:text-brand-500 dark:text-brand-400 dark:hover:text-brand-300 inline-flex items-center gap-2 text-sm font-medium transition-colors"
|
||||
>
|
||||
<svg
|
||||
@@ -133,7 +121,7 @@ const fullUrl = new URL(Astro.url.pathname, Astro.site).toString();
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M10 19l-7-7m0 0l7-7m-7 7h18"
|
||||
></path>
|
||||
</svg>
|
||||
{t('blog.back')}
|
||||
Back to Blog
|
||||
</a>
|
||||
|
||||
<!-- Share buttons -->
|
||||
@@ -152,8 +140,8 @@ const fullUrl = new URL(Astro.url.pathname, Astro.site).toString();
|
||||
|
||||
<!-- Follow Along -->
|
||||
<CTA variant="default" size="md" maxWidth="lg" glow={false} data-reveal class="bg-background-secondary">
|
||||
<h2 slot="heading" class="!text-4xl">{t('blog.follow.title')}</h2>
|
||||
<p slot="description" class="!text-lg text-balance">{t('blog.follow.desc')}</p>
|
||||
<h2 slot="heading" class="!text-4xl">Follow along</h2>
|
||||
<p slot="description" class="!text-lg text-balance">Stay in the loop — new articles, thoughts, and updates.</p>
|
||||
|
||||
<div class="flex flex-wrap items-center justify-center gap-3">
|
||||
<a
|
||||
@@ -185,5 +173,5 @@ const fullUrl = new URL(Astro.url.pathname, Astro.site).toString();
|
||||
</CTA>
|
||||
</div>
|
||||
|
||||
<AppFooter slot="footer" locale={locale} />
|
||||
<Footer slot="footer" layout="simple" background="secondary" />
|
||||
</BaseLayout>
|
||||
|
||||
@@ -3,12 +3,13 @@
|
||||
* LandingLayout
|
||||
* Layout wrapper for landing-style pages (marketing, demo, showcase)
|
||||
* Uses Header + Footer with landing-specific configuration
|
||||
*
|
||||
* This layout is part of the "demo" layer - it can be optionally installed
|
||||
* via CLI for users who want the full landing page experience.
|
||||
*/
|
||||
import BaseLayout from './BaseLayout.astro';
|
||||
import Header from '@/components/layout/Header.astro';
|
||||
import AppFooter from '@/components/layout/AppFooter.astro';
|
||||
import { useTranslations } from '@/i18n/utils';
|
||||
import type { Locale } from '@/i18n/ui';
|
||||
import Footer from '@/components/layout/Footer.astro';
|
||||
|
||||
interface Props {
|
||||
title?: string;
|
||||
@@ -20,7 +21,6 @@ interface Props {
|
||||
includeOrgSchema?: boolean;
|
||||
includePersonSchema?: boolean;
|
||||
includeProfessionalServiceSchema?: boolean;
|
||||
locale?: Locale;
|
||||
}
|
||||
|
||||
const {
|
||||
@@ -33,11 +33,9 @@ const {
|
||||
includeOrgSchema = false,
|
||||
includePersonSchema = false,
|
||||
includeProfessionalServiceSchema = false,
|
||||
locale = (Astro.currentLocale ?? 'de') as Locale,
|
||||
} = Astro.props;
|
||||
|
||||
const t = useTranslations(locale);
|
||||
const isHomePage = Astro.url.pathname === '/' || Astro.url.pathname === `/${locale}/`;
|
||||
const isHomePage = Astro.url.pathname === '/';
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
@@ -58,21 +56,14 @@ const isHomePage = Astro.url.pathname === '/' || Astro.url.pathname === `/${loca
|
||||
variant="default"
|
||||
colorScheme="default"
|
||||
size="lg"
|
||||
nav={[
|
||||
{ label: t('nav.blog'), href: t('nav.blog.href') },
|
||||
{ label: t('nav.features'), href: t('nav.features.href') },
|
||||
{ label: t('nav.about'), href: t('nav.about.href') },
|
||||
{ label: t('nav.contact'), href: t('nav.contact.href') },
|
||||
]}
|
||||
showCta
|
||||
cta={{ label: t('hero.login'), href: 'https://app.armarium.ch/login' }}
|
||||
showLanguageSwitcher
|
||||
currentLocale={locale}
|
||||
showCta={false}
|
||||
cta={{ label: 'Get in touch', href: '/contact' }}
|
||||
showThemeSelector
|
||||
showScrollProgress={isHomePage}
|
||||
scrollProgressPosition="top"
|
||||
/>
|
||||
|
||||
<slot />
|
||||
|
||||
<AppFooter slot="footer" locale={locale} />
|
||||
<Footer slot="footer" layout="simple" background="secondary" />
|
||||
</BaseLayout>
|
||||
|
||||
@@ -6,9 +6,7 @@
|
||||
*/
|
||||
import BaseLayout from './BaseLayout.astro';
|
||||
import Header from '@/components/layout/Header.astro';
|
||||
import AppFooter from '@/components/layout/AppFooter.astro';
|
||||
import { useTranslations } from '@/i18n/utils';
|
||||
import type { Locale } from '@/i18n/ui';
|
||||
import Footer from '@/components/layout/Footer.astro';
|
||||
|
||||
interface Props {
|
||||
title?: string;
|
||||
@@ -18,21 +16,10 @@ interface Props {
|
||||
noindex?: boolean;
|
||||
nofollow?: boolean;
|
||||
showScrollProgress?: boolean;
|
||||
locale?: Locale;
|
||||
}
|
||||
|
||||
const {
|
||||
title,
|
||||
description,
|
||||
image,
|
||||
imageAlt,
|
||||
noindex,
|
||||
nofollow,
|
||||
showScrollProgress = false,
|
||||
locale = (Astro.currentLocale ?? 'de') as Locale,
|
||||
} = Astro.props;
|
||||
const { title, description, image, imageAlt, noindex, nofollow, showScrollProgress = false } = Astro.props;
|
||||
|
||||
const t = useTranslations(locale);
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
@@ -50,22 +37,15 @@ const t = useTranslations(locale);
|
||||
variant="solid"
|
||||
size="lg"
|
||||
showActiveState
|
||||
showCta
|
||||
cta={{ label: t('hero.login'), href: 'https://app.armarium.ch/login' }}
|
||||
showThemeSelector
|
||||
showCta={false}
|
||||
cta={{ label: 'Get in touch', href: '/contact' }}
|
||||
showScrollProgress={showScrollProgress}
|
||||
showLanguageSwitcher
|
||||
currentLocale={locale}
|
||||
nav={[
|
||||
{ label: t('nav.blog'), href: t('nav.blog.href') },
|
||||
{ label: t('nav.features'), href: t('nav.features.href') },
|
||||
{ label: t('nav.about'), href: t('nav.about.href') },
|
||||
{ label: t('nav.contact'), href: t('nav.contact.href') },
|
||||
]}
|
||||
/>
|
||||
|
||||
<main class="min-h-screen pt-16">
|
||||
<slot />
|
||||
</main>
|
||||
|
||||
<AppFooter slot="footer" locale={locale} />
|
||||
<Footer slot="footer" layout="simple" />
|
||||
</BaseLayout>
|
||||
|
||||
@@ -3,15 +3,13 @@ import type { ImageMetadata } from 'astro';
|
||||
import type { CollectionEntry } from 'astro:content';
|
||||
import BaseLayout from './BaseLayout.astro';
|
||||
import Header from '@/components/layout/Header.astro';
|
||||
import AppFooter from '@/components/layout/AppFooter.astro';
|
||||
import Footer from '@/components/layout/Footer.astro';
|
||||
import Breadcrumbs from '@/components/seo/Breadcrumbs.astro';
|
||||
import ProjectHero from '@/components/projects/ProjectHero.astro';
|
||||
import Card from '@/components/ui/data-display/Card/Card.astro';
|
||||
import Badge from '@/components/ui/data-display/Badge/Badge.astro';
|
||||
import Icon from '@/components/ui/primitives/Icon/Icon.astro';
|
||||
import siteConfig from '@/config/site.config';
|
||||
import { useTranslations } from '@/i18n/utils';
|
||||
import type { Locale } from '@/i18n/ui';
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
@@ -26,7 +24,6 @@ interface Props {
|
||||
image?: ImageMetadata;
|
||||
imageAlt?: string;
|
||||
related?: CollectionEntry<'projects'>[];
|
||||
locale?: Locale;
|
||||
}
|
||||
|
||||
const {
|
||||
@@ -42,15 +39,13 @@ const {
|
||||
image,
|
||||
imageAlt,
|
||||
related = [],
|
||||
locale = (Astro.currentLocale ?? 'de') as Locale,
|
||||
} = Astro.props;
|
||||
|
||||
const t = useTranslations(locale);
|
||||
const ogImage = image?.src || siteConfig.ogImage;
|
||||
|
||||
const breadcrumbs = [
|
||||
{ label: 'Home', href: locale === 'de' ? '/' : `/${locale}/` },
|
||||
{ label: t('nav.features'), href: t('nav.features.href') },
|
||||
{ label: 'Home', href: '/' },
|
||||
{ label: 'Projects', href: '/projects' },
|
||||
{ label: title },
|
||||
];
|
||||
---
|
||||
@@ -67,17 +62,9 @@ const breadcrumbs = [
|
||||
shape="bar"
|
||||
variant="solid"
|
||||
size="lg"
|
||||
showCta
|
||||
cta={{ label: t('hero.login'), href: 'https://app.armarium.ch/login' }}
|
||||
showThemeSelector
|
||||
showCta={false}
|
||||
showScrollProgress
|
||||
showLanguageSwitcher
|
||||
currentLocale={locale}
|
||||
nav={[
|
||||
{ label: t('nav.blog'), href: t('nav.blog.href') },
|
||||
{ label: t('nav.features'), href: t('nav.features.href') },
|
||||
{ label: t('nav.about'), href: t('nav.about.href') },
|
||||
{ label: t('nav.contact'), href: t('nav.contact.href') },
|
||||
]}
|
||||
/>
|
||||
|
||||
<div class="min-h-screen pt-16">
|
||||
@@ -113,13 +100,13 @@ const breadcrumbs = [
|
||||
|
||||
<footer class="border-border mt-12 border-t pt-8" data-reveal>
|
||||
<a
|
||||
href={t('nav.features.href')}
|
||||
href="/projects"
|
||||
class="text-brand-600 hover:text-brand-500 dark:text-brand-400 dark:hover:text-brand-300 inline-flex items-center gap-2 text-sm font-medium transition-colors"
|
||||
>
|
||||
<svg class="h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
|
||||
</svg>
|
||||
{t('features.back')}
|
||||
Back to Projects
|
||||
</a>
|
||||
</footer>
|
||||
</div>
|
||||
@@ -128,14 +115,14 @@ const breadcrumbs = [
|
||||
{related.length > 0 && (
|
||||
<section class="py-[var(--space-section-md)] bg-background-secondary border-t border-border" data-reveal>
|
||||
<div class="mx-auto max-w-4xl px-6">
|
||||
<h2 class="font-display text-2xl font-bold text-foreground mb-6">{t('nav.features')}</h2>
|
||||
<h2 class="font-display text-2xl font-bold text-foreground mb-6">More projects</h2>
|
||||
<div class="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{related.map((project) => (
|
||||
<Card
|
||||
variant="elevated"
|
||||
hover
|
||||
padding="lg"
|
||||
href={`${t('nav.features.href')}/${project.id.replace(/\.mdx?$/, '')}`}
|
||||
href={`/projects/${project.id.replace(/\.mdx?$/, '')}`}
|
||||
class="group flex flex-col"
|
||||
>
|
||||
<div class="flex flex-1 flex-col">
|
||||
@@ -161,5 +148,5 @@ const breadcrumbs = [
|
||||
)}
|
||||
</div>
|
||||
|
||||
<AppFooter slot="footer" locale={locale} />
|
||||
<Footer slot="footer" layout="simple" background="secondary" />
|
||||
</BaseLayout>
|
||||
|
||||
@@ -32,21 +32,21 @@ import Icon from '@/components/ui/primitives/Icon/Icon.astro';
|
||||
|
||||
<!-- Copy -->
|
||||
<h1 class="font-display text-2xl md:text-3xl font-bold text-foreground mb-4">
|
||||
Diese Seite existiert nicht
|
||||
Looks like you've drifted off course
|
||||
</h1>
|
||||
|
||||
<p class="text-lg text-foreground-muted max-w-md mx-auto mb-10">
|
||||
Die gesuchte Seite wurde verschoben, gelöscht oder existiert nicht.
|
||||
The page you're looking for has either been moved, deleted, or never existed in the first place.
|
||||
</p>
|
||||
|
||||
<!-- CTAs -->
|
||||
<div class="flex flex-col sm:flex-row items-center justify-center gap-4 mb-12">
|
||||
<Button href="/" size="lg">
|
||||
<Icon name="arrow-left" size="sm" />
|
||||
Zur Startseite
|
||||
Back to home
|
||||
</Button>
|
||||
<Button href="/login" variant="outline" size="lg">
|
||||
Login
|
||||
<Button href="/blog" variant="outline" size="lg">
|
||||
Browse blog
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -61,10 +61,10 @@ import Icon from '@/components/ui/primitives/Icon/Icon.astro';
|
||||
</a>
|
||||
<span class="text-foreground-subtle">|</span>
|
||||
<a
|
||||
href="/register"
|
||||
href="/components"
|
||||
class="text-sm font-medium text-foreground hover:text-brand-500 transition-colors"
|
||||
>
|
||||
Registrieren
|
||||
Components
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,190 @@
|
||||
---
|
||||
import AboutPage from '@/components/landing/AboutPage.astro';
|
||||
---
|
||||
/**
|
||||
* About page
|
||||
* URL: /about
|
||||
*/
|
||||
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 TypingEffect from '@/components/ui/TypingEffect.astro';
|
||||
import TechStack from '@/components/landing/TechStack.astro';
|
||||
import siteConfig from '@/config/site.config';
|
||||
|
||||
<AboutPage locale="de" />
|
||||
const location = siteConfig.address?.city
|
||||
? `${siteConfig.address.city}${siteConfig.address.country ? `, ${siteConfig.address.country}` : ''}`
|
||||
: null;
|
||||
---
|
||||
<PageLayout
|
||||
title={`About — ${siteConfig.name}`}
|
||||
description="Astro Rocket is a production-ready Astro 6 starter with 12 beautiful themes, 57+ components, built-in i18n, dark mode and a fast, modern foundation to build anything on."
|
||||
>
|
||||
<Hero layout="centered" size="sm">
|
||||
<Badge slot="badge" variant="brand" pill>
|
||||
<Icon name="user" size="sm" />
|
||||
About me
|
||||
</Badge>
|
||||
<h1 slot="title">
|
||||
<span class="text-foreground [-webkit-text-fill-color:currentColor]">Astro Rocket —</span><br />
|
||||
<span class="text-brand-500 [-webkit-text-fill-color:var(--color-brand-500)]">
|
||||
<TypingEffect words={["Web Designer", "Web Developer", "Astro Developer", "Claude Code User", "UI / UX Enthusiast", "Blogger", "Coffee lover"]} />
|
||||
</span>
|
||||
</h1>
|
||||
|
||||
<p slot="description">
|
||||
I care about both sides of the craft — how a site looks and how it
|
||||
performs. Here's a bit more about how I think and work.
|
||||
</p>
|
||||
</Hero>
|
||||
<!-- What's possible now -->
|
||||
<section class="py-[var(--space-section-md)] bg-brand-500/8 dark:bg-background-secondary">
|
||||
<div class="mx-auto max-w-6xl px-6">
|
||||
<div class="grid gap-12 lg:grid-cols-2">
|
||||
<!-- Left: text -->
|
||||
<div class="flex flex-col gap-4" data-reveal>
|
||||
<div class="flex flex-col gap-6">
|
||||
<Badge variant="brand" pill class="self-start">The partnership</Badge>
|
||||
<h2 class="font-display text-3xl md:text-4xl font-bold text-foreground">
|
||||
Meet the team: Astro Rocket & Claude
|
||||
</h2>
|
||||
</div>
|
||||
<p class="text-lg text-foreground-muted leading-relaxed">
|
||||
I'm a web designer and developer with 16 years of hands‑on experience. Today I work alongside Claude — not as a shortcut, but as a genuine collaborator that helps me think through design decisions, review code, and build more thoroughly than I could alone.
|
||||
</p>
|
||||
<p class="text-lg text-foreground-muted leading-relaxed">
|
||||
This isn't about automating the work — it's about raising the standard of it. Every design decision still comes from craft and experience. Every line of code is still written with intention. Claude helps me think more rigorously, not less. The work stays mine. The thinking gets sharper.
|
||||
</p>
|
||||
</div>
|
||||
<!-- Right: two stacked partner cards -->
|
||||
<div class="flex flex-col gap-4 h-full" data-reveal data-reveal-delay="1">
|
||||
<Card variant="elevated" hover class="flex-1 flex flex-col justify-center">
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="w-11 h-11 rounded-xl bg-gradient-to-br from-brand-500/20 to-brand-500/5 flex items-center justify-center text-brand-500 shrink-0">
|
||||
<Icon name="code-2" size="md" />
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-xs font-bold tracking-wide casesensitive text-foreground-muted mb-1">Web Designer & Developer</p>
|
||||
<h3 class="text-base font-semibold text-foreground">Astro Rocket</h3>
|
||||
<p class="text-sm text-foreground-muted leading-relaxed mt-1.5">
|
||||
16 years of craft — design, code, performance, and honest communication.{location ? ` Based in ${location}, working worldwide.` : ' Working worldwide.'}
|
||||
</p>
|
||||
<div class="flex flex-wrap gap-1.5 mt-3">
|
||||
<Badge size="sm" variant="brand">Astro</Badge>
|
||||
<Badge size="sm" variant="brand">Tailwind</Badge>
|
||||
<Badge size="sm" variant="brand">TypeScript</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<Card variant="elevated" hover href="https://claude.ai/code" target="_blank" rel="noopener noreferrer" class="group flex-1 flex flex-col justify-center">
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div class="flex items-start gap-4 flex-1 min-w-0">
|
||||
<div class="w-11 h-11 rounded-xl bg-gradient-to-br from-brand-500/20 to-brand-500/5 flex items-center justify-center text-brand-500 shrink-0">
|
||||
<Icon name="sparkles" size="md" />
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-xs font-bold tracking-wide casesensitive text-foreground-muted mb-1">AI Thinking Partner</p>
|
||||
<h3 class="text-base font-semibold text-foreground">Claude</h3>
|
||||
<p class="text-sm text-foreground-muted leading-relaxed mt-1.5">
|
||||
Reasoning through decisions, reviewing code and copy, exploring alternatives — a second perspective at every stage of a project.
|
||||
</p>
|
||||
<div class="flex flex-wrap gap-1.5 mt-3">
|
||||
<Badge size="sm" variant="brand">Design review</Badge>
|
||||
<Badge size="sm" variant="brand">Code QA</Badge>
|
||||
<Badge size="sm" variant="brand">Deep thinking</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Icon name="arrow-up-right" size="sm" class="text-foreground-muted group-hover:text-brand-500 transition-colors shrink-0" />
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<!-- Values -->
|
||||
<section class="py-[var(--space-section-md)] border-t border-border">
|
||||
<div class="mx-auto max-w-6xl px-6">
|
||||
<div class="mb-8 text-center flex flex-col items-center gap-4" data-reveal>
|
||||
<div class="flex flex-col items-center gap-6">
|
||||
<Badge variant="brand" pill>My Process</Badge>
|
||||
<h2 class="font-display text-4xl font-bold text-foreground">
|
||||
How I Work
|
||||
</h2>
|
||||
</div>
|
||||
<p class="text-lg text-foreground-muted max-w-2xl mx-auto text-balance">
|
||||
These aren't aspirations — they're the constraints every project is held to, from the first conversation to the final handover.
|
||||
</p>
|
||||
</div>
|
||||
<div class="grid gap-8 md:grid-cols-3" data-reveal data-reveal-delay="1">
|
||||
<Card hover>
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="w-11 h-11 rounded-xl bg-gradient-to-br from-brand-500/20 to-brand-500/5 flex items-center justify-center text-brand-500 shrink-0">
|
||||
<Icon name="zap" size="md" />
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<h3 class="text-base font-semibold text-foreground">Performance First</h3>
|
||||
<p class="text-sm text-foreground-muted leading-relaxed mt-1.5">
|
||||
Fast sites rank better, convert better, and keep users happy. I optimise for speed from the ground up.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<Card hover>
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="w-11 h-11 rounded-xl bg-gradient-to-br from-brand-500/20 to-brand-500/5 flex items-center justify-center text-brand-500 shrink-0">
|
||||
<Icon name="award" size="md" />
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<h3 class="text-base font-semibold text-foreground">Experience That Shows</h3>
|
||||
<p class="text-sm text-foreground-muted leading-relaxed mt-1.5">
|
||||
Sixteen years of experience means I know what works, what doesn't, and exactly what your project needs.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<Card hover>
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="w-11 h-11 rounded-xl bg-gradient-to-br from-brand-500/20 to-brand-500/5 flex items-center justify-center text-brand-500 shrink-0">
|
||||
<Icon name="rocket" size="md" />
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<h3 class="text-base font-semibold text-foreground">Modern from the Ground Up</h3>
|
||||
<p class="text-sm text-foreground-muted leading-relaxed mt-1.5">
|
||||
Built with today's tooling. Fast, maintainable, and ready for whatever comes next.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<!-- My Stack -->
|
||||
<TechStack
|
||||
badge="Tools & Tech"
|
||||
title="My Stack"
|
||||
description="The tools I rely on every day to build fast, modern websites — chosen for performance, great developer experience, and reliability."
|
||||
/>
|
||||
<!-- CTA -->
|
||||
<section class="py-[var(--space-section-md)] bg-background">
|
||||
<div class="mx-auto max-w-2xl px-6 text-center" data-reveal>
|
||||
<h2 class="font-display text-4xl font-bold text-foreground mb-4 text-balance">
|
||||
Sound like a good fit?
|
||||
</h2>
|
||||
<p class="text-lg text-foreground-muted mb-8 text-balance">
|
||||
If my background and approach match what you're looking for, I'm open to new projects. Let's have a conversation.
|
||||
</p>
|
||||
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<Button size="lg" href="/contact">
|
||||
Start a Project
|
||||
<Icon name="arrow-right" size="sm" />
|
||||
</Button>
|
||||
<Button size="lg" variant="outline" href="/blog">
|
||||
Read the Blog
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</PageLayout>
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
export const prerender = false;
|
||||
|
||||
import type { APIRoute } from 'astro';
|
||||
import { z } from 'astro/zod';
|
||||
import { Resend } from 'resend';
|
||||
import siteConfig from '@/config/site.config';
|
||||
|
||||
const contactSchema = z.object({
|
||||
name: z.string().min(2, 'Name must be at least 2 characters').max(100),
|
||||
email: z.email('Please enter a valid email address'),
|
||||
subject: z.string().max(200).optional(),
|
||||
message: z.string().min(10, 'Message must be at least 10 characters').max(5000),
|
||||
honeypot: z.string().max(0), // Anti-spam: must be empty
|
||||
});
|
||||
|
||||
export const POST: APIRoute = async ({ request }) => {
|
||||
try {
|
||||
const formData = await request.formData();
|
||||
|
||||
const data = {
|
||||
name: formData.get('name')?.toString() || '',
|
||||
email: formData.get('email')?.toString() || '',
|
||||
subject: formData.get('subject')?.toString() || '',
|
||||
message: formData.get('message')?.toString() || '',
|
||||
honeypot: formData.get('honeypot')?.toString() || '',
|
||||
};
|
||||
|
||||
// Validate
|
||||
const result = contactSchema.safeParse(data);
|
||||
|
||||
if (!result.success) {
|
||||
const fieldErrors: Record<string, string[]> = {};
|
||||
for (const error of result.error.issues) {
|
||||
const field = error.path[0] as string;
|
||||
if (!fieldErrors[field]) {
|
||||
fieldErrors[field] = [];
|
||||
}
|
||||
fieldErrors[field].push(error.message);
|
||||
}
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({ success: false, errors: fieldErrors }),
|
||||
{ status: 400, headers: { 'Content-Type': 'application/json' } }
|
||||
);
|
||||
}
|
||||
|
||||
// Honeypot check (bot detection)
|
||||
if (result.data.honeypot) {
|
||||
return new Response(JSON.stringify({ success: true }), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
// Send email via Resend
|
||||
const apiKey = import.meta.env.RESEND_API_KEY;
|
||||
if (!apiKey) {
|
||||
console.error('RESEND_API_KEY is not set');
|
||||
return new Response(
|
||||
JSON.stringify({ success: false, errors: { form: ['Email service is not configured'] } }),
|
||||
{ status: 500, headers: { 'Content-Type': 'application/json' } }
|
||||
);
|
||||
}
|
||||
|
||||
const resend = new Resend(apiKey);
|
||||
|
||||
const toEmail = siteConfig.email;
|
||||
const fromEmail = import.meta.env.RESEND_FROM_EMAIL || toEmail;
|
||||
const siteLabel = siteConfig.name;
|
||||
|
||||
const subject = result.data.subject
|
||||
? `[${siteLabel}] ${result.data.subject}`
|
||||
: `[${siteLabel}] New contact from ${result.data.name}`;
|
||||
|
||||
const { error } = await resend.emails.send({
|
||||
from: `Contact Form <${fromEmail}>`,
|
||||
to: toEmail,
|
||||
replyTo: result.data.email,
|
||||
subject,
|
||||
html: `
|
||||
<p><strong>Name:</strong> ${result.data.name}</p>
|
||||
<p><strong>Email:</strong> ${result.data.email}</p>
|
||||
<p><strong>Message:</strong></p>
|
||||
<p>${result.data.message.replace(/\n/g, '<br>')}</p>
|
||||
`,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.error('Resend error:', error);
|
||||
return new Response(
|
||||
JSON.stringify({ success: false, errors: { form: [error.message || 'Failed to send email'] } }),
|
||||
{ status: 500, headers: { 'Content-Type': 'application/json' } }
|
||||
);
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify({ success: true }), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Contact form error:', error);
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({ success: false, errors: { form: ['An unexpected error occurred'] } }),
|
||||
{ status: 500, headers: { 'Content-Type': 'application/json' } }
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,95 @@
|
||||
import type { APIRoute } from 'astro';
|
||||
import { z } from 'astro/zod';
|
||||
import { Resend } from 'resend';
|
||||
|
||||
const newsletterSchema = z.object({
|
||||
email: z.email('Please enter a valid email address'),
|
||||
honeypot: z.string().max(0).optional(),
|
||||
});
|
||||
|
||||
export const POST: APIRoute = async ({ request }) => {
|
||||
try {
|
||||
const formData = await request.formData();
|
||||
const email = formData.get('email')?.toString() || '';
|
||||
const honeypot = formData.get('website')?.toString() || '';
|
||||
|
||||
// Check honeypot - if filled, it's likely a bot
|
||||
if (honeypot) {
|
||||
return new Response(JSON.stringify({ success: true }), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
}
|
||||
|
||||
const result = newsletterSchema.safeParse({ email, honeypot });
|
||||
|
||||
if (!result.success) {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
success: false,
|
||||
error: result.error.issues[0]?.message || 'Please enter a valid email address',
|
||||
}),
|
||||
{
|
||||
status: 400,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const apiKey = import.meta.env.RESEND_API_KEY;
|
||||
const audienceId = import.meta.env.RESEND_AUDIENCE_ID;
|
||||
|
||||
if (!apiKey || !audienceId) {
|
||||
console.error('Newsletter: RESEND_API_KEY or RESEND_AUDIENCE_ID is not configured');
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
success: false,
|
||||
error: 'Newsletter service is not configured.',
|
||||
}),
|
||||
{
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const resend = new Resend(apiKey);
|
||||
const { error } = await resend.contacts.create({
|
||||
audienceId,
|
||||
email: result.data.email,
|
||||
unsubscribed: false,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.error('Resend newsletter error:', error);
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
success: false,
|
||||
error: 'Subscription failed. Please try again.',
|
||||
}),
|
||||
{
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify({ success: true }), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Newsletter error:', error);
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
success: false,
|
||||
error: 'Subscription failed. Please try again.',
|
||||
}),
|
||||
{
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -1,5 +1,249 @@
|
||||
---
|
||||
import BlogIndexPage from '@/components/landing/BlogIndexPage.astro';
|
||||
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';
|
||||
|
||||
const socialLinks = resolveSocialLinks(siteConfig.socialLinks);
|
||||
|
||||
// Get all published posts
|
||||
const allPosts = await getCollection('blog', ({ data }) => {
|
||||
return import.meta.env.PROD ? data.draft !== true : true;
|
||||
});
|
||||
|
||||
// Filter by English locale and sort by date
|
||||
const posts = allPosts
|
||||
.filter((post) => post.data.locale === 'en')
|
||||
.sort((a, b) => b.data.publishedAt.valueOf() - a.data.publishedAt.valueOf());
|
||||
|
||||
// Separate featured posts (shown in hero section)
|
||||
const featuredPosts = posts.filter((post) => post.data.featured);
|
||||
// Non-featured posts for the main listing
|
||||
const nonFeaturedPosts = posts.filter((post) => !post.data.featured);
|
||||
// If ALL posts are featured, show them all in the main listing too (to avoid empty section)
|
||||
const regularPosts = nonFeaturedPosts.length > 0 ? nonFeaturedPosts : posts;
|
||||
|
||||
// Collect all unique tags across all posts
|
||||
const allTags = [...new Set(posts.flatMap((post) => post.data.tags))].sort();
|
||||
|
||||
const getPostUrl = (postId: string) => {
|
||||
// Remove locale prefix from id (e.g., "en/welcome-to-velocity" -> "welcome-to-velocity")
|
||||
const slug = postId.replace('en/', '');
|
||||
return `/blog/${slug}`;
|
||||
};
|
||||
---
|
||||
|
||||
<BlogIndexPage locale="de" />
|
||||
<PageLayout title="Blog — Astro Rocket" description="Articles on web design, development, and the web by Astro Rocket." showScrollProgress>
|
||||
<Hero layout="centered" size="sm">
|
||||
<Badge slot="badge" variant="brand" pill>
|
||||
<Icon name="book" size="sm" />
|
||||
All posts
|
||||
</Badge>
|
||||
|
||||
<h1 slot="title">Blog</h1>
|
||||
|
||||
<p slot="description">Articles on Astro, web performance, design systems, and building with Astro Rocket.</p>
|
||||
</Hero>
|
||||
|
||||
<!-- Featured Posts Section -->
|
||||
{
|
||||
featuredPosts.length > 0 && (
|
||||
<section id="featured-section" class="bg-background-secondary py-[var(--space-section-md)] border-t border-border">
|
||||
<div class="mx-auto max-w-6xl px-6 flex flex-col gap-8">
|
||||
<h2 class="font-display text-foreground text-3xl md:text-4xl font-bold" data-reveal>
|
||||
Featured
|
||||
</h2>
|
||||
|
||||
<div class="grid gap-6 md:grid-cols-2" data-reveal data-reveal-delay="1">
|
||||
{featuredPosts.map((post) => (
|
||||
<div class="post-card" data-tags={JSON.stringify(post.data.tags)}>
|
||||
<BlogCard
|
||||
title={post.data.title}
|
||||
description={post.data.description}
|
||||
href={getPostUrl(post.id)}
|
||||
publishedAt={post.data.publishedAt}
|
||||
tags={post.data.tags}
|
||||
author={post.data.author}
|
||||
image={post.data.image}
|
||||
svgSlug={post.data.svgSlug}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
<!-- All Posts Section -->
|
||||
<section class:list={[featuredPosts.length > 0 ? 'bg-background' : 'bg-background-secondary', 'py-[var(--space-section-md)] border-t border-border']}>
|
||||
<div class="mx-auto max-w-6xl px-6 flex flex-col gap-8">
|
||||
<div class="flex flex-wrap items-center justify-between gap-4" data-reveal>
|
||||
{
|
||||
featuredPosts.length > 0 && nonFeaturedPosts.length > 0 && (
|
||||
<h2 class="font-display text-foreground text-3xl md:text-4xl font-bold">All posts</h2>
|
||||
)
|
||||
}
|
||||
|
||||
{allTags.length > 0 && (
|
||||
<div class="flex items-center gap-3 ml-auto">
|
||||
<label for="tag-filter" class="text-sm font-medium text-foreground-muted whitespace-nowrap">
|
||||
Filter
|
||||
</label>
|
||||
<div class="relative">
|
||||
<select
|
||||
id="tag-filter"
|
||||
class="tag-select appearance-none pl-3 pr-8 py-1.5 rounded-full border border-border bg-background text-sm font-medium text-foreground cursor-pointer focus:outline-none focus:border-brand-500 transition-colors"
|
||||
>
|
||||
<option value="all">All posts</option>
|
||||
{allTags.map((tag) => (
|
||||
<option value={tag}>{tag}</option>
|
||||
))}
|
||||
</select>
|
||||
<span class="pointer-events-none absolute right-2.5 top-1/2 -translate-y-1/2 text-foreground-muted">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="m6 9 6 6 6-6"/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
<span id="filter-count" class="text-sm text-foreground-muted hidden"></span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{
|
||||
regularPosts.length > 0 ? (
|
||||
<div class="grid gap-6 md:grid-cols-2 lg:grid-cols-3" data-reveal data-reveal-delay="1">
|
||||
{regularPosts.map((post) => (
|
||||
<div class="post-card" data-tags={JSON.stringify(post.data.tags)}>
|
||||
<BlogCard
|
||||
title={post.data.title}
|
||||
description={post.data.description}
|
||||
href={getPostUrl(post.id)}
|
||||
publishedAt={post.data.publishedAt}
|
||||
tags={post.data.tags}
|
||||
author={post.data.author}
|
||||
image={post.data.image}
|
||||
svgSlug={post.data.svgSlug}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : posts.length === 0 ? (
|
||||
<div class="py-16 text-center">
|
||||
<div class="bg-background mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full">
|
||||
<Icon name="file-text" size="lg" class="text-foreground-muted" />
|
||||
</div>
|
||||
<p class="text-foreground-muted text-lg">No posts found</p>
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
|
||||
<div id="no-results" class="hidden py-16 text-center">
|
||||
<div class="bg-background mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full">
|
||||
<Icon name="file-text" size="lg" class="text-foreground-muted" />
|
||||
</div>
|
||||
<p class="text-foreground-muted text-lg">No posts found for this tag</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Follow CTA Section -->
|
||||
<CTA variant="default" size="md" maxWidth="lg" glow={false} data-reveal class:list={[featuredPosts.length > 0 && '!bg-background-secondary']}>
|
||||
<h2 slot="heading" class="!text-4xl">Follow along</h2>
|
||||
<p slot="description" class="!text-lg text-balance">Stay in the loop — new articles, thoughts, and updates.</p>
|
||||
|
||||
<div class="flex flex-wrap items-center justify-center gap-3">
|
||||
<a
|
||||
href="/rss.xml"
|
||||
class="inline-flex items-center gap-2 rounded-full border border-border bg-background px-4 py-2 text-sm font-medium text-foreground transition-colors hover:border-brand-500 hover:text-brand-500"
|
||||
>
|
||||
<Icon name="rss" size="sm" />
|
||||
RSS Feed
|
||||
</a>
|
||||
{socialLinks.map((link) => (
|
||||
<a
|
||||
href={link.href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="inline-flex items-center gap-2 rounded-full border border-border bg-background px-4 py-2 text-sm font-medium text-foreground transition-colors hover:border-brand-500 hover:text-brand-500"
|
||||
>
|
||||
<Icon name={link.icon} size="sm" />
|
||||
{link.label}
|
||||
</a>
|
||||
))}
|
||||
<a
|
||||
href={`mailto:${siteConfig.email}`}
|
||||
class="inline-flex items-center gap-2 rounded-full border border-border bg-background px-4 py-2 text-sm font-medium text-foreground transition-colors hover:border-brand-500 hover:text-brand-500"
|
||||
>
|
||||
<Icon name="mail" size="sm" />
|
||||
Email
|
||||
</a>
|
||||
</div>
|
||||
</CTA>
|
||||
</PageLayout>
|
||||
|
||||
<style>
|
||||
.tag-select {
|
||||
background-color: var(--color-background);
|
||||
color: var(--color-foreground);
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
function initTagFilter() {
|
||||
const select = document.getElementById('tag-filter') as HTMLSelectElement;
|
||||
const cards = document.querySelectorAll<HTMLElement>('.post-card');
|
||||
const featuredSection = document.getElementById('featured-section');
|
||||
const noResults = document.getElementById('no-results');
|
||||
const filterCount = document.getElementById('filter-count');
|
||||
|
||||
if (!select) return;
|
||||
|
||||
select.addEventListener('change', () => {
|
||||
const tag = select.value;
|
||||
|
||||
if (tag === 'all') {
|
||||
cards.forEach((card) => card.style.removeProperty('display'));
|
||||
if (featuredSection) featuredSection.style.removeProperty('display');
|
||||
noResults?.classList.add('hidden');
|
||||
if (filterCount) filterCount.classList.add('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
let visibleCount = 0;
|
||||
let featuredVisible = 0;
|
||||
|
||||
cards.forEach((card) => {
|
||||
const tags: string[] = JSON.parse(card.dataset.tags || '[]');
|
||||
if (tags.includes(tag)) {
|
||||
card.style.removeProperty('display');
|
||||
visibleCount++;
|
||||
if (featuredSection?.contains(card)) featuredVisible++;
|
||||
} else {
|
||||
card.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
if (featuredSection) {
|
||||
featuredSection.style.display = featuredVisible === 0 ? 'none' : '';
|
||||
}
|
||||
|
||||
if (noResults) {
|
||||
noResults.classList.toggle('hidden', visibleCount > 0);
|
||||
}
|
||||
|
||||
if (filterCount) {
|
||||
filterCount.textContent = `${visibleCount} post${visibleCount !== 1 ? 's' : ''}`;
|
||||
filterCount.classList.remove('hidden');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('astro:page-load', initTagFilter);
|
||||
</script>
|
||||
|
||||
@@ -1,5 +1,118 @@
|
||||
---
|
||||
import ContactPage from '@/components/landing/ContactPage.astro';
|
||||
/**
|
||||
* Contact page
|
||||
* URL: /contact
|
||||
*/
|
||||
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';
|
||||
|
||||
const socialLinks = resolveSocialLinks(siteConfig.socialLinks);
|
||||
|
||||
const channels = [
|
||||
{
|
||||
icon: 'mail',
|
||||
label: 'Email',
|
||||
value: siteConfig.email,
|
||||
note: 'Drop us a line',
|
||||
href: `mailto:${siteConfig.email}`,
|
||||
external: false,
|
||||
},
|
||||
...socialLinks.map((link) => ({
|
||||
icon: link.icon,
|
||||
label: link.label,
|
||||
value: link.href.replace(/^https?:\/\//, ''),
|
||||
note: 'Follow along',
|
||||
href: link.href,
|
||||
external: true,
|
||||
})),
|
||||
];
|
||||
---
|
||||
|
||||
<ContactPage locale="de" />
|
||||
<PageLayout
|
||||
title="Contact — Astro Rocket"
|
||||
description="Get in touch"
|
||||
>
|
||||
<Hero layout="centered" size="sm">
|
||||
<Badge slot="badge" variant="brand" pill>
|
||||
<Icon name="mail" size="sm" />
|
||||
Get in touch
|
||||
</Badge>
|
||||
|
||||
<h1 slot="title">
|
||||
Send me a <span class="text-brand-500">message.</span>
|
||||
</h1>
|
||||
|
||||
<p slot="description">
|
||||
Use the form below or reach out on any of these channels. I'll get back to you within 1 business day.
|
||||
</p>
|
||||
</Hero>
|
||||
|
||||
<!-- Contact Section -->
|
||||
<section class="py-[var(--space-section-md)] bg-background-secondary border-t border-border">
|
||||
<div class="mx-auto max-w-5xl px-6">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-5 gap-10 lg:gap-16 items-start">
|
||||
|
||||
<!-- Left column: contact form -->
|
||||
<div class="lg:col-span-3" data-reveal>
|
||||
<Card padding="lg">
|
||||
<h2 class="font-display text-xl font-bold text-foreground mb-6">Send a message</h2>
|
||||
<ContactForm />
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<!-- Right column: contact channels -->
|
||||
<div class="lg:col-span-2 space-y-4" data-reveal data-reveal-delay="1">
|
||||
<div>
|
||||
<h2 class="font-display text-lg font-bold text-foreground mb-1">Other channels</h2>
|
||||
<p class="text-sm text-foreground-muted">Prefer a direct channel? Pick whichever works best.</p>
|
||||
</div>
|
||||
|
||||
<!-- Channel list -->
|
||||
<div class="space-y-2">
|
||||
{channels.map((ch) => (
|
||||
<a
|
||||
href={ch.href}
|
||||
{...ch.external ? { target: '_blank', rel: 'noopener noreferrer' } : {}}
|
||||
class="block group"
|
||||
>
|
||||
<Card hover padding="sm" class="h-full">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 shrink-0 rounded-xl bg-gradient-to-br from-brand-500/20 to-brand-500/5 flex items-center justify-center text-brand-500">
|
||||
<Icon name={ch.icon} size="sm" />
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="font-semibold text-sm text-foreground">{ch.label}</p>
|
||||
<p class="text-xs text-foreground-muted truncate">{ch.value}</p>
|
||||
</div>
|
||||
<Icon name="arrow-up-right" size="sm" class="text-foreground-muted group-hover:text-brand-500 transition-colors shrink-0" />
|
||||
</div>
|
||||
</Card>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<!-- Location card -->
|
||||
<Card hover padding="sm" class="bg-brand-500/5 border-brand-500/20">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 shrink-0 rounded-xl bg-gradient-to-br from-brand-500/20 to-brand-500/5 flex items-center justify-center text-brand-500">
|
||||
<Icon name="map-pin" size="sm" />
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-semibold text-sm text-foreground">{siteConfig.address?.city ?? 'Your City'}</p>
|
||||
<p class="text-xs text-foreground-muted mt-0.5">Based in {siteConfig.address?.country ?? 'Your Country'}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</PageLayout>
|
||||
|
||||
@@ -1,241 +0,0 @@
|
||||
---
|
||||
/**
|
||||
* Datenschutz / Privacy Policy
|
||||
* URL: /datenschutz
|
||||
*/
|
||||
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';
|
||||
---
|
||||
|
||||
<PageLayout
|
||||
title="Datenschutz — Armarium"
|
||||
description="Datenschutzerklärung der Armarium Suite. Deine Daten werden ausschliesslich in der Schweiz gehostet."
|
||||
locale="de"
|
||||
>
|
||||
<Hero layout="centered" size="sm">
|
||||
<Badge slot="badge" variant="brand" pill>
|
||||
<Icon name="shield-check" size="sm" />
|
||||
Datenschutz
|
||||
</Badge>
|
||||
<h1 slot="title">
|
||||
Deine Daten,<br />
|
||||
<span class="text-brand-500 [-webkit-text-fill-color:var(--color-brand-500)]">dein Schutz.</span>
|
||||
</h1>
|
||||
<p slot="description">
|
||||
Armarium nimmt Datenschutz ernst. Deine Finanzdaten werden ausschliesslich in der Schweiz gespeichert und verarbeitet – ohne Datenverkauf, ohne Tracking.
|
||||
</p>
|
||||
</Hero>
|
||||
|
||||
<!-- Hosting: Infomaniak -->
|
||||
<section class="py-[var(--space-section-md)] bg-brand-500/8 dark:bg-background-secondary border-t border-border">
|
||||
<div class="mx-auto max-w-5xl px-6">
|
||||
<div class="flex flex-col gap-8" data-reveal>
|
||||
<div class="flex flex-col items-center gap-4 text-center">
|
||||
<Badge variant="brand" pill>Hosting & Infrastruktur</Badge>
|
||||
<h2 class="font-display text-3xl md:text-4xl font-bold text-foreground">
|
||||
100% Schweizer Hosting
|
||||
</h2>
|
||||
<p class="text-lg text-foreground-muted max-w-2xl">
|
||||
Armarium betreibt seine gesamte Infrastruktur ausschliesslich auf Servern von <strong class="text-foreground">Infomaniak</strong> in der Schweiz. Keine Daten verlassen jemals das Schweizer Staatsgebiet.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Infomaniak certifications grid -->
|
||||
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<!-- ISO 27001 -->
|
||||
<Card hover>
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="w-10 h-10 rounded-xl bg-gradient-to-br from-brand-500/20 to-brand-500/5 flex items-center justify-center text-brand-500 shrink-0">
|
||||
<Icon name="shield" size="sm" />
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs font-bold tracking-wide text-foreground-muted mb-0.5">Informationssicherheit</p>
|
||||
<h3 class="text-sm font-semibold text-foreground">ISO 27001:2022</h3>
|
||||
<p class="text-xs text-foreground-muted mt-1 leading-relaxed">
|
||||
Zertifiziertes Informationssicherheits-Managementsystem. Umfasst Entwicklung, Cloud-Infrastruktur, Webservices, Support und Rechenzentrumsbetrieb.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<!-- Swiss Hosting -->
|
||||
<Card hover>
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="w-10 h-10 rounded-xl bg-gradient-to-br from-brand-500/20 to-brand-500/5 flex items-center justify-center shrink-0">
|
||||
<svg class="w-5 h-6" viewBox="0 0 32 36" fill="none" xmlns="http://www.w3.org/2000/svg" aria-label="Swiss Hosting" role="img">
|
||||
<defs><clipPath id="zh-ds1"><path d="M16 1L1 7V23L6 30L16 34L26 30L31 23V7L16 1Z"/></clipPath></defs>
|
||||
<path d="M16 1L1 7V23L6 30L16 34L26 30L31 23V7L16 1Z" fill="white"/>
|
||||
<polygon points="1,7 16,1 31,7 1,34" fill="#003DA5" clip-path="url(#zh-ds1)"/>
|
||||
<path d="M16 1L1 7V23L6 30L16 34L26 30L31 23V7L16 1Z" fill="none" stroke="#003DA5" stroke-width="1.5"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs font-bold tracking-wide text-foreground-muted mb-0.5">Datensouveränität</p>
|
||||
<h3 class="text-sm font-semibold text-foreground">Swiss Hosting Label</h3>
|
||||
<p class="text-xs text-foreground-muted mt-1 leading-relaxed">
|
||||
Daten werden ausschliesslich in Schweizer Rechenzentren gespeichert, verarbeitet und betrieben. Keine Auslagerung ins Ausland.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<!-- Swiss Made Software -->
|
||||
<Card hover>
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="w-10 h-10 rounded-xl bg-gradient-to-br from-brand-500/20 to-brand-500/5 flex items-center justify-center text-brand-500 shrink-0">
|
||||
<Icon name="code-2" size="sm" />
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs font-bold tracking-wide text-foreground-muted mb-0.5">Entwicklung</p>
|
||||
<h3 class="text-sm font-semibold text-foreground">Swiss Made Software</h3>
|
||||
<p class="text-xs text-foreground-muted mt-1 leading-relaxed">
|
||||
Infomaniak entwickelt seine Software mehrheitlich in der Schweiz und erfüllt die Anforderungen des Swiss Made Software-Labels.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<!-- DSG / DSGVO -->
|
||||
<Card hover>
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="w-10 h-10 rounded-xl bg-gradient-to-br from-brand-500/20 to-brand-500/5 flex items-center justify-center text-brand-500 shrink-0">
|
||||
<Icon name="file-check" size="sm" />
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs font-bold tracking-wide text-foreground-muted mb-0.5">Rechtliche Konformität</p>
|
||||
<h3 class="text-sm font-semibold text-foreground">nDSG & DSGVO</h3>
|
||||
<p class="text-xs text-foreground-muted mt-1 leading-relaxed">
|
||||
Vollständige Konformität mit dem Schweizer Datenschutzgesetz (nDSG) sowie der europäischen Datenschutz-Grundverordnung (DSGVO/GDPR).
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<!-- ISO 14001 / Ökologie -->
|
||||
<Card hover>
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="w-10 h-10 rounded-xl bg-gradient-to-br from-brand-500/20 to-brand-500/5 flex items-center justify-center text-brand-500 shrink-0">
|
||||
<Icon name="leaf" size="sm" />
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs font-bold tracking-wide text-foreground-muted mb-0.5">Nachhaltigkeit</p>
|
||||
<h3 class="text-sm font-semibold text-foreground">ISO 14001 · B Corp™</h3>
|
||||
<p class="text-xs text-foreground-muted mt-1 leading-relaxed">
|
||||
100% erneuerbare Energie (60% Wasserkraft, 40% Solar). CO₂-Kompensation zu 200%. Zertifiziert als B Corp™ seit 2025.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<!-- Physische Sicherheit -->
|
||||
<Card hover>
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="w-10 h-10 rounded-xl bg-gradient-to-br from-brand-500/20 to-brand-500/5 flex items-center justify-center text-brand-500 shrink-0">
|
||||
<Icon name="server" size="sm" />
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs font-bold tracking-wide text-foreground-muted mb-0.5">Rechenzentrum</p>
|
||||
<h3 class="text-sm font-semibold text-foreground">Physische Sicherheit</h3>
|
||||
<p class="text-xs text-foreground-muted mt-1 leading-relaxed">
|
||||
Mehrere redundante Schweizer Rechenzentren. Jährliche Penetrationstests, AES-256-Verschlüsselung, PUE < 1.1.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Datenschutzerklärung -->
|
||||
<section class="py-[var(--space-section-md)] bg-background border-t border-border">
|
||||
<div class="mx-auto max-w-3xl px-6">
|
||||
<div class="prose prose-neutral dark:prose-invert max-w-none space-y-10" data-reveal>
|
||||
|
||||
<div>
|
||||
<h2 class="font-display text-2xl font-bold text-foreground mb-4">1. Verantwortliche Stelle</h2>
|
||||
<p class="text-foreground-muted leading-relaxed">
|
||||
Verantwortlich für die Datenverarbeitung im Rahmen der Armarium Suite ist Armarium, Zürich, Schweiz. Bei Fragen zum Datenschutz erreichst du uns unter:
|
||||
<a href="mailto:datenschutz@armarium.ch" class="text-brand-500 hover:text-brand-600 transition-colors">datenschutz@armarium.ch</a>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 class="font-display text-2xl font-bold text-foreground mb-4">2. Erhobene Daten</h2>
|
||||
<p class="text-foreground-muted leading-relaxed mb-3">Armarium erhebt nur die Daten, die für den Betrieb der App zwingend notwendig sind:</p>
|
||||
<ul class="space-y-2 text-foreground-muted">
|
||||
<li class="flex items-start gap-2"><Icon name="check" size="sm" class="text-brand-500 shrink-0 mt-0.5" /><span><strong class="text-foreground">Registrierungsdaten:</strong> E-Mail-Adresse und Passwort (gehasht, nie im Klartext gespeichert).</span></li>
|
||||
<li class="flex items-start gap-2"><Icon name="check" size="sm" class="text-brand-500 shrink-0 mt-0.5" /><span><strong class="text-foreground">Finanzdaten:</strong> Transaktionen, Budgets, Kategorien und Konten, die du selbst erfasst.</span></li>
|
||||
<li class="flex items-start gap-2"><Icon name="check" size="sm" class="text-brand-500 shrink-0 mt-0.5" /><span><strong class="text-foreground">Technische Daten:</strong> IP-Adresse (für Sicherheitszwecke), Browser-Typ, Zeitstempel der letzten Anmeldung.</span></li>
|
||||
</ul>
|
||||
<p class="text-foreground-muted leading-relaxed mt-3">
|
||||
Wir setzen <strong class="text-foreground">kein Tracking, keine Werbe-Cookies und keine Analytics-Dienste von Drittanbietern</strong> ein.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 class="font-display text-2xl font-bold text-foreground mb-4">3. Zweck der Datenverarbeitung</h2>
|
||||
<p class="text-foreground-muted leading-relaxed mb-3">Deine Daten werden ausschliesslich verwendet für:</p>
|
||||
<ul class="space-y-2 text-foreground-muted">
|
||||
<li class="flex items-start gap-2"><Icon name="check" size="sm" class="text-brand-500 shrink-0 mt-0.5" /><span>Bereitstellung und Betrieb der Armarium Suite</span></li>
|
||||
<li class="flex items-start gap-2"><Icon name="check" size="sm" class="text-brand-500 shrink-0 mt-0.5" /><span>Authentifizierung und Kontoschutz</span></li>
|
||||
<li class="flex items-start gap-2"><Icon name="check" size="sm" class="text-brand-500 shrink-0 mt-0.5" /><span>Technischen Support auf deine Anfrage hin</span></li>
|
||||
</ul>
|
||||
<p class="text-foreground-muted leading-relaxed mt-3">
|
||||
Eine Weitergabe deiner Daten an Dritte findet <strong class="text-foreground">nicht statt</strong> – weder für Werbezwecke noch für Analysen.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 class="font-display text-2xl font-bold text-foreground mb-4">4. Datenspeicherung & Hosting</h2>
|
||||
<p class="text-foreground-muted leading-relaxed">
|
||||
Sämtliche Daten werden ausschliesslich auf Servern von <strong class="text-foreground">Infomaniak Network SA</strong> in der Schweiz gespeichert und verarbeitet. Infomaniak ist ISO 27001:2022-zertifiziert und erfüllt alle Anforderungen des Schweizer Datenschutzgesetzes (nDSG) sowie der DSGVO. Es findet kein Transfer von Daten in Drittländer statt.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 class="font-display text-2xl font-bold text-foreground mb-4">5. Datensicherheit</h2>
|
||||
<p class="text-foreground-muted leading-relaxed">
|
||||
Alle Datenübertragungen erfolgen verschlüsselt via HTTPS/TLS. Passwörter werden ausschliesslich als gehashte Werte (bcrypt) gespeichert. Die Infrastruktur von Infomaniak verwendet AES-256-Verschlüsselung und wird jährlichen Penetrationstests unterzogen.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 class="font-display text-2xl font-bold text-foreground mb-4">6. Deine Rechte</h2>
|
||||
<p class="text-foreground-muted leading-relaxed mb-3">Gemäss nDSG und DSGVO hast du folgende Rechte:</p>
|
||||
<ul class="space-y-2 text-foreground-muted">
|
||||
<li class="flex items-start gap-2"><Icon name="check" size="sm" class="text-brand-500 shrink-0 mt-0.5" /><span><strong class="text-foreground">Auskunft:</strong> Welche Daten über dich gespeichert sind.</span></li>
|
||||
<li class="flex items-start gap-2"><Icon name="check" size="sm" class="text-brand-500 shrink-0 mt-0.5" /><span><strong class="text-foreground">Berichtigung:</strong> Unrichtige Daten korrigieren lassen.</span></li>
|
||||
<li class="flex items-start gap-2"><Icon name="check" size="sm" class="text-brand-500 shrink-0 mt-0.5" /><span><strong class="text-foreground">Löschung:</strong> Vollständige Löschung deines Kontos und aller Daten.</span></li>
|
||||
<li class="flex items-start gap-2"><Icon name="check" size="sm" class="text-brand-500 shrink-0 mt-0.5" /><span><strong class="text-foreground">Datenportabilität:</strong> Export deiner Daten in einem gängigen Format.</span></li>
|
||||
<li class="flex items-start gap-2"><Icon name="check" size="sm" class="text-brand-500 shrink-0 mt-0.5" /><span><strong class="text-foreground">Widerspruch:</strong> Widerspruch gegen bestimmte Verarbeitungen.</span></li>
|
||||
</ul>
|
||||
<p class="text-foreground-muted leading-relaxed mt-3">
|
||||
Anfragen richtest du an: <a href="mailto:datenschutz@armarium.ch" class="text-brand-500 hover:text-brand-600 transition-colors">datenschutz@armarium.ch</a>. Wir bearbeiten dein Anliegen innerhalb von 30 Tagen.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 class="font-display text-2xl font-bold text-foreground mb-4">7. Kontolöschung</h2>
|
||||
<p class="text-foreground-muted leading-relaxed">
|
||||
Du kannst dein Konto jederzeit in den App-Einstellungen löschen. Dabei werden alle deine Daten unwiderruflich und vollständig aus unseren Systemen entfernt. Eine Wiederherstellung ist danach nicht möglich.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 class="font-display text-2xl font-bold text-foreground mb-4">8. Änderungen dieser Erklärung</h2>
|
||||
<p class="text-foreground-muted leading-relaxed">
|
||||
Wir behalten uns vor, diese Datenschutzerklärung bei Bedarf anzupassen. Die jeweils aktuelle Version ist unter <a href="/datenschutz" class="text-brand-500 hover:text-brand-600 transition-colors">armarium.ch/datenschutz</a> abrufbar. Bei wesentlichen Änderungen informieren wir registrierte Nutzer per E-Mail.
|
||||
</p>
|
||||
<p class="text-foreground-muted leading-relaxed mt-2 text-sm">
|
||||
Stand: April 2026
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</PageLayout>
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
import AboutPage from '@/components/landing/AboutPage.astro';
|
||||
---
|
||||
|
||||
<AboutPage locale="en" />
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
import BlogIndexPage from '@/components/landing/BlogIndexPage.astro';
|
||||
---
|
||||
|
||||
<BlogIndexPage locale="en" />
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
import ContactPage from '@/components/landing/ContactPage.astro';
|
||||
---
|
||||
|
||||
<ContactPage locale="en" />
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
import FeaturesIndexPage from '@/components/landing/FeaturesIndexPage.astro';
|
||||
---
|
||||
|
||||
<FeaturesIndexPage locale="en" />
|
||||
@@ -1,73 +0,0 @@
|
||||
---
|
||||
/**
|
||||
* Legal Notice — EN
|
||||
* URL: /en/impressum
|
||||
*/
|
||||
import PageLayout from '@/layouts/PageLayout.astro';
|
||||
import Badge from '@/components/ui/data-display/Badge/Badge.astro';
|
||||
import Icon from '@/components/ui/primitives/Icon/Icon.astro';
|
||||
import { Hero } from '@/components/hero';
|
||||
---
|
||||
|
||||
<PageLayout
|
||||
title="Legal Notice — Armarium"
|
||||
description="Legal Notice of Armarium Suite."
|
||||
noindex
|
||||
locale="en"
|
||||
>
|
||||
<Hero layout="centered" size="sm">
|
||||
<Badge slot="badge" variant="brand" pill>
|
||||
<Icon name="file-text" size="sm" />
|
||||
Legal Notice
|
||||
</Badge>
|
||||
<h1 slot="title">Legal Notice</h1>
|
||||
</Hero>
|
||||
|
||||
<section class="py-[var(--space-section-md)] bg-background border-t border-border">
|
||||
<div class="mx-auto max-w-2xl px-6 space-y-8 text-foreground-muted" data-reveal>
|
||||
|
||||
<div>
|
||||
<h2 class="font-display text-lg font-bold text-foreground mb-2">Operator</h2>
|
||||
<p>Armarium<br />Zürich, Switzerland</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 class="font-display text-lg font-bold text-foreground mb-2">Contact</h2>
|
||||
<p>
|
||||
Email:
|
||||
<a href="mailto:hallo@armarium.ch" class="text-brand-500 hover:text-brand-600 transition-colors">
|
||||
hallo@armarium.ch
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 class="font-display text-lg font-bold text-foreground mb-2">Hosting</h2>
|
||||
<p>
|
||||
Infomaniak Network SA<br />
|
||||
Rue Eugène-Marziano 25, 1227 Geneva, Switzerland<br />
|
||||
<a href="https://www.infomaniak.com" target="_blank" rel="noopener noreferrer" class="text-brand-500 hover:text-brand-600 transition-colors">
|
||||
www.infomaniak.com
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 class="font-display text-lg font-bold text-foreground mb-2">Disclaimer</h2>
|
||||
<p class="leading-relaxed">
|
||||
Despite careful review of content, we accept no liability for the content of external links. The operators of linked pages are solely responsible for their content.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 class="font-display text-lg font-bold text-foreground mb-2">Copyright</h2>
|
||||
<p class="leading-relaxed">
|
||||
All content on this website is protected by copyright. Any use without express written permission is prohibited.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p class="text-sm">Last updated: April 2026</p>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
</PageLayout>
|
||||
@@ -1,12 +0,0 @@
|
||||
---
|
||||
import LandingLayout from '@/layouts/LandingLayout.astro';
|
||||
import HomePage from '@/components/landing/HomePage.astro';
|
||||
---
|
||||
|
||||
<LandingLayout
|
||||
title="Armarium Suite — Budget & More"
|
||||
description="Keep track of your finances. Record expenses, manage budgets and reach your savings goals – simply and clearly."
|
||||
locale="en"
|
||||
>
|
||||
<HomePage locale="en" />
|
||||
</LandingLayout>
|
||||
@@ -1,234 +0,0 @@
|
||||
---
|
||||
/**
|
||||
* Privacy Policy — EN
|
||||
* URL: /en/privacy
|
||||
*/
|
||||
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';
|
||||
---
|
||||
|
||||
<PageLayout
|
||||
title="Privacy Policy — Armarium"
|
||||
description="Privacy Policy of Armarium Suite. Your data is hosted exclusively in Switzerland."
|
||||
locale="en"
|
||||
>
|
||||
<Hero layout="centered" size="sm">
|
||||
<Badge slot="badge" variant="brand" pill>
|
||||
<Icon name="shield-check" size="sm" />
|
||||
Privacy Policy
|
||||
</Badge>
|
||||
<h1 slot="title">
|
||||
Your data,<br />
|
||||
<span class="text-brand-500 [-webkit-text-fill-color:var(--color-brand-500)]">your protection.</span>
|
||||
</h1>
|
||||
<p slot="description">
|
||||
Armarium takes data protection seriously. Your financial data is stored and processed exclusively in Switzerland – no data selling, no tracking.
|
||||
</p>
|
||||
</Hero>
|
||||
|
||||
<!-- Hosting: Infomaniak -->
|
||||
<section class="py-[var(--space-section-md)] bg-brand-500/8 dark:bg-background-secondary border-t border-border">
|
||||
<div class="mx-auto max-w-5xl px-6">
|
||||
<div class="flex flex-col gap-8" data-reveal>
|
||||
<div class="flex flex-col items-center gap-4 text-center">
|
||||
<Badge variant="brand" pill>Hosting & Infrastructure</Badge>
|
||||
<h2 class="font-display text-3xl md:text-4xl font-bold text-foreground">
|
||||
100% Swiss hosting
|
||||
</h2>
|
||||
<p class="text-lg text-foreground-muted max-w-2xl">
|
||||
Armarium runs its entire infrastructure exclusively on <strong class="text-foreground">Infomaniak</strong> servers in Switzerland. No data ever leaves Swiss territory.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<Card hover>
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="w-10 h-10 rounded-xl bg-gradient-to-br from-brand-500/20 to-brand-500/5 flex items-center justify-center text-brand-500 shrink-0">
|
||||
<Icon name="shield" size="sm" />
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs font-bold tracking-wide text-foreground-muted mb-0.5">Information Security</p>
|
||||
<h3 class="text-sm font-semibold text-foreground">ISO 27001:2022</h3>
|
||||
<p class="text-xs text-foreground-muted mt-1 leading-relaxed">
|
||||
Certified information security management system. Covers development, cloud infrastructure, web services, support and data center operations.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card hover>
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="w-10 h-10 rounded-xl bg-gradient-to-br from-brand-500/20 to-brand-500/5 flex items-center justify-center shrink-0">
|
||||
<svg class="w-5 h-6" viewBox="0 0 32 36" fill="none" xmlns="http://www.w3.org/2000/svg" aria-label="Swiss Hosting" role="img">
|
||||
<defs><clipPath id="zh-en-privacy"><path d="M16 1L1 7V23L6 30L16 34L26 30L31 23V7L16 1Z"/></clipPath></defs>
|
||||
<path d="M16 1L1 7V23L6 30L16 34L26 30L31 23V7L16 1Z" fill="white"/>
|
||||
<polygon points="1,7 16,1 31,7 1,34" fill="#003DA5" clip-path="url(#zh-en-privacy)"/>
|
||||
<path d="M16 1L1 7V23L6 30L16 34L26 30L31 23V7L16 1Z" fill="none" stroke="#003DA5" stroke-width="1.5"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs font-bold tracking-wide text-foreground-muted mb-0.5">Data Sovereignty</p>
|
||||
<h3 class="text-sm font-semibold text-foreground">Swiss Hosting Label</h3>
|
||||
<p class="text-xs text-foreground-muted mt-1 leading-relaxed">
|
||||
Data is stored, processed and operated exclusively in Swiss data centers. No outsourcing abroad.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card hover>
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="w-10 h-10 rounded-xl bg-gradient-to-br from-brand-500/20 to-brand-500/5 flex items-center justify-center text-brand-500 shrink-0">
|
||||
<Icon name="code-2" size="sm" />
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs font-bold tracking-wide text-foreground-muted mb-0.5">Development</p>
|
||||
<h3 class="text-sm font-semibold text-foreground">Swiss Made Software</h3>
|
||||
<p class="text-xs text-foreground-muted mt-1 leading-relaxed">
|
||||
Infomaniak develops its software primarily in Switzerland and meets the requirements of the Swiss Made Software label.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card hover>
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="w-10 h-10 rounded-xl bg-gradient-to-br from-brand-500/20 to-brand-500/5 flex items-center justify-center text-brand-500 shrink-0">
|
||||
<Icon name="file-check" size="sm" />
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs font-bold tracking-wide text-foreground-muted mb-0.5">Legal Compliance</p>
|
||||
<h3 class="text-sm font-semibold text-foreground">nFADP & GDPR</h3>
|
||||
<p class="text-xs text-foreground-muted mt-1 leading-relaxed">
|
||||
Full compliance with the Swiss Federal Act on Data Protection (nFADP) and the European General Data Protection Regulation (GDPR).
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card hover>
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="w-10 h-10 rounded-xl bg-gradient-to-br from-brand-500/20 to-brand-500/5 flex items-center justify-center text-brand-500 shrink-0">
|
||||
<Icon name="leaf" size="sm" />
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs font-bold tracking-wide text-foreground-muted mb-0.5">Sustainability</p>
|
||||
<h3 class="text-sm font-semibold text-foreground">ISO 14001 · B Corp™</h3>
|
||||
<p class="text-xs text-foreground-muted mt-1 leading-relaxed">
|
||||
100% renewable energy (60% hydro, 40% solar). 200% CO₂ compensation. Certified B Corp™ since 2025.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card hover>
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="w-10 h-10 rounded-xl bg-gradient-to-br from-brand-500/20 to-brand-500/5 flex items-center justify-center text-brand-500 shrink-0">
|
||||
<Icon name="server" size="sm" />
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs font-bold tracking-wide text-foreground-muted mb-0.5">Data Center</p>
|
||||
<h3 class="text-sm font-semibold text-foreground">Physical Security</h3>
|
||||
<p class="text-xs text-foreground-muted mt-1 leading-relaxed">
|
||||
Multiple redundant Swiss data centers. Annual penetration tests, AES-256 encryption, PUE < 1.1.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Privacy Policy -->
|
||||
<section class="py-[var(--space-section-md)] bg-background border-t border-border">
|
||||
<div class="mx-auto max-w-3xl px-6">
|
||||
<div class="prose prose-neutral dark:prose-invert max-w-none space-y-10" data-reveal>
|
||||
|
||||
<div>
|
||||
<h2 class="font-display text-2xl font-bold text-foreground mb-4">1. Data Controller</h2>
|
||||
<p class="text-foreground-muted leading-relaxed">
|
||||
The data controller for processing within Armarium Suite is Armarium, Zürich, Switzerland. For privacy inquiries, contact us at:
|
||||
<a href="mailto:privacy@armarium.ch" class="text-brand-500 hover:text-brand-600 transition-colors">privacy@armarium.ch</a>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 class="font-display text-2xl font-bold text-foreground mb-4">2. Data Collected</h2>
|
||||
<p class="text-foreground-muted leading-relaxed mb-3">Armarium only collects data strictly necessary for operating the app:</p>
|
||||
<ul class="space-y-2 text-foreground-muted">
|
||||
<li class="flex items-start gap-2"><Icon name="check" size="sm" class="text-brand-500 shrink-0 mt-0.5" /><span><strong class="text-foreground">Registration data:</strong> email address and password (hashed, never stored in plain text).</span></li>
|
||||
<li class="flex items-start gap-2"><Icon name="check" size="sm" class="text-brand-500 shrink-0 mt-0.5" /><span><strong class="text-foreground">Financial data:</strong> transactions, budgets, categories and accounts you enter yourself.</span></li>
|
||||
<li class="flex items-start gap-2"><Icon name="check" size="sm" class="text-brand-500 shrink-0 mt-0.5" /><span><strong class="text-foreground">Technical data:</strong> IP address (for security purposes), browser type, timestamp of last login.</span></li>
|
||||
</ul>
|
||||
<p class="text-foreground-muted leading-relaxed mt-3">
|
||||
We use <strong class="text-foreground">no tracking, no advertising cookies and no third-party analytics services</strong>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 class="font-display text-2xl font-bold text-foreground mb-4">3. Purpose of Processing</h2>
|
||||
<p class="text-foreground-muted leading-relaxed mb-3">Your data is used exclusively for:</p>
|
||||
<ul class="space-y-2 text-foreground-muted">
|
||||
<li class="flex items-start gap-2"><Icon name="check" size="sm" class="text-brand-500 shrink-0 mt-0.5" /><span>Providing and operating Armarium Suite</span></li>
|
||||
<li class="flex items-start gap-2"><Icon name="check" size="sm" class="text-brand-500 shrink-0 mt-0.5" /><span>Authentication and account protection</span></li>
|
||||
<li class="flex items-start gap-2"><Icon name="check" size="sm" class="text-brand-500 shrink-0 mt-0.5" /><span>Technical support at your request</span></li>
|
||||
</ul>
|
||||
<p class="text-foreground-muted leading-relaxed mt-3">
|
||||
Your data is <strong class="text-foreground">not shared with third parties</strong> – neither for advertising purposes nor for analysis.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 class="font-display text-2xl font-bold text-foreground mb-4">4. Data Storage & Hosting</h2>
|
||||
<p class="text-foreground-muted leading-relaxed">
|
||||
All data is stored and processed exclusively on servers of <strong class="text-foreground">Infomaniak Network SA</strong> in Switzerland. Infomaniak is ISO 27001:2022 certified and fulfills all requirements of the Swiss Data Protection Act (nFADP) and GDPR. No data is transferred to third countries.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 class="font-display text-2xl font-bold text-foreground mb-4">5. Data Security</h2>
|
||||
<p class="text-foreground-muted leading-relaxed">
|
||||
All data transmissions are encrypted via HTTPS/TLS. Passwords are stored only as hashed values (bcrypt). Infomaniak's infrastructure uses AES-256 encryption and undergoes annual penetration testing.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 class="font-display text-2xl font-bold text-foreground mb-4">6. Your Rights</h2>
|
||||
<p class="text-foreground-muted leading-relaxed mb-3">Under nFADP and GDPR, you have the following rights:</p>
|
||||
<ul class="space-y-2 text-foreground-muted">
|
||||
<li class="flex items-start gap-2"><Icon name="check" size="sm" class="text-brand-500 shrink-0 mt-0.5" /><span><strong class="text-foreground">Access:</strong> know what data is stored about you.</span></li>
|
||||
<li class="flex items-start gap-2"><Icon name="check" size="sm" class="text-brand-500 shrink-0 mt-0.5" /><span><strong class="text-foreground">Rectification:</strong> have inaccurate data corrected.</span></li>
|
||||
<li class="flex items-start gap-2"><Icon name="check" size="sm" class="text-brand-500 shrink-0 mt-0.5" /><span><strong class="text-foreground">Erasure:</strong> complete deletion of your account and all data.</span></li>
|
||||
<li class="flex items-start gap-2"><Icon name="check" size="sm" class="text-brand-500 shrink-0 mt-0.5" /><span><strong class="text-foreground">Portability:</strong> export of your data in a common format.</span></li>
|
||||
<li class="flex items-start gap-2"><Icon name="check" size="sm" class="text-brand-500 shrink-0 mt-0.5" /><span><strong class="text-foreground">Objection:</strong> object to certain processing activities.</span></li>
|
||||
</ul>
|
||||
<p class="text-foreground-muted leading-relaxed mt-3">
|
||||
Send requests to: <a href="mailto:privacy@armarium.ch" class="text-brand-500 hover:text-brand-600 transition-colors">privacy@armarium.ch</a>. We process your request within 30 days.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 class="font-display text-2xl font-bold text-foreground mb-4">7. Account Deletion</h2>
|
||||
<p class="text-foreground-muted leading-relaxed">
|
||||
You can delete your account at any time in the app settings. All your data will be irreversibly and completely removed from our systems. Restoration is not possible afterwards.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 class="font-display text-2xl font-bold text-foreground mb-4">8. Changes to This Policy</h2>
|
||||
<p class="text-foreground-muted leading-relaxed">
|
||||
We reserve the right to update this privacy policy as needed. The current version is available at <a href="/en/privacy" class="text-brand-500 hover:text-brand-600 transition-colors">armarium.ch/en/privacy</a>. For material changes, registered users will be notified by email.
|
||||
</p>
|
||||
<p class="text-foreground-muted leading-relaxed mt-2 text-sm">
|
||||
Last updated: April 2026
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</PageLayout>
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
import FeaturesIndexPage from '@/components/landing/FeaturesIndexPage.astro';
|
||||
---
|
||||
|
||||
<FeaturesIndexPage locale="de" />
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
import AboutPage from '@/components/landing/AboutPage.astro';
|
||||
---
|
||||
|
||||
<AboutPage locale="fr" />
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
import BlogIndexPage from '@/components/landing/BlogIndexPage.astro';
|
||||
---
|
||||
|
||||
<BlogIndexPage locale="fr" />
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
import ContactPage from '@/components/landing/ContactPage.astro';
|
||||
---
|
||||
|
||||
<ContactPage locale="fr" />
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
import FeaturesIndexPage from '@/components/landing/FeaturesIndexPage.astro';
|
||||
---
|
||||
|
||||
<FeaturesIndexPage locale="fr" />
|
||||
@@ -1,73 +0,0 @@
|
||||
---
|
||||
/**
|
||||
* Mentions légales — FR
|
||||
* URL: /fr/impressum
|
||||
*/
|
||||
import PageLayout from '@/layouts/PageLayout.astro';
|
||||
import Badge from '@/components/ui/data-display/Badge/Badge.astro';
|
||||
import Icon from '@/components/ui/primitives/Icon/Icon.astro';
|
||||
import { Hero } from '@/components/hero';
|
||||
---
|
||||
|
||||
<PageLayout
|
||||
title="Mentions légales — Armarium"
|
||||
description="Mentions légales d'Armarium Suite."
|
||||
noindex
|
||||
locale="fr"
|
||||
>
|
||||
<Hero layout="centered" size="sm">
|
||||
<Badge slot="badge" variant="brand" pill>
|
||||
<Icon name="file-text" size="sm" />
|
||||
Mentions légales
|
||||
</Badge>
|
||||
<h1 slot="title">Mentions légales</h1>
|
||||
</Hero>
|
||||
|
||||
<section class="py-[var(--space-section-md)] bg-background border-t border-border">
|
||||
<div class="mx-auto max-w-2xl px-6 space-y-8 text-foreground-muted" data-reveal>
|
||||
|
||||
<div>
|
||||
<h2 class="font-display text-lg font-bold text-foreground mb-2">Exploitant</h2>
|
||||
<p>Armarium<br />Zürich, Suisse</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 class="font-display text-lg font-bold text-foreground mb-2">Contact</h2>
|
||||
<p>
|
||||
E-mail :
|
||||
<a href="mailto:hallo@armarium.ch" class="text-brand-500 hover:text-brand-600 transition-colors">
|
||||
hallo@armarium.ch
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 class="font-display text-lg font-bold text-foreground mb-2">Hébergement</h2>
|
||||
<p>
|
||||
Infomaniak Network SA<br />
|
||||
Rue Eugène-Marziano 25, 1227 Genève, Suisse<br />
|
||||
<a href="https://www.infomaniak.com" target="_blank" rel="noopener noreferrer" class="text-brand-500 hover:text-brand-600 transition-colors">
|
||||
www.infomaniak.com
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 class="font-display text-lg font-bold text-foreground mb-2">Clause de non-responsabilité</h2>
|
||||
<p class="leading-relaxed">
|
||||
Malgré la vérification soigneuse du contenu, nous déclinons toute responsabilité quant au contenu des liens externes. Les exploitants des pages liées sont seuls responsables de leur contenu.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 class="font-display text-lg font-bold text-foreground mb-2">Droits d'auteur</h2>
|
||||
<p class="leading-relaxed">
|
||||
Tous les contenus de ce site sont protégés par le droit d'auteur. Toute utilisation sans autorisation écrite expresse est interdite.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p class="text-sm">Version : avril 2026</p>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
</PageLayout>
|
||||
@@ -1,12 +0,0 @@
|
||||
---
|
||||
import LandingLayout from '@/layouts/LandingLayout.astro';
|
||||
import HomePage from '@/components/landing/HomePage.astro';
|
||||
---
|
||||
|
||||
<LandingLayout
|
||||
title="Armarium Suite — Budget & More"
|
||||
description="Gardez le contrôle de vos finances. Enregistrez vos dépenses, gérez vos budgets et atteignez vos objectifs d'épargne – simplement et clairement."
|
||||
locale="fr"
|
||||
>
|
||||
<HomePage locale="fr" />
|
||||
</LandingLayout>
|
||||
@@ -1,234 +0,0 @@
|
||||
---
|
||||
/**
|
||||
* Politique de confidentialité — FR
|
||||
* URL: /fr/privacy
|
||||
*/
|
||||
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';
|
||||
---
|
||||
|
||||
<PageLayout
|
||||
title="Confidentialité — Armarium"
|
||||
description="Politique de confidentialité d'Armarium Suite. Vos données sont hébergées exclusivement en Suisse."
|
||||
locale="fr"
|
||||
>
|
||||
<Hero layout="centered" size="sm">
|
||||
<Badge slot="badge" variant="brand" pill>
|
||||
<Icon name="shield-check" size="sm" />
|
||||
Confidentialité
|
||||
</Badge>
|
||||
<h1 slot="title">
|
||||
Vos données,<br />
|
||||
<span class="text-brand-500 [-webkit-text-fill-color:var(--color-brand-500)]">votre protection.</span>
|
||||
</h1>
|
||||
<p slot="description">
|
||||
Armarium prend la protection des données au sérieux. Vos données financières sont stockées et traitées exclusivement en Suisse – sans vente de données, sans tracking.
|
||||
</p>
|
||||
</Hero>
|
||||
|
||||
<!-- Hébergement: Infomaniak -->
|
||||
<section class="py-[var(--space-section-md)] bg-brand-500/8 dark:bg-background-secondary border-t border-border">
|
||||
<div class="mx-auto max-w-5xl px-6">
|
||||
<div class="flex flex-col gap-8" data-reveal>
|
||||
<div class="flex flex-col items-center gap-4 text-center">
|
||||
<Badge variant="brand" pill>Hébergement & Infrastructure</Badge>
|
||||
<h2 class="font-display text-3xl md:text-4xl font-bold text-foreground">
|
||||
100% hébergement suisse
|
||||
</h2>
|
||||
<p class="text-lg text-foreground-muted max-w-2xl">
|
||||
Armarium exploite l'intégralité de son infrastructure exclusivement sur les serveurs <strong class="text-foreground">Infomaniak</strong> en Suisse. Aucune donnée ne quitte jamais le territoire suisse.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<Card hover>
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="w-10 h-10 rounded-xl bg-gradient-to-br from-brand-500/20 to-brand-500/5 flex items-center justify-center text-brand-500 shrink-0">
|
||||
<Icon name="shield" size="sm" />
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs font-bold tracking-wide text-foreground-muted mb-0.5">Sécurité de l'information</p>
|
||||
<h3 class="text-sm font-semibold text-foreground">ISO 27001:2022</h3>
|
||||
<p class="text-xs text-foreground-muted mt-1 leading-relaxed">
|
||||
Système de management de la sécurité certifié. Couvre le développement, l'infrastructure cloud, les services web, le support et l'exploitation des centres de données.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card hover>
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="w-10 h-10 rounded-xl bg-gradient-to-br from-brand-500/20 to-brand-500/5 flex items-center justify-center shrink-0">
|
||||
<svg class="w-5 h-6" viewBox="0 0 32 36" fill="none" xmlns="http://www.w3.org/2000/svg" aria-label="Swiss Hosting" role="img">
|
||||
<defs><clipPath id="zh-fr-privacy"><path d="M16 1L1 7V23L6 30L16 34L26 30L31 23V7L16 1Z"/></clipPath></defs>
|
||||
<path d="M16 1L1 7V23L6 30L16 34L26 30L31 23V7L16 1Z" fill="white"/>
|
||||
<polygon points="1,7 16,1 31,7 1,34" fill="#003DA5" clip-path="url(#zh-fr-privacy)"/>
|
||||
<path d="M16 1L1 7V23L6 30L16 34L26 30L31 23V7L16 1Z" fill="none" stroke="#003DA5" stroke-width="1.5"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs font-bold tracking-wide text-foreground-muted mb-0.5">Souveraineté des données</p>
|
||||
<h3 class="text-sm font-semibold text-foreground">Swiss Hosting Label</h3>
|
||||
<p class="text-xs text-foreground-muted mt-1 leading-relaxed">
|
||||
Les données sont stockées, traitées et exploitées exclusivement dans des centres de données suisses. Aucune externalisation à l'étranger.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card hover>
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="w-10 h-10 rounded-xl bg-gradient-to-br from-brand-500/20 to-brand-500/5 flex items-center justify-center text-brand-500 shrink-0">
|
||||
<Icon name="code-2" size="sm" />
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs font-bold tracking-wide text-foreground-muted mb-0.5">Développement</p>
|
||||
<h3 class="text-sm font-semibold text-foreground">Swiss Made Software</h3>
|
||||
<p class="text-xs text-foreground-muted mt-1 leading-relaxed">
|
||||
Infomaniak développe ses logiciels majoritairement en Suisse et répond aux exigences du label Swiss Made Software.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card hover>
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="w-10 h-10 rounded-xl bg-gradient-to-br from-brand-500/20 to-brand-500/5 flex items-center justify-center text-brand-500 shrink-0">
|
||||
<Icon name="file-check" size="sm" />
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs font-bold tracking-wide text-foreground-muted mb-0.5">Conformité légale</p>
|
||||
<h3 class="text-sm font-semibold text-foreground">LPD & RGPD</h3>
|
||||
<p class="text-xs text-foreground-muted mt-1 leading-relaxed">
|
||||
Conformité complète avec la Loi fédérale sur la protection des données (nLPD) et le Règlement général sur la protection des données (RGPD/GDPR).
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card hover>
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="w-10 h-10 rounded-xl bg-gradient-to-br from-brand-500/20 to-brand-500/5 flex items-center justify-center text-brand-500 shrink-0">
|
||||
<Icon name="leaf" size="sm" />
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs font-bold tracking-wide text-foreground-muted mb-0.5">Durabilité</p>
|
||||
<h3 class="text-sm font-semibold text-foreground">ISO 14001 · B Corp™</h3>
|
||||
<p class="text-xs text-foreground-muted mt-1 leading-relaxed">
|
||||
100% énergie renouvelable (60% hydraulique, 40% solaire). Compensation CO₂ à 200%. Certifié B Corp™ depuis 2025.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card hover>
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="w-10 h-10 rounded-xl bg-gradient-to-br from-brand-500/20 to-brand-500/5 flex items-center justify-center text-brand-500 shrink-0">
|
||||
<Icon name="server" size="sm" />
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-xs font-bold tracking-wide text-foreground-muted mb-0.5">Centre de données</p>
|
||||
<h3 class="text-sm font-semibold text-foreground">Sécurité physique</h3>
|
||||
<p class="text-xs text-foreground-muted mt-1 leading-relaxed">
|
||||
Plusieurs centres de données suisses redondants. Tests de pénétration annuels, chiffrement AES-256, PUE < 1.1.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Politique de confidentialité -->
|
||||
<section class="py-[var(--space-section-md)] bg-background border-t border-border">
|
||||
<div class="mx-auto max-w-3xl px-6">
|
||||
<div class="prose prose-neutral dark:prose-invert max-w-none space-y-10" data-reveal>
|
||||
|
||||
<div>
|
||||
<h2 class="font-display text-2xl font-bold text-foreground mb-4">1. Responsable du traitement</h2>
|
||||
<p class="text-foreground-muted leading-relaxed">
|
||||
Le responsable du traitement des données dans le cadre d'Armarium Suite est Armarium, Zürich, Suisse. Pour toute question relative à la protection des données, contactez-nous à :
|
||||
<a href="mailto:privacy@armarium.ch" class="text-brand-500 hover:text-brand-600 transition-colors">privacy@armarium.ch</a>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 class="font-display text-2xl font-bold text-foreground mb-4">2. Données collectées</h2>
|
||||
<p class="text-foreground-muted leading-relaxed mb-3">Armarium ne collecte que les données strictement nécessaires au fonctionnement de l'application :</p>
|
||||
<ul class="space-y-2 text-foreground-muted">
|
||||
<li class="flex items-start gap-2"><Icon name="check" size="sm" class="text-brand-500 shrink-0 mt-0.5" /><span><strong class="text-foreground">Données d'inscription :</strong> adresse e-mail et mot de passe (haché, jamais stocké en clair).</span></li>
|
||||
<li class="flex items-start gap-2"><Icon name="check" size="sm" class="text-brand-500 shrink-0 mt-0.5" /><span><strong class="text-foreground">Données financières :</strong> transactions, budgets, catégories et comptes que vous saisissez vous-même.</span></li>
|
||||
<li class="flex items-start gap-2"><Icon name="check" size="sm" class="text-brand-500 shrink-0 mt-0.5" /><span><strong class="text-foreground">Données techniques :</strong> adresse IP (à des fins de sécurité), type de navigateur, horodatage de la dernière connexion.</span></li>
|
||||
</ul>
|
||||
<p class="text-foreground-muted leading-relaxed mt-3">
|
||||
Nous n'utilisons <strong class="text-foreground">aucun tracking, aucun cookie publicitaire et aucun service d'analyse tiers</strong>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 class="font-display text-2xl font-bold text-foreground mb-4">3. Finalité du traitement</h2>
|
||||
<p class="text-foreground-muted leading-relaxed mb-3">Vos données sont utilisées exclusivement pour :</p>
|
||||
<ul class="space-y-2 text-foreground-muted">
|
||||
<li class="flex items-start gap-2"><Icon name="check" size="sm" class="text-brand-500 shrink-0 mt-0.5" /><span>La fourniture et l'exploitation d'Armarium Suite</span></li>
|
||||
<li class="flex items-start gap-2"><Icon name="check" size="sm" class="text-brand-500 shrink-0 mt-0.5" /><span>L'authentification et la protection du compte</span></li>
|
||||
<li class="flex items-start gap-2"><Icon name="check" size="sm" class="text-brand-500 shrink-0 mt-0.5" /><span>Le support technique à votre demande</span></li>
|
||||
</ul>
|
||||
<p class="text-foreground-muted leading-relaxed mt-3">
|
||||
Vos données ne sont <strong class="text-foreground">pas transmises à des tiers</strong> – ni à des fins publicitaires, ni à des fins d'analyse.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 class="font-display text-2xl font-bold text-foreground mb-4">4. Stockage des données & hébergement</h2>
|
||||
<p class="text-foreground-muted leading-relaxed">
|
||||
Toutes les données sont stockées et traitées exclusivement sur les serveurs d'<strong class="text-foreground">Infomaniak Network SA</strong> en Suisse. Infomaniak est certifié ISO 27001:2022 et satisfait à toutes les exigences de la loi suisse sur la protection des données (nLPD) ainsi qu'au RGPD. Aucun transfert de données vers des pays tiers n'a lieu.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 class="font-display text-2xl font-bold text-foreground mb-4">5. Sécurité des données</h2>
|
||||
<p class="text-foreground-muted leading-relaxed">
|
||||
Toutes les transmissions de données sont chiffrées via HTTPS/TLS. Les mots de passe sont stockés uniquement sous forme hachée (bcrypt). L'infrastructure d'Infomaniak utilise le chiffrement AES-256 et fait l'objet de tests de pénétration annuels.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 class="font-display text-2xl font-bold text-foreground mb-4">6. Vos droits</h2>
|
||||
<p class="text-foreground-muted leading-relaxed mb-3">Conformément à la nLPD et au RGPD, vous disposez des droits suivants :</p>
|
||||
<ul class="space-y-2 text-foreground-muted">
|
||||
<li class="flex items-start gap-2"><Icon name="check" size="sm" class="text-brand-500 shrink-0 mt-0.5" /><span><strong class="text-foreground">Accès :</strong> connaître les données stockées vous concernant.</span></li>
|
||||
<li class="flex items-start gap-2"><Icon name="check" size="sm" class="text-brand-500 shrink-0 mt-0.5" /><span><strong class="text-foreground">Rectification :</strong> faire corriger des données inexactes.</span></li>
|
||||
<li class="flex items-start gap-2"><Icon name="check" size="sm" class="text-brand-500 shrink-0 mt-0.5" /><span><strong class="text-foreground">Suppression :</strong> suppression complète de votre compte et de toutes vos données.</span></li>
|
||||
<li class="flex items-start gap-2"><Icon name="check" size="sm" class="text-brand-500 shrink-0 mt-0.5" /><span><strong class="text-foreground">Portabilité :</strong> export de vos données dans un format courant.</span></li>
|
||||
<li class="flex items-start gap-2"><Icon name="check" size="sm" class="text-brand-500 shrink-0 mt-0.5" /><span><strong class="text-foreground">Opposition :</strong> opposition à certains traitements.</span></li>
|
||||
</ul>
|
||||
<p class="text-foreground-muted leading-relaxed mt-3">
|
||||
Adressez vos demandes à : <a href="mailto:privacy@armarium.ch" class="text-brand-500 hover:text-brand-600 transition-colors">privacy@armarium.ch</a>. Nous traitons votre demande dans un délai de 30 jours.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 class="font-display text-2xl font-bold text-foreground mb-4">7. Suppression du compte</h2>
|
||||
<p class="text-foreground-muted leading-relaxed">
|
||||
Vous pouvez supprimer votre compte à tout moment dans les paramètres de l'application. Toutes vos données seront alors supprimées de manière définitive et complète de nos systèmes. Cette action est irréversible.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 class="font-display text-2xl font-bold text-foreground mb-4">8. Modifications de cette déclaration</h2>
|
||||
<p class="text-foreground-muted leading-relaxed">
|
||||
Nous nous réservons le droit d'adapter cette politique de confidentialité si nécessaire. La version actuelle est disponible à l'adresse <a href="/fr/privacy" class="text-brand-500 hover:text-brand-600 transition-colors">armarium.ch/fr/privacy</a>. En cas de modifications importantes, les utilisateurs enregistrés seront informés par e-mail.
|
||||
</p>
|
||||
<p class="text-foreground-muted leading-relaxed mt-2 text-sm">
|
||||
Version : avril 2026
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</PageLayout>
|
||||
@@ -1,73 +0,0 @@
|
||||
---
|
||||
/**
|
||||
* Impressum
|
||||
* URL: /impressum
|
||||
*/
|
||||
import PageLayout from '@/layouts/PageLayout.astro';
|
||||
import Badge from '@/components/ui/data-display/Badge/Badge.astro';
|
||||
import Icon from '@/components/ui/primitives/Icon/Icon.astro';
|
||||
import { Hero } from '@/components/hero';
|
||||
---
|
||||
|
||||
<PageLayout
|
||||
title="Impressum — Armarium"
|
||||
description="Impressum der Armarium Suite."
|
||||
noindex
|
||||
locale="de"
|
||||
>
|
||||
<Hero layout="centered" size="sm">
|
||||
<Badge slot="badge" variant="brand" pill>
|
||||
<Icon name="file-text" size="sm" />
|
||||
Impressum
|
||||
</Badge>
|
||||
<h1 slot="title">Impressum</h1>
|
||||
</Hero>
|
||||
|
||||
<section class="py-[var(--space-section-md)] bg-background border-t border-border">
|
||||
<div class="mx-auto max-w-2xl px-6 space-y-8 text-foreground-muted" data-reveal>
|
||||
|
||||
<div>
|
||||
<h2 class="font-display text-lg font-bold text-foreground mb-2">Betreiber</h2>
|
||||
<p>Armarium<br />Zürich, Schweiz</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 class="font-display text-lg font-bold text-foreground mb-2">Kontakt</h2>
|
||||
<p>
|
||||
E-Mail:
|
||||
<a href="mailto:hallo@armarium.ch" class="text-brand-500 hover:text-brand-600 transition-colors">
|
||||
hallo@armarium.ch
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 class="font-display text-lg font-bold text-foreground mb-2">Hosting</h2>
|
||||
<p>
|
||||
Infomaniak Network SA<br />
|
||||
Rue Eugène-Marziano 25, 1227 Genf, Schweiz<br />
|
||||
<a href="https://www.infomaniak.com" target="_blank" rel="noopener noreferrer" class="text-brand-500 hover:text-brand-600 transition-colors">
|
||||
www.infomaniak.com
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 class="font-display text-lg font-bold text-foreground mb-2">Haftungsausschluss</h2>
|
||||
<p class="leading-relaxed">
|
||||
Trotz sorgfältiger inhaltlicher Kontrolle übernehmen wir keine Haftung für die Inhalte externer Links. Für den Inhalt verlinkter Seiten sind ausschliesslich deren Betreiber verantwortlich.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 class="font-display text-lg font-bold text-foreground mb-2">Urheberrecht</h2>
|
||||
<p class="leading-relaxed">
|
||||
Alle Inhalte dieser Website sind urheberrechtlich geschützt. Eine Verwendung ohne ausdrückliche schriftliche Genehmigung ist untersagt.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p class="text-sm">Stand: April 2026</p>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
</PageLayout>
|
||||
@@ -1,12 +1,377 @@
|
||||
---
|
||||
import LandingLayout from '@/layouts/LandingLayout.astro';
|
||||
import HomePage from '@/components/landing/HomePage.astro';
|
||||
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 BlogCard from '@/components/blog/BlogCard.astro';
|
||||
import { getCollection } from 'astro:content';
|
||||
import siteConfig from '@/config/site.config';
|
||||
|
||||
const projectItems = await getCollection('projects');
|
||||
const featuredProjects = projectItems
|
||||
.sort((a, b) => a.data.order - b.data.order)
|
||||
.slice(0, 4);
|
||||
|
||||
const posts = (await getCollection('blog', ({ data }) => !data.draft))
|
||||
.sort((a, b) => {
|
||||
if (a.data.featured !== b.data.featured) return a.data.featured ? -1 : 1;
|
||||
return b.data.publishedAt.valueOf() - a.data.publishedAt.valueOf();
|
||||
})
|
||||
.slice(0, 3);
|
||||
---
|
||||
|
||||
<LandingLayout
|
||||
title="Armarium Suite — Budget & More"
|
||||
description="Behalte den Überblick über deine Finanzen. Erfasse Ausgaben, verwalte Budgets und erreiche deine Sparziele – einfach und übersichtlich."
|
||||
locale="de"
|
||||
title="Astro Rocket — Astro 6 starter theme"
|
||||
description="A production-ready Astro 6 starter with 12 beautiful themes, 57+ components, built-in i18n, dark mode and a fast, modern foundation to build anything on."
|
||||
image="/og-default.svg"
|
||||
includePersonSchema={true}
|
||||
includeProfessionalServiceSchema={true}
|
||||
>
|
||||
<HomePage locale="de" />
|
||||
<!-- Hero -->
|
||||
<Hero layout="centered" size="xl" class="hero-dark-gradient">
|
||||
<Badge slot="badge" variant="brand" pill pulse class="dark:text-brand-200">Available for new projects</Badge>
|
||||
|
||||
<h1 slot="title">
|
||||
<span class="text-foreground [-webkit-text-fill-color:currentColor]">Astro Rocket —</span><br />
|
||||
<span class="text-brand-500 [-webkit-text-fill-color:var(--color-brand-500)] dark:text-foreground dark:[-webkit-text-fill-color:currentColor]">Web Designer</span><span class="text-foreground [-webkit-text-fill-color:currentColor]"> & Developer</span>
|
||||
</h1>
|
||||
|
||||
<p slot="description">
|
||||
A production-ready Astro 6 starter with 12 beautiful themes, 57+ components, built-in i18n, dark mode and a fast, modern foundation to build anything on.
|
||||
</p>
|
||||
|
||||
<Fragment slot="actions">
|
||||
<Button size="lg" href="https://github.com/hansmartens68/Astro-Rocket">
|
||||
<Icon name="github" size="sm" />
|
||||
Get it on GitHub
|
||||
</Button>
|
||||
<Button size="lg" variant="outline" href="/blog/astro-rocket-getting-started">
|
||||
<Icon name="rocket" size="sm" />
|
||||
Get started
|
||||
<Icon name="arrow-right" size="sm" />
|
||||
</Button>
|
||||
</Fragment>
|
||||
</Hero>
|
||||
|
||||
<!-- Services Section -->
|
||||
<section class="relative z-10 py-[var(--space-section-md)] bg-background-secondary border-t border-border">
|
||||
<div class="mx-auto max-w-6xl px-6 flex flex-col gap-8">
|
||||
<div class="flex flex-col items-center gap-4 text-center" data-reveal>
|
||||
<div class="flex flex-col items-center gap-6">
|
||||
<Badge variant="brand" pill>Services</Badge>
|
||||
<h2 class="font-display text-3xl md:text-4xl font-bold text-foreground">
|
||||
What I do
|
||||
</h2>
|
||||
</div>
|
||||
<p class="text-lg text-foreground-muted max-w-2xl mx-auto">
|
||||
From concept to launch.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-6 md:grid-cols-2 lg:grid-cols-3" data-reveal data-reveal-delay="1">
|
||||
<!-- Web Design -->
|
||||
<Card hover>
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="w-10 h-10 rounded-xl bg-gradient-to-br from-brand-500/20 to-brand-500/5 flex items-center justify-center text-brand-500 shrink-0">
|
||||
<Icon name="layout" size="sm" />
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<h3 class="text-base font-semibold text-foreground">Web Design</h3>
|
||||
<p class="text-sm text-foreground-muted leading-relaxed mt-1.5">
|
||||
Clean, modern interfaces that look great on every screen. Focused on clarity, usability, and your brand identity.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<!-- Web Development -->
|
||||
<Card hover>
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="w-10 h-10 rounded-xl bg-gradient-to-br from-brand-500/20 to-brand-500/5 flex items-center justify-center text-brand-500 shrink-0">
|
||||
<Icon name="code" size="sm" />
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<h3 class="text-base font-semibold text-foreground">Web Development</h3>
|
||||
<p class="text-sm text-foreground-muted leading-relaxed mt-1.5">
|
||||
Fast, accessible websites built with modern tooling. Astro, Tailwind, and best-practice architecture from day one.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<!-- Performance -->
|
||||
<Card hover>
|
||||
<div class="flex items-start gap-4">
|
||||
<div class="w-10 h-10 rounded-xl bg-gradient-to-br from-brand-500/20 to-brand-500/5 flex items-center justify-center text-brand-500 shrink-0">
|
||||
<Icon name="zap" size="sm" />
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<h3 class="text-base font-semibold text-foreground">Performance Optimization</h3>
|
||||
<p class="text-sm text-foreground-muted leading-relaxed mt-1.5">
|
||||
Core Web Vitals, SEO, and page speed — built in from the start, not bolted on at the end.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- About Teaser -->
|
||||
<section class="relative z-10 py-[var(--space-section-md)] bg-background border-t border-border">
|
||||
<div class="mx-auto max-w-6xl px-6">
|
||||
<div class="grid gap-12 lg:grid-cols-2">
|
||||
<div class="flex flex-col justify-center" data-reveal>
|
||||
<div class="flex flex-col gap-6 mb-6">
|
||||
<Badge variant="brand" pill class="self-start">About me</Badge>
|
||||
<h2 class="font-display text-3xl md:text-4xl font-bold text-foreground">
|
||||
Designer, developer,<br />
|
||||
<span class="text-brand-500">and blogger.</span>
|
||||
</h2>
|
||||
</div>
|
||||
<p class="text-lg text-foreground-muted leading-relaxed mb-4">
|
||||
Sixteen years of hands-on experience{siteConfig.address?.city ? `, based in ${siteConfig.address.city}` : ''} and available for clients worldwide. I build with Astro because it aligns with how I think about the web: lean, purposeful, and built to last.
|
||||
</p>
|
||||
<p class="text-lg text-foreground-muted leading-relaxed mb-8">
|
||||
On the blog, I share the ideas and decisions that shape my work.
|
||||
</p>
|
||||
<Button variant="outline" href="/about" class="self-start">
|
||||
More about me
|
||||
</Button>
|
||||
</div>
|
||||
<Card variant="elevated" padding="lg" class="h-full flex flex-col" data-reveal data-reveal-delay="1">
|
||||
<p class="text-sm text-foreground mb-5 text-center">A few things about me.</p>
|
||||
<div class="grid grid-cols-2 gap-3 flex-1 auto-rows-fr">
|
||||
<div class="bg-background-tertiary rounded-xl p-5 flex flex-col items-center justify-center text-center gap-2 border-t-2 border-brand-500/40 transition-all duration-200 hover:-translate-y-1 hover:shadow-md hover:border-brand-500 cursor-default h-full">
|
||||
<Icon name="history" class="w-10 h-10 text-brand-500" />
|
||||
<p class="text-sm font-semibold text-foreground">16 Years Experience</p>
|
||||
</div>
|
||||
<a href="https://astro.build" target="_blank" rel="noopener noreferrer" class="bg-background-tertiary rounded-xl p-5 flex flex-col items-center justify-center text-center gap-2 border-t-2 border-brand-500/40 transition-all duration-200 hover:-translate-y-1 hover:shadow-md hover:border-brand-500 h-full">
|
||||
<svg class="w-10 h-10 text-brand-500" viewBox="0 0 1280 1280" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M815.039 94.6439C824.758 106.709 829.714 122.99 839.626 155.553L1056.17 866.901C976.107 825.368 889.072 795.413 797.281 779.252L656.29 302.798C653.983 295.002 646.822 289.654 638.693 289.654C630.542 289.654 623.368 295.03 621.08 302.853L481.795 779.011C389.579 795.1 302.146 825.109 221.741 866.793L439.347 155.388L439.348 155.388C449.291 122.882 454.262 106.629 463.982 94.5853C472.562 83.9531 483.723 75.6958 496.4 70.6002C510.76 64.8284 527.756 64.8284 561.749 64.8284H717.174C751.212 64.8284 768.23 64.8284 782.603 70.6123C795.292 75.7184 806.459 83.9923 815.039 94.6439Z" fill="currentColor"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M840.951 900.754C805.253 931.279 734.002 952.097 651.929 952.097C551.197 952.097 466.767 920.737 444.363 878.561C436.354 902.732 434.558 930.396 434.558 948.068C434.558 948.068 429.281 1034.84 489.636 1095.2C489.636 1063.86 515.042 1038.46 546.381 1038.46C600.097 1038.46 600.036 1085.32 599.987 1123.34C599.986 1124.48 599.984 1125.61 599.984 1126.73C599.984 1184.44 635.255 1233.91 685.416 1254.77C677.924 1239.36 673.721 1222.05 673.721 1203.77C673.721 1148.73 706.034 1128.23 743.588 1104.41L743.588 1104.41C773.469 1085.46 806.668 1064.41 829.548 1022.17C841.486 1000.13 848.265 974.893 848.265 948.068C848.265 931.573 845.702 915.676 840.951 900.754Z" fill="currentColor"/>
|
||||
</svg>
|
||||
<p class="text-sm font-semibold text-foreground">Building with Astro</p>
|
||||
</a>
|
||||
<div class="bg-background-tertiary rounded-xl p-5 flex flex-col items-center justify-center text-center gap-2 border-t-2 border-brand-500/40 transition-all duration-200 hover:-translate-y-1 hover:shadow-md hover:border-brand-500 cursor-default h-full">
|
||||
<Icon name="globe" class="w-10 h-10 text-brand-500" />
|
||||
<p class="text-sm font-semibold text-foreground">Working worldwide</p>
|
||||
</div>
|
||||
<div class="bg-background-tertiary rounded-xl p-5 flex flex-col items-center justify-center text-center gap-2 border-t-2 border-brand-500/40 transition-all duration-200 hover:-translate-y-1 hover:shadow-md hover:border-brand-500 cursor-default h-full">
|
||||
<Icon name="map-pin" class="w-10 h-10 text-brand-500" />
|
||||
<p class="text-sm font-semibold text-foreground">Based in <br class="sm:hidden" />{siteConfig.address?.city ?? 'Your City'}{siteConfig.address?.country ? `, ${siteConfig.address.country}` : ''}</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Portfolio -->
|
||||
<section id="work" class="relative z-10 py-[var(--space-section-md)] bg-background-secondary border-t border-border">
|
||||
<div class="mx-auto max-w-6xl px-6 flex flex-col gap-8">
|
||||
<div class="flex flex-col items-center gap-4 text-center" data-reveal>
|
||||
<div class="flex flex-col items-center gap-6">
|
||||
<Badge variant="brand" pill>Selected work</Badge>
|
||||
<h2 class="font-display text-3xl md:text-4xl font-bold text-foreground">
|
||||
Projects I've built
|
||||
</h2>
|
||||
</div>
|
||||
<p class="text-lg text-foreground-muted max-w-2xl mx-auto">
|
||||
A selection of recent work.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-6 md:grid-cols-2" data-reveal data-reveal-delay="1">
|
||||
{featuredProjects.map((project) => (
|
||||
<Card variant="elevated" hover padding="lg" href={`/projects/${project.id.replace(/\.mdx?$/, '')}`} class="group flex flex-col">
|
||||
<div class="flex flex-1 flex-col">
|
||||
<div class="mb-3 flex items-start justify-between gap-4">
|
||||
<div class="w-10 h-10 rounded-xl bg-gradient-to-br from-brand-500/20 to-brand-500/5 flex items-center justify-center text-brand-500 shrink-0">
|
||||
<Icon name="layers" size="sm" />
|
||||
</div>
|
||||
<Icon name="arrow-up-right" size="sm" class="text-foreground-muted group-hover:text-brand-500 transition-colors shrink-0" />
|
||||
</div>
|
||||
<div class="mb-2 flex items-baseline gap-2">
|
||||
<h3 class="font-display text-lg font-bold text-foreground">{project.data.title}</h3>
|
||||
{project.data.year && (
|
||||
<span class="text-xs text-foreground-muted">{project.data.year}</span>
|
||||
)}
|
||||
</div>
|
||||
<p class="text-sm text-foreground-muted leading-relaxed mb-4 flex-1">{project.data.description}</p>
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
{project.data.tags.map((tag) => (
|
||||
<Badge variant="brand">{tag}</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Testimonials -->
|
||||
<section class="relative z-10 py-[var(--space-section-md)] bg-background border-t border-border">
|
||||
<div class="mx-auto max-w-6xl px-6 flex flex-col gap-8">
|
||||
<div class="flex flex-col items-center gap-4 text-center" data-reveal>
|
||||
<div class="flex flex-col items-center gap-6">
|
||||
<Badge variant="brand" pill>Testimonials</Badge>
|
||||
<h2 class="font-display text-3xl md:text-4xl font-bold text-foreground">
|
||||
What clients say
|
||||
</h2>
|
||||
</div>
|
||||
<p class="text-lg text-foreground-muted">
|
||||
Replace these with your own client quotes.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-6 md:grid-cols-2" data-reveal data-reveal-delay="1">
|
||||
<!-- Testimonial 1 -->
|
||||
<Card variant="elevated" padding="lg" hover>
|
||||
<div class="flex flex-col h-full gap-4">
|
||||
<div class="flex gap-1 text-brand-500" role="img" aria-label="5 out of 5 stars">
|
||||
{[...Array(5)].map(() => (
|
||||
<svg class="w-4 h-4 fill-current" viewBox="0 0 20 20" aria-hidden="true">
|
||||
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
|
||||
</svg>
|
||||
))}
|
||||
</div>
|
||||
<blockquote class="flex-1 text-foreground-muted leading-relaxed">
|
||||
"The new site launched on time and hit every goal we set. Performance improved dramatically — load times dropped and the feedback from our customers has been overwhelmingly positive."
|
||||
</blockquote>
|
||||
<div class="flex items-center gap-3 pt-4 border-t border-border">
|
||||
<div class="w-9 h-9 rounded-full bg-gradient-to-br from-brand-500/30 to-brand-500/10 flex items-center justify-center text-brand-500 font-bold text-sm shrink-0">
|
||||
A
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-semibold text-foreground">Alex Morgan</p>
|
||||
<p class="text-xs text-foreground-muted">Founder, Bright Studio</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<!-- Testimonial 2 -->
|
||||
<Card variant="elevated" padding="lg" hover>
|
||||
<div class="flex flex-col h-full gap-4">
|
||||
<div class="flex gap-1 text-brand-500" role="img" aria-label="5 out of 5 stars">
|
||||
{[...Array(5)].map(() => (
|
||||
<svg class="w-4 h-4 fill-current" viewBox="0 0 20 20" aria-hidden="true">
|
||||
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
|
||||
</svg>
|
||||
))}
|
||||
</div>
|
||||
<blockquote class="flex-1 text-foreground-muted leading-relaxed">
|
||||
"Straightforward from first conversation to final handover. The right questions were asked, we were kept in the loop throughout, and the end result exceeded what we imagined. Would recommend without hesitation."
|
||||
</blockquote>
|
||||
<div class="flex items-center gap-3 pt-4 border-t border-border">
|
||||
<div class="w-9 h-9 rounded-full bg-gradient-to-br from-brand-500/30 to-brand-500/10 flex items-center justify-center text-brand-500 font-bold text-sm shrink-0">
|
||||
S
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-semibold text-foreground">Sarah Chen</p>
|
||||
<p class="text-xs text-foreground-muted">CEO, Luma Agency</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Blog -->
|
||||
<section class="relative z-10 py-[var(--space-section-md)] bg-background-secondary border-t border-border">
|
||||
<div class="mx-auto max-w-6xl px-6 flex flex-col gap-8">
|
||||
<div class="flex items-end justify-between" data-reveal>
|
||||
<div class="flex flex-col gap-6">
|
||||
<Badge variant="brand" pill class="self-start">From the blog</Badge>
|
||||
<h2 class="font-display text-3xl md:text-4xl font-bold text-foreground">
|
||||
Featured
|
||||
</h2>
|
||||
</div>
|
||||
<a href="/blog" class="hidden sm:inline-flex items-center gap-1.5 text-sm font-medium text-brand-500 hover:text-brand-600 transition-colors shrink-0">
|
||||
View all posts
|
||||
<Icon name="arrow-right" size="sm" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-6 md:grid-cols-3" data-reveal data-reveal-delay="1">
|
||||
{posts.map((post) => (
|
||||
<BlogCard
|
||||
title={post.data.title}
|
||||
description={post.data.description}
|
||||
href={`/blog/${post.id.replace('en/', '')}`}
|
||||
publishedAt={post.data.publishedAt}
|
||||
tags={post.data.tags}
|
||||
author={post.data.author}
|
||||
image={post.data.image}
|
||||
svgSlug={post.data.svgSlug}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div class="text-center sm:hidden">
|
||||
<Button variant="outline" href="/blog">
|
||||
View all posts
|
||||
<Icon name="arrow-right" size="sm" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- CTA -->
|
||||
<section class="relative z-10 py-[var(--space-section-md)] bg-background">
|
||||
<div class="mx-auto max-w-2xl px-6 text-center" data-reveal>
|
||||
<h2 class="font-display text-4xl font-bold text-foreground mb-4 text-balance">
|
||||
Have a project in mind?
|
||||
</h2>
|
||||
<p class="text-lg text-foreground-muted mb-8 text-balance">
|
||||
Whether it's a new build or something that needs a fresh perspective — I'd love to hear about it.
|
||||
</p>
|
||||
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<Button size="lg" href="/contact">
|
||||
Start a project
|
||||
<Icon name="arrow-right" size="sm" />
|
||||
</Button>
|
||||
<Button size="lg" variant="outline" href="/about">
|
||||
More about me
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
function initCountup() {
|
||||
const counters = document.querySelectorAll<HTMLElement>('[data-countup]:not([data-countup-init])');
|
||||
if (counters.length === 0) return;
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (!entry.isIntersecting) return;
|
||||
observer.unobserve(entry.target);
|
||||
const el = entry.target as HTMLElement;
|
||||
const target = parseInt(el.dataset.countup ?? '0', 10);
|
||||
const suffix = el.dataset.suffix ?? '';
|
||||
const duration = 1200;
|
||||
const start = performance.now();
|
||||
const tick = (now: number) => {
|
||||
const elapsed = now - start;
|
||||
const progress = Math.min(elapsed / duration, 1);
|
||||
const eased = 1 - Math.pow(1 - progress, 3);
|
||||
el.textContent = Math.round(eased * target) + suffix;
|
||||
if (progress < 1) requestAnimationFrame(tick);
|
||||
};
|
||||
requestAnimationFrame(tick);
|
||||
});
|
||||
}, { threshold: 0.4 });
|
||||
counters.forEach((el) => {
|
||||
el.dataset.countupInit = 'true';
|
||||
observer.observe(el);
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('astro:page-load', initCountup);
|
||||
</script>
|
||||
</LandingLayout>
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
import AboutPage from '@/components/landing/AboutPage.astro';
|
||||
---
|
||||
|
||||
<AboutPage locale="it" />
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
import BlogIndexPage from '@/components/landing/BlogIndexPage.astro';
|
||||
---
|
||||
|
||||
<BlogIndexPage locale="it" />
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
import ContactPage from '@/components/landing/ContactPage.astro';
|
||||
---
|
||||
|
||||
<ContactPage locale="it" />
|
||||