A modern, accessible, and SEO-optimized content management solution built with Astro, TypeScript, and Keystatic CMS
- Overview
- Key Features
- Tech Stack
- Quick Start
- Project Structure
- Internationalization (i18n)
- Content Management
- Accessibility
- Performance
- Deployment
- Contributing
- License
Besaide is a production-ready, multilingual content management website showcasing modern web development best practices with Astro. Originally built for a Basque mountaineering club, this project demonstrates a complete implementation of i18n, headless CMS integration, and accessible component architecture.
Perfect for developers looking to build SEO-friendly multilingual websites with modern tooling and minimal JavaScript overhead.
- ✅ Reference implementation of custom i18n routing in Astro
- ✅ Real-world example of Keystatic CMS integration
- ✅ Accessible-first component library with ARIA support
- ✅ Type-safe multilingual content management
- ✅ Zero-JS navigation with progressive enhancement
- ✅ Production-tested patterns and architecture
- Custom multilingual URL routing with language-specific slugs (
/berriakvs/noticias) - Type-safe translation system with TypeScript
- SEO-optimized with proper
hreflangtags and language meta tags - No query parameters - clean URLs for better UX and SEO
- Bidirectional language switching with automatic URL mapping
- Keystatic CMS for visual content editing
- Git-based workflow - all content stored as Markdown/MDX
- No database required - fully static generation
- Live preview in development mode
- Multilingual content editing with validation
- WCAG 2.1 Level AA compliant components
- Keyboard navigation throughout
- Focus management for modals and dropdowns
- Screen reader optimized with proper ARIA attributes
- Semantic HTML structure
- Minimal JavaScript - most interactions work without JS
- TypeScript for type safety
- Component-driven architecture
- Hot module replacement in development
- Path aliases (
@/components,@/config) - Consistent code formatting with Prettier
- Type checking with Astro Check
| Category | Technologies |
|---|---|
| Framework | Astro - Static Site Generator |
| Language | TypeScript |
| CMS | Keystatic - Git-based Headless CMS |
| Content | Markdoc, Markdown, YAML |
| Styling | CSS with design tokens, CSS nesting |
| UI Components | Astro components, React (Keystatic only) |
| Icons | Lucide Icons |
| CI/CD | GitHub Actions for automated builds and deployments |
| Package Manager | pnpm |
- Node.js 18.0 or higher
- pnpm 9.0 or higher (recommended)
# Clone the repository
git clone https://github.com/yourusername/besaide.git
cd besaide
# Install dependencies
pnpm install
# Start development server
pnpm devThe site will be available at http://localhost:1234
Access the Keystatic admin interface at http://localhost:1234/keystatic to manage events and content visually.
# Type check and build
pnpm build
# Preview production build
pnpm previewbesaide/
├── src/
│ ├── assets/ # Images, icons, static assets
│ │ ├── icons/ # SVG icons
│ │ └── images/ # Optimized images
│ ├── components/ # Reusable Astro components
│ │ ├── buttons/ # Button components
│ │ ├── events/ # Event-specific components
│ │ ├── head/ # SEO and meta components
│ │ └── theme-switcher/ # Dark mode toggle
│ ├── components-pages/ # Page-specific components
│ ├── config/ # Configuration files
│ │ ├── company.ts # Business info
│ │ ├── nav.ts # Navigation structure
│ │ └── settings.ts # Site settings
│ ├── data/ # Content collections
│ │ ├── events/ # Event markdown files
│ │ ├── news/ # News articles
│ │ └── library-maps/ # Static content
│ ├── i18n/ # Internationalization
│ │ ├── ui.ts # Translation strings
│ │ └── utils.ts # i18n utilities
│ ├── layouts/ # Page layouts
│ │ ├── Base.astro # Base HTML template
│ │ └── Page.astro # Page wrapper
│ ├── pages/ # File-based routing
│ │ ├── index.astro # Homepage (Basque)
│ │ ├── es.astro # Homepage (Spanish)
│ │ ├── agenda/ # Event listings
│ │ └── berriak/ # News (Basque)
│ ├── schemas/ # TypeScript schemas
│ ├── styles/ # Global styles
│ │ ├── global.css # Base styles
│ │ ├── theme.css # Design tokens
│ │ └── typography.css # Type system
│ ├── types/ # TypeScript types
│ └── utils/ # Utility functions
├── public/ # Static files
├── astro.config.mjs # Astro configuration
├── keystatic.config.tsx # Keystatic CMS config
├── tsconfig.json # TypeScript config
└── package.json # Dependencies
- Component composition - Small, reusable components over large monoliths
- Type safety - TypeScript throughout for better DX and fewer bugs
- Design tokens - CSS variables for consistent theming
- Progressive enhancement - Core functionality works without JavaScript
This project implements a custom multilingual URL system supporting Basque (eu) and Spanish (es). The implementation includes:
The site uses language-specific URLs rather than query parameters or subdomains:
- Basque (default):
/→/ibilbideak→/berriak - Spanish:
/es→/rutas→/noticias
Each page has a unique slug per language that maps to a shared page ID internally. This approach provides:
- SEO-friendly URLs in each language
- Clear language context for users and search engines
- No confusion between language variants
The i18n system is built on three core utilities in src/i18n/utils.ts:
getLangFromUrl(url)— Detects the current language from the URL pathgetUrlFromID(slug, lang)— Generates the correct URL for a page ID in a specific languageswitchLanguage(url)— Creates the alternate language URL for the current page
A central langMapping object maintains the relationship between URL slugs, languages, and page IDs:
const langMapping = {
'agenda': { lang: 'eu', id: 'agenda' },
'agenda-es': { lang: 'es', id: 'agenda' },
'albisteak': { lang: 'eu', id: 'news' },
'noticias': { lang: 'es', id: 'news' },
// ... more mappings
};---
import { getLangFromUrl, getUrlFromID, useTranslations } from '@/i18n/utils';
const url = Astro.url;
const lang = getLangFromUrl(url);
const t = useTranslations(lang);
---
<!-- Get translated text -->
<h1>{t('Welcome')}</h1>
<!-- Generate language-specific URLs -->
<a href={getUrlFromID('news', lang)}>{t('News')}</a>- Create the page files with language-specific slugs (e.g.,
my-page.astroandmy-page-es.astro) - Add entries to the
langMappinginsrc/i18n/utils.ts:'my-page': { lang: 'eu', id: 'my-page' }, 'my-page-es': { lang: 'es', id: 'my-page' },
- Add translations to
src/i18n/ui.tsif needed
The language switcher and navigation will automatically work with the new pages.
Besaide uses Keystatic, a Git-based headless CMS that stores content as Markdown files. This approach combines the benefits of a visual editor with version control.
The site includes Keystatic for easy content management, especially for events. To use the content editor:
-
Start the development server:
pnpm dev
-
Access the admin interface: Navigate to
http://localhost:4321/keystaticin your browser -
Manage events:
- Create, edit, and delete events through the web interface
- All fields are properly labeled in both Basque and Spanish
- Events are automatically saved as
.mdocfiles insrc/data/events/
Content can also be authored directly as Markdown files:
- News articles:
src/data/news/(with language variants like.mdand-es.md) - Events:
src/data/events/(use.mdocformat for Keystatic compatibility)
When adding content manually:
- Follow the existing frontmatter fields in similar files
- For multilingual content, create separate files for each language
- Use consistent filename conventions
Example news frontmatter:
---
title: 'Sample News Item'
date: '2025-09-22'
language: 'eu'
summary: 'Short summary shown in listings.'
---
Markdown content goes here.- File-based: All content remains as Markdown files in Git
- Multilingual support: Built-in language selection for Basque/Spanish
- Rich editing: User-friendly interface with validation
- Local storage: No external dependencies or databases required
This project prioritizes accessibility as a core feature, not an afterthought:
- ✅ Keyboard navigation - All interactive elements accessible via keyboard
- ✅ Focus management - Visual focus indicators and logical tab order
- ✅ Screen reader support - Proper ARIA labels, roles, and live regions
- ✅ Semantic HTML - Correct use of headings, landmarks, and lists
- ✅ Color contrast - Meets WCAG AA standards (4.5:1 minimum)
- ✅ Responsive typography - Readable at any zoom level
All interactive components follow accessibility best practices:
- Navigation dropdown - Arrow key navigation, Escape to close, proper ARIA
- Mobile drawer menu - Focus trap, keyboard controls, aria-modal
- Language switcher - Clear labels, expanded/collapsed states
- Forms - Associated lhttps://www.linkedin.com/in/jon-ramos-8ba55a14a/abels, error messaging, keyboard submission
- Modals/Dialogs - Focus restoration, backdrop clicks, ESC handling
Besaide is optimized for speed and Core Web Vitals:
- Zero JavaScript on most pages (only Keystatic admin requires JS)
- Optimized images - WebP/AVIF with lazy loading
- Critical CSS inlined for faster FCP
- Prefetching - Automatic link prefetching for instant navigation
| Feature | Implementation |
|---|---|
| Image optimization | Sharp + Astro Image |
| Font loading | Font subset + font-display: swap |
| Code splitting | Automatic per-route JS bundles |
| CSS optimization | Minified + unused CSS removal |
| Caching | Aggressive caching headers |
| CDN | Static assets on Vercel Edge Network |
Typical Lighthouse scores (production build):
- Performance: 95-100
- Accessibility: 100
- Best Practices: 100
- SEO: 100
The project is configured for Vercel deployment:
# Install Vercel CLI
pnpm add -g vercel
# Deploy
vercelOr use the Vercel Dashboard:
- Import your repository
- Framework preset: Astro
- Build command:
pnpm build - Output directory:
dist
# Build command
pnpm build
# Publish directory
dist
# Build command
pnpm build
# Build output directory
dist
# Environment variables
NODE_VERSION=18Build the site and upload the dist folder:
pnpm build
# Upload ./dist to your static hostNote: For Keystatic CMS functionality in production, set the environment variables listed in .env.example on your hosting platform.
- Atomic components - Start small, compose upward
- Colocation - Keep related files together
- Naming conventions - PascalCase for components, camelCase for utils
- TypeScript strict mode - Catch errors at compile time
- CSS custom properties for theming (
--theme-primary,--s4) - BEM-inspired naming for component styles
- Mobile-first responsive design
- Utility-first where appropriate (but not utility CSS framework)
When adding new translatable content:
- Add translation keys to
src/i18n/ui.ts - Update
langMappingfor new routes - Create language-specific page files
- Test language switching thoroughly
- Frontmatter first - Metadata at the top of markdown files
- Consistent dates - ISO 8601 format (
YYYY-MM-DD) - Image references - Relative paths from content files
Contributions are welcome! This project aims to be a reference implementation for the Astro community.
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Make your changes - Follow existing patterns
- Test thoroughly - Build, accessibility, i18n
- Commit with clear messages (
git commit -m 'Add amazing feature') - Push to your fork (
git push origin feature/amazing-feature) - Open a Pull Request
- 📚 Improve documentation
- 🌍 Add support for more languages
- ♿ Enhance accessibility
- 🎨 Create new component patterns
- 🐛 Fix bugs
- ⚡ Performance optimizations
- 🧪 Add tests
- Run
pnpm formatbefore committing - Follow existing TypeScript patterns
- Document complex logic with comments
- Keep components focused and reusable
This project is licensed under the MIT License - see the LICENSE file for details.
You are free to use this code for personal or commercial projects. Attribution appreciated but not required.
- Design by Nerea Dorronsoro - UI/UX Design
- Development by Jon Ramos - Full Stack Development
- Built with Astro - The web framework for content-driven websites
- CMS powered by Keystatic
- Icons from Lucide
- Deployed on Vercel
⭐ If you found this project helpful, please consider giving it a star!