← Back to site

Routing & Layouts

File-Based Routing

Astro maps files in src/pages/ directly to URLs. The site supports two locales: English (default, no prefix) and German (/de/ prefix):

FileURLDescription
index.astro/Homepage (English)
de/index.astro/de/Homepage (German)
events.astro/eventsEvents listing
de/events.astro/de/eventsEvents listing (German)
projects.astro/projectsProjects listing
about-us.astro/about-usAbout page
sponsors.astro/sponsorsSponsors page
contact.astro/contactContact page
media.astro/mediaMedia gallery
imprint.astro/imprintLegal imprint
datenschutz.astro/datenschutzPrivacy policy
404.astroN/ACustom 404 page
events/[slug].astro/events/:slugEvent detail page
de/events/[slug].astro/de/events/:slugEvent detail page (German)
projects/[slug].astro/projects/:slugProject detail page
de/projects/[slug].astro/de/projects/:slugProject detail page (German)
docs/index.astro/docs/Documentation index
docs/[...slug].astro/docs/*Documentation pages

Every public page has a de/ counterpart. Documentation is English-only.

Locale Wrappers (src/pages/de/)

German pages are thin wrapper files that import and re-render the root page component. The root page detects its locale from the URL via getLocale(Astro.url) and passes it to all content queries and components.

Static page wrapper

---
// src/pages/de/about-us.astro
import Page from '../about-us.astro';
---
<Page />

Dynamic route wrapper

Dynamic routes also re-export getStaticPaths (a named export that doesn’t conflict with Astro’s implicit default):

---
// src/pages/de/events/[slug].astro
import Page from '../../events/[slug].astro';
export { getStaticPaths } from '../../events/[slug].astro';
---
<Page {...Astro.props} />

Dynamic Routes

Dynamic route pages export a getStaticPaths() function that returns all valid paths at build time.

Single-segment ([slug].astro)

Used by events and projects. Paths are generated from English entries (default locale). At render time, the component detects the locale and swaps in the locale-specific entry with fallback:

---
import { getPublishedEvents, stripLocaleFromSlug } from "@utils/contentQueries";
import { getLocale } from "@utils/i18n";
import { loadCoverImage } from "@utils/imageLoader";

export async function getStaticPaths() {
  const events = await getPublishedEvents();
  return events.map((entry) => ({
    params: { slug: stripLocaleFromSlug(entry.slug) },
    props: { entry },
  }));
}

const { entry: defaultEntry } = Astro.props;
const locale = getLocale(Astro.url);

// Swap to locale-specific entry if available
let entry = defaultEntry;
if (locale !== 'en') {
  const localeEvents = await getPublishedEvents(locale);
  const bareSlug = stripLocaleFromSlug(defaultEntry.slug);
  const localeEntry = localeEvents.find(e => stripLocaleFromSlug(e.slug) === bareSlug);
  if (localeEntry) entry = localeEntry;
}

const coverImage = await loadCoverImage(entry.data.coverImage, "event", { itemTitle: entry.data.title, itemSlug: entry.slug });
const { Content } = await entry.render();
---

Rest parameter ([...slug].astro)

Used by docs to support nested paths (e.g., /docs/guides/managing-events):

---
import { getCollection } from "astro:content";

export async function getStaticPaths() {
  const docs = await getCollection("docs");
  return docs.map((entry) => ({
    params: { slug: entry.slug },
    props: { entry },
  }));
}
---

Layout Hierarchy

BaseLayout.astro          ← All public pages
├── PostLayout.astro      ← Event & project detail pages
└── (page).astro          ← Direct usage by listing/static pages

DocsLayout.astro          ← Standalone (not based on BaseLayout)

BaseLayout

File: src/layouts/BaseLayout.astro Props: title, description?, ogImage?

Provides the HTML shell for all public pages:

  • Locale detection — derives locale from URL via getLocale(), sets <html lang>, generates hreflang <link> tags
  • SEO meta tags — canonical URL, Open Graph, Twitter Card, og:locale
  • Structured data — Organization schema with social links (loaded from the social page-text entry)
  • Hash scroll handling — intercepts hash before browser’s native scroll, then smooth-scrolls after Alpine initializes (with a load event fallback for non-Alpine pages)
  • Global layout — Header (with language switcher) + main content slot + Footer + BackToTopButton

PostLayout

File: src/layouts/PostLayout.astro Extends: BaseLayout Props: title, description, slug, date, coverImage?, category, isCompleted?, headOfProject?, type ('event' | 'project')

Used for event and project detail pages:

  • Structured data — Event schema (for events) or Article schema (for projects)
  • Reading progress — Top-of-page progress bar
  • Two-column layout — Prose content + sticky metadata sidebar (mobile: sidebar above content, desktop: sidebar on right)
  • Prose styling — Custom styles for h2-h4, lists, blockquotes, code blocks, tables

DocsLayout

File: src/layouts/DocsLayout.astro Props: title, description?, headings?

Standalone layout (does not extend BaseLayout) used for documentation pages:

  • Sticky header — “BEARS Docs” branding with back-to-site link and mobile hamburger
  • Three-column layout — Sidebar (left) + article content (center) + table of contents (right, XL+ only)
  • Code block copy buttons — Auto-generated with checkmark feedback
  • Docs prose styling — Custom heading, list, table, and code block styles
  • Hash scroll — Same pattern as BaseLayout

Hash Scroll Handling

Both BaseLayout and DocsLayout share the same hash scroll pattern. It works in two phases:

  1. Inline head script — Captures window.location.hash into window.__scrollTarget and strips the hash from the URL (prevents the browser’s default jump)
  2. Body script — After Alpine initializes (or on load as fallback), smooth-scrolls to the target element using requestAnimationFrame double-call for timing safety