Layout Components
Layout components wrap every page of a trainer’s website. They are always visible regardless of which page the visitor is on.
Trainer logos (required pattern)
Trainers upload optional logos for light and dark mode from the Gymlium dashboard or signup wizard. Your template must not ship a business-specific logo file.
Use the platform TemplateLogo component in Header and Footer:
import { TemplateLogo } from '@/components/TemplateLogo'
<TemplateLogo
size="header"
className={styles.logoImage}
fallback={<span className={styles.wordmark}>{clientName}</span>}
/>- If the trainer uploaded logos,
TemplateLogoswaps them by theme (data-themeon<html>). - If not, your fallback renders — typically a styled wordmark of
clientName. - Recommended sizes: header
clamp(38px, 3.2vw, 60px)max width ~250px; footer36px. - Declare support in
template.manifest.jsonunderbranding.logo(see Manifest).
Do not import static logo PNGs for the trainer’s business into your template package. Preview/thumbnail assets in preview/ are for the marketplace listing only.
Favicon (handled by the platform)
The browser-tab favicon is not part of your template. Trainers upload an optional favicon in the signup wizard and in Dashboard → Appearance, and Gymlium injects it into the document <head> automatically for every page (falling back to the Gymlium icon when none is set).
- Do not add a
favicon.ico,icon.png, orapple-icon.pngto your template, and do not render a<link rel="icon">tag — it would compete with the trainer’s chosen favicon. - Optionally declare
branding.favicon.supported: truein your manifest to confirm your template follows this rule (see Manifest).
Header
The Header is the main navigation bar displayed at the top of every page. It must be a named export.
Responsibilities
- Display the trainer’s business name or logo
- Primary navigation links
- Locale/language switcher
- Authentication state (login button or account link)
- Mobile-responsive hamburger menu
Example implementation
import { useState } from 'react'
import styles from './Header.module.css'
import { useApp } from '@/hooks/useApp'
import { useAuth } from '@/hooks/useAuth'
export function Header() {
const { clientName, localePath, t, isRtl, locale, locales } = useApp()
const { user } = useAuth()
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
const navLinks = [
{ href: localePath('/'), label: t('home') },
{ href: localePath('/about'), label: t('about') },
{ href: localePath('/services'), label: t('services') },
{ href: localePath('/blog'), label: t('blog') },
{ href: localePath('/contact'), label: t('contact') },
]
return (
<header className={styles.header}>
<div className={styles.inner}>
<a href={localePath('/')} className={styles.logo}>
{clientName}
</a>
<nav className={styles.desktopNav}>
{navLinks.map(link => (
<a key={link.href} href={link.href}>
{link.label}
</a>
))}
</nav>
<div className={styles.actions}>
{/* Locale switcher */}
<select
value={locale}
onChange={e => {
window.location.href = `/${e.target.value}`
}}
className={styles.localeSwitcher}
>
{locales.map(loc => (
<option key={loc} value={loc}>{loc.toUpperCase()}</option>
))}
</select>
{/* Auth */}
{user ? (
<a href={localePath('/account')}>{t('myAccount')}</a>
) : (
<a href={localePath('/login')} className={styles.loginBtn}>
{t('login')}
</a>
)}
</div>
{/* Mobile menu toggle */}
<button
className={styles.hamburger}
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
aria-label={t('toggleMenu')}
aria-expanded={mobileMenuOpen}
>
<span />
<span />
<span />
</button>
</div>
{/* Mobile menu */}
{mobileMenuOpen && (
<nav className={styles.mobileNav}>
{navLinks.map(link => (
<a key={link.href} href={link.href}>
{link.label}
</a>
))}
</nav>
)}
</header>
)
}Export pattern
// index.ts
export { Header } from './Header/Header'Header must be a named export, not a default export. The platform imports it as { Header }.
Footer
The Footer appears at the bottom of every page. It must be a named export.
Responsibilities
- Secondary navigation links
- Social media links
- Copyright notice
- Attribution link (for
free_with_attributiontemplates)
Example implementation
import styles from './Footer.module.css'
import { useApp } from '@/hooks/useApp'
export function Footer() {
const { clientName, localePath, t } = useApp()
const year = new Date().getFullYear()
return (
<footer className={styles.footer}>
<div className={styles.inner}>
<div className={styles.brand}>
<span className={styles.name}>{clientName}</span>
<p className={styles.tagline}>{t('footerTagline')}</p>
</div>
<nav className={styles.links}>
<a href={localePath('/about')}>{t('about')}</a>
<a href={localePath('/services')}>{t('services')}</a>
<a href={localePath('/contact')}>{t('contact')}</a>
<a href={localePath('/privacy')}>{t('privacy')}</a>
<a href={localePath('/terms')}>{t('terms')}</a>
</nav>
<p className={styles.copyright}>
© {year} {clientName}. {t('allRightsReserved')}
</p>
</div>
</footer>
)
}Export pattern
// index.ts
export { Footer } from './Footer/Footer'Loading
The Loading component is displayed while page content is being fetched. It must be a default export.
Responsibilities
- Visual loading indicator (spinner, skeleton, or animation)
- Lightweight and fast-rendering
- Consistent with your template’s design language
Example implementation
Spinner
import styles from './Loading.module.css'
export default function Loading() {
return (
<div className={styles.container}>
<div className={styles.spinner} />
</div>
)
}/* Loading.module.css */
.container {
display: flex;
align-items: center;
justify-content: center;
min-height: 200px;
}
.spinner {
width: 40px;
height: 40px;
border: 3px solid var(--text-secondary);
border-top-color: var(--color-accent);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}Export pattern
// index.ts
export { default as Loading } from './Loading/Loading'The Loading component should render quickly and avoid heavy dependencies. It’s the first thing visitors see while a page loads — keep it simple and performant.