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

PropTypeDefaultDescription
textstringrequiredButton label text
urlstringrequiredLink destination
new_tabbooleanfalseOpen in new tab
classnamestring-Additional CSS class

Callout

Highlighted callout box with emoji. Great for tips, warnings, or important notes.

💡
This is a helpful tip for your journey!
⚠️
Warning: This action cannot be undone.
Success! Your changes have been saved.

Usage

<Callout text="This is a helpful tip!" />

<Callout
  text="Warning: This action cannot be undone."
  emoji="⚠️"
  background_color="#fff3cd"
  text_color="#856404"
/>

Props

PropTypeDefaultDescription
textstringrequiredCallout message
emojistring"💡"Emoji icon
background_colorstringvar(--background-color)Background color
text_colorstringvar(--text-color)Text color

ImageCard

Display a single image from the assets folder with responsive sizing.

Sample blog image

Usage

<ImageCard
  image_slug="blog_image"
  alt="Description of image"
  size="Wide"
/>

<!-- Size options: "Wide" | "Full Width" | "Auto" -->

Props

PropTypeDefaultDescription
image_slugstringrequiredFilename without extension (in src/assets/)
altstringrequiredAlt text for accessibility
sizestring"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

PropTypeDefaultDescription
image_slugsarrayrequiredArray of { slug, alt } objects
images_per_rownumber3Images 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

PropTypeDefaultDescription
toggle_cardsarrayrequiredArray 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

PropTypeDefaultDescription
media_iframe_srcstringrequiredEmbed URL
titlestring"Embedded media"Accessibility title

StarRating

Display a star rating. Used in testimonials and reviews.

5 stars
4 stars
3 stars

Usage

<StarRating rating={5} />
<StarRating rating={4} />
<StarRating rating={3} maxStars={10} />

Props

PropTypeDefaultDescription
ratingnumberrequiredFilled stars count
maxStarsnumber5Total stars

Card Components

Pre-styled cards for displaying content. Used in page templates.

PostCard

Standard blog post card with image, title, excerpt, and metadata.

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.

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

PropTypeDescription
tobjectTranslation object (loaded from locales)
postCollectionEntryBlog 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

The Odyssey Experience

Return to your power

Increase Inner Happiness

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

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

PropTypeDescription
tobjectTranslation object (loaded from locales)
postCollectionEntryBlog post from getSortedPosts()

TinaCMS Fields (per post)

FieldTypeDescription
feature_videostringVideo path (plays on hover)
overlay_opacitynumberDark overlay 0-100% (default: 50)
hide_from_collectionsbooleanHide 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

PropTypeDescription
topicobject{ original, slug, count } from getUniqueTopicsWithCount()

TestimonialCard

Review/testimonial card with star rating and author info.

JS
John Smith
CEO at Acme Corp
The Odyssey Path program has been incredibly helpful in organizing my thoughts and setting clear goals. The tools provided are very easy to use and the structured approach to personal development has made a real difference.

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

PropTypeDefaultDescription
namestring"Anonymous"Reviewer name
titlestring-Job title
companystring-Company name
starsnumberrequiredStar rating (1-5)
reviewTextstringrequiredReview content
dateDaterequiredReview date
showDatebooleantrueShow 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

  • Slider wraps children in a Flickity carousel with navigation arrows
  • SliderPostCard displays 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

  • TestimonialGrid uses Masonry.js for Pinterest-style layout
  • TestimonialCard renders text reviews with star ratings
  • TestimonialVideoCard shows 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

  • ArchiveTopics renders radio buttons for filtering by topic
  • Client-side JS filters cards when topics are selected
  • Pagination handles 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

  • CategoryFilter renders radio buttons with category counts
  • ResourceCard shows resource type icon, download button
  • Client-side JS filters cards based on data-category attribute

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

  • Circles creates two infinite scroll rows (normal and reversed)
  • CirclePair renders 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

OptionTypeDescription
limitnumberMaximum posts to return
topicstringFilter by topic slug
authorstringFilter by author slug

Returns

PropertyTypeDescription
postsarrayArray of blog post entries
total_postsnumberTotal 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

PropTypeDescription
globalSettingsobjectSite configuration from config.json.ts
tobjectTranslation strings from locales
titlestringPage title (for SEO)
descriptionstringPage 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.

The Odyssey Path The Odyssey Path

Footer preview area - actual footers are full-width

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

ClassEffectUse 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: 0 over 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: true for direct scroll-linking
  • Cursor tracking uses requestAnimationFrame with 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

SettingDurationFeel
"Subtle"0.4sLight smoothing, close to native
"Moderate"0.7sBalanced smooth scrolling
"Intense"1.2sHeavy 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