Theme Components
A reference guide for all available components. Copy the code snippets to use in your pages and MDX content.
Content Components
Components for use in MDX blog posts and pages.
ButtonSimple
A simple button link. Use in MDX content for call-to-action buttons.
Usage
<ButtonSimple text="Get Started" url="/membership/" />
<ButtonSimple
text="External Link"
url="https://example.com"
new_tab={true}
/> Props
| Prop | Type | Default | Description |
|---|---|---|---|
text | string | required | Button label text |
url | string | required | Link destination |
new_tab | boolean | false | Open in new tab |
classname | string | - | Additional CSS class |
Callout
Highlighted callout box with emoji. Great for tips, warnings, or important notes.
Usage
<Callout text="This is a helpful tip!" />
<Callout
text="Warning: This action cannot be undone."
emoji="⚠️"
background_color="#fff3cd"
text_color="#856404"
/> Props
| Prop | Type | Default | Description |
|---|---|---|---|
text | string | required | Callout message |
emoji | string | "💡" | Emoji icon |
background_color | string | var(--background-color) | Background color |
text_color | string | var(--text-color) | Text color |
ImageCard
Display a single image from the assets folder with responsive sizing.
Usage
<ImageCard
image_slug="blog_image"
alt="Description of image"
size="Wide"
/>
<!-- Size options: "Wide" | "Full Width" | "Auto" --> Props
| Prop | Type | Default | Description |
|---|---|---|---|
image_slug | string | required | Filename without extension (in src/assets/) |
alt | string | required | Alt text for accessibility |
size | string | "Wide" | "Wide" | "Full Width" | "Auto" |
ImageGallery
Display multiple images in a responsive grid layout.
Usage
<ImageGallery
image_slugs={[
{ slug: "image-1", alt: "First image" },
{ slug: "image-2", alt: "Second image" },
{ slug: "image-3", alt: "Third image" }
]}
images_per_row={3}
/> Props
| Prop | Type | Default | Description |
|---|---|---|---|
image_slugs | array | required | Array of { slug, alt } objects |
images_per_row | number | 3 | Images per row |
ToggleCards (Accordion)
Expandable accordion cards. Perfect for FAQs.
What is The Odyssey Path?
The Odyssey Path is a transformative coaching platform designed to help you unlock your potential and create meaningful change in your life.
How do I get started?
Simply sign up for a membership tier that suits your needs, and you'll get immediate access to our resources, community, and coaching sessions.
Can I cancel anytime?
Yes, you can cancel your subscription at any time. There are no long-term commitments or cancellation fees.
Usage
<ToggleCards toggle_cards={[
{ title: "Question 1?", text: "Answer 1..." },
{ title: "Question 2?", text: "Answer 2..." }
]} /> Props
| Prop | Type | Default | Description |
|---|---|---|---|
toggle_cards | array | required | Array of { title, text } objects |
MediaEmbed
Embed YouTube, Vimeo, Spotify, or any iframe content.
Usage
<!-- YouTube -->
<MediaEmbed
media_iframe_src="https://www.youtube.com/embed/VIDEO_ID"
title="Video Title"
/>
<!-- Vimeo -->
<MediaEmbed
media_iframe_src="https://player.vimeo.com/video/VIDEO_ID"
title="Vimeo Video"
/>
<!-- Spotify -->
<MediaEmbed
media_iframe_src="https://open.spotify.com/embed/track/TRACK_ID"
title="Spotify Track"
/> Props
| Prop | Type | Default | Description |
|---|---|---|---|
media_iframe_src | string | required | Embed URL |
title | string | "Embedded media" | Accessibility title |
StarRating
Display a star rating. Used in testimonials and reviews.
Usage
<StarRating rating={5} />
<StarRating rating={4} />
<StarRating rating={3} maxStars={10} /> Props
| Prop | Type | Default | Description |
|---|---|---|---|
rating | number | required | Filled stars count |
maxStars | number | 5 | Total stars |
Card Components
Pre-styled cards for displaying content. Used in page templates.
PostCard
Standard blog post card with image, title, excerpt, and metadata.
A accumsan amet odio at metus. Blandit eget volutpat pretium a vehicula id. Ac amet pretium massa aliquam porttitor feugiat aliquam tellus congue.
by Daniela Wattson, Nov 10, 2025
Usage
---
import PostCard from "@egobot/webcore/components/PostCard.astro";
import { getSortedPosts } from "@utils/utils.js";
const { posts } = await getSortedPosts({ limit: 6 });
---
<div class="grid">
{posts.map(post => (
<PostCard t={t} post={post} />
))}
</div> Props
| Prop | Type | Description |
|---|---|---|
t | object | Translation object (loaded from locales) |
post | CollectionEntry | Blog post from getSortedPosts() |
PostCardv2
Enhanced post card with video hover, centered title overlay, and excerpt. Ideal for featured content rows on the homepage. The entire card is clickable. Video plays on hover (if available), otherwise shows feature image with subtle zoom.
The Odyssey Experience
Return to your power
Increase Inner Happiness
A accumsan amet odio at metus. Blandit eget volutpat pretium a vehicula id. Ac amet pretium massa aliquam porttitor feugiat aliquam tellus congue.
How AI Shapes Our World
Enim amet sagittis fringilla amet nisl non risus. Faucibus blandit sit in faucibus interdum. Imperdiet dolor porta quis egestas egestas id congue ut.
Usage
---
import PostCardv2 from "@egobot/webcore/components/PostCardv2.astro";
import { getSortedPosts } from "@utils/utils.js";
const { posts } = await getSortedPosts({ limit: 3 });
---
<div class="post-row">
{posts.map(post => (
<PostCardv2 t={t} post={post} />
))}
</div> Props
| Prop | Type | Description |
|---|---|---|
t | object | Translation object (loaded from locales) |
post | CollectionEntry | Blog post from getSortedPosts() |
TinaCMS Fields (per post)
| Field | Type | Description |
|---|---|---|
feature_video | string | Video path (plays on hover) |
overlay_opacity | number | Dark overlay 0-100% (default: 50) |
hide_from_collections | boolean | Hide from archive/topic pages |
Features
- 4:3 fixed aspect ratio that scales based on screen width
- Video playback on hover (pauses and resets on mouse leave)
- White text always visible over dark overlay
- Per-post overlay opacity control (0-100%) in TinaCMS
- Feature image fallback with subtle zoom on hover when no video
- Title centered over image, excerpt below title, no topics
- Toggle to hide post from collection pages while keeping direct URL accessible
CreativePostCard
Alternative post card style with a more creative layout.
Usage
<CreativePostCard t={t} post={post} /> TopicCard
Topic/category card showing topic name and post count.
Usage
---
import TopicCard from "@egobot/webcore/components/TopicCard.astro";
import { getTopics, getUniqueTopicsWithCount } from "@utils/utils.js";
const topics = await getTopics();
const uniqueTopics = getUniqueTopicsWithCount(topics);
---
{uniqueTopics.map(topic => (
<TopicCard topic={topic} />
))} Props
| Prop | Type | Description |
|---|---|---|
topic | object | { original, slug, count } from getUniqueTopicsWithCount() |
TestimonialCard
Review/testimonial card with star rating and author info.
Usage
<TestimonialCard
name="John Smith"
title="CEO"
company="Acme Corp"
stars={5}
reviewText="The review text goes here..."
date={new Date('2024-01-15')}
showDate={true}
/> Props
| Prop | Type | Default | Description |
|---|---|---|---|
name | string | "Anonymous" | Reviewer name |
title | string | - | Job title |
company | string | - | Company name |
stars | number | required | Star rating (1-5) |
reviewText | string | required | Review content |
date | Date | required | Review date |
showDate | boolean | true | Show date |
Other Card Components
Additional card components for specific use cases.
FeaturedPostCard- Large featured post display with background image. Used on homepage hero.AllPostsCard- Compact post listing with hover image reveal. Used on the All Posts page.AuthorCard- Author profile card with avatar and bio.MembershipCard- Pricing tier card with benefits list. Used on membership page.TestimonialVideoCard- Video testimonial with play button overlay.ResourceCard- Downloadable resource card with file type indicator.
Component Groups
Components that work together as a system.
Slider + SliderPostCard
The Slider component provides a carousel container with navigation controls.
SliderPostCard is the card format designed for display inside the slider.
Complete Example
---
import Slider from "@egobot/webcore/components/Slider.astro";
import SliderPostCard from "@egobot/webcore/components/SliderPostCard.astro";
import { getSortedPosts } from "@utils/utils.js";
const { posts } = await getSortedPosts({ limit: 6 });
---
<Slider>
{posts.map(post => (
<SliderPostCard
t={t}
post={post}
globalSettings={globalSettings}
/>
))}
</Slider> How it works
Sliderwraps children in a Flickity carousel with navigation arrowsSliderPostCarddisplays post data in a format optimized for sliding/swiping- The slider automatically handles responsive breakpoints
TestimonialGrid + TestimonialCard + TestimonialVideoCard
Display customer reviews in a masonry grid layout. Supports both text and video testimonials.
Complete Example
---
import TestimonialGrid from "@egobot/webcore/components/TestimonialGrid.astro";
import TestimonialCard from "@egobot/webcore/components/TestimonialCard.astro";
import TestimonialVideoCard from "@egobot/webcore/components/TestimonialVideoCard.astro";
import { getCollection } from "astro:content";
const testimonials = await getCollection("testimonials");
---
<TestimonialGrid>
{testimonials.map(testimonial => (
testimonial.data.video_url ? (
<TestimonialVideoCard
name={testimonial.data.name}
video_url={testimonial.data.video_url}
stars={testimonial.data.stars}
/>
) : (
<TestimonialCard
name={testimonial.data.name}
stars={testimonial.data.stars}
reviewText={testimonial.body}
date={testimonial.data.date}
/>
)
))}
</TestimonialGrid> How it works
TestimonialGriduses Masonry.js for Pinterest-style layoutTestimonialCardrenders text reviews with star ratingsTestimonialVideoCardshows video embed with thumbnail
ArchiveTopics + PostCard + Pagination
The archive page system with topic filtering and paginated post grid.
Complete Example
---
import ArchiveTopics from "@egobot/webcore/components/ArchiveTopics.astro";
import PostCard from "@egobot/webcore/components/PostCard.astro";
import Pagination from "@egobot/webcore/components/Pagination.astro";
import { getSortedPosts } from "@utils/utils.js";
const { posts, total_posts } = await getSortedPosts({
limit: globalSettings.pagination_posts_number
});
---
<!-- Topic filter buttons -->
<ArchiveTopics
globalSettings={globalSettings}
t={t}
total_posts={total_posts}
/>
<!-- Post grid -->
<div id="pagination-grid" class="grid pagination-grid">
{posts.map(post => (
<PostCard t={t} post={post} />
))}
</div>
<!-- Pagination controls -->
<Pagination
t={t}
posts_per_page={globalSettings.pagination_posts_number}
total_posts={total_posts}
is_archivepage={true}
/> How it works
ArchiveTopicsrenders radio buttons for filtering by topic- Client-side JS filters cards when topics are selected
Paginationhandles page navigation for large post sets
CategoryFilter + ResourceCard
Filter and display downloadable resources by category.
Complete Example
---
import CategoryFilter from "@egobot/webcore/components/resources/CategoryFilter.astro";
import ResourceCard from "@egobot/webcore/components/resources/ResourceCard.astro";
import { getCollection } from "astro:content";
const resources = await getCollection("resources");
const categories = [...new Set(resources.map(r => r.data.category))];
const categoryCounts = categories.reduce((acc, cat) => {
acc[cat] = resources.filter(r => r.data.category === cat).length;
return acc;
}, {});
---
<!-- Category filter buttons -->
<CategoryFilter
categories={categories}
categoryCounts={categoryCounts}
totalCount={resources.length}
/>
<!-- Resource grid -->
<div id="resource-grid" class="grid">
{resources.map(resource => (
<ResourceCard resource={resource.data} t={t} />
))}
</div>
<script>
// Client-side filtering
document.addEventListener('DOMContentLoaded', () => {
const radios = document.querySelectorAll('input[name="resource-category"]');
const cards = document.querySelectorAll('#resource-grid .resource-card');
radios.forEach(radio => {
radio.addEventListener('change', (e) => {
const category = e.target.dataset.category;
cards.forEach(card => {
const show = category === 'all' || card.dataset.category === category;
card.style.display = show ? '' : 'none';
});
});
});
});
</script> How it works
CategoryFilterrenders radio buttons with category countsResourceCardshows resource type icon, download button- Client-side JS filters cards based on
data-categoryattribute
Circles + CirclePair
Animated infinite-scroll circles decoration. Used in hero sections.
Usage
---
import Circles from "@egobot/webcore/components/Circles.astro";
---
<Circles t={t} heading="Our Readers" /> Configuration
Circle content is configured in src/config/circles.json:
[
{ "title": "Reader Name", "text": "Quote...", "image_slug": "reader1" },
{ "title": "Another Reader", "text": "Quote...", "image_slug": "reader2" }
] How it works
Circlescreates two infinite scroll rows (normal and reversed)CirclePairrenders individual circle with image and text- Animation is CSS-based for smooth performance
Utility Functions
Helper functions for fetching and transforming content.
getSortedPosts()
Fetch blog posts sorted by date with optional filtering.
Usage
import { getSortedPosts } from "@utils/utils.js";
// Get all posts
const { posts, total_posts } = await getSortedPosts();
// Limit to 6 posts
const { posts } = await getSortedPosts({ limit: 6 });
// Filter by topic
const { posts } = await getSortedPosts({
topic: "personal-growth",
limit: 10
});
// Filter by author
const { posts } = await getSortedPosts({
author: "john-smith"
}); Options
| Option | Type | Description |
|---|---|---|
limit | number | Maximum posts to return |
topic | string | Filter by topic slug |
author | string | Filter by author slug |
Returns
| Property | Type | Description |
|---|---|---|
posts | array | Array of blog post entries |
total_posts | number | Total count (for pagination) |
getTopics() / getUniqueTopicsWithCount()
Fetch topics and get unique topics with post counts.
Usage
import { getTopics, getUniqueTopicsWithCount } from "@utils/utils.js";
// Get all topics from all posts (may have duplicates)
const topics = await getTopics();
// Get unique topics with post counts
const uniqueTopics = getUniqueTopicsWithCount(topics);
// Returns: [{ original: "Personal Growth", slug: "personal-growth", count: 5 }, ...]
// Use in template
{uniqueTopics.map(topic => (
<a href={`/topics/${topic.slug}/`}>
{topic.original} ({topic.count})
</a>
))} formatDate() / slugify()
Format dates and create URL-safe slugs.
Usage
import { formatDate, slugify } from "@egobot/webcore/utils/presentation.ts";
// Format a date
const formatted = formatDate(new Date('2024-01-15'));
// Returns: "Jan 15, 2024"
// Create a slug
const slug = slugify("Personal Growth Tips");
// Returns: "personal-growth-tips"
// Common use case - author links
const authors = post.data.authors.map(name => ({
name: name,
slug: slugify(name)
})); Layout Components
Page wrapper components. Used in .astro page files.
BaseLayout
Main page wrapper with navigation, meta tags, and global scripts.
Usage
---
import BaseLayout from "@egobot/webcore/layouts/BaseLayout.astro";
import globalSettings from '@config/config.json.ts';
const t = await import(`@locales/${globalSettings.language}.json`)
.catch(() => import('@locales/en.json'));
---
<BaseLayout
globalSettings={globalSettings}
t={t}
title="Page Title"
description="Page description for SEO"
>
<main>
<!-- Your page content -->
</main>
<!-- Add footer -->
<FooterNormal t={t} globalSettings={globalSettings} />
</BaseLayout> Props
| Prop | Type | Description |
|---|---|---|
globalSettings | object | Site configuration from config.json.ts |
t | object | Translation strings from locales |
title | string | Page title (for SEO) |
description | string | Page description (for SEO) |
PostLayout
Blog post page layout with article styles and navigation.
Usage
---
import PostLayout from "@egobot/webcore/layouts/PostLayout.astro";
---
<PostLayout
globalSettings={globalSettings}
t={t}
post={post}
previous_post={previousPost}
next_post={nextPost}
/> FooterCreative / FooterNormal
Two footer styles. Set in config or use conditionally.
Usage
<!-- Use setting from config -->
{globalSettings.footer_type == "Creative" ? (
<FooterCreative t={t} globalSettings={globalSettings} />
) : (
<FooterNormal t={t} globalSettings={globalSettings} />
)}
<!-- Or use directly -->
<FooterNormal t={t} globalSettings={globalSettings} /> Animations & Effects
CSS classes and JavaScript-powered animations. Requires GSAP + ScrollTrigger (loaded in BaseLayout).
Page Load Animations
Fade-in animations triggered on page load. Controlled by globalSettings.use_page_load_animations.
Animation Classes
| Class | Effect | Use Case |
|---|---|---|
.ease-in-animation | Fade up from 30px below, staggered 0.03s | Content blocks, headings, cards |
.ease-in-animation-no-stagger | Fade up without stagger delay | Single elements, images |
.opacity-animation | Fade in only (no movement) | Borders, dividers, backgrounds |
.image-animation | Image-specific fade in | Hero images, backgrounds |
Usage
<!-- Apply conditionally based on settings -->
<div class:list={[
'my-content',
{ 'ease-in-animation': globalSettings.use_page_load_animations }
]}>
Content fades in on page load
</div>
<!-- Or apply directly -->
<h1 class="ease-in-animation">Animated Heading</h1>
<div class="ease-in-animation-no-stagger">No stagger delay</div>
<div class="opacity-animation">Fade only</div> How it works
- Elements start with
opacity: 0; transform: translateY(30px) - GSAP animates to
opacity: 1; y: 0over 1.2s with expo.out easing - Staggered elements animate 0.03s apart for a cascade effect
- Triggered by
pageLoadAnimations()in global.js on DOMContentLoaded
Hover Effects
CSS-only hover animations for interactive elements.
Image Zoom
<!-- Subtle zoom on hover (1 → 1.02 scale) -->
<div class="hover-image-animation">
<img src="..." alt="..." />
</div> Button Animations
<!-- Bracket button - text slides up, brackets shift -->
<button class="bracket-button">
<span class="bracket">[</span>
<span class="text">Button Text</span>
<span class="bracket">]</span>
</button>
<!-- Arrow button - icon/text slide opposite directions -->
<a class="arrow-button" href="/link">
<span>Link Text</span>
<svg><!-- arrow icon --></svg>
</a>
<!-- Card circle - scales from 0 with bounce -->
<div class="card">
<div class="card-circle"></div>
</div> Easing Variables
/* Available in global.css */
--ease-transition: cubic-bezier(.23, 1, .32, 1);
--bounce-past-transition: cubic-bezier(0.68, -0.55, 0.27, 1.55); Scroll-Triggered Effects
GSAP ScrollTrigger animations that respond to scroll position.
Parallax Images
<!-- Image moves slower than scroll (-5% to +5% Y) -->
<figure class="parallax-image">
<img src="..." alt="..." />
</figure>
<!-- Desktop only parallax -->
<figure class="parallax-image-desktop">...</figure>
<!-- Mobile only parallax -->
<figure class="parallax-image-mobile">...</figure> Creative Post Cursor Tracking
<!-- Image follows mouse cursor -->
<div class="creative-post-card">
<div class="creative-post-card-image-container">
<img src="..." />
</div>
</div>
<!-- Initialized by setCreativePostImages() in global.js --> How it works
- Parallax uses ScrollTrigger with
scrub: truefor direct scroll-linking - Cursor tracking uses
requestAnimationFramewith 0.11 ease factor - Mobile fallback uses ScrollTrigger opacity toggles instead of cursor
Smooth Scrolling (Lenis)
Smooth scroll behavior powered by Lenis library. Configured in globalSettings.scrolling_type.
Configuration Options
| Setting | Duration | Feel |
|---|---|---|
"Subtle" | 0.4s | Light smoothing, close to native |
"Moderate" | 0.7s | Balanced smooth scrolling |
"Intense" | 1.2s | Heavy smoothing, floaty feel |
"None" | - | Native browser scrolling |
Set in config.json.ts
// src/config/config.json.ts
export default {
scrolling_type: "Moderate", // or "Subtle", "Intense", "None"
// ...
} Component-Specific Animations
Built-in animations for specific components.
Slider (Flickity)
<!-- Carousel with fade transitions -->
<Slider>
{posts.map(post => <SliderPostCard ... />)}
</Slider>
<!-- Features:
- Fade between slides
- Auto-play support
- Touch/swipe gestures
- Slide counter animation
--> TestimonialGrid (Masonry + Fade)
<!-- Masonry layout with staggered fade-in -->
<TestimonialGrid>
{testimonials.map(t => <TestimonialCard ... />)}
</TestimonialGrid>
<!-- Features:
- Pinterest-style masonry layout
- 2s fade-in per card with random 0-0.3s delay
- "Load More" adds cards with animation
--> ToggleCards (Accordion)
<!-- Expand/collapse animation -->
<ToggleCards toggle_cards={[...]} />
<!-- Features:
- Height animates from 0 to auto
- Icon rotates 180° on toggle
--> Navbar
<!-- Built into BaseLayout, features:
- Circle follows cursor on hover
- Menu icon rotates to X (±45°)
- Background blur effect
- Responsive at 1080px breakpoint
--> FooterCreative
<!-- Sticky footer with animations -->
<FooterCreative t={t} globalSettings={globalSettings} />
<!-- Features:
- Sticky positioning with scroll reveal
- Link hover: circle scales, text translates
- Spring bounce easing on interactive elements
--> Animation Utility Functions
JavaScript functions exported from global.js for animation control.
Available Functions
// Import from global.js (already loaded in BaseLayout)
// Refresh all ScrollTrigger animations (call on resize)
resetScrollTriggers();
// Kill specific ScrollTrigger (cleanup before remount)
killScrollTrigger('.my-selector');
// Disable/enable page scroll (for modals, menus)
disableScrolling();
enableScrolling();
// Initialize specific animation systems
setSmoothScroll(); // Lenis smooth scrolling
pageLoadAnimations(); // Page load fade-ins
setCreativePostImages(); // Cursor tracking
setImageParallax(); // Parallax scroll
setToggle(); // Accordion toggles
setLightense(); // Image zoom on click