feat: Flowbite redesign, real content and new assets (v0.9.1)

- Complete Flowbite layout redesign for HomePage, FeaturesIndexPage,
  AboutPage, ContactPage, BlogIndexPage
- HomePage: Flowbite hero split, feature section with armarium_image.jpg,
  blog preview section, register-only CTA
- AboutPage: founder profile with photo, i18n team/FAQ content in all 4
  locales (about.team.*, about.founder.*, about.faq.*)
- Footer: renamed App → Produkt, added LinkedIn icon, 2-column layout
- Renamed /projects → /features (DE), removed feature detail pages
- Added assets: header_img.jpg, armarium_image.jpg, content_image.jpg,
  about_photo.jpg, Icon.svg, Logo_vertikal.svg

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Daniel Krähenbühl
2026-05-21 12:53:48 +02:00
parent 724080b7c6
commit 59da5eee0b
40 changed files with 775 additions and 2371 deletions
+42
View File
@@ -6,6 +6,48 @@ 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.q1q4`, `about.faq.a1a4`
- **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
-707
View File
@@ -1,707 +0,0 @@
<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).
-638
View File
@@ -1,638 +0,0 @@
{
"$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
}
}
}
-133
View File
@@ -1,133 +0,0 @@
{
"$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"
}
}
}
}
}
-30
View File
@@ -1,30 +0,0 @@
[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
+3
View File
@@ -0,0 +1,3 @@
<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>

After

Width:  |  Height:  |  Size: 421 B

+4
View File
@@ -0,0 +1,4 @@
<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>

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

-38
View File
@@ -1,38 +0,0 @@
<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.522, y≈221 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>

Before

Width:  |  Height:  |  Size: 2.0 KiB

+39 -108
View File
@@ -5,6 +5,8 @@ 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';
@@ -22,10 +24,6 @@ const t = useTranslations(locale);
locale={locale}
>
<Hero layout="centered" size="sm">
<Badge slot="badge" variant="brand" pill>
<Icon name="info" size="sm" />
{t('about.badge')}
</Badge>
<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>
@@ -33,117 +31,50 @@ const t = useTranslations(locale);
<p slot="description">{t('about.desc')}</p>
</Hero>
<!-- Mission -->
<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">
<div class="flex flex-col gap-4" data-reveal>
<div class="flex flex-col gap-6">
<Badge variant="brand" pill class="self-start">{t('about.mission.badge')}</Badge>
<h2 class="font-display text-3xl md:text-4xl font-bold text-foreground">
{t('about.mission.title')}
</h2>
<!-- 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>
<p class="text-lg text-foreground-muted leading-relaxed">{t('about.mission.p1')}</p>
<p class="text-lg text-foreground-muted leading-relaxed">{t('about.mission.p2')}</p>
</div>
<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="wallet" size="md" />
</div>
<div class="flex-1 min-w-0">
<p class="text-xs font-bold tracking-wide text-foreground-muted mb-1">{t('about.app.label')}</p>
<h3 class="text-base font-semibold text-foreground">{t('about.app.title')}</h3>
<p class="text-sm text-foreground-muted leading-relaxed mt-1.5">{t('about.app.desc')}</p>
<div class="flex flex-wrap gap-1.5 mt-3">
<Badge size="sm" variant="brand">Angular</Badge>
<Badge size="sm" variant="brand">Budget</Badge>
<Badge size="sm" variant="brand">{t('about.privacy.label')}</Badge>
</div>
</div>
</div>
</Card>
<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="shield-check" size="md" />
</div>
<div class="flex-1 min-w-0">
<p class="text-xs font-bold tracking-wide text-foreground-muted mb-1">{t('about.privacy.label')}</p>
<h3 class="text-base font-semibold text-foreground">{t('about.privacy.title')}</h3>
<p class="text-sm text-foreground-muted leading-relaxed mt-1.5">{t('about.privacy.desc')}</p>
</div>
</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>{t('about.values.badge')}</Badge>
<h2 class="font-display text-4xl font-bold text-foreground">{t('about.values.title')}</h2>
</div>
</div>
<div class="grid gap-8 sm:grid-cols-2 lg:grid-cols-4" 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="eye" size="md" />
<!-- 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>
<div class="flex-1 min-w-0">
<h3 class="text-base font-semibold text-foreground">{t('about.v1.title')}</h3>
<p class="text-sm text-foreground-muted leading-relaxed mt-1.5">{t('about.v1.desc')}</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="zap" size="md" />
</div>
<div class="flex-1 min-w-0">
<h3 class="text-base font-semibold text-foreground">{t('about.v2.title')}</h3>
<p class="text-sm text-foreground-muted leading-relaxed mt-1.5">{t('about.v2.desc')}</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="lock" size="md" />
</div>
<div class="flex-1 min-w-0">
<h3 class="text-base font-semibold text-foreground">{t('about.v3.title')}</h3>
<p class="text-sm text-foreground-muted leading-relaxed mt-1.5">{t('about.v3.desc')}</p>
</div>
</div>
</Card>
<!-- Made in Zürich -->
<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 shrink-0">
<svg class="w-6 h-7" viewBox="0 0 32 36" fill="none" xmlns="http://www.w3.org/2000/svg" aria-label="Zürich" role="img">
<defs><clipPath id="zh-about"><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-about)"/>
<path d="M16 1L1 7V23L6 30L16 34L26 30L31 23V7L16 1Z" fill="none" stroke="#003DA5" stroke-width="1.5"/>
</svg>
</div>
<div class="flex-1 min-w-0">
<h3 class="text-base font-semibold text-foreground">{t('about.v4.title')}</h3>
<p class="text-sm text-foreground-muted leading-relaxed mt-1.5">{t('about.v4.desc')}</p>
</div>
</div>
</Card>
</details>
))}
</div>
</div>
</section>
+48 -209
View File
@@ -1,13 +1,12 @@
---
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 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 siteConfig from '@/config/site.config';
import { resolveSocialLinks } from '@/lib/utils';
import { formatDate } from '@/lib/utils';
import { useTranslations } from '@/i18n/utils';
import type { Locale } from '@/i18n/ui';
@@ -17,23 +16,14 @@ interface Props {
const { locale } = Astro.props;
const t = useTranslations(locale);
const socialLinks = resolveSocialLinks(siteConfig.socialLinks);
// Get all published posts (all locales)
const allPosts = await getCollection('blog', ({ data }) => {
return import.meta.env.PROD ? data.draft !== true : true;
});
const posts = allPosts.sort((a, b) => b.data.publishedAt.valueOf() - a.data.publishedAt.valueOf());
const featuredPosts = posts.filter((post) => post.data.featured);
const nonFeaturedPosts = posts.filter((post) => !post.data.featured);
const regularPosts = nonFeaturedPosts.length > 0 ? nonFeaturedPosts : posts;
const allTags = [...new Set(posts.flatMap((post) => post.data.tags))].sort();
const getPostUrl = (postId: string) => {
// Strip locale folder prefix (e.g., "en/post-slug" → "post-slug")
const slug = postId.replace(/^[a-z]{2}\//, '');
return `/blog/${slug}`;
};
@@ -46,210 +36,59 @@ const getPostUrl = (postId: string) => {
locale={locale}
>
<Hero layout="centered" size="sm">
<Badge slot="badge" variant="brand" pill>
<Icon name="book" size="sm" />
{t('blog.badge')}
</Badge>
<h1 slot="title">{t('blog.title')}</h1>
<p slot="description">{t('blog.desc')}</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>
{t('blog.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">{t('blog.allposts')}</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">
{t('blog.filter.label')}
</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">{t('blog.filter.all')}</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>
<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>
</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}
/>
)}
<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>
) : (
<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">{t('blog.noposts')}</p>
</div>
)
}
<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">{t('blog.noposts.tag')}</p>
</article>
))}
</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">{t('blog.follow.title')}</h2>
<p slot="description" class="!text-lg text-balance">{t('blog.follow.desc')}</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>
<!-- 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>
</CTA>
</section>
</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}`;
filterCount.classList.remove('hidden');
}
});
}
document.addEventListener('astro:page-load', initTagFilter);
</script>
+48 -87
View File
@@ -1,12 +1,7 @@
---
import PageLayout from '@/layouts/PageLayout.astro';
import Icon from '@/components/ui/primitives/Icon/Icon.astro';
import Badge from '@/components/ui/data-display/Badge/Badge.astro';
import Card from '@/components/ui/data-display/Card/Card.astro';
import { Hero } from '@/components/hero';
import ContactForm from '@/components/patterns/ContactForm.astro';
import siteConfig from '@/config/site.config';
import { resolveSocialLinks } from '@/lib/utils';
import { useTranslations } from '@/i18n/utils';
import type { Locale } from '@/i18n/ui';
@@ -16,26 +11,6 @@ interface Props {
const { locale } = Astro.props;
const t = useTranslations(locale);
const socialLinks = resolveSocialLinks(siteConfig.socialLinks);
const channels = [
{
icon: 'mail',
label: 'Email',
value: siteConfig.email,
note: t('contact.follow'),
href: `mailto:${siteConfig.email}`,
external: false,
},
...socialLinks.map((link) => ({
icon: link.icon,
label: link.label,
value: link.href.replace(/^https?:\/\/(www\.)?/, ''),
note: t('contact.follow'),
href: link.href,
external: true,
})),
];
---
<PageLayout
@@ -44,77 +19,63 @@ const channels = [
locale={locale}
>
<Hero layout="centered" size="sm">
<Badge slot="badge" variant="brand" pill>
<Icon name="mail" size="sm" />
{t('contact.badge')}
</Badge>
<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>
<!-- 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">{t('contact.form.title')}</h2>
<ContactForm />
</Card>
<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>
<!-- 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">{t('contact.direct.title')}</h2>
<p class="text-sm text-foreground-muted">{t('contact.direct.desc')}</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 ?? 'Zürich'}</p>
<p class="text-xs text-foreground-muted mt-0.5">{siteConfig.address?.country ?? 'Switzerland'}</p>
</div>
</div>
</Card>
<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="space-y-8 text-center md:grid md:grid-cols-2 lg:grid-cols-3 md:gap-12 md:space-y-0">
<div>
<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" xmlns="http://www.w3.org/2000/svg"><path d="M2.003 5.884L10 9.882l7.997-3.998A2 2 0 0016 4H4a2 2 0 00-1.997 1.884z"></path><path d="M18 8.118l-8 4-8-4V14a2 2 0 002 2h12a2 2 0 002-2V8.118z"></path></svg>
</div>
<p class="mb-2 text-xl font-bold text-foreground dark:text-white">Email us:</p>
<p class="mb-3 text-foreground-muted dark:text-gray-400">Email us for general queries, including marketing and partnership opportunities.</p>
<a href={`mailto:${siteConfig.email}`} class="font-semibold text-brand-500 hover:underline">{siteConfig.email}</a>
</div>
<div>
<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" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-2 0c0 .993-.241 1.929-.668 2.754l-1.524-1.525a3.997 3.997 0 00.078-2.183l1.562-1.562C15.802 8.249 16 9.1 16 10zm-5.165 3.913l1.58 1.58A5.98 5.98 0 0110 16a5.976 5.976 0 01-2.516-.552l1.562-1.562a4.006 4.006 0 001.789.027zm-4.677-2.796a4.002 4.002 0 01-.041-2.08l-.08.08-1.53-1.533A5.98 5.98 0 004 10c0 .954.223 1.856.619 2.657l1.54-1.54zm1.088-6.45A5.974 5.974 0 0110 4c.954 0 1.856.223 2.657.619l-1.54 1.54a4.002 4.002 0 00-2.346.033L7.246 4.668zM12 10a2 2 0 11-4 0 2 2 0 014 0z" clip-rule="evenodd"></path></svg>
</div>
<p class="mb-2 text-xl font-bold text-foreground dark:text-white">Support</p>
<p class="mb-3 text-foreground-muted dark:text-gray-400">Email us for general queries, including marketing and partnership opportunities.</p>
<a href={`mailto:${siteConfig.email}`} class="inline-flex py-2 px-4 text-sm font-medium text-center rounded-lg border text-brand-500 border-brand-500 hover:text-white hover:bg-brand-500 focus:ring-4 focus:outline-none focus:ring-brand-300 transition-colors">Support center</a>
</div>
<div>
<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" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z" clip-rule="evenodd"></path></svg>
</div>
<p class="mb-2 text-xl font-bold text-foreground dark:text-white">Find us:</p>
<p class="mb-3 text-foreground-muted dark:text-gray-400">{siteConfig.address?.city ?? 'Zürich'}, {siteConfig.address?.country ?? 'Switzerland'}</p>
</div>
</div>
</div>
</section>
+64 -33
View File
@@ -1,9 +1,7 @@
---
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 Icon from '@/components/ui/primitives/Icon/Icon.astro';
import { Hero } from '@/components/hero';
import { getCollection } from 'astro:content';
import { useTranslations } from '@/i18n/utils';
@@ -37,10 +35,6 @@ const featuresHref = t('nav.features.href');
locale={locale}
>
<Hero layout="centered" size="sm">
<Badge slot="badge" variant="brand" pill>
<Icon name="zap" size="sm" />
{t('features.badge')}
</Badge>
<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>
@@ -48,32 +42,69 @@ const featuresHref = t('nav.features.href');
</Hero>
<!-- Feature cards -->
<section class="py-[var(--space-section-md)] bg-background-secondary border-t border-border">
<div class="mx-auto max-w-6xl px-6">
<div class="grid gap-6 md:grid-cols-2 lg:grid-cols-3" data-reveal>
{features.map((feature) => {
const slug = feature.id.replace(/\.mdx?$/, '');
const icon = iconMap[slug] ?? 'check-circle';
return (
<Card variant="elevated" hover padding="lg" href={`/projects/${slug}`} class="group flex flex-col">
<div class="flex flex-1 flex-col">
<div class="mb-4 flex items-start justify-between 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={icon} size="md" />
</div>
<Icon name="arrow-up-right" size="sm" class="text-foreground-muted group-hover:text-brand-500 transition-colors shrink-0" />
</div>
<h3 class="font-display text-lg font-bold text-foreground mb-2">{feature.data.title}</h3>
<p class="text-sm text-foreground-muted leading-relaxed flex-1">{feature.data.description}</p>
<div class="flex flex-wrap gap-1.5 mt-4">
{feature.data.tags.map((tag) => (
<Badge variant="brand">{tag}</Badge>
))}
</div>
</div>
</Card>
);
})}
<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>
+204 -93
View File
@@ -1,12 +1,10 @@
---
/**
* Shared home page component — renders in all 4 locales.
*/
import { Hero } from '@/components/hero';
import Button from '@/components/ui/form/Button/Button.astro';
import Badge from '@/components/ui/data-display/Badge/Badge.astro';
import Card from '@/components/ui/data-display/Card/Card.astro';
import Icon from '@/components/ui/primitives/Icon/Icon.astro';
import { 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';
@@ -17,94 +15,210 @@ interface Props {
const { locale } = Astro.props;
const t = useTranslations(locale);
const features = [
{ key: 'f1', icon: 'layout-dashboard' },
{ key: 'f2', icon: 'list' },
{ key: 'f3', icon: 'pie-chart' },
{ key: 'f4', icon: 'wallet' },
{ key: 'f5', icon: 'target' },
{ key: 'f6', icon: 'shield-check' },
] as const;
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 -->
<Hero layout="centered" size="xl" class="hero-dark-gradient">
<Badge slot="badge" variant="brand" pill pulse class="dark:text-brand-200">
{t('hero.badge')}
</Badge>
<h1 slot="title">
<span class="text-foreground [-webkit-text-fill-color:currentColor]">Armarium Suite —</span><br />
<span class="text-brand-500 [-webkit-text-fill-color:var(--color-brand-500)] dark:text-foreground dark:[-webkit-text-fill-color:currentColor]">Budget</span><span class="text-foreground [-webkit-text-fill-color:currentColor]"> &amp; More</span>
</h1>
<p slot="description">{t('hero.description')}</p>
<Fragment slot="actions">
<Button size="lg" href="https://app.armarium.ch/register">
{t('hero.register')}
<Icon name="arrow-right" size="sm" />
</Button>
<Button size="lg" variant="outline" href="https://app.armarium.ch/login">
{t('hero.login')}
</Button>
</Fragment>
</Hero>
<!-- Trust bar -->
<div class="relative z-10 py-5 bg-background border-t border-border">
<div class="mx-auto max-w-6xl px-6">
<div class="flex flex-wrap justify-center gap-x-8 gap-y-3 text-sm text-foreground-muted">
<span class="flex items-center gap-2">
<svg class="w-5 h-6 shrink-0" viewBox="0 0 32 36" fill="none" xmlns="http://www.w3.org/2000/svg" aria-label="Zürich" role="img">
<defs><clipPath id="zh-home"><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-home)"/>
<path d="M16 1L1 7V23L6 30L16 34L26 30L31 23V7L16 1Z" fill="none" stroke="#003DA5" stroke-width="1.5"/>
</svg>
Made in Zürich, Switzerland
</span>
<span class="flex items-center gap-2">
<Icon name="shield-check" size="sm" class="text-brand-500" />
{t('trust.privacy')}
</span>
<span class="flex items-center gap-2">
<Icon name="check-circle" size="sm" class="text-brand-500" />
{t('trust.free')}
</span>
<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> &amp; 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>
</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>
<!-- Features -->
<section id="features" 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>{t('features.badge')}</Badge>
<h2 class="font-display text-3xl md:text-4xl font-bold text-foreground">
{t('features.title')}
</h2>
</div>
<p class="text-lg text-foreground-muted max-w-2xl mx-auto">
<!-- 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-6 md:grid-cols-2 lg:grid-cols-3" data-reveal data-reveal-delay="1">
{features.map(({ key, icon }) => (
<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={icon} size="sm" />
</div>
<div class="flex-1 min-w-0">
<h3 class="text-base font-semibold text-foreground">{t(`${key}.title` as any)}</h3>
<p class="text-sm text-foreground-muted leading-relaxed mt-1.5">{t(`${key}.desc` as any)}</p>
</div>
</div>
</Card>
))}
<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>
@@ -116,16 +230,13 @@ const features = [
{t('cta.title')}
</h2>
<p class="text-lg text-foreground-muted mb-8 text-balance">
{t('cta.desc')}
Kostenlos mitmachen und sofort loslegen.
</p>
<div class="flex flex-col sm:flex-row gap-4 justify-center">
<Button size="lg" href="https://app.armarium.ch/register">
<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" />
</Button>
<Button size="lg" variant="outline" href="https://app.armarium.ch/login">
{t('cta.login')}
</Button>
<Icon name="arrow-right" size="sm" class="ml-2 -mr-1" />
</a>
</div>
</div>
</section>
+58
View File
@@ -0,0 +1,58 @@
---
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>
+1 -1
View File
@@ -13,7 +13,7 @@ export interface NavItem {
export const navItems: NavItem[] = [
{ label: 'Blog', href: '/blog', order: 1 },
{ label: 'Features', href: '/projects', order: 2 },
{ label: 'Features', href: '/features', order: 2 },
{ label: 'About', href: '/about', order: 3 },
{ label: 'Contact', href: '/contact', order: 4 },
];
+32
View File
@@ -0,0 +1,32 @@
---
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.
+24
View File
@@ -0,0 +1,24 @@
---
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.
+27
View File
@@ -0,0 +1,27 @@
---
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.
+1
View File
@@ -4,6 +4,7 @@ description: "Dein persönlicher Finanzbegleiter ist live. Budget tracken, Trans
publishedAt: 2026-04-13
author: "Armarium"
tags: ["launch", "budget", "finanzen", "schweiz"]
svgSlug: "astro-rocket-is-live"
featured: true
locale: de
---
@@ -1,18 +0,0 @@
---
title: "Budget-Übersicht"
description: "Alle Einnahmen und Ausgaben auf einen Blick. Dein aktueller Kontostand immer im Griff übersichtlich und in Echtzeit."
tags: ["Übersicht", "Dashboard", "Kontostand"]
featured: true
order: 1
---
Die Budget-Übersicht ist das Herzstück von Armarium. Auf einem einzigen Dashboard siehst du deinen aktuellen Kontostand, die Ausgaben und Einnahmen des aktuellen Monats sowie deine wichtigsten Budgetziele.
## Was du siehst
- **Kontostand** aller verbundenen Konten in einer Gesamtübersicht
- **Monatliche Bilanz** wie viel wurde eingenommen, wie viel ausgegeben
- **Budgetfortschritt** wie weit bist du noch von deinen Limits entfernt
- **Letzte Transaktionen** die neuesten Buchungen auf einen Blick
Die Übersicht aktualisiert sich in Echtzeit, sobald du eine neue Transaktion erfasst.
@@ -1,32 +0,0 @@
---
title: "Sicher & Privat"
description: "Deine Daten gehören dir. Kein Datenverkauf, kein Tracking 100% Schweizer Hosting bei Infomaniak, ISO 27001:2022-zertifiziert."
tags: ["Datenschutz", "Schweiz", "ISO 27001"]
featured: true
order: 6
---
Finanzdaten sind sensibel. Armarium behandelt sie entsprechend.
## Schweizer Hosting
Alle Daten werden ausschliesslich auf Servern von **Infomaniak** in der Schweiz gespeichert und verarbeitet. Kein Transfer ins Ausland, keine US-Cloud, keine versteckten Datenflüsse.
Infomaniak ist **ISO 27001:2022-zertifiziert** und trägt das **Swiss Hosting Label**.
## Was wir nicht tun
- Kein Verkauf deiner Daten an Dritte
- Kein Tracking oder Werbung
- Keine Weitergabe an Analysedienste
## Technische Sicherheit
- Alle Übertragungen verschlüsselt via HTTPS/TLS
- Passwörter gehasht (bcrypt), nie im Klartext gespeichert
- AES-256-Verschlüsselung auf Infrastrukturebene
- Jährliche Penetrationstests durch Infomaniak
## Rechtliches
Armarium ist konform mit dem Schweizer **nDSG** (neues Datenschutzgesetz) sowie der europäischen **DSGVO**.
@@ -1,22 +0,0 @@
---
title: "Kategorien & Berichte"
description: "Eigene Kategorien definieren und Ausgaben analysieren. Verstehe mit klaren Auswertungen, wohin dein Geld fliesst."
tags: ["Kategorien", "Berichte", "Analyse"]
featured: true
order: 3
---
Kategorien geben deinen Ausgaben eine Struktur. Berichte machen sie sichtbar.
## Kategorien
Erstelle Kategorien, die zu deinem Alltag passen ob Lebensmittel, Mobilität, Freizeit oder Sparen. Jede Transaktion wird einer Kategorie zugewiesen und erscheint automatisch in deinen Auswertungen.
## Berichte
- **Monatsübersicht** Ausgaben nach Kategorie aufgeteilt
- **Vergleich** zwischen verschiedenen Monaten
- **Top-Ausgaben** wo fliesst am meisten Geld hin?
- **Einnahmen vs. Ausgaben** Bilanz auf einen Blick
Die Berichte helfen dir, Muster zu erkennen und bewusster Entscheidungen zu treffen.
-20
View File
@@ -1,20 +0,0 @@
---
title: "Mehrere Konten"
description: "Verwalte Bankkonto, Kreditkarte und Bargeld getrennt alles in einer App zusammengefasst und übersichtlich."
tags: ["Konten", "Kreditkarte", "Bargeld"]
featured: true
order: 4
---
Die meisten Menschen haben mehr als ein Konto. Armarium bildet das ab einfach und übersichtlich.
## Unterstützte Kontotypen
- **Bankkonto** dein Hauptkonto für Einnahmen und laufende Kosten
- **Kreditkarte** Ausgaben separat verfolgen, Abrechnung im Überblick
- **Bargeld** Barzahlungen nicht vergessen
- **Sparkonto** gezielt Geld zur Seite legen
## Gesamtübersicht
Alle Konten werden in der Budget-Übersicht zusammengefasst. Du siehst den Gesamtstand sowie jeden Account einzeln. Transaktionen lassen sich kontoübergreifend filtern und auswerten.
-20
View File
@@ -1,20 +0,0 @@
---
title: "Sparziele"
description: "Setze dir finanzielle Ziele und verfolge deinen Fortschritt Schritt für Schritt zum Ziel."
tags: ["Sparziele", "Ziele", "Sparen"]
featured: true
order: 5
---
Ob Notgroschen, Urlaub oder neues Fahrrad Sparziele machen abstrakte Wünsche greifbar.
## So funktionieren Sparziele
1. **Ziel definieren** Name, Zielbetrag und optionales Zieldatum
2. **Fortschritt verfolgen** wie viel hast du bereits gespart?
3. **Einzahlungen zuweisen** Transaktionen direkt einem Ziel zuordnen
4. **Ziel erreicht** Armarium zeigt dir, wann du am Ziel bist
## Warum Ziele funktionieren
Wer ein konkretes Ziel vor Augen hat, spart nachweislich mehr. Armarium visualisiert deinen Fortschritt und erinnert dich daran, was du dir vorgenommen hast ohne störende Benachrichtigungen.
-19
View File
@@ -1,19 +0,0 @@
---
title: "Transaktionen erfassen"
description: "Ausgaben und Einnahmen schnell erfassen, kategorisieren und filtern für lückenlosen Überblick über dein Geldfluss."
tags: ["Transaktionen", "Ausgaben", "Einnahmen"]
featured: true
order: 2
---
Transaktionen sind das Fundament deines Budgets. Armarium macht das Erfassen so einfach wie möglich damit du es wirklich tust.
## Wie es funktioniert
- **Schnelleingabe** für Ausgaben und Einnahmen mit wenigen Klicks
- **Kategorisierung** direkt beim Erfassen oder nachträglich anpassbar
- **Notizfeld** für zusätzliche Details
- **Datum** frei wählbar auch rückwirkend eintragen möglich
- **Filterung** nach Zeitraum, Kategorie oder Konto
Je konsequenter du Transaktionen erfasst, desto aussagekräftiger werden deine Berichte und Auswertungen.
+131 -26
View File
@@ -17,17 +17,13 @@ export const ui = {
'nav.contact': 'Kontakt',
// Nav hrefs (locale-aware)
'nav.blog.href': '/blog',
'nav.features.href': '/projects',
'nav.features.href': '/features',
'nav.about.href': '/about',
'nav.contact.href': '/contact',
// Hero
'hero.badge': 'Version 1.0 ist da',
'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',
// Trust bar
'trust.privacy': 'Datenschutz garantiert',
'trust.free': 'Kostenlos starten',
// Features section
'features.badge': 'Features',
'features.title': 'Alles was du brauchst',
@@ -45,15 +41,32 @@ export const ui = {
'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. 🇨🇭 Made in Zürich, Switzerland.',
'footer.copyright': '© {year} Armarium. Alle Rechte vorbehalten.',
'footer.app': 'App',
'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',
@@ -89,6 +102,20 @@ export const ui = {
'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.',
@@ -125,12 +152,9 @@ export const ui = {
'nav.features.href': '/fr/features',
'nav.about.href': '/fr/about',
'nav.contact.href': '/fr/contact',
'hero.badge': 'Version 1.0 disponible',
'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',
'trust.privacy': 'Protection des données garantie',
'trust.free': 'Commencer gratuitement',
'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.',
@@ -146,13 +170,28 @@ export const ui = {
'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. 🇨🇭 Made in Zürich, Switzerland.',
'footer.copyright': '© {year} Armarium. Tous droits réservés.',
'footer.app': 'App',
'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',
@@ -187,6 +226,20 @@ export const ui = {
'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.',
@@ -220,12 +273,9 @@ export const ui = {
'nav.features.href': '/it/features',
'nav.about.href': '/it/about',
'nav.contact.href': '/it/contact',
'hero.badge': 'Versione 1.0 disponibile',
'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',
'trust.privacy': 'Privacy garantita',
'trust.free': 'Inizia gratuitamente',
'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.',
@@ -241,13 +291,28 @@ export const ui = {
'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. 🇨🇭 Made in Zürich, Switzerland.',
'footer.copyright': '© {year} Armarium. Tutti i diritti riservati.',
'footer.app': 'App',
'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',
@@ -282,6 +347,20 @@ export const ui = {
'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.',
@@ -315,12 +394,9 @@ export const ui = {
'nav.features.href': '/en/features',
'nav.about.href': '/en/about',
'nav.contact.href': '/en/contact',
'hero.badge': 'Version 1.0 is here',
'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',
'trust.privacy': 'Privacy guaranteed',
'trust.free': 'Start for free',
'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.',
@@ -336,13 +412,28 @@ export const ui = {
'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. 🇨🇭 Made in Zürich, Switzerland.',
'footer.copyright': '© {year} Armarium. All rights reserved.',
'footer.app': 'App',
'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',
@@ -377,6 +468,20 @@ export const ui = {
'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.',
+15
View File
@@ -88,6 +88,21 @@ 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 () {
+4 -4
View File
@@ -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 Footer from '@/components/layout/Footer.astro';
import AppFooter from '@/components/layout/AppFooter.astro';
import Breadcrumbs from '@/components/seo/Breadcrumbs.astro';
import ArticleHero from '@/components/blog/ArticleHero.astro';
import ShareButtons from '@/components/blog/ShareButtons.astro';
@@ -75,8 +75,8 @@ const fullUrl = new URL(Astro.url.pathname, Astro.site).toString();
shape="bar"
variant="solid"
size="lg"
showThemeSelector
showCta={false}
showCta
cta={{ label: t('hero.login'), href: 'https://app.armarium.ch/login' }}
showScrollProgress
showLanguageSwitcher
currentLocale={locale}
@@ -185,5 +185,5 @@ const fullUrl = new URL(Astro.url.pathname, Astro.site).toString();
</CTA>
</div>
<Footer slot="footer" layout="simple" background="secondary" />
<AppFooter slot="footer" locale={locale} />
</BaseLayout>
+4 -29
View File
@@ -6,7 +6,7 @@
*/
import BaseLayout from './BaseLayout.astro';
import Header from '@/components/layout/Header.astro';
import Footer from '@/components/layout/Footer.astro';
import AppFooter from '@/components/layout/AppFooter.astro';
import { useTranslations } from '@/i18n/utils';
import type { Locale } from '@/i18n/ui';
@@ -64,8 +64,8 @@ const isHomePage = Astro.url.pathname === '/' || Astro.url.pathname === `/${loca
{ label: t('nav.about'), href: t('nav.about.href') },
{ label: t('nav.contact'), href: t('nav.contact.href') },
]}
showCta={false}
showThemeSelector
showCta
cta={{ label: t('hero.login'), href: 'https://app.armarium.ch/login' }}
showLanguageSwitcher
currentLocale={locale}
showScrollProgress={isHomePage}
@@ -74,30 +74,5 @@ const isHomePage = Astro.url.pathname === '/' || Astro.url.pathname === `/${loca
<slot />
<Footer
slot="footer"
layout="columns"
columns={3}
background="secondary"
tagline={t('footer.tagline')}
copyright={t('footer.copyright')}
linkGroups={[
{
title: t('footer.app'),
links: [
{ label: t('nav.features'), href: t('nav.features.href') },
{ label: t('footer.start'), href: 'https://app.armarium.ch/login' },
{ label: t('footer.register'), href: 'https://app.armarium.ch/register' },
],
},
{
title: t('footer.legal'),
links: [
{ label: t('footer.privacy'), href: t('footer.privacy.href') },
{ label: t('footer.imprint'), href: t('footer.imprint.href') },
],
},
]}
showSocial={false}
/>
<AppFooter slot="footer" locale={locale} />
</BaseLayout>
+4 -4
View File
@@ -6,7 +6,7 @@
*/
import BaseLayout from './BaseLayout.astro';
import Header from '@/components/layout/Header.astro';
import Footer from '@/components/layout/Footer.astro';
import AppFooter from '@/components/layout/AppFooter.astro';
import { useTranslations } from '@/i18n/utils';
import type { Locale } from '@/i18n/ui';
@@ -50,8 +50,8 @@ const t = useTranslations(locale);
variant="solid"
size="lg"
showActiveState
showThemeSelector
showCta={false}
showCta
cta={{ label: t('hero.login'), href: 'https://app.armarium.ch/login' }}
showScrollProgress={showScrollProgress}
showLanguageSwitcher
currentLocale={locale}
@@ -67,5 +67,5 @@ const t = useTranslations(locale);
<slot />
</main>
<Footer slot="footer" layout="simple" />
<AppFooter slot="footer" locale={locale} />
</BaseLayout>
+4 -4
View File
@@ -3,7 +3,7 @@ import type { ImageMetadata } from 'astro';
import type { CollectionEntry } from 'astro:content';
import BaseLayout from './BaseLayout.astro';
import Header from '@/components/layout/Header.astro';
import Footer from '@/components/layout/Footer.astro';
import AppFooter from '@/components/layout/AppFooter.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';
@@ -67,8 +67,8 @@ const breadcrumbs = [
shape="bar"
variant="solid"
size="lg"
showThemeSelector
showCta={false}
showCta
cta={{ label: t('hero.login'), href: 'https://app.armarium.ch/login' }}
showScrollProgress
showLanguageSwitcher
currentLocale={locale}
@@ -161,5 +161,5 @@ const breadcrumbs = [
)}
</div>
<Footer slot="footer" layout="simple" background="secondary" />
<AppFooter slot="footer" locale={locale} />
</BaseLayout>
-40
View File
@@ -1,40 +0,0 @@
---
import ProjectLayout from '@/layouts/ProjectLayout.astro';
import { getCollection, render } from 'astro:content';
export async function getStaticPaths() {
const allProjects = await getCollection('projects', ({ data }) => {
return import.meta.env.PROD ? data.draft !== true : true;
});
return allProjects.map((project) => ({
params: { slug: project.id.replace(/\.mdx?$/, '') },
props: {
project,
related: allProjects
.filter((p) => p.id !== project.id)
.slice(0, 3),
},
}));
}
const { project, related } = Astro.props;
const { Content } = await render(project);
---
<ProjectLayout
title={project.data.title}
description={project.data.description}
year={project.data.year}
client={project.data.client}
role={project.data.role}
services={project.data.services}
url={project.data.url}
repo={project.data.repo}
tags={project.data.tags}
image={project.data.image}
imageAlt={project.data.imageAlt}
related={related}
>
<Content />
</ProjectLayout>
+18
View File
@@ -1034,3 +1034,21 @@
.dark .hero-btn-learn:hover {
background-color: rgba(255, 255, 255, 0.9) !important;
}
/* Hero register button — brand colour in both light and dark mode */
.btn-brand-hero {
background-color: var(--color-brand-500) !important;
background-image: none !important;
color: white !important;
border-color: transparent !important;
}
.btn-brand-hero:hover {
background-color: color-mix(in srgb, var(--color-brand-500) 85%, black) !important;
opacity: 1 !important;
}
.btn-brand-hero svg {
transition: transform 200ms ease;
}
.btn-brand-hero:hover svg {
transform: translateX(4px);
}
-48
View File
@@ -1,48 +0,0 @@
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"headers": [
{
"source": "/(.*)",
"headers": [
{
"key": "X-Content-Type-Options",
"value": "nosniff"
},
{
"key": "X-Frame-Options",
"value": "DENY"
},
{
"key": "Referrer-Policy",
"value": "strict-origin-when-cross-origin"
},
{
"key": "Permissions-Policy",
"value": "camera=(), microphone=(), geolocation=()"
},
{
"key": "Strict-Transport-Security",
"value": "max-age=31536000; includeSubDomains"
}
]
},
{
"source": "/fonts/(.*)",
"headers": [
{
"key": "Cache-Control",
"value": "public, max-age=31536000, immutable"
}
]
},
{
"source": "/_astro/(.*)",
"headers": [
{
"key": "Cache-Control",
"value": "public, max-age=31536000, immutable"
}
]
}
]
}
-8
View File
@@ -1,8 +0,0 @@
# Cloudflare Pages configuration
# This file is used when deploying to Cloudflare Pages with wrangler
name = "astro-rocket"
compatibility_date = "2026-01-31"
[site]
bucket = "./dist"