Personal blog about mountain adventures, ski touring, hiking, and alpine photography by Matteo Leoni. Built with Astro and deployed on Netlify. All content and UI copy is in Italian.
- Framework: Astro 7 -
output: "static"with the Netlify adapter (imageCDN: false, Sharp processes images at build time); four routes opt out of prerendering for SSR (/meteo-concenedo,/cerca,/api/meteo-concenedo,/api/webcam) - Content: MDX files with frontmatter and co-located media
- Styling: Custom CSS with
light-dark()CSS variables for theming, JetBrains Mono everywhere, fully flat cards (no backgrounds/borders/radius/shadows) - Hosting: Netlify
- Image Format: AVIF for optimized file sizes
- Maps: Leaflet with GPX track visualization
- Charts: Chart.js for elevation profiles and weather graphs
- UI: No component/UI libraries β native
<dialog>, the Popover API, andcommand/commandforinvokers instead (see "UI philosophy" below)
/
βββ public/
β βββ gpx/ # GPX track files for posts
β βββ images/ # Static images (icons, markers)
β βββ fonts/ # Web fonts
βββ src/
β βββ components/ # Reusable Astro components
β βββ content/
β β βββ posts/ # Blog posts (MDX + images)
β β βββ portfolio/ # Portfolio photos
β βββ content.config.ts # Content collection schemas (glob loaders)
β βββ constants.js # Site-wide config: title, categories, map tiles, social links
β βββ layouts/ # Page layouts
β βββ pages/ # Routes (file-based routing)
β βββ lib/ # Utilities and helpers (weatherlink, forecast, dom, etc.)
β βββ assets/ # Weather icons and other bundled assets
βββ netlify.toml # Netlify configuration
βββ package.json
- Node.js 22+
- npm
# Install dependencies
npm install
# Start development server
npm run devThe site will be available at http://localhost:4321
The SSR-only meteo page needs two env vars locally: SIGNALKUPPE_WEBSITE_WEATHERLINK_APIKEY and SIGNALKUPPE_WEBSITE_WEATHERLINK_SECRET (WeatherLink API credentials).
Posts are stored in src/content/posts/ as MDX files with co-located images. There is no post-generation script β folders and frontmatter are created by hand:
src/content/posts/
βββ 2025-12-13-testa-dei-fra/
βββ 2025-12-13-testa-dei-fra.mdx
βββ cover.avif
βββ gallery-0.avif
βββ gallery-1.avif
βββ ...
The folder name doesn't drive the URL β the page route comes from the frontmatter slug (e.g. scialpinismo/2025/12/13/testa-dei-fra).
---
title: "Post Title"
description: "Brief description for SEO and previews"
date: "2025-12-13T00:00"
slug: "category/2025/12/13/post-slug"
category:
- "Scialpinismo" # or Alpinismo, Trekking, etc. β see src/constants.js
tags:
- "tag1"
- "tag2"
cover:
src: "./cover.avif"
alt: "Image description"
gallery:
- src: "./gallery-0.avif"
alt: "Photo description"
- src: "./gallery-1.avif"
alt: "Photo description"
gpxTracks:
- src: "track-name.gpx"
fileName: "track-name"
location:
lat: 45.7689
lon: 7.6535
elevationGain: 1200
distance: 12.5
minimumAltitude: 1500
maximumAltitude: 2700
---- Format: AVIF (optimized for web)
- Location: Co-located with posts in content collections
- Optimization: Sharp at build time (generates all size variants during
astro build) - Alt Text: Required for accessibility
Why AVIF is kept in Git:
- AVIF files are the canonical, optimized assets used for page rendering
- Keeping them versioned makes local builds deterministic and reproducible
- Content and media stay synchronized in the same commit history
- Repository size and clone time stay lower than storing large original JPGs
- Location:
public/gpx/ - Format: Standard GPX files
- Usage: Referenced in post frontmatter via filename
- Features: Interactive Leaflet map + Chart.js elevation profile (rendered by
PostGpx.astro)
- Semantic HTML with proper heading hierarchy
- ARIA attributes for interactive components
- Keyboard navigation (arrow keys in search, image galleries)
- Skip to main content link
- Alt text on all images
- Focus management for dialogs
- SSR route, real-time search by title and tags
- Keyboard navigation (β/β arrows, Enter, Escape)
- Visual highlighting of selected results
- Native
<dialog>-based lightbox (GalleryModal.astro) β no lightbox library - Touch/swipe support
- Keyboard navigation
- Lazy loading
- Interactive Leaflet maps (posts map on category listings, per-post GPX map)
- GPX track overlay with start/end markers
- Elevation profile chart with smooth curves
- Statistics: elevation gain, distance, min/max altitude
- SSR page pulling live data from a personal WeatherLink station, edge-cached via
Netlify-CDN-Cache-Control - 3-day forecast from Open-Meteo
- Cached webcam proxy (
/api/webcam) shown in a native<dialog> - Chart.js graphs on a category axis, lazy-loaded via
IntersectionObserver
- Dark/light mode with system preference detection via
light-dark()CSS - Persistent theme override (localStorage), read pre-paint in
Layout.astro - No JS color constants β every color is a
:roottoken inGlobalStyles.astro
This site deliberately avoids UI component/interaction libraries in favor of native platform features:
- Native
<dialog>, the Popover API (with CSS anchor positioning + a centered fallback),command/commandforinvokers,<details> - Vanilla inline
<script>in Astro components;src/lib/dom.jsholds shared client helpers - Dynamic
import()for the remaining heavy deps (chart.js, leaflet) so they never block first paint
Libraries like photoswipe and @floating-ui/dom were removed in favor of the above and should not be reintroduced.
| Command | Description |
|---|---|
npm run dev |
Start dev server at localhost:4321 |
npm run build |
Build production site to ./dist/ |
npm run preview |
Preview production build locally |
npm run astro |
Run Astro CLI commands |
npm run api |
Serve Netlify functions locally |
npm run deploy:test |
Build and deploy to test--signalkuppe.netlify.app |
npm run deploy:prod |
Build and deploy to production |
There is no lint script and no test suite. Type errors surface via npm run dev (or your editor's TS server) β a full npm run build is slow enough that it's not a good feedback loop for quick checks.
Deployment is done with Netlify CLI from the local machine.
# Test deploy (stable alias URL: test--signalkuppe.netlify.app)
npm run deploy:test
# Production deploy
npm run deploy:prodBoth commands run astro build and then have the Netlify CLI deploy the generated dist/ output.
[build]
command = "npm run build"
publish = "dist"
[build.environment]
NODE_OPTIONS = "--max-old-space-size=4096"
NODE_VERSION = "22"
AWS_LAMBDA_JS_RUNTIME = "nodejs22.x"
# Allow the Concenedo webcam source through Netlify Image CDN
[images]
remote_images = ["https://www\\.caiseregno\\.it/webcam_concenedo/.*"]It also carries the redesign's SEO redirects (old /blog/* URLs β their root equivalents, /portfolio/1 β /portfolio) and long-lived cache headers for /_astro/*, /gpx/*, and /images/*. See the file for the full list β keep the redirects intact if you touch this file.
- Output:
static, withprefetch: { prefetchAll: true }(every internal link prefetches on hover/tap) - Adapter:
@astrojs/netlify(imageCDN: falseβ uses Sharp at build time instead of Netlify's on-demand image CDN) - Integrations:
@astrojs/mdx,@astrojs/sitemap(SSR pages are excluded automatically;/meteo-concenedois added back as a custom page) - Fonts: JetBrains Mono via
fontProviders.fontsource(), variable weight range400 800
Located in src/content.config.ts (glob loaders):
- posts β ~320 folders in
src/content/posts/YYYY-MM-DD-slug/, full blog posts with images, GPX tracks, metadata - portfolio β numbered folders, one photo each
- Size: ~7GB (mostly images)
- Build Time: First build: 10β20+ minutes locally (Sharp generates all image variants for ~320 posts); subsequent builds: fast thanks to Astro's image cache
- Upload Time: First deploy: varies by connection; subsequent: <2 minutes
If you see type errors on Image components:
# Clear Astro cache
rm -rf .astro node_modules/.astro
# Restart dev server
npm run dev- Check GPX file exists in
public/gpx/ - Verify filename matches frontmatter
gpxTracks.src - Check browser console for network errors
The WeatherLink historic-window timestamps in src/lib/weatherlink.js must be computed per request, not cached at module scope β computing them once froze the data window on warm serverless instances. If this regresses, check that code path first.
Increase Node memory if needed:
export NODE_OPTIONS="--max-old-space-size=8192"
npm run buildastro- Static site generator@astrojs/netlify- Netlify adapter@astrojs/mdx- MDX support@astrojs/sitemap- Sitemap generationleaflet/leaflet-gpx- Interactive maps and GPX track renderingchart.js- Elevation and weather chartsdayjs,lodash,simplify-js- General utilities (GPX simplification, dates)netlify-cli- Manual deployment (dev dependency)
- Image Format: AVIF (50-80% smaller than JPEG)
- Image Optimization: Sharp at build time (all size variants pre-generated as static files)
- Lazy Loading: Images below fold
- Code Splitting: Automatic via Astro
- CSS: Scoped component styles, no UI framework/runtime overhead
Personal project - All content and code Β© Matteo Leoni
This is a personal blog, but if you find bugs or have suggestions, feel free to open an issue.
Built with β€οΈ and β°οΈ by Matteo