Initial release — Astro Rocket v1.0.0
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "Astro Rocket",
|
||||
"bio": "Astro Rocket is a free, open-source Astro 6 starter theme. Built for speed, accessibility, and developer experience — clone it and start shipping.",
|
||||
"social": {
|
||||
"github": "https://github.com",
|
||||
"linkedin": "https://linkedin.com",
|
||||
"twitter": "https://x.com"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
---
|
||||
title: "Animations in Astro Rocket — Every Effect Explained"
|
||||
description: "A complete breakdown of every animation built into Astro Rocket — page transitions, scroll-triggered counters, the reactive header, card hovers, and the full micro-animation library."
|
||||
publishedAt: 2026-03-23
|
||||
author: "Hans Martens"
|
||||
tags: ["astro-rocket", "animations", "components", "customization", "css"]
|
||||
featured: false
|
||||
locale: en
|
||||
image: "../../../assets/blog/animations-in-astro-rocket.svg"
|
||||
imageAlt: "Lightning bolt icon above the word 'animations' on a dark background"
|
||||
svgSlug: "animations-in-astro-rocket"
|
||||
---
|
||||
|
||||
Astro Rocket ships with animations on every page. Not decorative noise — purposeful motion that makes the site feel fast, polished, and alive. This post breaks down every animation in the theme: what it does, where it lives, and how to tune or disable it.
|
||||
|
||||
All animations in Astro Rocket respect the `prefers-reduced-motion` media query. Users who have enabled reduced motion in their operating system preferences will see no micro-animations. The implementation is in `src/styles/global.css` and requires no extra work on your part.
|
||||
|
||||
## Page transitions
|
||||
|
||||
The most noticeable animation is the one between pages. Astro Rocket uses Astro's built-in `<ClientRouter />` component, which leverages the browser's View Transitions API to animate from one page to the next.
|
||||
|
||||
Instead of a hard reload, the page content slides up and out while the new page slides up into view — a smooth, app-like transition that makes navigation feel immediate. This is enabled globally in `src/layouts/BaseLayout.astro`:
|
||||
|
||||
```astro
|
||||
import { ClientRouter } from 'astro:transitions';
|
||||
|
||||
<!-- inside <head> -->
|
||||
<ClientRouter />
|
||||
```
|
||||
|
||||
That single line is all it takes. Every internal link in the site benefits from it automatically. No per-page configuration, no JavaScript hydration overhead.
|
||||
|
||||
The default transition is `animate-slide-up`, applied to all pages. Astro also supports `fade` and `none` transitions per element via `transition:animate`, and you can assign persistent elements a `transition:name` so they morph in place rather than slide out and back in — useful for shared headers, logos, or images that appear on multiple pages.
|
||||
|
||||
## Scroll-triggered counter animation
|
||||
|
||||
On the homepage, the stats block — Years Experience, Projects Delivered, Worldwide Clients — doesn't just sit there as static numbers. When the block scrolls into the viewport, each number counts up from zero to its target value.
|
||||
|
||||
The animation is driven by an `IntersectionObserver` in `src/pages/index.astro`:
|
||||
|
||||
- The observer fires once per element, when it first reaches 40% visibility
|
||||
- Each counter runs for 1.2 seconds with a cubic ease-out curve (`1 - (1 - progress)³`)
|
||||
- After completing, the observer disconnects — so scrolling back up and down doesn't re-trigger it
|
||||
|
||||
To add a counter to any element, give it a `data-countup` attribute with the target number and an optional `data-suffix`:
|
||||
|
||||
```html
|
||||
<p data-countup="50" data-suffix="+">50+</p>
|
||||
```
|
||||
|
||||
The script picks up any element with `[data-countup]` on the page, so you can place counter elements anywhere.
|
||||
|
||||
## Scroll-triggered Lighthouse scores
|
||||
|
||||
The `LighthouseScores` landing component in `src/components/landing/LighthouseScores.astro` uses its own `IntersectionObserver` to animate the score bars and numbers into place when the section enters the viewport.
|
||||
|
||||
The bars expand from zero width to their final value as the section becomes visible. Like the counter animation, this fires once and cleans itself up. It gives the performance section a satisfying reveal that draws attention to the scores without requiring the user to read static numbers.
|
||||
|
||||
## Scroll-reactive header
|
||||
|
||||
The floating header changes its appearance as the user scrolls. This is driven by a scroll event listener in `src/components/layout/Header.astro`.
|
||||
|
||||
When the page is at the top (within 60px of the scroll origin), the header renders without a background — transparent, with inverted text designed to sit over the hero section. Once the user scrolls past the 60px threshold, the header receives a `data-scrolled` attribute:
|
||||
|
||||
```javascript
|
||||
if (window.scrollY > SCROLL_THRESHOLD) {
|
||||
header.setAttribute('data-scrolled', '');
|
||||
} else {
|
||||
header.removeAttribute('data-scrolled');
|
||||
}
|
||||
```
|
||||
|
||||
CSS transitions on the header element animate the background, border, and text color changes smoothly. There is no layout shift — the header stays in place and only its visual style transitions.
|
||||
|
||||
The threshold is 60px. To adjust it, change the `SCROLL_THRESHOLD` constant in `Header.astro`.
|
||||
|
||||
## Card hover effects
|
||||
|
||||
Every interactive card in the site lifts slightly when hovered. This is a Tailwind utility applied directly in the markup:
|
||||
|
||||
```html
|
||||
<div class="transition-all duration-200 hover:-translate-y-1 hover:shadow-md">
|
||||
```
|
||||
|
||||
The card moves 4px upward and gains a subtle shadow over 200ms. It creates a tactile feeling that helps users understand which cards are clickable. The transition uses `duration-200` (200ms linear) — fast enough to feel immediate, slow enough to be visible.
|
||||
|
||||
## UI micro-animations
|
||||
|
||||
The full animation library lives in `src/styles/global.css`. These classes are used throughout the component library and are available for use in your own components.
|
||||
|
||||
### Entrance animations
|
||||
|
||||
```css
|
||||
.animate-fade-in /* fades from transparent to visible — 0.5s ease-out */
|
||||
.animate-slide-up /* slides up from 12px below while fading in — 0.5s ease-out */
|
||||
.animate-slide-down /* slides down from 12px above while fading in — 0.5s ease-out */
|
||||
```
|
||||
|
||||
Use these to reveal content sections, modals, or any element that appears after interaction.
|
||||
|
||||
### Overlay and menu animations
|
||||
|
||||
```css
|
||||
.animate-sheet-up /* bottom sheet slides up from off-screen — 0.25s spring */
|
||||
.animate-sheet-down /* bottom sheet exits downward — 0.2s */
|
||||
.animate-menu-down /* mobile nav drawer opens downward — 0.25s spring */
|
||||
.animate-menu-up /* mobile nav drawer closes upward — 0.2s */
|
||||
.animate-backdrop /* backdrop fades in — 0.2s ease-out */
|
||||
.animate-backdrop-out /* backdrop fades out — 0.2s ease-out */
|
||||
```
|
||||
|
||||
These are used by the `Dialog`, mobile menu sheet, and overlay components. The spring easing (`cubic-bezier(0.32, 0.72, 0, 1)`) gives the open motion a slight overshoot that feels natural and fast.
|
||||
|
||||
### Dropdown animations
|
||||
|
||||
```css
|
||||
.animate-dropdown-in /* slides down and scales in — 0.2s spring */
|
||||
.animate-dropdown-out /* collapses upward and scales out — 0.15s */
|
||||
```
|
||||
|
||||
Used by the `Dropdown` component. The dropdown originates from its trigger point and expands outward, which keeps the motion spatially coherent.
|
||||
|
||||
### Feedback animations
|
||||
|
||||
```css
|
||||
.animate-tab-enter /* crossfades tab panel content — uses --transition-normal */
|
||||
.animate-toast-in /* slides toast in from the edge — 350ms spring */
|
||||
.animate-tooltip-in /* fades and scales tooltip into view */
|
||||
.animate-shake /* brief shake for error feedback — 400ms */
|
||||
```
|
||||
|
||||
The toast uses `--ease-spring` for a satisfying bounce on entry. The shake animation is useful for form validation — apply it to an input when the user submits an invalid value.
|
||||
|
||||
### Loading states
|
||||
|
||||
```css
|
||||
.animate-pulse /* breathing opacity pulse for skeleton loaders — 2s infinite */
|
||||
.animate-spin /* continuous rotation for loading spinners — 1s linear */
|
||||
```
|
||||
|
||||
These are used by the `Skeleton` and `Progress` components. They loop indefinitely until the element is removed from the DOM.
|
||||
|
||||
### Stagger utilities
|
||||
|
||||
```css
|
||||
.delay-0 /* 0ms */
|
||||
.delay-1 /* 50ms */
|
||||
.delay-2 /* 100ms */
|
||||
.delay-3 /* 150ms */
|
||||
.delay-4 /* 200ms */
|
||||
.delay-5 /* 250ms */
|
||||
```
|
||||
|
||||
Combine with any entrance animation to stagger multiple elements into view:
|
||||
|
||||
```html
|
||||
<div class="animate-slide-up delay-0">First item</div>
|
||||
<div class="animate-slide-up delay-1">Second item</div>
|
||||
<div class="animate-slide-up delay-2">Third item</div>
|
||||
```
|
||||
|
||||
Each item appears 50ms after the last, creating a cascading reveal that guides the eye down the list.
|
||||
|
||||
## Adding animations to your own content
|
||||
|
||||
All of these classes are available anywhere in the project — in your page content, custom components, or Tailwind HTML. To animate a section heading into view, for example:
|
||||
|
||||
```html
|
||||
<h2 class="animate-slide-up">My heading</h2>
|
||||
```
|
||||
|
||||
For scroll-triggered reveals (elements that should only animate when they enter the viewport, not immediately on page load), you can replicate the pattern from the homepage counter — create an `IntersectionObserver` that adds the animation class when the element becomes visible:
|
||||
|
||||
```javascript
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (!entry.isIntersecting) return;
|
||||
observer.unobserve(entry.target);
|
||||
entry.target.classList.add('animate-slide-up');
|
||||
});
|
||||
}, { threshold: 0.2 });
|
||||
|
||||
document.querySelectorAll('[data-reveal]').forEach((el) => observer.observe(el));
|
||||
```
|
||||
|
||||
Add `data-reveal` to any element you want to reveal on scroll, and the observer handles the rest.
|
||||
|
||||
## Disabling animations
|
||||
|
||||
To disable all micro-animations globally (while keeping page transitions), remove or comment out the animation class definitions in `src/styles/global.css`. The `@media (prefers-reduced-motion: reduce)` block at the bottom of that file already disables the most intensive ones for users with that preference set.
|
||||
|
||||
To disable page transitions, remove `<ClientRouter />` from `src/layouts/BaseLayout.astro`.
|
||||
|
||||
To disable individual component animations, remove the animation class from the component's markup.
|
||||
@@ -0,0 +1,555 @@
|
||||
---
|
||||
title: "Astro Rocket Configuration — Every Toggle, Theme, and Layout Option Explained"
|
||||
description: "A complete walkthrough of Astro Rocket's configuration options: 12 colour themes, OKLCH colours, typography, radius and shadow tokens, header styles, dark mode, and more."
|
||||
publishedAt: 2026-03-24
|
||||
author: "Hans Martens"
|
||||
tags: ["astro-rocket", "configuration", "customization", "themes"]
|
||||
featured: false
|
||||
locale: en
|
||||
image: "../../../assets/blog/astro-rocket-configuration-guide.svg"
|
||||
svgSlug: "astro-rocket-configuration-guide"
|
||||
imageAlt: "Feature overview graphic for Astro Rocket on a dark background"
|
||||
---
|
||||
|
||||
Astro Rocket ships configured with sensible defaults. This post documents every meaningful option you can change — what it does, where to find it, and what value to set.
|
||||
|
||||
|
||||
## The main config file
|
||||
|
||||
Almost everything lives in `src/config/site.config.ts`. Open it and you'll see the full `siteConfig` object. The fields you're most likely to change:
|
||||
|
||||
```ts
|
||||
name: 'Your Site Name', // Logo, titles, footer copyright
|
||||
description: 'Short description', // Default meta description
|
||||
url: 'https://yoursite.com', // Canonical URLs and sitemap
|
||||
author: 'Your Name',
|
||||
email: 'hello@yoursite.com',
|
||||
```
|
||||
|
||||
The `name` field is not cosmetic only — it feeds into structured data, Open Graph tags, and the auto-generated favicon and logo badge. Set it accurately from the start.
|
||||
|
||||
### Full site config reference
|
||||
|
||||
All fields available in `src/config/site.config.ts`:
|
||||
|
||||
| Field | Type | Required | What it does |
|
||||
|-------|------|:--------:|--------------|
|
||||
| `name` | string | ✓ | Site name — appears in logo, page titles, copyright, and structured data |
|
||||
| `description` | string | ✓ | Default meta description used on pages without their own |
|
||||
| `url` | string | ✓ | Canonical base URL — used in sitemap, OG tags, and JSON-LD |
|
||||
| `ogImage` | string | ✓ | Path to the default fallback OG image |
|
||||
| `author` | string | ✓ | Author name for blog posts and Person schema |
|
||||
| `email` | string | ✓ | Contact email — used in structured data and the contact form |
|
||||
| `phone` | string | — | Phone number for structured data (local business schema) |
|
||||
| `address` | object | — | Physical address for structured data — `street`, `city`, `state`, `zip`, `country` |
|
||||
| `authorImage` | string | — | Path to author avatar (e.g. `'/avatar.jpg'`) — used in Person schema |
|
||||
| `socialLinks` | string[] | — | Social profile URLs — icons resolved automatically |
|
||||
| `twitter.site` | string | — | Twitter site handle — populates `twitter:site` meta tag |
|
||||
| `twitter.creator` | string | — | Twitter creator handle — populates `twitter:creator` meta tag |
|
||||
| `verification.google` | string | — | Google Search Console verification code |
|
||||
| `verification.bing` | string | — | Bing Webmaster verification code |
|
||||
| `blogImageOverlay` | boolean | — | Brand-colour tint over blog cover images (default: `true`) |
|
||||
| `branding.logo.alt` | string | ✓ | Accessible alt text for the logo |
|
||||
| `branding.logo.imageUrl` | string | — | Path to a PNG logo — used in Organization schema for rich results |
|
||||
| `branding.favicon.svg` | string | ✓ | Path to favicon SVG in `public/` |
|
||||
| `branding.colors.themeColor` | hex | ✓ | Browser toolbar colour on mobile Chrome/Safari |
|
||||
| `branding.colors.backgroundColor` | hex | ✓ | PWA splash screen background colour |
|
||||
|
||||
The `phone` and `address` fields are optional but improve local business structured data — relevant if you're building a site for a business with a physical location. Leave them empty or remove them for a personal site.
|
||||
|
||||
## Boolean switches
|
||||
|
||||
### Blog image overlay
|
||||
|
||||
```ts
|
||||
// src/config/site.config.ts
|
||||
blogImageOverlay: true,
|
||||
```
|
||||
|
||||
When `true`, a translucent brand-colour tint is applied over blog cover images. This helps images blend with your theme if they have neutral or mismatched colours. Set it to `false` if your images are already on-brand or if you prefer photographs to appear unaltered.
|
||||
|
||||
### Dark mode default
|
||||
|
||||
The default mode is set directly in the HTML element in `src/layouts/BaseLayout.astro`:
|
||||
|
||||
```html
|
||||
<html lang="en" class="scroll-smooth dark" data-theme="blue">
|
||||
```
|
||||
|
||||
The `dark` class is what activates dark mode on first load. Remove it to default to light:
|
||||
|
||||
```html
|
||||
<html lang="en" class="scroll-smooth" data-theme="blue">
|
||||
```
|
||||
|
||||
The user's preference during their session is stored in `sessionStorage` — so it persists while the tab is open but resets when they open a new tab. This is intentional for a portfolio or marketing site. If you want it to persist across sessions, swap the `sessionStorage` calls for `localStorage` in the same file.
|
||||
|
||||
### Structured data: schema switches
|
||||
|
||||
The landing page (`src/pages/index.astro`) passes three boolean props to `LandingLayout` that control which JSON-LD schemas are injected into the page `<head>`:
|
||||
|
||||
```astro
|
||||
<LandingLayout
|
||||
includePersonSchema={true}
|
||||
includeOrgSchema={false}
|
||||
includeProfessionalServiceSchema={false}
|
||||
>
|
||||
```
|
||||
|
||||
| Prop | Default | What it adds |
|
||||
|------|:-------:|--------------|
|
||||
| `includePersonSchema` | `false` | `Person` schema — name, job title, email, social profiles, author image |
|
||||
| `includeOrgSchema` | `false` | `Organization` schema — name, URL, logo, contact email |
|
||||
| `includeProfessionalServiceSchema` | `false` | `ProfessionalService` schema — adds address, opening hours, area served |
|
||||
|
||||
Enable `includePersonSchema` for a personal portfolio. Enable `includeOrgSchema` if the site represents a company. Enable `includeProfessionalServiceSchema` only if you have a physical address set in `site.config.ts` — search engines will show it in local results.
|
||||
|
||||
### SEO: noindex and nofollow
|
||||
|
||||
Any page layout accepts `noindex` and `nofollow` props that are passed through to the `<meta name="robots">` tag:
|
||||
|
||||
```astro
|
||||
<PageLayout noindex={true} nofollow={false}>
|
||||
```
|
||||
|
||||
| Prop | Default | What it does |
|
||||
|------|:-------:|--------------|
|
||||
| `noindex` | `false` | Tells search engines not to index the page |
|
||||
| `nofollow` | `false` | Tells search engines not to follow outbound links |
|
||||
|
||||
Use `noindex` on pages you don't want in search results (thank-you pages, internal preview pages, staging environments). Leave both at `false` for all public content.
|
||||
|
||||
### Branding: favicon and browser colours
|
||||
|
||||
The favicon is auto-generated from the first letter of `siteConfig.name` and the active `--brand-500` colour — it updates live when the theme changes. No image file is needed. To replace it with a custom SVG, drop your file into `public/` and update the path:
|
||||
|
||||
```ts
|
||||
// src/config/site.config.ts
|
||||
branding: {
|
||||
favicon: {
|
||||
svg: '/my-logo.svg', // replaces the auto-generated monogram
|
||||
},
|
||||
colors: {
|
||||
themeColor: '#1d4ed8', // browser toolbar colour on mobile (use your brand hex)
|
||||
backgroundColor: '#ffffff', // PWA splash screen background
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
`themeColor` is the colour shown in Chrome's address bar and Safari's status bar on mobile — set it to your brand colour for a polished native-app feel.
|
||||
|
||||
For structured data (Google rich results), you can also supply a static logo image:
|
||||
|
||||
```ts
|
||||
branding: {
|
||||
logo: {
|
||||
alt: 'My Company',
|
||||
imageUrl: '/logo.png', // add a PNG to public/ and point here
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Colour themes
|
||||
|
||||
Astro Rocket ships with **12 colour themes**. All 12 are shown as coloured swatches in the header via the `ThemeSelector` component. Clicking a swatch switches the active theme instantly — no file edits, no rebuild. The logo badge, blog image gradients, and every brand colour on the page update live together.
|
||||
|
||||
### Changing the active theme
|
||||
|
||||
The default theme is set with `data-theme` on the `<html>` element in `src/layouts/BaseLayout.astro`:
|
||||
|
||||
```html
|
||||
data-theme="blue"
|
||||
```
|
||||
|
||||
Available values — all 12 themes:
|
||||
|
||||
| Value | Hue | Best for |
|
||||
|-------|-----|----------|
|
||||
| `orange` | 38° | Bold and warm — the original Velocity/International Orange |
|
||||
| `amber` | 75° | Editorial, luxury, food, warm creative brands |
|
||||
| `lime` | 130° | Dev tools, high-energy product sites (Vercel/Linear aesthetic) |
|
||||
| `emerald` | 160° | All-rounder — works for SaaS, portfolios, and organic brands |
|
||||
| `teal` | 190° | Developer tooling, modern SaaS, trustworthy services |
|
||||
| `cyan` | 200° | Fresh, vibrant tech — more energetic than teal |
|
||||
| `sky` | 222° | Clean and airy — between teal and blue, great for services |
|
||||
| `blue` | 255° | Studio aesthetic (Linear/Raycast feel), authoritative — the default |
|
||||
| `indigo` | 264° | Enterprise, productivity tools, serious B2B |
|
||||
| `violet` | 277° | Creative and premium — sweet spot between indigo and purple |
|
||||
| `purple` | 292° | Expressive, personality-forward, vivid |
|
||||
| `magenta` | 330° | Creative agencies, bold personal brands — maximum vivid |
|
||||
|
||||
### The live theme selector
|
||||
|
||||
All 12 themes appear as colour swatches in the header dropdown (desktop) and in the mobile menu when `showThemeSelector` is enabled on the `<Header>` component. The visitor's choice is saved to `localStorage` and persists across sessions.
|
||||
|
||||
The `themes` array in `src/components/layout/ThemeSelector.astro` controls which swatches are shown and in what order. The current set:
|
||||
|
||||
```ts
|
||||
const themes = [
|
||||
{ id: 'orange', name: 'Orange', color: 'oklch(62.5% 0.22 38)' },
|
||||
{ id: 'amber', name: 'Amber', color: 'oklch(68% 0.19 75)' },
|
||||
{ id: 'lime', name: 'Lime', color: 'oklch(64% 0.27 130)' },
|
||||
{ id: 'emerald', name: 'Emerald', color: 'oklch(62.5% 0.22 160)' },
|
||||
{ id: 'teal', name: 'Teal', color: 'oklch(62.5% 0.22 190)' },
|
||||
{ id: 'cyan', name: 'Cyan', color: 'oklch(65% 0.22 200)' },
|
||||
{ id: 'sky', name: 'Sky', color: 'oklch(67% 0.21 222)' },
|
||||
{ id: 'blue', name: 'Blue', color: 'oklch(62.5% 0.22 255)' },
|
||||
{ id: 'indigo', name: 'Indigo', color: 'oklch(60% 0.24 264)' },
|
||||
{ id: 'violet', name: 'Violet', color: 'oklch(62.5% 0.26 277)' },
|
||||
{ id: 'purple', name: 'Purple', color: 'oklch(62.5% 0.25 303)' },
|
||||
{ id: 'magenta', name: 'Magenta', color: 'oklch(58% 0.28 330)' },
|
||||
];
|
||||
```
|
||||
|
||||
Once you've settled on your brand colour, you can remove the selector from the header entirely. Open `src/layouts/LandingLayout.astro` and delete the `showThemeSelector` prop — the swatches disappear without breaking anything else.
|
||||
|
||||
### Understanding OKLCH — the three numbers
|
||||
|
||||
All colour values in Astro Rocket use the OKLCH colour space: `oklch(lightness% chroma hue)`.
|
||||
|
||||
| Parameter | Range | What it means |
|
||||
|-----------|-------|----------------|
|
||||
| **Lightness** | 0% (black) → 100% (white) | How light or dark the colour is |
|
||||
| **Chroma** | 0 (grey) → ~0.37 (maximum vivid) | How saturated the colour is |
|
||||
| **Hue** | 0–360° | The colour angle on the wheel |
|
||||
|
||||
Common hue landmarks: 0°/360° = red, 38° = orange, 75° = amber, 130° = lime, 155–160° = green, 190–200° = teal/cyan, 222° = sky, 255° = blue, 264° = indigo, 292° = purple, 330° = magenta.
|
||||
|
||||
To shift a theme to a completely different colour, you only need to change the hue number across the `--brand-50` to `--brand-900` scale — keep the lightness and chroma values the same and the whole palette moves together. Use [oklch.com](https://oklch.com) to pick visually.
|
||||
|
||||
### Customising a theme's brand colour
|
||||
|
||||
Every theme is a CSS file in `src/styles/themes/`. Each file defines the full set of colour tokens for both light and dark mode. Open the relevant file and adjust the `--brand-*` scale — all ten steps from 50 to 900:
|
||||
|
||||
```css
|
||||
html[data-theme="blue"] {
|
||||
--brand-50: oklch(97.5% 0.02 255);
|
||||
--brand-100: oklch(94.8% 0.04 255);
|
||||
--brand-200: oklch(87.5% 0.08 255);
|
||||
--brand-300: oklch(77.8% 0.14 255);
|
||||
--brand-400: oklch(68.5% 0.19 255);
|
||||
--brand-500: oklch(62.5% 0.22 255); /* primary accent in light mode */
|
||||
--brand-600: oklch(53.2% 0.19 255);
|
||||
--brand-700: oklch(45.5% 0.16 255);
|
||||
--brand-800: oklch(37.2% 0.13 255);
|
||||
--brand-900: oklch(26.5% 0.09 255);
|
||||
}
|
||||
```
|
||||
|
||||
To shift to a different colour, replace `255` (blue) with your target hue across all ten lines. That's the entire change required.
|
||||
|
||||
You can also edit the shared brand scale in `src/styles/tokens/primitives.css` if you want a single consistent palette that isn't theme-dependent:
|
||||
|
||||
```css
|
||||
:root {
|
||||
--brand-500: oklch(62.5% 0.22 255); /* primary brand color */
|
||||
/* full --brand-50 to --brand-900 scale */
|
||||
}
|
||||
```
|
||||
|
||||
### Light mode vs dark mode: which brand step is used
|
||||
|
||||
The theme files use **different brand steps** for light and dark mode:
|
||||
|
||||
| Context | Brand step used | Why |
|
||||
|---------|----------------|-----|
|
||||
| Light mode accent (`--accent`) | `--brand-500` | Strong enough on white backgrounds |
|
||||
| Dark mode accent (`--accent`) | `--brand-400` | One step lighter — needed on dark backgrounds for contrast |
|
||||
| Light mode input focus / ring | `--brand-500` | |
|
||||
| Dark mode input focus / ring | `--brand-400` | |
|
||||
|
||||
When you customise the brand colour, adjust **both** steps in each mode's block. If you only change `--brand-500`, dark mode accents will still use the old `--brand-400` value.
|
||||
|
||||
### Typography tokens
|
||||
|
||||
Each theme file defines the font stack and typographic rhythm for the entire site. You can override any of these per theme:
|
||||
|
||||
```css
|
||||
html[data-theme="blue"] {
|
||||
/* Font families */
|
||||
--theme-font-sans: 'Manrope Variable', ui-sans-serif, system-ui, sans-serif;
|
||||
--theme-font-display: 'Outfit Variable', var(--theme-font-sans);
|
||||
--theme-font-mono: 'JetBrains Mono Variable', ui-monospace, monospace;
|
||||
|
||||
/* Typographic rhythm */
|
||||
--theme-heading-weight: 700; /* headings font weight */
|
||||
--theme-heading-tracking: -0.02em; /* letter-spacing on headings */
|
||||
--theme-body-leading: 1.6; /* line-height for body text */
|
||||
}
|
||||
```
|
||||
|
||||
All 12 themes currently share the same font stack. To use a custom font, install it in `public/fonts/`, add a `@font-face` declaration in `src/styles/global.css`, and update `--theme-font-sans` or `--theme-font-display` in your active theme file.
|
||||
|
||||
### Border radius tokens
|
||||
|
||||
The global roundness of the UI is set per theme with five radius levels:
|
||||
|
||||
```css
|
||||
--theme-radius-sm: 0.25rem; /* 4px — badges, small buttons */
|
||||
--theme-radius-md: 0.375rem; /* 6px — inputs, cards */
|
||||
--theme-radius-lg: 0.5rem; /* 8px — panels, larger cards */
|
||||
--theme-radius-xl: 0.625rem; /* 10px — large containers */
|
||||
--theme-radius-full: 9999px; /* pill shapes, circular avatars */
|
||||
```
|
||||
|
||||
To give your site a sharper, more editorial feel, set all values to `0`. For a rounder, friendlier look, increase them (e.g. `--theme-radius-md: 0.75rem`). All components reference these tokens, so a single change cascades across the entire design.
|
||||
|
||||
### Shadow tokens
|
||||
|
||||
Each theme defines four shadow elevation levels, tinted with the theme's hue:
|
||||
|
||||
```css
|
||||
--theme-shadow-sm /* subtle depth: cards, list items */
|
||||
--theme-shadow-md /* dropdowns, popovers */
|
||||
--theme-shadow-lg /* modals, drawers */
|
||||
--theme-shadow-xl /* high-emphasis overlays */
|
||||
```
|
||||
|
||||
In dark mode the shadows use higher opacity (35–55%) and are tinted with the brand hue, so the depth reads naturally against dark backgrounds. To make shadows stronger or more neutral, adjust the opacity values or replace the hue-tinted OKLCH colour with a plain `rgba(0,0,0,...)`.
|
||||
|
||||
### Inverted sections
|
||||
|
||||
The `--surface-invert` family of tokens controls sections that have a dark background in light mode — the CTA blocks, some hero variants, and any area where you use `background="invert"` on the Footer or a section component:
|
||||
|
||||
```css
|
||||
--surface-invert: /* main dark surface background */
|
||||
--surface-invert-secondary: /* slightly lighter surface */
|
||||
--surface-invert-tertiary: /* even lighter layer */
|
||||
--on-invert: /* primary text on dark surface */
|
||||
--on-invert-secondary: /* secondary text on dark surface */
|
||||
--on-invert-muted: /* muted text on dark surface */
|
||||
--border-invert: /* border on dark surface */
|
||||
--border-invert-strong: /* stronger border on dark surface */
|
||||
```
|
||||
|
||||
In light mode these default to near-black values (around 10–18% lightness) with a subtle hue tint. In dark mode they become slightly lighter than the main background — providing contrast between sections without the jarring jump of a fully inverted surface.
|
||||
|
||||
### Creating a new theme
|
||||
|
||||
1. Duplicate any file from `src/styles/themes/` as your starting point.
|
||||
2. Implement all ~35 semantic tokens for both `:root` (light) and `.dark` (dark mode).
|
||||
3. Add your new theme file's import to `src/styles/tokens/colors.css`.
|
||||
4. Add an entry to the `themes` array in `ThemeSelector.astro` if you want it in the live picker.
|
||||
|
||||
## Header: floating capsule vs. fixed bar
|
||||
|
||||
Astro Rocket has two header shapes. The landing and marketing layouts use a floating capsule:
|
||||
|
||||
```astro
|
||||
<Header shape="floating" variant="transparent" colorScheme="invert" position="fixed" />
|
||||
```
|
||||
|
||||
The blog and standard page layouts use a full-width bar:
|
||||
|
||||
```astro
|
||||
<Header position="fixed" size="lg" />
|
||||
```
|
||||
|
||||
### Switching the blog header to a floating style
|
||||
|
||||
Open `src/layouts/BlogLayout.astro` and find the `<Header>` line. Change it to:
|
||||
|
||||
```astro
|
||||
<Header shape="floating" variant="transparent" position="fixed" />
|
||||
```
|
||||
|
||||
### Switching any page header from floating to a bar
|
||||
|
||||
Find the Header component in the relevant layout file and remove `shape="floating"`:
|
||||
|
||||
```astro
|
||||
<!-- Before -->
|
||||
<Header shape="floating" variant="transparent" position="fixed" />
|
||||
|
||||
<!-- After -->
|
||||
<Header position="fixed" size="lg" />
|
||||
```
|
||||
|
||||
### Header prop reference
|
||||
|
||||
All Header props and what they do:
|
||||
|
||||
| Prop | Options | Default | What it controls |
|
||||
|------|---------|---------|-----------------|
|
||||
| `position` | `fixed` `sticky` `static` | `fixed` | Whether the header stays at the top while scrolling |
|
||||
| `shape` | `bar` `floating` | `bar` | Full-width bar or centred floating capsule |
|
||||
| `size` | `sm` `md` `lg` | `md` | Header height |
|
||||
| `variant` | `default` `solid` `transparent` | `default` | Background fill |
|
||||
| `colorScheme` | `default` `invert` | `default` | Use inverted colours — for dark hero backgrounds |
|
||||
| `layout` | `default` `centered` `minimal` | `default` | Logo and nav arrangement |
|
||||
| `showThemeToggle` | `true` `false` | `true` | Dark/light mode toggle button |
|
||||
| `showThemeSelector` | `true` `false` | `false` | Colour theme swatch picker (desktop dropdown + mobile menu) |
|
||||
| `showSocialLinks` | `true` `false` | `false` | Social icon links (desktop only, reads from `siteConfig.socialLinks`) |
|
||||
| `showCta` | `true` `false` | `true` | CTA button in the header |
|
||||
| `showMobileMenu` | `true` `false` | `true` | Hamburger menu on small screens |
|
||||
| `showActiveState` | `true` `false` | `true` | Highlight for the current page link |
|
||||
| `hideLogo` | `true` `false` | `false` | Hide the logo entirely |
|
||||
| `showScrollProgress` | `true` `false` | `false` | Thin brand-coloured scroll progress bar on the header edge |
|
||||
| `scrollProgressPosition` | `top` `bottom` | `bottom` | Edge of the header where the progress bar sits — `top` suits the floating capsule header, `bottom` suits the solid bar header |
|
||||
|
||||
Set any prop on the `<Header>` component in the layout file for the page type you want to adjust.
|
||||
|
||||
## Footer
|
||||
|
||||
### Changing the copyright text
|
||||
|
||||
The footer copyright line reads from the `copyright` prop. Passing a custom value overrides it:
|
||||
|
||||
```astro
|
||||
<Footer copyright="© {year} Your Name. All rights reserved." />
|
||||
```
|
||||
|
||||
The `{year}` and `{siteName}` placeholders are replaced automatically at build time. Without a `copyright` prop it falls back to the site name from `site.config.ts`.
|
||||
|
||||
The Footer is used in three layout files:
|
||||
|
||||
| Layout file | Pages it covers |
|
||||
|-------------|----------------|
|
||||
| `src/layouts/PageLayout.astro` | Blog index, about, contact, any standard page |
|
||||
| `src/layouts/BlogLayout.astro` | Individual blog posts |
|
||||
| `src/layouts/LandingLayout.astro` | The landing page |
|
||||
|
||||
Edit the `<Footer>` line in whichever file covers the pages you want to change.
|
||||
|
||||
### Footer layout options
|
||||
|
||||
```astro
|
||||
<Footer layout="simple" /> <!-- Single row: logo, nav, social, copyright -->
|
||||
<Footer layout="stacked" /> <!-- Vertically stacked sections -->
|
||||
<Footer layout="columns" /> <!-- Multi-column link groups -->
|
||||
<Footer layout="minimal" /> <!-- Copyright line only -->
|
||||
```
|
||||
|
||||
### Footer prop reference
|
||||
|
||||
| Prop | Type | Default | What it controls |
|
||||
|------|------|:-------:|-----------------|
|
||||
| `layout` | `simple` `stacked` `columns` `minimal` | `simple` | Overall footer structure |
|
||||
| `background` | `default` `secondary` `invert` | `default` | Footer background colour |
|
||||
| `columns` | `2` `3` `4` | `3` | Number of link columns (only applies with `layout="columns"`) |
|
||||
| `showSocial` | boolean | `true` | Social media icons |
|
||||
| `showCopyright` | boolean | `true` | Copyright line |
|
||||
| `hideLogo` | boolean | `false` | Hide the footer logo |
|
||||
| `tagline` | string | — | Short tagline shown under the logo |
|
||||
| `copyright` | string | — | Custom copyright text — supports `{year}` and `{siteName}` placeholders |
|
||||
|
||||
### Legal links
|
||||
|
||||
The `legalLinks` prop adds a row of small links (Privacy Policy, Terms of Service, etc.) alongside the copyright line:
|
||||
|
||||
```astro
|
||||
<Footer
|
||||
legalLinks={[
|
||||
{ label: 'Privacy Policy', href: '/privacy' },
|
||||
{ label: 'Terms of Service', href: '/terms' },
|
||||
]}
|
||||
/>
|
||||
```
|
||||
|
||||
### Columns layout with link groups
|
||||
|
||||
The `columns` layout renders grouped link sections — useful for a site with many pages:
|
||||
|
||||
```astro
|
||||
<Footer
|
||||
layout="columns"
|
||||
columns={3}
|
||||
linkGroups={[
|
||||
{
|
||||
title: 'Product',
|
||||
links: [
|
||||
{ label: 'Features', href: '/features' },
|
||||
{ label: 'Pricing', href: '/pricing' },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Company',
|
||||
links: [
|
||||
{ label: 'About', href: '/about' },
|
||||
{ label: 'Contact', href: '/contact' },
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
```
|
||||
|
||||
## Navigation
|
||||
|
||||
Edit `src/config/nav.config.ts` to change the header and footer navigation in one place:
|
||||
|
||||
```ts
|
||||
export const navItems: NavItem[] = [
|
||||
{ label: 'Blog', href: '/blog', order: 1 },
|
||||
{ label: 'About', href: '/about', order: 2 },
|
||||
{ label: 'Contact', href: '/contact', order: 3 },
|
||||
];
|
||||
```
|
||||
|
||||
Add, remove, or reorder items freely. Both the header and footer read from this array.
|
||||
|
||||
## Analytics and verification
|
||||
|
||||
Set these in your `.env` file (copy from `.env.example`):
|
||||
|
||||
```bash
|
||||
# Analytics
|
||||
PUBLIC_GA_MEASUREMENT_ID=G-XXXXXXXXXX # Google Analytics 4
|
||||
PUBLIC_GTM_ID=GTM-XXXXXXX # Google Tag Manager
|
||||
|
||||
# Cookie consent
|
||||
PUBLIC_CONSENT_ENABLED=true # Show cookie consent banner
|
||||
PUBLIC_PRIVACY_POLICY_URL=/privacy # Link in the consent banner
|
||||
|
||||
# Search console verification
|
||||
GOOGLE_SITE_VERIFICATION=your-code
|
||||
BING_SITE_VERIFICATION=your-code
|
||||
```
|
||||
|
||||
None of these are required during development. The analytics components simply render nothing if the IDs are absent.
|
||||
|
||||
## Social links
|
||||
|
||||
Social links in the footer come from the `socialLinks` array in `src/config/site.config.ts`:
|
||||
|
||||
```ts
|
||||
socialLinks: [
|
||||
'https://github.com/yourname',
|
||||
'https://x.com/yourhandle',
|
||||
'https://instagram.com/yourname',
|
||||
],
|
||||
```
|
||||
|
||||
Remove any URL you don't use. The footer won't render an empty social section. Icons are resolved automatically from the URL — GitHub, X, Instagram, LinkedIn, and Bluesky are all recognised out of the box.
|
||||
|
||||
## Quick reference
|
||||
|
||||
| What | Where | Key |
|
||||
|------|-------|-----|
|
||||
| Site name, author, email | `src/config/site.config.ts` | `name`, `author`, `email` |
|
||||
| Phone, address | `src/config/site.config.ts` | `phone`, `address` |
|
||||
| Author avatar (Person schema) | `src/config/site.config.ts` | `authorImage` |
|
||||
| Browser toolbar colour | `src/config/site.config.ts` | `branding.colors.themeColor` |
|
||||
| Blog image colour overlay | `src/config/site.config.ts` | `blogImageOverlay: true/false` |
|
||||
| Social links | `src/config/site.config.ts` | `socialLinks` array |
|
||||
| Twitter card handles | `src/config/site.config.ts` | `twitter.site`, `twitter.creator` |
|
||||
| Search console verification | `.env` or `site.config.ts` | `verification.google/bing` |
|
||||
| Navigation items | `src/config/nav.config.ts` | `navItems` array |
|
||||
| Default colour theme | `src/layouts/BaseLayout.astro` | `data-theme="blue"` |
|
||||
| Default dark/light mode | `src/layouts/BaseLayout.astro` | `class="dark"` on `<html>` |
|
||||
| JSON-LD schema switches | `src/pages/index.astro` | `includePersonSchema`, `includeOrgSchema` |
|
||||
| noindex / nofollow | any layout | `noindex={true}`, `nofollow={true}` |
|
||||
| Live theme selector (12 swatches) | `src/components/layout/ThemeSelector.astro` | `themes` array |
|
||||
| All 12 theme files | `src/styles/themes/` | One CSS file per theme |
|
||||
| Brand colour scale (light & dark) | `src/styles/themes/*.css` | `--brand-50` → `--brand-900` |
|
||||
| Typography (fonts, weight, tracking) | `src/styles/themes/*.css` | `--theme-font-*`, `--theme-heading-*` |
|
||||
| Border radius (global roundness) | `src/styles/themes/*.css` | `--theme-radius-sm/md/lg/xl/full` |
|
||||
| Shadow elevation levels | `src/styles/themes/*.css` | `--theme-shadow-sm/md/lg/xl` |
|
||||
| Inverted section colours | `src/styles/themes/*.css` | `--surface-invert`, `--on-invert` |
|
||||
| Header shape, position | `src/layouts/*.astro` | `shape`, `position` props |
|
||||
| Header show/hide toggles | `src/layouts/*.astro` | `showThemeToggle`, `showCta`, `showSocialLinks`, etc. |
|
||||
| Footer copyright text | `src/layouts/*.astro` | `copyright` prop |
|
||||
| Footer layout & columns | `src/layouts/*.astro` | `layout`, `columns` props |
|
||||
| Footer legal links | `src/layouts/*.astro` | `legalLinks` array |
|
||||
| Analytics IDs | `.env` | `PUBLIC_GA_MEASUREMENT_ID` etc. |
|
||||
|
||||
If you like Astro Rocket, a [star on GitHub](https://github.com/hansmartens68/astro-rocket) helps other developers find it. Takes two seconds.
|
||||
@@ -0,0 +1,172 @@
|
||||
---
|
||||
title: "Getting Started with Astro Rocket — From Install to Live in Minutes"
|
||||
description: "How to install, configure, and deploy Astro Rocket — covering site.config.ts, brand colours, navigation, writing posts, and deploying to Vercel."
|
||||
publishedAt: 2026-03-27
|
||||
author: "Hans Martens"
|
||||
tags: ["astro-rocket", "getting-started", "configuration", "vercel"]
|
||||
featured: true
|
||||
locale: en
|
||||
image: "../../../assets/blog/astro-rocket-getting-started.svg"
|
||||
svgSlug: "astro-rocket-getting-started"
|
||||
imageAlt: "Rocket icon above the words 'get started' on a dark background"
|
||||
---
|
||||
|
||||
This guide walks you through everything needed to get Astro Rocket running locally, configured to your brand, and deployed to the web. No previous Astro experience required.
|
||||
|
||||
## What you need before you start
|
||||
|
||||
- [Node.js](https://nodejs.org) version 22.12.0 or later
|
||||
- A package manager: `npm`, `pnpm`, or `yarn`
|
||||
- A [Vercel account](https://vercel.com) (free) for deployment
|
||||
- A code editor — [VS Code](https://code.visualstudio.com) with the [Astro extension](https://marketplace.visualstudio.com/items?itemName=astro-build.astro-vscode) is recommended
|
||||
|
||||
## Installation
|
||||
|
||||
Astro Rocket is free and open source. Clone the repository from GitHub and install dependencies:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/hansmartens68/astro-rocket my-site
|
||||
cd my-site
|
||||
pnpm install
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
Open `http://localhost:4321` and you'll see the full site running locally. Every change you make updates instantly without a full page reload.
|
||||
|
||||
## Configuring your site
|
||||
|
||||
Almost everything about the site is controlled from a single file: `src/config/site.config.ts`.
|
||||
|
||||
```ts
|
||||
const siteConfig: SiteConfig = {
|
||||
name: 'Your Site Name',
|
||||
description: 'A short description for search engines.',
|
||||
url: 'https://yoursite.com',
|
||||
author: 'Your Name',
|
||||
email: 'hello@yoursite.com',
|
||||
```
|
||||
|
||||
Start here. The `name` field populates the logo badge, page titles, footer copyright, and structured data. The `description` becomes the default meta description on any page that doesn't provide its own.
|
||||
|
||||
### Brand colour
|
||||
|
||||
Set your brand colour in the `branding.colors` section:
|
||||
|
||||
```ts
|
||||
branding: {
|
||||
colors: {
|
||||
themeColor: '#F94C10', // Your primary brand colour
|
||||
backgroundColor: '#ffffff', // Used in the web app manifest
|
||||
},
|
||||
},
|
||||
```
|
||||
|
||||
The `themeColor` drives every brand-coloured element across the site — buttons, links, the logo badge, highlights, and the favicon — through a single CSS custom property. Change it once and the entire site updates.
|
||||
|
||||
### Navigation
|
||||
|
||||
Open `src/config/nav.config.ts`. This is the only file you need to touch to change the site navigation:
|
||||
|
||||
```ts
|
||||
export const navItems: NavItem[] = [
|
||||
{ label: 'Home', href: '/' },
|
||||
{ label: 'Blog', href: '/blog' },
|
||||
{ label: 'About', href: '/about' },
|
||||
{ label: 'Contact', href: '/contact' },
|
||||
];
|
||||
```
|
||||
|
||||
Add, remove, or reorder items. The header and footer both read from this config automatically.
|
||||
|
||||
## Writing blog posts
|
||||
|
||||
Create a new `.mdx` file in `src/content/blog/en/`. Use this frontmatter template:
|
||||
|
||||
```mdx
|
||||
---
|
||||
title: "Your Post Title"
|
||||
description: "A short summary under 200 characters."
|
||||
publishedAt: 2026-03-15
|
||||
author: "Your Name"
|
||||
tags: ["tag-one", "tag-two"]
|
||||
featured: false
|
||||
locale: en
|
||||
image: "../../../assets/blog/your-image.svg"
|
||||
imageAlt: "Describe the image for screen readers."
|
||||
---
|
||||
|
||||
Your post content starts here. Standard Markdown works — headings,
|
||||
lists, links, bold, italic — plus any MDX components you need.
|
||||
```
|
||||
|
||||
Save the file and the post appears in the blog index immediately. No rebuilds, no cache clearing required.
|
||||
|
||||
### Cover images
|
||||
|
||||
Blog post cover images live in `src/assets/blog/`. The recommended format is SVG at 1200×630 pixels — the standard Open Graph image size. SVGs keep file sizes minimal and scale perfectly to every screen. PNG and JPG work equally well if you prefer photographs; Astro's `<Image>` component handles optimisation automatically.
|
||||
|
||||
## Setting up the contact form
|
||||
|
||||
The contact form uses [Resend](https://resend.com) for email delivery. To activate it:
|
||||
|
||||
1. Create a free Resend account and get your API key
|
||||
2. Verify your sending domain in Resend
|
||||
3. Set your `email` in `src/config/site.config.ts` — this is where form submissions are delivered
|
||||
4. Add your API key to `.env`:
|
||||
|
||||
```
|
||||
RESEND_API_KEY=re_xxxxxxxxxxxx
|
||||
```
|
||||
|
||||
Optionally, set `RESEND_FROM_EMAIL` to a verified sender address on your domain. If omitted, the form uses the `email` value from `site.config.ts` as the sender address.
|
||||
|
||||
The form works without these variables — it simply disables submission — so you can develop and deploy the rest of the site before setting up email.
|
||||
|
||||
## Deploying to Vercel
|
||||
|
||||
1. Create a new repository on GitHub (or GitLab / Bitbucket) and push your local project to it
|
||||
2. Go to [vercel.com](https://vercel.com) and click **Add New Project**
|
||||
3. Import your repository — Vercel detects Astro automatically
|
||||
4. Add your environment variables under **Settings → Environment Variables**
|
||||
5. Click **Deploy**
|
||||
|
||||
Your site is live. Every subsequent push to `main` triggers a new deployment automatically. Pull request previews are created for every branch.
|
||||
|
||||
Set your `SITE_URL` environment variable to your production domain:
|
||||
|
||||
```
|
||||
SITE_URL=https://yoursite.com
|
||||
```
|
||||
|
||||
This ensures canonical URLs, the sitemap, and structured data all use the correct domain.
|
||||
|
||||
## Deploying to Netlify
|
||||
|
||||
Astro Rocket also supports [Netlify](https://netlify.com) out of the box. A `netlify.toml` is included with the correct build settings and security headers pre-configured.
|
||||
|
||||
1. Create a new repository on GitHub (or GitLab / Bitbucket) and push your local project to it
|
||||
2. Go to [netlify.com](https://netlify.com), click **Add new site → Import an existing project**, and connect your repository
|
||||
3. Under **Site configuration → Environment variables**, add your variables including:
|
||||
|
||||
```
|
||||
SITE_URL=https://yoursite.com
|
||||
DEPLOY_TARGET=netlify
|
||||
```
|
||||
|
||||
4. Click **Deploy site**
|
||||
|
||||
Setting `DEPLOY_TARGET=netlify` tells the build to use the Netlify adapter instead of the default Vercel one. Everything else — the contact form, API routes, image optimisation — works identically on both platforms.
|
||||
|
||||
## What to do next
|
||||
|
||||
With the site live, here's a sensible order for next steps:
|
||||
|
||||
1. **Replace the placeholder content** — update the hero text, the about section, and landing page copy to reflect your actual work
|
||||
2. **Write your first real post** — publishing regularly is the highest-value thing you can do for search visibility
|
||||
3. **Set up Google Search Console** — add `GOOGLE_SITE_VERIFICATION` to your `.env` file and submit your sitemap
|
||||
4. **Add your social links** — the `socialLinks` array in `site.config.ts` populates the footer social icons
|
||||
5. **Set up analytics** — add `PUBLIC_GA_MEASUREMENT_ID` or `PUBLIC_GTM_ID` to activate the built-in analytics integration
|
||||
|
||||
The site is yours. Everything is documented, everything is changeable, and nothing is hidden behind abstractions you can't read.
|
||||
|
||||
If Astro Rocket saved you time, a [star on GitHub](https://github.com/hansmartens68/astro-rocket) helps other developers find it. Takes two seconds.
|
||||
@@ -0,0 +1,73 @@
|
||||
---
|
||||
title: "Astro Rocket Is Live — My New Open Source Astro Theme"
|
||||
description: "Astro Rocket is a production-ready Astro 6 theme with a full blog, 57 components, 12 colour themes, dark mode, SEO, and a contact form. Free and open source."
|
||||
publishedAt: 2026-03-28
|
||||
author: "Hans Martens"
|
||||
tags: ["astro-rocket", "open-source", "astro", "launch"]
|
||||
featured: true
|
||||
locale: en
|
||||
svgSlug: "astro-rocket-is-live"
|
||||
---
|
||||
|
||||
I'm so happy to share this: Astro Rocket is live. It's a fully built website theme for Astro 6 and Tailwind CSS v4, and it's free for anyone to use.
|
||||
|
||||
Astro Rocket is a fork of [Velocity](https://github.com/southwellmedia/velocity) by [Southwell Media](https://southwellmedia.com) — a powerful Astro boilerplate with a comprehensive design system and component library. All credit to the Southwell Media team for that work. That was the foundation. I built Astro Rocket on top of it with a different goal: a complete website you can launch immediately — you change the text, and your site is ready.
|
||||
|
||||
## Who is it for?
|
||||
|
||||
Astro Rocket is made for web designers, developers, bloggers, and anyone who needs a portfolio or business website. Every page is already built and styled — you update the text and your site is live.
|
||||
|
||||
## What I added to Velocity
|
||||
|
||||
Velocity is the foundation. Here's everything I built on top of it:
|
||||
|
||||
**Live theme switching with 12 colour themes** — Velocity required editing a CSS import file and rebuilding every time you wanted to change your brand colour. In Astro Rocket, you click one of the 12 colour swatches in the header and everything updates on screen instantly: the logo badge, the blog image gradients, and every brand colour across the site. No file edits, no rebuilds. Once you've settled on a colour, you remove the selector from the header with a single line deletion.
|
||||
|
||||
All 12 themes are shown as swatches in the header: Orange, Amber, Lime, Emerald, Teal, Cyan, Sky, Blue, Indigo, Violet, Purple, and Magenta — Blue is the default. Every theme is based on the official Tailwind CSS colour palette.
|
||||
|
||||
**Auto-generated logo badge** — Velocity required a custom logo file. Astro Rocket generates the logo automatically: the first letter of your site name on the active brand colour. It updates live when you switch themes.
|
||||
|
||||
**Auto-generated favicon** — no design tools needed. The favicon is an SVG pre-rendered at build time from `site.config.ts` — the same letter, the same colour as your logo badge.
|
||||
|
||||
**Blog image gradients** — every blog cover and card uses a brand-colour gradient background that updates live when the active theme changes.
|
||||
|
||||
**Unified icon system via Iconify** — Velocity had a basic SVG Icon component. Astro Rocket has a unified `Icon` component with 350+ Lucide UI icons and 3000+ Simple Icons brand icons, all via one component.
|
||||
|
||||
**Animated typing effect** — the hero section includes an animated typing effect that cycles through words, with configurable typing and delete speed.
|
||||
|
||||
**Full animation library** — smooth page transitions via Astro View Transitions, scroll-triggered counter and score animations, a scroll-reactive header, card hover effects, and a complete set of UI micro-animations for dropdowns, toasts, modals, tab switches, and more. All with full `prefers-reduced-motion` support.
|
||||
|
||||
**Scroll progress bar** — a thin 2px brand-coloured line in the header that fills from left to right as you scroll down the page. It's enabled on the homepage (sitting on top of the floating capsule header), the blog index, and individual post pages (both underneath the solid header). Each position is independently configurable via two `Header` props: `showScrollProgress` and `scrollProgressPosition`. [Read the full post](/blog/scroll-progress-bar).
|
||||
|
||||
**Dark mode hero gradient** — the homepage hero fades from your active brand colour at the top to pure black at the bottom in dark mode. One prop on the `Hero` component (`gradient`), zero JavaScript, zero impact on light mode. The homepage header is a floating capsule that lets the brand colour bleed through behind it. On all other pages the header is a full-width solid bar. [Read the full post](/blog/dark-mode-hero-gradient).
|
||||
|
||||
**sessionStorage for dark mode** — Velocity uses `localStorage`, which stores the user's preference permanently. I deliberately switched this to `sessionStorage`, so every new visitor sees the site the way it was designed: dark. Dark mode is a design choice here, not a user setting. I wrote a [full post explaining why](/blog/dark-mode-sessionstorage).
|
||||
|
||||
## What Astro Rocket ships with
|
||||
|
||||
Everything from Velocity is still there, and it's all fully integrated:
|
||||
|
||||
**A full blog** — built on Astro Content Collections with MDX support. Every post has a typed frontmatter schema, tag filtering, a related posts section, a reading time indicator, and automatically generated Open Graph metadata.
|
||||
|
||||
**57 components** — 31 UI components (Button, Input, Card, Badge, Avatar, Table, Tabs, Dialog, Accordion, and more), 7 pattern components (ContactForm, NewsletterForm, StatCard, and more), plus Hero, Header, Footer, BlogCard, ShareButtons, and SEO components.
|
||||
|
||||
**A complete SEO layer** — meta tags, Open Graph, Twitter Cards, JSON-LD structured data (WebSite, Organization, BlogPosting, Breadcrumb, FAQ), an auto-generated sitemap, and robots.txt.
|
||||
|
||||
**Static OG image** — a single polished default Open Graph image serves as the social preview for all pages and blog posts. No build-time generation required.
|
||||
|
||||
**Dark mode without a flash** — dark-first design with sessionStorage (see above), so every new visitor sees the site as intended.
|
||||
|
||||
**Contact form and newsletter form** — both connected to Resend for email delivery, with server-side Zod validation and honeypot spam protection.
|
||||
|
||||
**One-click deployment** — configuration files for Vercel, Netlify, and Cloudflare Pages are all included.
|
||||
|
||||
**Lighthouse 95+** across Performance, Accessibility, Best Practices, and SEO.
|
||||
|
||||
## Give it a star on GitHub
|
||||
|
||||
It's open source, MIT-licensed, and ready to use.
|
||||
|
||||
- **[Astro Rocket on GitHub](https://github.com/hansmartens68/astro-rocket)** — if you find it useful, a star on GitHub is hugely appreciated. It costs you one click and helps other developers discover the theme. Thank you!
|
||||
- **[Live demo → astrorocket.dev](https://astrorocket.dev)**
|
||||
|
||||
I'm looking forward to seeing what you build with it.
|
||||
@@ -0,0 +1,286 @@
|
||||
---
|
||||
title: "57 Components Ready to Use — Astro Rocket's Full UI Library"
|
||||
description: "Astro Rocket ships with 57 production-ready components from the Velocity library — buttons, cards, dialogs, forms, data display, and full page-structure components. All accessible, all themed."
|
||||
publishedAt: 2026-03-28
|
||||
author: "Hans Martens"
|
||||
tags: ["astro-rocket", "components", "ui", "velocity"]
|
||||
featured: false
|
||||
locale: en
|
||||
image: "../../../assets/blog/component-library.svg"
|
||||
svgSlug: "component-library"
|
||||
imageAlt: "Dashboard layout icon above the word 'components' with a large faint '57' in the background"
|
||||
---
|
||||
|
||||
Astro Rocket inherits a complete UI component library from [Velocity](https://github.com/southwellmedia/velocity) by Southwell Media. That means 57 production-ready components are available the moment you install the theme — no npm packages to install, no extra setup. Every component is already styled with the design system's color tokens, so they adapt automatically when you switch themes or toggle dark mode.
|
||||
|
||||
**See every component live:** [/components](/components)
|
||||
|
||||
The library is organized in three layers: 31 UI primitives that form the building blocks, 7 higher-level pattern components, and a set of page-structure components for the Hero, Header, Footer, Blog, and SEO layers.
|
||||
|
||||
---
|
||||
|
||||
## UI primitives
|
||||
|
||||
These 31 components cover every common UI need. They accept variants and sizes through props and are built with accessibility in mind — keyboard navigation, ARIA attributes, and focus management are handled for you.
|
||||
|
||||
### Form (7 components)
|
||||
|
||||
The form layer covers every standard input type.
|
||||
|
||||
| Component | What it does |
|
||||
|-----------|--------------|
|
||||
| `Button` | Primary interactive element. Supports `variant` (brand, secondary, outline, ghost, destructive) and `size` (sm, md, lg). Works as a `<button>` or renders as an `<a>` when an `href` is provided. |
|
||||
| `Input` | Single-line text input with label, helper text, and error state support. |
|
||||
| `Textarea` | Multi-line text input with the same label/error pattern as Input. |
|
||||
| `Select` | Styled native select element with the same form field pattern. |
|
||||
| `Checkbox` | Accessible checkbox with custom styling that follows the active theme. |
|
||||
| `Radio` | Radio button with the same theming as Checkbox. |
|
||||
| `Switch` | Toggle switch for on/off states, with smooth CSS transition. |
|
||||
|
||||
```astro
|
||||
---
|
||||
import Button from '@/components/ui/form/Button/Button.astro';
|
||||
import Input from '@/components/ui/form/Input/Input.astro';
|
||||
---
|
||||
|
||||
<Button variant="brand" size="md">Get started</Button>
|
||||
<Button variant="outline" size="md">Learn more</Button>
|
||||
|
||||
<Input label="Email address" type="email" placeholder="you@example.com" />
|
||||
```
|
||||
|
||||
### Data display (8 components)
|
||||
|
||||
| Component | What it does |
|
||||
|-----------|--------------|
|
||||
| `Card` | Container with border and rounded corners. Use it for any grouped content block. |
|
||||
| `Badge` | Small inline label. Supports `variant` (brand, secondary, success, warning, destructive) and `pill` prop for rounded style. |
|
||||
| `Avatar` | Circular image or initials fallback. |
|
||||
| `AvatarGroup` | Stacked row of Avatars for team or contributor lists. |
|
||||
| `Table` | Styled HTML table with header, body, and striped row support. |
|
||||
| `Pagination` | Page navigation with prev/next and numbered page buttons. |
|
||||
| `Progress` | Horizontal progress bar that fills to a percentage value. |
|
||||
| `Skeleton` | Loading placeholder that pulses in the shape of the content it replaces. |
|
||||
|
||||
```astro
|
||||
---
|
||||
import Badge from '@/components/ui/data-display/Badge/Badge.astro';
|
||||
import Card from '@/components/ui/data-display/Card/Card.astro';
|
||||
---
|
||||
|
||||
<Badge variant="brand" pill>New</Badge>
|
||||
<Badge variant="success">Active</Badge>
|
||||
|
||||
<Card>
|
||||
<p>Any content goes here.</p>
|
||||
</Card>
|
||||
```
|
||||
|
||||
### Feedback (3 components)
|
||||
|
||||
| Component | What it does |
|
||||
|-----------|--------------|
|
||||
| `Alert` | Inline message for info, success, warning, or error states. |
|
||||
| `Toast` | Temporary notification that appears in the corner of the screen and auto-dismisses. |
|
||||
| `Tooltip` | Text label that appears on hover or focus above any element. |
|
||||
|
||||
### Overlay (6 components)
|
||||
|
||||
| Component | What it does |
|
||||
|-----------|--------------|
|
||||
| `Accordion` | Collapsible content panels — any number of items, only one open at a time. |
|
||||
| `Dialog` | Modal window with backdrop, focus trapping, and keyboard close. |
|
||||
| `Dropdown` | Floating menu that opens below a trigger button. |
|
||||
| `Tabs` | Horizontal tab bar that switches between content panels. |
|
||||
| `VerticalTabs` | Same as Tabs but with the tab list on the left. Good for settings panels. |
|
||||
| `ConsentBanner` | Cookie consent banner with accept/decline actions. |
|
||||
|
||||
```astro
|
||||
---
|
||||
import Accordion from '@/components/ui/overlay/Accordion/Accordion.astro';
|
||||
---
|
||||
|
||||
<Accordion items={[
|
||||
{ title: 'What is Astro Rocket?', content: 'A production-ready Astro 6 theme.' },
|
||||
{ title: 'Is it free?', content: 'Yes. MIT licensed.' },
|
||||
]} />
|
||||
```
|
||||
|
||||
### Layout & content (3 components)
|
||||
|
||||
| Component | What it does |
|
||||
|-----------|--------------|
|
||||
| `Icon` | Unified icon component. Covers 350+ Lucide UI icons and 3,000+ Simple Icons brand icons via Iconify. |
|
||||
| `Separator` | Horizontal or vertical divider line. |
|
||||
| `CodeBlock` | Syntax-highlighted code block with copy button and language label. |
|
||||
|
||||
The `Icon` component is used throughout the theme. You reference any Lucide icon by name, or any Simple Icons brand icon by its shorthand name (e.g. `github`, `x-twitter`, `instagram`) or with the full `simple-icons:` prefix:
|
||||
|
||||
```astro
|
||||
---
|
||||
import Icon from '@/components/ui/primitives/Icon/Icon.astro';
|
||||
---
|
||||
|
||||
<Icon name="arrow-right" size="sm" />
|
||||
<Icon name="github" size="md" />
|
||||
<Icon name="simple-icons:vercel" size="md" />
|
||||
```
|
||||
|
||||
### Marketing UI (5 components)
|
||||
|
||||
These are UI-level marketing components, distinct from the full landing-page section components below.
|
||||
|
||||
| Component | What it does |
|
||||
|-----------|--------------|
|
||||
| `CTA` | Call-to-action block with heading, description, and button. |
|
||||
| `Logo` | Auto-generated logo badge — the first letter of your site name on the active brand color. Updates live on theme switch. |
|
||||
| `SocialProof` | Avatar stack plus a short proof statement (e.g., "Join 2,000+ developers"). |
|
||||
| `NpmCopyButton` | One-click npm install command with copy icon — useful for open-source project pages. |
|
||||
| `TerminalDemo` | Animated terminal window showing a sequence of commands. |
|
||||
|
||||
---
|
||||
|
||||
## Pattern components
|
||||
|
||||
Pattern components compose the UI primitives into specific real-world use cases. They are higher-level and more opinionated.
|
||||
|
||||
| Component | What it does |
|
||||
|-----------|--------------|
|
||||
| `ContactForm` | Full contact form connected to Resend. Server-side Zod validation and honeypot spam protection included. |
|
||||
| `NewsletterForm` | Email capture form, also connected to Resend. |
|
||||
| `StatCard` | Metric display card with a large number, label, and optional trend indicator. Used on dashboard or about pages. |
|
||||
| `EmptyState` | Placeholder for empty lists or zero-result searches. Accepts icon, heading, description, and action button. |
|
||||
| `FormField` | Shared label + input + error wrapper used internally by all form components. |
|
||||
| `SearchInput` | Input with a search icon and clear button. |
|
||||
| `PasswordInput` | Input with a show/hide password toggle. |
|
||||
|
||||
---
|
||||
|
||||
## Page-structure components
|
||||
|
||||
These components are fixed parts of the site architecture. They are used once per page type and are not generally composed with other components — they define the page.
|
||||
|
||||
**Hero** — the `Hero` component supports four layouts (`centered`, `split`, `minimal`, `card`) and three sizes. All hero content uses named slots: `badge`, `title`, `description`, `actions`, and optionally `image`.
|
||||
|
||||
**Header** — fixed top navigation bar with logo, nav links, dark mode toggle, theme selector, and optional CTA button. Three rendering variants: `solid` (solid bar), `transparent` (transparent, used on blog posts), and `floating` (the capsule header on the homepage). Includes the scroll progress bar when enabled.
|
||||
|
||||
**Footer** — bottom navigation with site links, social icons, and copyright text.
|
||||
|
||||
**Breadcrumbs** — auto-generated from the current URL path. Used at the top of blog posts and inner pages.
|
||||
|
||||
**ThemeToggle / ThemeSelector** — dark/light mode toggle and the live theme switcher with 12-colour swatch picker.
|
||||
|
||||
**Blog components** — `BlogCard` (post listing card with cover image, tags, and reading time), `ArticleHero` (full-width post header with cover image and metadata), `RelatedPosts` (three-card section at the bottom of every post), `ShareButtons` (Twitter/X, LinkedIn, clipboard copy), and `BlogImageSVG` (the brand-color SVG backgrounds for post covers).
|
||||
|
||||
**Landing page sections** — `Credibility` (logo bar), `FeatureTabs` (interactive tabbed feature showcase), `LighthouseScores` (animated score dials), and `TechStack` (technology badge strip).
|
||||
|
||||
**SEO** — `SEO` (all meta tags, Open Graph, Twitter Cards), `JsonLd` (structured data for WebSite, Organization, BlogPosting, Breadcrumb, FAQ), and `Analytics` (analytics script slot).
|
||||
|
||||
---
|
||||
|
||||
## Everything responds to the active theme
|
||||
|
||||
Every component uses the design system's CSS color tokens — `--color-brand-500`, `--color-foreground`, `--color-background`, and so on. Switch from Emerald to Purple and every button, badge, progress bar, and focus ring updates instantly. No component has a hardcoded brand color.
|
||||
|
||||
The same applies to dark mode: all token values change when the `dark` class is toggled on `<html>`, and every component updates without any extra code.
|
||||
|
||||
---
|
||||
|
||||
## Adding your own components
|
||||
|
||||
When you need something that isn't in the library, follow the same patterns used throughout the codebase to create new components that fit the existing system.
|
||||
|
||||
### Where to put it
|
||||
|
||||
Place new components in the matching subdirectory under `src/components/`:
|
||||
|
||||
```
|
||||
src/components/
|
||||
├── ui/
|
||||
│ ├── form/ # Button, Input, Select, Checkbox, Radio, Switch, Textarea
|
||||
│ ├── data-display/ # Card, Badge, Avatar, Table, Pagination, Progress, Skeleton
|
||||
│ ├── feedback/ # Alert, Toast, Tooltip
|
||||
│ ├── overlay/ # Dialog, Dropdown, Tabs, VerticalTabs, Accordion
|
||||
│ ├── layout/ # Separator
|
||||
│ ├── primitives/ # Icon
|
||||
│ ├── content/ # CodeBlock
|
||||
│ └── marketing/ # Logo, CTA, NpmCopyButton, SocialProof, TerminalDemo
|
||||
├── patterns/ # Composed patterns (ContactForm, SearchInput, etc.)
|
||||
├── layout/ # Page structure (Header, Footer)
|
||||
├── blog/ # Blog-specific
|
||||
└── landing/ # Marketing pages
|
||||
```
|
||||
|
||||
### Basic component structure
|
||||
|
||||
A typical UI component defines its props in the frontmatter, maps variants to class strings, and uses `class:list` to compose the final class:
|
||||
|
||||
```astro
|
||||
---
|
||||
interface Props {
|
||||
variant?: 'default' | 'primary' | 'success';
|
||||
size?: 'sm' | 'md';
|
||||
class?: string;
|
||||
}
|
||||
|
||||
const {
|
||||
variant = 'default',
|
||||
size = 'md',
|
||||
class: className = '',
|
||||
} = Astro.props;
|
||||
|
||||
const variants = {
|
||||
default: 'bg-background-secondary text-foreground',
|
||||
primary: 'bg-primary text-primary-foreground',
|
||||
success: 'bg-success-light text-success-foreground',
|
||||
};
|
||||
|
||||
const sizes = {
|
||||
sm: 'px-2 py-0.5 text-xs',
|
||||
md: 'px-3 py-1 text-sm',
|
||||
};
|
||||
---
|
||||
<span
|
||||
class:list={[
|
||||
'inline-flex items-center rounded-full font-medium',
|
||||
variants[variant],
|
||||
sizes[size],
|
||||
className,
|
||||
]}
|
||||
>
|
||||
<slot />
|
||||
</span>
|
||||
```
|
||||
|
||||
### Three rules to follow
|
||||
|
||||
**Use design tokens, not hardcoded values.** Every built-in component uses `bg-background`, `text-foreground`, `border-border`, and the brand tokens. If you hardcode `bg-white` or `text-gray-900`, your component will break in dark mode and ignore the active colour theme.
|
||||
|
||||
**Support named slots for flexible content.** Slots let the consumer pass in icons, actions, or footer content without the component needing to know about them:
|
||||
|
||||
```astro
|
||||
<FeatureCard title="Fast">
|
||||
<Icon name="zap" slot="icon" />
|
||||
Lightning quick builds.
|
||||
<Button slot="footer">Learn more</Button>
|
||||
</FeatureCard>
|
||||
```
|
||||
|
||||
**Use React for interactivity.** Astro components are server-rendered and have no client-side state. For anything that needs `useState`, `onClick`, or real-time updates, create a `.tsx` file and use the `client:visible` directive when you include it:
|
||||
|
||||
```astro
|
||||
---
|
||||
import { Counter } from '@/components/ui';
|
||||
---
|
||||
<Counter client:visible />
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Browse the live demo
|
||||
|
||||
The fastest way to get a feel for all 57 components is the live component demo. Every component is rendered with live props you can inspect:
|
||||
|
||||
**[/components](/components)**
|
||||
|
||||
This showcase is built into Astro Rocket itself. All 57 components are rendered there — same markup, same variants, same props you will use in your own project.
|
||||
@@ -0,0 +1,123 @@
|
||||
---
|
||||
title: "Dark Mode Hero Gradient — Brand Colour at the Top, Black at the Bottom"
|
||||
description: "Astro Rocket's homepage hero fades from your active brand colour at the top to pure black at the bottom in dark mode. One prop on the Hero component, zero JavaScript, zero impact on light mode."
|
||||
publishedAt: 2026-03-28
|
||||
author: "Hans Martens"
|
||||
tags: ["astro-rocket", "dark-mode", "css", "design", "features"]
|
||||
featured: false
|
||||
locale: en
|
||||
image: "../../../assets/blog/dark-mode-hero-gradient.svg"
|
||||
svgSlug: "dark-mode-hero-gradient"
|
||||
imageAlt: "Stacked gradient bands fading from brand colour to dark above the word 'gradient' on a brand background"
|
||||
---
|
||||
|
||||
The homepage hero in Astro Rocket carries a dark mode gradient: your active brand colour at the top, fading into pure black at the bottom. Switch to light mode and nothing changes — the effect is fully scoped to `.dark`. On every other page, the hero uses its standard background with no gradient.
|
||||
|
||||
## What it looks like
|
||||
|
||||
In dark mode, the homepage hero opens with your theme's `--brand-700` colour at the very top and transitions smoothly down to pure black — `oklch(0% 0 0)`. The brand colour shows through behind the floating header, creating a seamless visual connection between the navigation and the hero content below.
|
||||
|
||||
In light mode, the hero uses its normal background unchanged. The gradient is a pure dark-mode enhancement — your light-mode design is untouched.
|
||||
|
||||
## The CSS
|
||||
|
||||
A single utility class in `src/styles/global.css` does the work:
|
||||
|
||||
```css
|
||||
/* Dark mode hero gradient: brand to black */
|
||||
.dark .hero-dark-gradient {
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
var(--brand-700) 0%,
|
||||
oklch(0% 0 0) 100%
|
||||
) !important;
|
||||
}
|
||||
```
|
||||
|
||||
The `!important` is safe here: the rule is fully scoped to `.dark`, so it only applies in dark mode. It overrides the `bg-background` Tailwind utility on the hero element without leaking into light mode.
|
||||
|
||||
A second rule keeps brand-coloured heading text at its original shade inside the gradient. In dark mode, `text-brand-500` on a `<h1>` or `<h2>` inside a gradient section stays at `--brand-500` — readable against the brand-coloured top of the gradient:
|
||||
|
||||
```css
|
||||
.dark .hero-dark-gradient :is(h1, h2) .text-brand-500 {
|
||||
color: var(--brand-500);
|
||||
-webkit-text-fill-color: var(--brand-500);
|
||||
}
|
||||
```
|
||||
|
||||
Both rules live in the "Dark mode hero gradient" section at the bottom of `global.css`. No images, no JavaScript, no build step required.
|
||||
|
||||
## Where it is applied
|
||||
|
||||
### Homepage hero
|
||||
|
||||
The `Hero` component accepts a `gradient` boolean prop. When `gradient` is set, the component adds `hero-dark-gradient` to its section element:
|
||||
|
||||
```ts
|
||||
// src/components/hero/hero.variants.ts (simplified)
|
||||
const sectionClasses = cn(
|
||||
heroSectionVariants({ size }),
|
||||
gradient && 'hero-dark-gradient',
|
||||
className
|
||||
);
|
||||
```
|
||||
|
||||
On the homepage, the Hero is called with `gradient`:
|
||||
|
||||
```astro
|
||||
<!-- src/pages/index.astro -->
|
||||
<Hero layout="centered" size="xl" gradient class="sticky top-0 z-0 overflow-clip">
|
||||
```
|
||||
|
||||
No other page passes `gradient`, so the effect stays exclusive to the homepage.
|
||||
|
||||
## Adding it to your own sections
|
||||
|
||||
Any section or Hero that should carry the gradient in dark mode needs just one change. For a `<Hero>` component, add the prop:
|
||||
|
||||
```astro
|
||||
<Hero gradient>
|
||||
```
|
||||
|
||||
For a plain section element:
|
||||
|
||||
```html
|
||||
<section class="your-existing-classes hero-dark-gradient">
|
||||
```
|
||||
|
||||
In dark mode the gradient overrides the background. In light mode the class has no visual effect whatsoever — you can add it safely without touching your light-mode design.
|
||||
|
||||
To remove the gradient from the homepage, delete the `gradient` prop from the `<Hero>` in `src/pages/index.astro`. The normal `bg-background` immediately takes over.
|
||||
|
||||
## The floating header
|
||||
|
||||
The homepage header is a floating capsule — it sits above the hero content and lets the gradient show through behind it. The `LandingLayout` uses:
|
||||
|
||||
```astro
|
||||
<Header
|
||||
shape="floating"
|
||||
variant="default"
|
||||
colorScheme="default"
|
||||
position="fixed"
|
||||
showThemeSelector
|
||||
showScrollProgress={isHomePage}
|
||||
scrollProgressPosition="top"
|
||||
/>
|
||||
```
|
||||
|
||||
The `variant="default"` gives the header a semi-transparent background and backdrop blur. At the top of the page the gradient colour bleeds through; once you scroll past 60 px, the header gains a solid fill via the `data-scrolled` attribute. On all other pages the header is a full-width solid bar — the floating style is exclusive to the landing/homepage layout.
|
||||
|
||||
## How the colours work
|
||||
|
||||
The gradient endpoints use the active theme's tokens:
|
||||
|
||||
| Stop | Value | What it does |
|
||||
|------|-------|--------------|
|
||||
| `0%` | `var(--brand-700)` | A deep, saturated shade of your active brand colour — vivid at the very top |
|
||||
| `100%` | `oklch(0% 0 0)` | Pure black — theme-agnostic, always dark at the bottom |
|
||||
|
||||
`--brand-700` sits two steps darker than the primary brand accent (`--brand-500`). When you switch themes in the header selector, the gradient top colour updates instantly — no rebuild, no page reload.
|
||||
|
||||
## Light mode behaviour
|
||||
|
||||
In light mode there is nothing to disable or special-case. The `.dark .hero-dark-gradient` rule does not match when `.dark` is absent, so the `gradient` prop on the Hero has no visual effect in light mode. You can leave it in place without affecting light-mode visitors at all.
|
||||
@@ -0,0 +1,136 @@
|
||||
---
|
||||
title: "Why Astro Rocket Uses sessionStorage for Dark Mode (Not localStorage)"
|
||||
description: "Dark mode is the default experience in Astro Rocket — and that's a deliberate design decision. Here's the reasoning, the code, and exactly how to change it."
|
||||
publishedAt: 2026-03-15
|
||||
author: "Hans Martens"
|
||||
tags: ["dark-mode", "astro-rocket", "design", "tutorial"]
|
||||
featured: false
|
||||
locale: en
|
||||
image: "../../../assets/blog/dark-mode-sessionstorage.svg"
|
||||
svgSlug: "dark-mode-sessionstorage"
|
||||
imageAlt: "Split panel showing dark mode on the left with a moon and sessionStorage badge, and light mode on the right with a sun and localStorage badge"
|
||||
---
|
||||
|
||||
Most dark mode implementations ask: "what did the user last choose?" Astro Rocket asks a different question: "what is the right default for this site?" The answer is dark — always — and `sessionStorage` is the technical expression of that decision.
|
||||
|
||||
This post explains the reasoning, shows exactly how the implementation works, and gives you the precise code to change it if your own site calls for something different.
|
||||
|
||||
|
||||
## The design decision
|
||||
|
||||
Dark mode is not a fallback for Astro Rocket. It is the primary visual experience. The typography, colour palette, and contrast ratios were all designed and tested in dark mode first. Light mode works and is fully supported, but the site is at its best in the dark.
|
||||
|
||||
`sessionStorage` encodes this intent precisely. The site always loads in dark mode. If a visitor switches to light during a session, that preference is respected for the duration of that visit. When they open a new tab or return the next day, they are back in the dark — the designed state.
|
||||
|
||||
`localStorage` would say "your last choice is always right." `sessionStorage` says "dark is the default; light is available when you need it." For this site, that distinction matters.
|
||||
|
||||
## How it works
|
||||
|
||||
The implementation has three parts.
|
||||
|
||||
### 1. The HTML default
|
||||
|
||||
`BaseLayout.astro` renders the `<html>` element with `class="dark"` baked in:
|
||||
|
||||
```html
|
||||
<html lang="en" class="scroll-smooth dark" data-theme="blue">
|
||||
```
|
||||
|
||||
This means the server always sends dark markup. There is no flash of an incorrect theme on load, regardless of what any script does next.
|
||||
|
||||
### 2. The inline script (before first paint)
|
||||
|
||||
An inline `<script is:inline>` runs immediately in `<head>`, before the browser paints anything. It checks `sessionStorage` and removes the `dark` class only if the visitor explicitly chose light in this session:
|
||||
|
||||
```js
|
||||
if (sessionStorage.getItem('theme') === 'light') {
|
||||
el.classList.remove('dark');
|
||||
} else {
|
||||
el.classList.add('dark'); // dark is the default — always
|
||||
}
|
||||
```
|
||||
|
||||
Because `dark` is the HTML default and the script only removes it, there is no flash in either direction. Dark-mode visitors see dark immediately. Light-mode visitors see light immediately, with a single class removal so fast it is invisible.
|
||||
|
||||
### 3. The toggle
|
||||
|
||||
`ThemeToggle.astro` writes to `sessionStorage` on each click:
|
||||
|
||||
```js
|
||||
toggle.addEventListener('click', () => {
|
||||
const isDark = document.documentElement.classList.contains('dark');
|
||||
|
||||
if (isDark) {
|
||||
document.documentElement.classList.remove('dark');
|
||||
sessionStorage.setItem('theme', 'light');
|
||||
} else {
|
||||
document.documentElement.classList.add('dark');
|
||||
sessionStorage.removeItem('theme'); // dark needs no storage — it is the default
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
Note that switching back to dark removes the key entirely rather than storing `'dark'`. Dark does not need to be remembered — it is what the site is.
|
||||
|
||||
## How to switch to localStorage
|
||||
|
||||
If you are building a site for a general audience and want the user's preference to persist across sessions, two small changes are all it takes.
|
||||
|
||||
**In `BaseLayout.astro`**, change the inline script's storage check:
|
||||
|
||||
```js
|
||||
// Before
|
||||
if (sessionStorage.getItem('theme') === 'light') {
|
||||
|
||||
// After
|
||||
if (localStorage.getItem('theme') === 'light') {
|
||||
```
|
||||
|
||||
**In `ThemeToggle.astro`**, update both reads and writes in the click handler:
|
||||
|
||||
```js
|
||||
// Before
|
||||
sessionStorage.setItem('theme', 'light');
|
||||
sessionStorage.removeItem('theme');
|
||||
|
||||
// After
|
||||
localStorage.setItem('theme', 'light');
|
||||
localStorage.removeItem('theme');
|
||||
```
|
||||
|
||||
That is the complete change. The toggle now remembers the user's choice permanently — across tabs, across sessions, across visits.
|
||||
|
||||
## The third option: respect the OS preference
|
||||
|
||||
If you want to defer entirely to the user's operating system setting — the most widely recommended approach for general-purpose sites — replace the `sessionStorage` check in `BaseLayout.astro` with a `prefers-color-scheme` media query:
|
||||
|
||||
```js
|
||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
const stored = localStorage.getItem('theme');
|
||||
|
||||
if (stored === 'light' || (!stored && !prefersDark)) {
|
||||
el.classList.remove('dark');
|
||||
} else {
|
||||
el.classList.add('dark');
|
||||
}
|
||||
```
|
||||
|
||||
This gives you a layered fallback: a stored preference wins if it exists; otherwise the OS setting is used. A visitor who has set their system to light mode gets light mode automatically. One who has set it to dark gets dark. The toggle still works and overrides both.
|
||||
|
||||
This is the right pattern for a site with a broad, unknown audience. It is more work to maintain, but it is the most respectful of user intent.
|
||||
|
||||
## Which approach is right for your site?
|
||||
|
||||
| | sessionStorage | localStorage | OS preference |
|
||||
|---|---|---|---|
|
||||
| Dark is always the default | ✓ | only if set first | depends on system |
|
||||
| No flash on load | ✓ | ✓ | ✓ |
|
||||
| Preference survives new tab | ✗ | ✓ | n/a |
|
||||
| Preference survives next visit | ✗ | ✓ | n/a |
|
||||
| Respects OS dark/light setting | ✗ | ✗ | ✓ |
|
||||
| Best for opinionated design sites | ✓ | — | — |
|
||||
| Best for general-audience sites | — | ✓ | ✓ |
|
||||
|
||||
Astro Rocket uses `sessionStorage` because the site has a clear visual identity and dark mode is part of it. If you are building a business site, a documentation portal, or anything serving a broad audience, `localStorage` or OS preference is the more considerate default.
|
||||
|
||||
The code is yours. Change two lines and it behaves exactly the way your users expect.
|
||||
@@ -0,0 +1,141 @@
|
||||
---
|
||||
title: "How Astro Rocket's Design System Works — Tokens, Colors, and Dark Mode"
|
||||
description: "Astro Rocket uses a three-tier token architecture with OKLCH colors. Change one value and the entire site updates. Here's how it works and how to make it yours."
|
||||
publishedAt: 2026-03-16
|
||||
author: "Hans Martens"
|
||||
tags: ["astro-rocket", "design-system", "tailwind", "customization", "tutorial"]
|
||||
featured: false
|
||||
locale: en
|
||||
image: "../../../assets/blog/design-system-color-tokens.svg"
|
||||
svgSlug: "design-system-color-tokens"
|
||||
imageAlt: "Palette icon above the words 'design system' on a dark background"
|
||||
---
|
||||
|
||||
Most themes give you a stylesheet full of hardcoded values. Change the brand colour and you are hunting through dozens of files. Astro Rocket works differently — one change propagates everywhere, instantly, in both light and dark mode.
|
||||
|
||||
The reason is a three-tier token architecture. Understanding it takes ten minutes. Using it makes every customisation faster and more consistent.
|
||||
|
||||
## The three-tier architecture
|
||||
|
||||
Tokens are organised in three layers, each building on the one below.
|
||||
|
||||
### Tier 1 — Reference tokens
|
||||
|
||||
Reference tokens are raw values: specific colors, sizes, radii. They have no semantic meaning — they just define the palette.
|
||||
|
||||
```css
|
||||
--color-lime-500: oklch(0.768 0.233 130.85);
|
||||
--color-lime-600: oklch(0.702 0.213 130.85);
|
||||
```
|
||||
|
||||
You rarely touch these directly. They are the raw material.
|
||||
|
||||
### Tier 2 — Semantic tokens
|
||||
|
||||
Semantic tokens map intent to reference values. `--color-brand-500` does not know what hex value it holds — it knows what role it plays.
|
||||
|
||||
```css
|
||||
:root {
|
||||
--color-brand-500: var(--color-lime-500);
|
||||
--color-brand-600: var(--color-lime-600);
|
||||
}
|
||||
```
|
||||
|
||||
This is the layer you edit when switching themes. Change which reference token feeds into `--color-brand-500` and every button, link, badge, and highlight on the entire site updates in one move.
|
||||
|
||||
### Tier 3 — Component tokens
|
||||
|
||||
Component tokens are scoped to specific UI elements. The button does not reach for `--color-brand-500` directly — it uses `--btn-primary-bg`, which itself points to `--color-brand-500`.
|
||||
|
||||
```css
|
||||
--btn-primary-bg: var(--color-brand-500);
|
||||
--btn-primary-bg-hover: var(--color-brand-600);
|
||||
```
|
||||
|
||||
This means you can restyle a single component without disturbing anything else. Change `--btn-primary-bg` and only buttons change. The badge, the logo highlight, the link colour — all untouched.
|
||||
|
||||
## Why OKLCH
|
||||
|
||||
Astro Rocket uses [OKLCH](https://oklch.com/) for all colour values instead of hex or HSL.
|
||||
|
||||
OKLCH describes colour in three dimensions: lightness (`L`), chroma (`C`), and hue (`H`). The key advantage is perceptual uniformity — two colours with the same `L` value look equally bright to the human eye. Hex and HSL do not guarantee this, which is why manually constructed palettes often look uneven.
|
||||
|
||||
In practice, this means the design token palette stays visually consistent across the full range of shades without manual correction. Dark mode colours remain readable. Brand accents stay vivid without looking garish.
|
||||
|
||||
```css
|
||||
/* The same hue (130.85°) across lightness levels stays visually balanced */
|
||||
--color-lime-300: oklch(0.897 0.181 130.85);
|
||||
--color-lime-500: oklch(0.768 0.233 130.85);
|
||||
--color-lime-700: oklch(0.593 0.174 130.85);
|
||||
```
|
||||
|
||||
## Dark mode through tokens
|
||||
|
||||
Dark mode in Astro Rocket is not a separate stylesheet. It is the same token system with different values applied under the `.dark` selector.
|
||||
|
||||
```css
|
||||
:root {
|
||||
--color-background: oklch(0.98 0 0);
|
||||
--color-foreground: oklch(0.15 0 0);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--color-background: oklch(0.10 0.02 265);
|
||||
--color-foreground: oklch(0.95 0 0);
|
||||
}
|
||||
```
|
||||
|
||||
When the `dark` class is toggled on `<html>`, every semantic token remaps simultaneously. No JavaScript. No style recalculation cascade. One class change, entire site repaints.
|
||||
|
||||
Component tokens inherit this automatically because they point to semantic tokens. `--btn-primary-bg` → `--color-brand-500` → correct value for the active mode. Nothing in the component layer needs to know about dark mode at all.
|
||||
|
||||
## Changing the brand colour
|
||||
|
||||
The base brand colour scale lives in `src/styles/tokens/primitives.css`. To switch from lime to, say, violet:
|
||||
|
||||
**1. Add or verify your reference tokens exist:**
|
||||
|
||||
```css
|
||||
--color-violet-500: oklch(0.606 0.258 292.72);
|
||||
--color-violet-600: oklch(0.541 0.229 292.72);
|
||||
```
|
||||
|
||||
**2. Remap the semantic brand tokens:**
|
||||
|
||||
```css
|
||||
:root {
|
||||
--color-brand-500: var(--color-violet-500);
|
||||
--color-brand-600: var(--color-violet-600);
|
||||
}
|
||||
```
|
||||
|
||||
That is the complete change. Every button, link, badge, logo badge, favicon letter, and highlighted text across the entire site now uses violet — in both light and dark mode — without touching a single component file.
|
||||
|
||||
The `themeColor` value in `site.config.ts` controls the browser's `<meta name="theme-color">` tag (the mobile browser chrome colour) and should be updated to match:
|
||||
|
||||
```ts
|
||||
themeColor: '#7c3aed', // violet-600 as hex for browser chrome
|
||||
```
|
||||
|
||||
## The Tailwind connection
|
||||
|
||||
Astro Rocket uses Tailwind CSS v4, which reads CSS custom properties directly. The `brand-500` utility class maps to `--color-brand-500` — no `tailwind.config.js` entry required.
|
||||
|
||||
```html
|
||||
<!-- This reads --color-brand-500 automatically -->
|
||||
<span class="text-brand-500">highlighted text</span>
|
||||
```
|
||||
|
||||
When you update the semantic token, the Tailwind utility updates with it. You do not need to touch the templates.
|
||||
|
||||
## What this means in practice
|
||||
|
||||
The token system is not an abstraction for its own sake. It has three concrete benefits:
|
||||
|
||||
**Single source of truth** — brand colour, spacing scale, border radius, and typography sizes are all defined once. Inconsistencies cannot creep in because there is only one value to change.
|
||||
|
||||
**Safe component customisation** — you can restyle a specific component at the component token level without risking side effects elsewhere. The blast radius of any change is exactly as large as you intend.
|
||||
|
||||
**Dark mode for free** — because all colours are semantic, every new component you build inherits dark mode automatically as long as you use token-based utilities rather than hardcoded values.
|
||||
|
||||
The next time you want to adjust the site's look — a slightly different brand hue, a tighter border radius, a different heading weight — start in `src/styles/tokens/primitives.css` or the relevant theme file in `src/styles/themes/`. The change will be smaller than you expect.
|
||||
@@ -0,0 +1,345 @@
|
||||
---
|
||||
title: "The Hero Typing Effect in Astro Rocket — How It Works and How to Tune It"
|
||||
description: "Astro Rocket's hero headline cycles through words with a typing animation. Learn how it works, how to tune every speed and pause, and how to disable it entirely."
|
||||
publishedAt: 2026-03-16
|
||||
author: "Hans Martens"
|
||||
tags: ["astro-rocket", "components", "customization", "tutorial", "javascript"]
|
||||
featured: false
|
||||
locale: en
|
||||
image: "../../../assets/blog/hero-typing-effect.svg"
|
||||
svgSlug: "hero-typing-effect"
|
||||
imageAlt: "Rocket icon above the words 'typing effect' on a dark background"
|
||||
---
|
||||
|
||||
Open the Astro Rocket About page and the headline does not sit still. It types one word, pauses, deletes it, and types the next — looping forever. This post explains exactly how that effect is built, what each value controls, and how to make it faster, slower, or gone entirely.
|
||||
|
||||
## Where the component lives
|
||||
|
||||
The entire effect is self-contained in one file:
|
||||
|
||||
```
|
||||
src/components/ui/TypingEffect.astro
|
||||
```
|
||||
|
||||
It is a standard Astro component with a scoped `<style>` block for the cursor blink and a `<script>` block for the typing logic. No third-party library, no external dependency.
|
||||
|
||||
## Where it is used
|
||||
|
||||
The component currently lives in the About page hero, inside a brand-coloured `<span>`:
|
||||
|
||||
```astro
|
||||
<h1 slot="title">
|
||||
<span class="text-foreground [-webkit-text-fill-color:currentColor]">Astro Rocket —</span><br />
|
||||
<span class="text-brand-500 [-webkit-text-fill-color:var(--color-brand-500)]">
|
||||
<TypingEffect words={["Web Designer", "Web Developer", "Astro Developer", "Blogger", "Coffee lover"]} />
|
||||
</span>
|
||||
</h1>
|
||||
```
|
||||
|
||||
The homepage hero uses static text. The typing effect was kept on the About page where it acts as a personal "who am I" cycling statement rather than a product tagline.
|
||||
|
||||
You can place `<TypingEffect>` inside any heading or inline context. The `words` prop is the only required value.
|
||||
|
||||
## How the animation works
|
||||
|
||||
The component uses a single recursive `setTimeout` loop — no `setInterval`, no `requestAnimationFrame`. Each call to `tick()` decides whether to add or remove one character, then schedules the next call after the appropriate delay.
|
||||
|
||||
```
|
||||
Start
|
||||
└─ wait 600 ms (initial settle delay)
|
||||
└─ tick()
|
||||
├─ typing: add one character, wait typeSpeed ms
|
||||
│ └─ when word is complete: wait pauseAfterType ms, then switch to deleting
|
||||
└─ deleting: remove one character, wait deleteSpeed ms
|
||||
└─ when empty: wait pauseAfterDelete ms, advance to next word, switch to typing
|
||||
```
|
||||
|
||||
The 600 ms initial delay exists so the animation does not start mid-paint on a slow connection.
|
||||
|
||||
The full script, exactly as it runs today:
|
||||
|
||||
```js
|
||||
function startTyping() {
|
||||
const root = document.getElementById(id);
|
||||
if (!root) return;
|
||||
const textEl = root.querySelector('.typing-text');
|
||||
|
||||
// Lock the element width to the widest word so the layout never shifts
|
||||
const measurer = document.createElement('span');
|
||||
measurer.setAttribute('aria-hidden', 'true');
|
||||
measurer.style.cssText = 'visibility:hidden;position:absolute;white-space:nowrap;pointer-events:none;';
|
||||
const cs = getComputedStyle(root);
|
||||
measurer.style.font = cs.font;
|
||||
measurer.style.letterSpacing = cs.letterSpacing;
|
||||
document.body.appendChild(measurer);
|
||||
|
||||
let maxWidth = 0;
|
||||
for (const word of words) {
|
||||
measurer.textContent = word + '|'; // include cursor character in measurement
|
||||
maxWidth = Math.max(maxWidth, measurer.offsetWidth);
|
||||
}
|
||||
document.body.removeChild(measurer);
|
||||
root.style.minWidth = maxWidth + 'px';
|
||||
|
||||
let wordIndex = 0;
|
||||
let charIndex = 0;
|
||||
let isDeleting = false;
|
||||
let timer;
|
||||
|
||||
function tick() {
|
||||
const current = words[wordIndex];
|
||||
|
||||
if (isDeleting) {
|
||||
charIndex--;
|
||||
textEl.textContent = current.slice(0, charIndex);
|
||||
|
||||
if (charIndex === 0) {
|
||||
isDeleting = false;
|
||||
wordIndex = (wordIndex + 1) % words.length;
|
||||
timer = setTimeout(tick, pauseAfterDelete);
|
||||
return;
|
||||
}
|
||||
timer = setTimeout(tick, deleteSpeed);
|
||||
} else {
|
||||
charIndex++;
|
||||
textEl.textContent = current.slice(0, charIndex);
|
||||
|
||||
if (charIndex === current.length) {
|
||||
isDeleting = true;
|
||||
timer = setTimeout(tick, pauseAfterType);
|
||||
return;
|
||||
}
|
||||
timer = setTimeout(tick, typeSpeed);
|
||||
}
|
||||
}
|
||||
|
||||
// Start after a short initial delay so the page paint settles
|
||||
timer = setTimeout(tick, 600);
|
||||
|
||||
// Clean up pending timer when navigating away
|
||||
document.addEventListener('astro:before-swap', () => clearTimeout(timer), { once: true });
|
||||
}
|
||||
|
||||
// Run on initial load and on every client-side navigation back to this page
|
||||
document.addEventListener('astro:page-load', startTyping);
|
||||
```
|
||||
|
||||
## Why this component forks from the obvious implementation
|
||||
|
||||
A naive typing effect takes about twenty lines: set an interval, increment a character index, write to a DOM node. That works fine in isolation. Astro Rocket runs Astro's `ClientRouter` for client-side navigation, lives in a heading (where descenders are visible), and cycles through words of different lengths — each of those facts breaks the naive version in a different way. Here is what was added and why.
|
||||
|
||||
### Fix 1 — Client-side navigation (astro:page-load)
|
||||
|
||||
Astro's `ClientRouter` swaps pages by replacing DOM nodes without a full browser reload. A plain top-level script runs once when the browser first parses the page. When the user clicks a link and then hits back, the DOM is swapped back in but the script does not re-run — the animation stays frozen.
|
||||
|
||||
The fix wraps the entire animation in a `startTyping()` function and registers it on `astro:page-load`, which Astro fires on both the initial load *and* every subsequent client-side navigation:
|
||||
|
||||
```js
|
||||
document.addEventListener('astro:page-load', startTyping);
|
||||
```
|
||||
|
||||
The companion cleanup is equally important. If a pending `setTimeout` from a previous visit is still in flight when the user navigates away, it can fire against a DOM element that no longer exists. `astro:before-swap` fires just before Astro tears down the current page, so clearing the timer there prevents stale callbacks:
|
||||
|
||||
```js
|
||||
document.addEventListener('astro:before-swap', () => clearTimeout(timer), { once: true });
|
||||
```
|
||||
|
||||
`{ once: true }` ensures the listener removes itself after the first navigation so it does not accumulate across repeated visits.
|
||||
|
||||
### Fix 2 — Layout shift (width locking)
|
||||
|
||||
When the animation cycles through words of different lengths — "Web Designer" is longer than "Blogger" — the element changes width on every word transition. Everything to the right of it (or below it on a wrapped line) shifts. This is a jarring visual jump and a real Core Web Vitals hit.
|
||||
|
||||
The fix measures every word before the animation starts, using a hidden off-screen `<span>` that inherits the same font and letter-spacing as the real element. The widest measurement (including the cursor character `|`) is applied as `minWidth`:
|
||||
|
||||
```js
|
||||
const measurer = document.createElement('span');
|
||||
measurer.style.cssText = 'visibility:hidden;position:absolute;white-space:nowrap;pointer-events:none;';
|
||||
measurer.style.font = getComputedStyle(root).font;
|
||||
measurer.style.letterSpacing = getComputedStyle(root).letterSpacing;
|
||||
document.body.appendChild(measurer);
|
||||
|
||||
let maxWidth = 0;
|
||||
for (const word of words) {
|
||||
measurer.textContent = word + '|';
|
||||
maxWidth = Math.max(maxWidth, measurer.offsetWidth);
|
||||
}
|
||||
document.body.removeChild(measurer);
|
||||
root.style.minWidth = maxWidth + 'px';
|
||||
```
|
||||
|
||||
The measurer is appended to `<body>` (not inserted inline) so it does not inherit any overflow clipping from ancestor elements. It is removed immediately after measurement.
|
||||
|
||||
### Fix 3 — Descender clipping (overflow hidden removed)
|
||||
|
||||
The `.typing-effect` span originally had `overflow: hidden` — a common guard when animating text to prevent runaway characters from bleeding outside the box. The problem is that `overflow: hidden` clips the descenders of letters like `g`, `j`, `p`, `q`, and `y`. In a heading at large font sizes this is very visible: the bottom of those letters looks cut off.
|
||||
|
||||
The fix is simply to remove `overflow: hidden`. Width is already controlled by `minWidth` from Fix 2, so there is nothing to clip. The remaining styles are:
|
||||
|
||||
```css
|
||||
.typing-effect {
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
```
|
||||
|
||||
`vertical-align: bottom` aligns the inline-block to the text baseline of the surrounding line, which keeps the heading vertically stable as words change length.
|
||||
|
||||
## The props and their defaults
|
||||
|
||||
| Prop | Default | What it controls |
|
||||
|---|---|---|
|
||||
| `words` | *(required)* | Array of strings to cycle through |
|
||||
| `typeSpeed` | `120` | Milliseconds between each character typed |
|
||||
| `deleteSpeed` | `70` | Milliseconds between each character deleted |
|
||||
| `pauseAfterType` | `1800` | Pause in ms after the word is fully typed |
|
||||
| `pauseAfterDelete` | `400` | Pause in ms after the word is fully deleted |
|
||||
|
||||
## How to adjust the speed
|
||||
|
||||
Pass any combination of props directly on the component. You only need to set the values you want to override:
|
||||
|
||||
```astro
|
||||
<TypingEffect
|
||||
words={["Web Designer", "Web Developer", "Astro Developer"]}
|
||||
typeSpeed={80}
|
||||
deleteSpeed={40}
|
||||
pauseAfterType={2500}
|
||||
pauseAfterDelete={200}
|
||||
/>
|
||||
```
|
||||
|
||||
**Faster, snappier feel** — lower `typeSpeed` and `deleteSpeed`, shorten both pauses:
|
||||
|
||||
```astro
|
||||
<TypingEffect
|
||||
words={["Designer", "Developer", "Builder"]}
|
||||
typeSpeed={60}
|
||||
deleteSpeed={30}
|
||||
pauseAfterType={1200}
|
||||
pauseAfterDelete={200}
|
||||
/>
|
||||
```
|
||||
|
||||
**Slower, more deliberate feel** — raise `typeSpeed` and extend `pauseAfterType` so readers have time to absorb each word:
|
||||
|
||||
```astro
|
||||
<TypingEffect
|
||||
words={["Designer", "Developer", "Builder"]}
|
||||
typeSpeed={160}
|
||||
deleteSpeed={80}
|
||||
pauseAfterType={3000}
|
||||
pauseAfterDelete={600}
|
||||
/>
|
||||
```
|
||||
|
||||
## How to change the words
|
||||
|
||||
Edit the `words` array wherever you use the component. You can have as many strings as you like — the component loops back to the first word when it reaches the end:
|
||||
|
||||
```astro
|
||||
<TypingEffect
|
||||
words={[
|
||||
"Web Designer",
|
||||
"Web Developer",
|
||||
"Astro Developer",
|
||||
"UI/UX Enthusiast",
|
||||
"Performance Nerd",
|
||||
]}
|
||||
/>
|
||||
```
|
||||
|
||||
Keep words at a similar length if possible. The width-locking logic sets `minWidth` to the widest word, so very short words will have visible empty space to their right while the longer words are deleted.
|
||||
|
||||
## The cursor
|
||||
|
||||
The blinking cursor is a `<span>` rendered immediately after the text span:
|
||||
|
||||
```html
|
||||
<span class="typing-text"></span><span class="typing-cursor" aria-hidden="true">|</span>
|
||||
```
|
||||
|
||||
It is styled with a 0.75 s `step-end` blink animation and coloured with the active theme's brand colour:
|
||||
|
||||
```css
|
||||
.typing-cursor {
|
||||
display: inline-block;
|
||||
margin-left: 1px;
|
||||
animation: blink 0.75s step-end infinite;
|
||||
color: var(--color-brand-500, currentColor);
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0; }
|
||||
}
|
||||
```
|
||||
|
||||
To change the cursor character, open `TypingEffect.astro` and replace the `|` inside the cursor span. Common alternatives are `▌` (block cursor) or `_` (underscore).
|
||||
|
||||
To change the blink speed, adjust the `0.75s` value. Faster blinking (`0.5s`) reads as more urgent; slower (`1.2s`) is more relaxed.
|
||||
|
||||
## Accessibility
|
||||
|
||||
The component wraps everything in a `<span>` with an `aria-label` set to all words joined by a comma:
|
||||
|
||||
```html
|
||||
<span id="typing-abc123" class="typing-effect" aria-label="Web Designer, Web Developer, Astro Developer, Blogger, Coffee lover">
|
||||
```
|
||||
|
||||
Screen readers announce the full list of words from the `aria-label` and ignore the animated content inside (the cursor has `aria-hidden="true"`). The text is therefore both readable and not disruptive to assistive technology.
|
||||
|
||||
## SEO impact
|
||||
|
||||
Because Astro renders the heading on the server, the full element — including the `<span aria-label="…">` with all words — is present in the HTML source before any JavaScript runs. Google's crawler reads the static HTML and indexes all words from the `aria-label`. The visual animation is a progressive enhancement on top of that static foundation.
|
||||
|
||||
## How to disable the typing effect
|
||||
|
||||
**Option 1 — Replace with static text**
|
||||
|
||||
Remove the `<TypingEffect>` component and its import, then put your static heading copy directly in the slot:
|
||||
|
||||
```astro
|
||||
---
|
||||
// Remove this line:
|
||||
// import TypingEffect from '@/components/ui/TypingEffect.astro';
|
||||
---
|
||||
|
||||
<h1 slot="title">
|
||||
<span class="text-foreground [-webkit-text-fill-color:currentColor]">Astro Rocket —</span><br />
|
||||
Web Developer
|
||||
</h1>
|
||||
```
|
||||
|
||||
**Option 2 — Single static word without the cursor**
|
||||
|
||||
If you want the styled text container but no animation and no cursor, just drop the text inside a plain `<span>`:
|
||||
|
||||
```astro
|
||||
<h1 slot="title">
|
||||
<span class="text-foreground [-webkit-text-fill-color:currentColor]">Astro Rocket —</span><br />
|
||||
<span>Web Developer</span>
|
||||
</h1>
|
||||
```
|
||||
|
||||
**Option 3 — Keep one word with the cursor but stop cycling**
|
||||
|
||||
Pass an array with a single string. The component will type it once, pause, delete it, and retype it — giving you a "hello, I am typing" feel without ever changing the word. If you also want it to stop after the first type, that requires editing the component logic directly.
|
||||
|
||||
## Cheat sheet
|
||||
|
||||
| Goal | What to change |
|
||||
|---|---|
|
||||
| Faster typing | Lower `typeSpeed` (e.g. `120` → `60`) |
|
||||
| Slower typing | Raise `typeSpeed` (e.g. `120` → `180`) |
|
||||
| Faster deleting | Lower `deleteSpeed` (e.g. `70` → `35`) |
|
||||
| Longer pause after typing | Raise `pauseAfterType` (e.g. `1800` → `3000`) |
|
||||
| Shorter pause between words | Lower `pauseAfterDelete` (e.g. `400` → `150`) |
|
||||
| Different words | Edit the `words` array |
|
||||
| Different cursor character | Replace `\|` in `TypingEffect.astro` |
|
||||
| Different cursor colour | Override `--color-brand-500` in your theme |
|
||||
| Remove the effect entirely | Replace `<TypingEffect>` with a plain `<span>` |
|
||||
|
||||
Five props, one file, zero dependencies — the typing effect is deliberately simple so it stays easy to own. The three forks above are the minimum needed to make it work correctly in a real Astro project with client-side navigation, variable-length words, and a heading with descenders.
|
||||
@@ -0,0 +1,88 @@
|
||||
---
|
||||
title: "Scroll Progress Bar — Reading Progress at a Glance"
|
||||
description: "Astro Rocket now has a scroll progress bar: a thin brand-coloured line that fills as you scroll. Here's how it works, where it lives, and how to enable it on any page."
|
||||
publishedAt: 2026-03-25
|
||||
author: "Hans Martens"
|
||||
tags: ["astro-rocket", "features", "header", "ux"]
|
||||
featured: false
|
||||
locale: en
|
||||
image: "../../../assets/blog/scroll-progress-bar.svg"
|
||||
svgSlug: "scroll-progress-bar"
|
||||
imageAlt: "A brand-coloured horizontal progress bar, partially filled, on a brand background"
|
||||
---
|
||||
|
||||
Astro Rocket now has a scroll progress bar. It's the thin 2px line at the top or bottom of the header that fills from left to right as you scroll down the page — a quiet signal to the reader of how far through the content they are.
|
||||
|
||||
It's off by default. When you enable it, it tracks the page scroll position in real time using `requestAnimationFrame`, so it stays smooth even on long pages.
|
||||
|
||||
## Where it shows up
|
||||
|
||||
The bar is enabled on three page types, each with a different position:
|
||||
|
||||
**Homepage** — the bar sits on top of the floating capsule header. On the homepage the header is transparent and floating, so placing the bar above it keeps it visible and out of the way of the header content. As the page scrolls and the header gains its solid background, the bar sits cleanly on the very top edge.
|
||||
|
||||
**Blog index** — the bar sits underneath the solid bar header, exactly at the bottom border of the header. This is the standard position for a reading progress indicator on a content listing page.
|
||||
|
||||
**Individual blog posts** — same as the blog index: the bar runs along the bottom of the header. On a long-form post this is where it matters most — a reader can glance at the header and know at a glance how far through the article they are.
|
||||
|
||||
## How it works
|
||||
|
||||
The bar is an absolutely positioned `div` inside the `<header>` element. A small script updates its `width` on every scroll event, capped with `requestAnimationFrame` to avoid layout thrashing:
|
||||
|
||||
```ts
|
||||
function update() {
|
||||
const scrollTop = window.scrollY;
|
||||
const docHeight = document.documentElement.scrollHeight - window.innerHeight;
|
||||
const pct = docHeight > 0 ? (scrollTop / docHeight) * 100 : 0;
|
||||
bar.style.width = `${pct}%`;
|
||||
}
|
||||
|
||||
window.addEventListener('scroll', () => {
|
||||
if (!ticking) {
|
||||
ticking = true;
|
||||
requestAnimationFrame(update);
|
||||
}
|
||||
}, { passive: true });
|
||||
```
|
||||
|
||||
The bar colour is `var(--color-brand-500)` — it automatically matches your active colour theme and updates instantly when the visitor switches themes.
|
||||
|
||||
## The two props
|
||||
|
||||
The `Header` component exposes two props that control the scroll progress bar:
|
||||
|
||||
| Prop | Type | Default | What it does |
|
||||
|------|------|:-------:|--------------|
|
||||
| `showScrollProgress` | `boolean` | `false` | Renders the progress bar |
|
||||
| `scrollProgressPosition` | `'top'` \| `'bottom'` | `'bottom'` | Places the bar on the top or bottom edge of the header |
|
||||
|
||||
## Enabling it on any page
|
||||
|
||||
To add the scroll progress bar to a page, open the layout file that the page uses and add `showScrollProgress` to the `<Header>` component. For the standard page layout (`PageLayout.astro`) the layout accepts a `showScrollProgress` prop that passes through automatically:
|
||||
|
||||
```astro
|
||||
<!-- src/pages/your-page.astro -->
|
||||
<PageLayout title="Your Page" showScrollProgress>
|
||||
...
|
||||
</PageLayout>
|
||||
```
|
||||
|
||||
To control which edge of the header the bar sits on, pass `scrollProgressPosition` directly to the `<Header>` component in the layout file:
|
||||
|
||||
```astro
|
||||
<!-- Bottom of header (default) -->
|
||||
<Header showScrollProgress />
|
||||
|
||||
<!-- Top of header -->
|
||||
<Header showScrollProgress scrollProgressPosition="top" />
|
||||
```
|
||||
|
||||
The homepage uses `scrollProgressPosition="top"` because the floating capsule header looks better with the bar above it. All other pages use the default `'bottom'` position.
|
||||
|
||||
## Turning it off
|
||||
|
||||
Each page layout controls the bar independently. To disable it on a specific page, remove `showScrollProgress` from the layout call or set it to `false`. The blog index, for example, can have the bar removed by opening `src/pages/blog/index.astro` and deleting the `showScrollProgress` prop from `<PageLayout>`.
|
||||
|
||||
## Performance note
|
||||
|
||||
The scroll listener uses `{ passive: true }` and `requestAnimationFrame` throttling, so it adds no measurable overhead to scrolling performance. The bar has `transition-none` applied so there is no CSS transition lag — the fill tracks the scroll position directly with no animation delay.
|
||||
@@ -0,0 +1,168 @@
|
||||
---
|
||||
title: "SEO in Astro Rocket: What's Built In and How to Configure It"
|
||||
description: "Astro Rocket handles structured data, Open Graph, canonical URLs, sitemaps, and more out of the box. Here's exactly what ships and where to configure it."
|
||||
publishedAt: 2026-03-17
|
||||
author: "Hans Martens"
|
||||
tags: ["astro-rocket", "seo", "structured-data", "tutorial", "configuration"]
|
||||
featured: false
|
||||
locale: en
|
||||
image: "../../../assets/blog/seo-in-astro-rocket.svg"
|
||||
svgSlug: "seo-in-astro-rocket"
|
||||
imageAlt: "Search icon above the word 'seo' on a dark navy background"
|
||||
---
|
||||
|
||||
SEO is one of those areas where "we've handled it" can mean anything from three meta tags to a complete implementation. Astro Rocket ships a complete one. Here is exactly what is included and where each piece lives.
|
||||
|
||||
## What ships out of the box
|
||||
|
||||
The SEO layer covers six distinct concerns:
|
||||
|
||||
1. **Structured data (JSON-LD)** — schema.org markup for the home page and every blog post
|
||||
2. **Open Graph and Twitter Cards** — social sharing metadata for every page
|
||||
3. **Canonical URLs** — preventing duplicate content penalties
|
||||
4. **Sitemap** — auto-generated and kept in sync with your pages
|
||||
5. **Robots meta** — per-page `noindex` / `nofollow` control
|
||||
6. **Verification tags** — Google Search Console and Bing Webmaster
|
||||
|
||||
None of these require per-page setup. They are wired into the base layout and run automatically.
|
||||
|
||||
## Structured data
|
||||
|
||||
Structured data is the part most themes skip. Astro Rocket outputs [JSON-LD](https://json-ld.org/) schema.org markup on every page.
|
||||
|
||||
**Home page** outputs three schemas:
|
||||
|
||||
- `WebSite` — site name, URL, and search action
|
||||
- `Organization` — your organisation name, logo, and contact details
|
||||
- `Person` — the site author
|
||||
|
||||
**Blog posts** output `BlogPosting` with full metadata:
|
||||
|
||||
```json
|
||||
{
|
||||
"@type": "BlogPosting",
|
||||
"headline": "Post title",
|
||||
"description": "Post description",
|
||||
"datePublished": "2026-03-17",
|
||||
"dateModified": "2026-03-17",
|
||||
"author": { "@type": "Person", "name": "Hans Martens" },
|
||||
"image": "https://yoursite.com/og-image.jpg"
|
||||
}
|
||||
```
|
||||
|
||||
Structured data does not guarantee rich results in Google Search, but it is a prerequisite for them. Posts with `BlogPosting` markup are eligible for article rich results and knowledge panel entries. Pages without it are not.
|
||||
|
||||
All values are pulled from `site.config.ts` and the post frontmatter — nothing is hardcoded.
|
||||
|
||||
## Open Graph and Twitter Cards
|
||||
|
||||
Every page generates a complete set of social metadata. For regular pages:
|
||||
|
||||
```html
|
||||
<meta property="og:title" content="Page Title" />
|
||||
<meta property="og:description" content="Page description" />
|
||||
<meta property="og:image" content="https://yoursite.com/og-default.jpg" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
```
|
||||
|
||||
Blog posts use their cover image automatically:
|
||||
|
||||
```html
|
||||
<meta property="og:image" content="https://yoursite.com/blog/post-cover.jpg" />
|
||||
<meta property="og:type" content="article" />
|
||||
```
|
||||
|
||||
The default OG image for non-blog pages is configured in `site.config.ts`:
|
||||
|
||||
```ts
|
||||
ogImage: '/og-default.svg',
|
||||
```
|
||||
|
||||
Drop your 1200×630 image into `/public/` and update the path. Every page that does not have its own image will use it.
|
||||
|
||||
## Canonical URLs
|
||||
|
||||
Every page outputs a canonical URL tag pointing to the primary URL of that page:
|
||||
|
||||
```html
|
||||
<link rel="canonical" href="https://yoursite.com/blog/my-post" />
|
||||
```
|
||||
|
||||
This runs automatically — no frontmatter field required. The canonical URL is always constructed from the production domain set in `site.config.ts`, so it stays correct regardless of staging environments or preview deployments.
|
||||
|
||||
## Sitemap
|
||||
|
||||
The sitemap is generated by `@astrojs/sitemap` and includes every page that Astro renders at build time. Blog posts, landing pages, the contact page — all included automatically.
|
||||
|
||||
The sitemap URL is `https://yoursite.com/sitemap-index.xml`. Submit it to [Google Search Console](https://search.google.com/search-console) once after deployment and Google will pick up new posts as they are published.
|
||||
|
||||
To exclude a page from the sitemap, mark it with `noindex` — the integration respects the same pages you would not want indexed.
|
||||
|
||||
## Robots meta
|
||||
|
||||
Pages can be excluded from search engine indexing via the `SEO` component's props:
|
||||
|
||||
```astro
|
||||
<SEO noindex nofollow />
|
||||
```
|
||||
|
||||
This renders:
|
||||
|
||||
```html
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
```
|
||||
|
||||
Use this for admin pages, thank-you pages after form submissions, staging pages, or any content you want crawlers to skip. The base layout applies `index, follow` by default to all other pages.
|
||||
|
||||
## Verification tags
|
||||
|
||||
Search console verification codes go in your `.env` file — no template editing:
|
||||
|
||||
```bash
|
||||
GOOGLE_SITE_VERIFICATION=your-code-here
|
||||
BING_SITE_VERIFICATION=your-code-here
|
||||
```
|
||||
|
||||
These are read by `site.config.ts` via `astro:env/server` and rendered in `<head>` on every page through the `verification.google` and `verification.bing` fields. Leave them empty and nothing is rendered.
|
||||
|
||||
## Configuring the site identity
|
||||
|
||||
All structured data pulls from `siteConfig` in `src/config/site.config.ts`. The fields that feed directly into SEO output:
|
||||
|
||||
```ts
|
||||
const siteConfig = {
|
||||
name: 'Your Site Name',
|
||||
url: 'https://yoursite.com',
|
||||
description: 'Your site description for meta tags',
|
||||
author: 'Your Name',
|
||||
email: 'hello@yoursite.com',
|
||||
authorImage: '/avatar.jpg', // used in Person schema
|
||||
branding: {
|
||||
logo: {
|
||||
alt: 'Your Site Name',
|
||||
imageUrl: '/logo.png', // used in Organization schema
|
||||
},
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
Fill these in accurately. The `url` field in particular must be the production URL — it feeds into canonical tags, OG image URLs, and structured data. An incorrect URL there breaks your canonical implementation and can cause indexing issues.
|
||||
|
||||
## Blog post SEO
|
||||
|
||||
Each blog post has two frontmatter fields that feed directly into SEO:
|
||||
|
||||
- **`title`** — becomes the page `<title>`, `og:title`, and `BlogPosting.headline`. Keep it under 60 characters for clean display in search results.
|
||||
- **`description`** — becomes the meta description and `og:description`. Keep it under 155 characters. Write it as a plain-language summary of the post — search engines use it as the snippet in results pages.
|
||||
|
||||
These two fields are the highest-leverage SEO work you will do for each post. Everything else is automatic.
|
||||
|
||||
## Checking your SEO output
|
||||
|
||||
After deploying, verify with two tools:
|
||||
|
||||
- [Google Rich Results Test](https://search.google.com/test/rich-results) — paste your URL and confirm structured data is parsed correctly
|
||||
- [Open Graph Debugger](https://developers.facebook.com/tools/debug/) — verify social sharing metadata and force-refresh Facebook's cache for updated images
|
||||
|
||||
Both are free and give you the ground truth on what search engines and social platforms actually see when they crawl your pages.
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"question": "What is Astro Rocket?",
|
||||
"answer": "Astro Rocket is a free, open-source website theme built on Astro 6 and Tailwind CSS v4. It includes a full component library, a blog with MDX support, built-in dark mode, SEO, and everything else you need to ship a fast, modern website without starting from scratch.",
|
||||
"category": "general",
|
||||
"order": 1,
|
||||
"locale": "en"
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
---
|
||||
title: "Astro Rocket"
|
||||
description: "A production-ready Astro 6 starter for designers and developers — 12 beautiful themes, 57+ components, built-in i18n, dark mode and a fast, modern foundation to build anything on."
|
||||
url: "https://astrorocket.dev"
|
||||
repo: "https://github.com/hansmartens68/Astro-Rocket"
|
||||
tags: ["Astro", "TypeScript", "Tailwind CSS", "Open Source"]
|
||||
featured: true
|
||||
order: 1
|
||||
year: 2026
|
||||
role: "Designer & Developer"
|
||||
services: ["Design System", "Component Library", "Open Source"]
|
||||
---
|
||||
|
||||
## The Brief
|
||||
|
||||
I wanted a starter theme that was genuinely ready to launch — not a boilerplate that still requires hours of setup. The goal was simple: clone it, change the text, go live. Everything else should already be done.
|
||||
|
||||
Most Astro starters are developer-focused scaffolding. Astro Rocket is different — it ships as a complete, styled website aimed at designers, freelancers, and anyone who needs a portfolio or marketing site without starting from scratch.
|
||||
|
||||
## What I Built
|
||||
|
||||
Astro Rocket is built on top of [Velocity](https://github.com/southwellmedia/velocity) by Southwell Media, which provided a strong foundation — a well-structured Astro boilerplate with a solid design system. I extended it substantially:
|
||||
|
||||
- **12 colour themes** switchable live in the header with no rebuild required
|
||||
- **57+ components** across UI, patterns, layout, blog, landing, and SEO categories
|
||||
- **Auto-generated logo and favicon** from the site name — no design tools needed
|
||||
- **Typing effect** in the hero section
|
||||
- **Dark mode** with `sessionStorage` persistence (resets on new tab by design)
|
||||
- **Scroll progress bar**, parallax grid, and full animation suite
|
||||
- **Content collections** for blog, projects, authors, FAQs, and tech stack
|
||||
- **RSS feed**, sitemap, and full JSON-LD structured data
|
||||
|
||||
## Design Decisions
|
||||
|
||||
**Theme switching without rebuilds.** Velocity required editing a CSS import file and rebuilding to change themes. Astro Rocket uses CSS custom properties scoped to `data-theme` attributes, so switching is instant — no server round-trip, no rebuild.
|
||||
|
||||
**`sessionStorage` over `localStorage` for dark mode.** A portfolio site should always show its best face. With `localStorage`, a returning visitor might see the wrong mode if they changed it on a whim. `sessionStorage` resets on each new tab, so every first impression is intentional.
|
||||
|
||||
**Auto-generated branding.** Logo badges and favicons are generated at runtime from the site name and active brand colour. Cloners don't need Figma or Illustrator to get a polished result.
|
||||
|
||||
## Results
|
||||
|
||||
- Lighthouse scores of 95+ across all categories
|
||||
- Zero JavaScript shipped by default (Astro islands architecture)
|
||||
- Submitted to the official Astro themes directory
|
||||
- Open source under the MIT licence
|
||||
@@ -0,0 +1,33 @@
|
||||
---
|
||||
title: "Blog Starter"
|
||||
description: "A minimal, opinionated blog starter built on Astro — fast by default, with MDX support, RSS, dark mode, and a clean reading experience."
|
||||
tags: ["Astro", "MDX", "Tailwind CSS", "Open Source"]
|
||||
featured: false
|
||||
order: 5
|
||||
year: 2025
|
||||
role: "Designer & Developer"
|
||||
services: ["Design", "Development", "Open Source"]
|
||||
---
|
||||
|
||||
## The Brief
|
||||
|
||||
Most blog starters come with too much — heavy frameworks, complex setups, and styling opinions baked so deep they're impossible to change. I wanted something that got out of the way. A starting point that was genuinely minimal, genuinely fast, and opinionated only where it counts: the reading experience.
|
||||
|
||||
## What I Built
|
||||
|
||||
A lean Astro-based blog starter with everything needed to write and nothing extra.
|
||||
|
||||
- MDX support with component embedding
|
||||
- RSS feed included by default
|
||||
- Dark mode with no flash on load
|
||||
- Responsive, well-spaced typography tuned for reading
|
||||
- Sitemap and SEO meta tags out of the box
|
||||
- Zero client-side JavaScript on static pages
|
||||
|
||||
## Typography & Reading Experience
|
||||
|
||||
The reading experience was the primary constraint. Every decision — from line length to spacing to code block contrast — was made to keep the reader focused on the content. Body text is set at a comfortable measure, headings are restrained, and the layout collapses cleanly on small screens without losing hierarchy.
|
||||
|
||||
## Results
|
||||
|
||||
Used as a starting point for several client projects and available as a free open-source template. Feedback from developers has been consistent: it's easy to understand, easy to extend, and doesn't fight back.
|
||||
@@ -0,0 +1,33 @@
|
||||
---
|
||||
title: "Component Library"
|
||||
description: "An open-source set of accessible, themeable UI components — built with Astro and Tailwind CSS, documented with live examples."
|
||||
tags: ["Astro", "TypeScript", "Tailwind CSS", "Open Source"]
|
||||
featured: false
|
||||
order: 4
|
||||
year: 2025
|
||||
role: "Designer & Developer"
|
||||
services: ["Design System", "Component Library", "Documentation", "Open Source"]
|
||||
---
|
||||
|
||||
## The Brief
|
||||
|
||||
Replace this with your actual motivation for building this. Why did you start it? What problem were you solving — for yourself or for others?
|
||||
|
||||
*Example: Every project I built was repeating the same components from scratch. I needed a library that was opinionated enough to be useful but flexible enough to adapt to any brand.*
|
||||
|
||||
## What I Built
|
||||
|
||||
Describe the scope of the library. How many components? What categories? What design decisions underpinned the whole system?
|
||||
|
||||
- Component categories and count
|
||||
- Theming system
|
||||
- Accessibility approach
|
||||
- Documentation approach
|
||||
|
||||
## Design System Approach
|
||||
|
||||
Walk through how the design tokens, variants, and documentation were structured. This is where designers and developers reading your portfolio will pay attention.
|
||||
|
||||
## Results
|
||||
|
||||
Downloads, GitHub stars, community adoption, or how it's saved time across your own projects.
|
||||
@@ -0,0 +1,34 @@
|
||||
---
|
||||
title: "Documentation Site"
|
||||
description: "Developer docs for an open-source CLI tool — versioned content, full-text search, and a clean reading experience across all screen sizes."
|
||||
tags: ["Astro", "MDX", "Tailwind CSS", "Open Source"]
|
||||
featured: false
|
||||
order: 7
|
||||
year: 2025
|
||||
role: "Designer & Developer"
|
||||
services: ["Design", "Development", "Technical Writing Support"]
|
||||
---
|
||||
|
||||
## The Brief
|
||||
|
||||
Replace this with the actual context. What was being documented? Who were the readers? What did the old docs look like?
|
||||
|
||||
*Example: An open-source CLI tool had docs scattered across a README and a GitHub wiki. Contributors were confused, new users dropped off during setup, and there was no search. It needed a real documentation site.*
|
||||
|
||||
## What I Built
|
||||
|
||||
Describe the structure and features of the documentation site.
|
||||
|
||||
- Versioned content structure
|
||||
- Full-text search
|
||||
- Code blocks with syntax highlighting and copy button
|
||||
- Navigation that scales across a large content tree
|
||||
- Mobile-friendly reading experience
|
||||
|
||||
## Content Architecture
|
||||
|
||||
Good documentation is as much about structure as it is about writing. Walk through how you organised the content — guides vs. reference vs. tutorials, versioning strategy, URL structure.
|
||||
|
||||
## Results
|
||||
|
||||
Time-to-first-success for new users, contribution rate, search traffic, or maintainer feedback. Replace with your real outcome.
|
||||
@@ -0,0 +1,34 @@
|
||||
---
|
||||
title: "E-Commerce Store"
|
||||
description: "Custom storefront for an independent fashion brand — product pages, cart, and checkout built for performance and a smooth mobile experience."
|
||||
tags: ["Astro", "TypeScript", "Tailwind CSS", "Client Work"]
|
||||
featured: false
|
||||
order: 6
|
||||
year: 2025
|
||||
client: "Your Client Name"
|
||||
role: "Designer & Developer"
|
||||
services: ["Web Design", "Web Development", "E-Commerce"]
|
||||
---
|
||||
|
||||
## The Brief
|
||||
|
||||
Replace this with the actual client brief. What was the brand? What did their old shop look like? What were the conversion problems they were trying to solve?
|
||||
|
||||
*Example: An independent fashion label was losing sales to a slow, poorly structured Shopify theme. They wanted a custom storefront that reflected their aesthetic and loaded instantly on mobile.*
|
||||
|
||||
## What I Built
|
||||
|
||||
Describe the store's structure and what made it technically interesting.
|
||||
|
||||
- Product listing and detail pages
|
||||
- Cart and checkout flow
|
||||
- Mobile-first design
|
||||
- Performance optimisation strategy
|
||||
|
||||
## Mobile-First Approach
|
||||
|
||||
E-commerce lives on mobile. Describe the specific decisions you made to serve mobile shoppers — touch targets, image loading, checkout simplification.
|
||||
|
||||
## Results
|
||||
|
||||
Conversion rate change, page speed improvements, revenue impact, or client feedback. Replace with your real outcome.
|
||||
@@ -0,0 +1,35 @@
|
||||
---
|
||||
title: "SaaS Landing Page"
|
||||
description: "Marketing site for a B2B analytics platform — clear messaging, fast load times, and a conversion-focused layout."
|
||||
tags: ["Astro", "TypeScript", "Tailwind CSS", "Client Work"]
|
||||
featured: false
|
||||
order: 3
|
||||
year: 2025
|
||||
client: "Your Client Name"
|
||||
role: "Designer & Developer"
|
||||
services: ["Web Design", "Web Development", "Copywriting Support"]
|
||||
---
|
||||
|
||||
## The Brief
|
||||
|
||||
Replace this with your actual project brief. What was the client trying to achieve? What did the old site lack? What did a successful launch look like?
|
||||
|
||||
*Example: A B2B analytics startup had a product that was ahead of competitors but a site that didn't reflect it. They needed a marketing site that could explain a complex product simply, rank on search, and convert trial sign-ups.*
|
||||
|
||||
## What I Built
|
||||
|
||||
Describe the pages, sections, and components you built. What made this project interesting from a design or development perspective?
|
||||
|
||||
- Hero section with clear value proposition
|
||||
- Feature sections with interactive demos
|
||||
- Pricing table
|
||||
- Trust signals and social proof
|
||||
- Optimised for conversion
|
||||
|
||||
## Key Decisions
|
||||
|
||||
Walk through the decisions that shaped the project. Copywriting approach, layout choices, performance trade-offs, animation decisions.
|
||||
|
||||
## Results
|
||||
|
||||
Share what happened after launch — trial sign-ups, bounce rate, page speed scores, or client feedback. Replace with your actual numbers.
|
||||
@@ -0,0 +1,33 @@
|
||||
---
|
||||
title: "Studio Portfolio"
|
||||
description: "Complete redesign of a creative studio's portfolio — fast, focused, and built to convert visitors into clients."
|
||||
tags: ["Astro", "Tailwind CSS", "Design", "Client Work"]
|
||||
featured: false
|
||||
order: 2
|
||||
year: 2025
|
||||
client: "Your Client Name"
|
||||
role: "Designer & Developer"
|
||||
services: ["Web Design", "Web Development", "Performance Optimisation"]
|
||||
---
|
||||
|
||||
## The Brief
|
||||
|
||||
Replace this with your actual project brief. Describe what the client came to you with — their goals, their existing site's problems, and what success would look like.
|
||||
|
||||
*Example: The studio had been running on a WordPress theme for six years. It was slow, hard to maintain, and no longer reflected the quality of their work. They needed a site that felt as considered as the projects they put on it.*
|
||||
|
||||
## What I Built
|
||||
|
||||
Describe what you built. Walk through the key decisions — layout, structure, technology choices, and anything that makes this project worth showing.
|
||||
|
||||
- What pages were built?
|
||||
- What was technically interesting about it?
|
||||
- What design decisions did you make and why?
|
||||
|
||||
## The Process
|
||||
|
||||
Walk through how you worked. Discovery, wireframes, design, build, feedback, launch. This is where you show how you think.
|
||||
|
||||
## Results
|
||||
|
||||
Share the outcome. Metrics, client feedback, what changed after launch.
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
name: "Astro"
|
||||
description: "Static-first rendering with island architecture — load JavaScript only where you actually need it."
|
||||
version: "v6"
|
||||
url: "https://astro.build"
|
||||
icon: "brand-astro"
|
||||
colorOklch: "62.5% 0.22 38"
|
||||
order: 1
|
||||
---
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
name: "React"
|
||||
description: "Optional island interactivity — drop React components in anywhere you need client-side behaviour, without affecting the rest of the page."
|
||||
version: "v19"
|
||||
url: "https://react.dev"
|
||||
icon: "brand-react"
|
||||
colorOklch: "87% 0.13 205"
|
||||
order: 4
|
||||
---
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
name: "Tailwind CSS"
|
||||
description: "Utility-first CSS with v4's native cascade layers, CSS-first configuration, and perceptually uniform OKLCH colours."
|
||||
version: "v4"
|
||||
url: "https://tailwindcss.com"
|
||||
icon: "brand-tailwind"
|
||||
colorOklch: "72% 0.14 210"
|
||||
order: 2
|
||||
---
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
name: "TypeScript"
|
||||
description: "Strict mode throughout — every component, utility, and config is fully typed, so you catch errors at compile time instead of in production."
|
||||
version: "5.8+"
|
||||
url: "https://www.typescriptlang.org"
|
||||
icon: "brand-typescript"
|
||||
colorOklch: "52% 0.15 260"
|
||||
order: 3
|
||||
---
|
||||
Reference in New Issue
Block a user