Skip to content

Latest commit

Β 

History

History
646 lines (526 loc) Β· 18.6 KB

File metadata and controls

646 lines (526 loc) Β· 18.6 KB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Project Overview

Bloomfolio is an Astro-based portfolio template using Tailwind CSS 4.x and DaisyUI for styling. The project follows Astro's standard project structure with file-based routing.

Development Commands

# Install dependencies
npm install

# Start development server (localhost:4321)
npm run dev

# Build for production (outputs to ./dist/)
npm run build

# Preview production build locally
npm run preview

# Run Astro CLI commands
npm run astro -- <command>

# Type checking
npm run astro check

Architecture

Framework & Build Setup

  • Astro 5.x: Static site generator with component islands architecture
  • Tailwind CSS 4.x: Integrated via Vite plugin (@tailwindcss/vite)
  • DaisyUI: Loaded as Tailwind plugin in src/styles/global.css
  • TypeScript: Strict mode enabled via astro/tsconfigs/strict
  • Keystatic CMS: Git-based headless CMS for content management
    • Admin UI at /keystatic route (auto-generated by @keystatic/astro)
    • Configuration in keystatic.config.ts
    • Local storage mode (stores content in src/content/)
    • Integrates with Astro Content Collections

Styling System

The styling architecture uses Tailwind CSS 4.x with the new CSS-first configuration:

  1. Global styles are defined in src/styles/global.css using @import "tailwindcss" and @plugin "daisyui"
  2. The Tailwind Vite plugin is configured in astro.config.mjs
  3. DaisyUI components are available project-wide through the plugin system
  4. Component-scoped styles can be added in <style> tags within .astro files

Project Structure

src/
β”œβ”€β”€ assets/          # Static assets (images, SVGs)
β”œβ”€β”€ components/      # Reusable Astro components
β”œβ”€β”€ layouts/         # Layout templates (wraps page content)
β”œβ”€β”€ pages/           # File-based routing (each file = route)
└── styles/          # Global CSS (Tailwind + DaisyUI imports)

Component Architecture

  • Layouts (src/layouts/): Base HTML structure, imports global CSS, defines <slot /> for page content
  • Pages (src/pages/): Map directly to routes, import layouts and components
  • Components (src/components/): Reusable UI elements with isolated scopes

Key Patterns

  1. Layout Usage: Pages should import and wrap content in Layout.astro:

    ---
    import Layout from "../layouts/Layout.astro";
    ---
    <Layout>
      <!-- Page content -->
    </Layout>
  2. Global CSS Import: Only import global.css in the main layout to avoid duplicate Tailwind imports

  3. Styling Priority: Use Tailwind utility classes first, then DaisyUI components, and component-scoped <style> tags for custom styling only when necessary

  4. TypeScript: Astro's strict TypeScript config is enabled - expect type checking on component props and imports

Template Structure

Bloomfolio is designed as a portfolio website template with the following sections and components:

Hero Section (Component)

  • Title
  • Description
  • Avatar

About Section (Component)

  • Title
  • Description

Work Section (Component + Content Loaded)

  • Company Name
  • Position
  • Position Description
  • Period (e.g., May 2012 - Feb 2020)

Education Section (Component + Content Loaded)

  • University Name
  • Course Name
  • Description
  • Period (e.g., May 2012 - Feb 2020)
  • Link to college website

Projects Section (Component + Content Loaded)

  • Image
  • Title
  • Period (e.g., May 2012 - Feb 2020)
  • Description
  • Skills
  • Link Demo
  • Link Source

Hackathon Section (Component + Content Loaded)

  • Period (e.g., Nov 23rd - 25th, 2018)
  • Title
  • Location
  • Description
  • Link Source

Contact Section (Component)

Contact information

Blog Page (Page + Content Loaded)

  • Format: Markdoc (.md files)
  • Image
  • Title
  • Publish Date
  • Content (supports Markdoc tags for Spotify, YouTube, Twitter embeds)

Content Management Architecture

Keystatic CMS Integration

Bloomfolio uses Keystatic - a Git-based headless CMS that provides a visual content editor while keeping content as files in the repository.

Architecture Overview

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Keystatic Admin    β”‚
β”‚  (/keystatic route) β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
           β”‚
           β”œβ”€ Edit content via forms
           β”‚
           β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  keystatic.config.ts        β”‚
β”‚  - Defines content schemas  β”‚
β”‚  - Configures collections   β”‚
β”‚  - Registers components     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
           β”‚
           β”œβ”€ Saves to filesystem
           β”‚
           β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  src/content/               β”‚
β”‚  - hero/index.yaml          β”‚
β”‚  - about/about.md           β”‚
β”‚  - blog/*.md/*.mdoc         β”‚
β”‚  - projects/*.md            β”‚
β”‚  - work/*.md                β”‚
β”‚  - education/*.md           β”‚
β”‚  - hackathons/*.md          β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
           β”‚
           β”œβ”€ Validated by Astro
           β”‚
           β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  src/content.config.ts      β”‚
β”‚  - Zod schemas              β”‚
β”‚  - Type generation          β”‚
β”‚  - Content Collections API  β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Key Integration Points

1. Configuration File (keystatic.config.ts)

  • Defines all content types using Keystatic's schema API
  • Configures field types, validation, and storage paths
  • Registers custom content components (Spotify, YouTube, Twitter)
  • Sets storage mode (currently local)

2. Astro Integration (astro.config.mjs)

import keystatic from "@keystatic/astro";

export default defineConfig({
  integrations: [keystatic()],
  output: "server",  // Required for Keystatic
});

3. Content Collections (src/content.config.ts)

  • Mirrors Keystatic schemas using Zod
  • Provides type safety for content queries
  • Enables Astro's Content Collections API
  • Validates content at build time

4. Markdoc Configuration (markdoc.config.mjs)

  • Registers custom tags for rich media embeds
  • Maps Keystatic content components to Astro components
  • Enables media embeds in .mdoc files

Content Schema Patterns

Singletons (Single-instance content):

// keystatic.config.ts
hero: singleton({
  label: "Hero Section",
  path: "src/content/hero/",
  schema: {
    name: fields.text({ label: "Name" }),
    avatar: fields.image({
      directory: "src/assets/hero",
      publicPath: "@assets/hero/",
    }),
  },
});

Collections (Multi-instance content):

// keystatic.config.ts
blog: collection({
  label: "Blog Posts",
  path: "src/content/blog/**",
  slugField: "title",
  format: { contentField: "content" },
  schema: {
    title: fields.slug({ name: { label: "Post Title" } }),
    content: fields.markdoc({
      label: "Content",
      components: {
        Spotify: block({ /* config */ }),
        YouTube: block({ /* config */ }),
        Twitter: block({ /* config */ }),
      },
    }),
  },
});

Storage Strategy

File Formats:

  • .yaml - Singletons without Markdown content (hero)
  • .md - Markdown content (about, work, education, projects, hackathons, simple blog posts)
  • .mdoc - Markdoc content with component support (blog posts with embeds)

Image Handling:

  • Stored in src/assets/[collection-name]/
  • Referenced with @assets/ prefix
  • Automatically optimized by Astro's image service
  • Uploaded via Keystatic's image field

Path Structure:

src/content/
β”œβ”€β”€ hero/index.yaml              # Singleton
β”œβ”€β”€ about/about.md               # Singleton
β”œβ”€β”€ blog/
β”‚   β”œβ”€β”€ post-1.md               # Collection entry
β”‚   β”œβ”€β”€ post-2.mdoc             # With components
β”‚   └── guides/
β”‚       └── advanced.mdoc       # Nested path
└── projects/
    β”œβ”€β”€ project-1.md            # Collection entry
    └── project-2.md

Accessing Keystatic Admin

Development:

npm run dev
# Access at: http://localhost:4321/keystatic

The /keystatic route is auto-generated by the Keystatic Astro integration. No manual page creation needed.

Requirements:

  • Server mode must be enabled (output: "server" in astro.config.mjs)
  • @keystatic/astro must be in integrations array

GitHub Mode Configuration

Storage Modes:

Keystatic supports two storage modes in this project:

Local Mode (Default):

storage: { kind: "local" }
  • Content stored in src/content/ directory
  • No authentication required
  • Perfect for local development
  • Changes committed directly to repository

GitHub Mode (Optional):

storage: {
  kind: "github",
  repo: {
    owner: import.meta.env.PUBLIC_KEYSTATIC_REPO_OWNER!,
    name: import.meta.env.PUBLIC_KEYSTATIC_REPO_NAME!,
  },
}
  • Content synced with GitHub repository
  • Requires GitHub OAuth authentication
  • Enables remote editing from anywhere
  • Creates pull requests or commits based on permissions

Automatic Mode Detection:

The configuration uses conditional storage based on environment variables:

storage: import.meta.env.PUBLIC_KEYSTATIC_GITHUB_APP_SLUG
  ? { kind: "github", repo: { ... } }
  : { kind: "local" }

Environment Variables Required for GitHub Mode:

  • KEYSTATIC_GITHUB_CLIENT_ID - OAuth app client ID (server-side only)
  • KEYSTATIC_GITHUB_CLIENT_SECRET - OAuth app secret (server-side only)
  • KEYSTATIC_SECRET - Random secret for cookie signing (server-side only)
  • PUBLIC_KEYSTATIC_REPO_OWNER - GitHub username/organization (PUBLIC_ prefix required)
  • PUBLIC_KEYSTATIC_REPO_NAME - Repository name (PUBLIC_ prefix required)
  • PUBLIC_KEYSTATIC_GITHUB_APP_SLUG - GitHub App slug to enable GitHub mode (PUBLIC_ prefix required)

Important: Variables prefixed with PUBLIC_ are accessible in import.meta.env (required for client-side config). Variables without PUBLIC_ are only available server-side.

Setup Instructions:

  1. Create GitHub OAuth App: https://github.com/settings/developers
  2. Copy .env.example to .env
  3. Fill in environment variables
  4. Restart dev server
  5. Sign in at /keystatic with GitHub

Learn more: Keystatic GitHub Mode

Keystatic vs. Direct Editing

Use Keystatic when:

  • Non-technical users need to edit content
  • You want form validation and field constraints
  • Live preview is helpful
  • Uploading images through a UI is preferred

Use direct file editing when:

  • Making bulk content changes
  • Writing complex Markdown with your preferred editor
  • Scripting content generation
  • You prefer version control diffs to be clean

Both approaches work together - changes made in files appear in Keystatic and vice versa.

Adding New Content Types

To add a new content type:

  1. Define in Keystatic (keystatic.config.ts):

    export default config({
      collections: {
        newType: collection({
          label: "New Type",
          path: "src/content/newType/*",
          slugField: "title",
          schema: {
            title: fields.slug({ name: { label: "Title" } }),
            // ... more fields
          },
        }),
      },
    });
  2. Mirror in Astro (src/content.config.ts):

    const newType = defineCollection({
      loader: glob({ pattern: "**/*.md", base: "./src/content/newType" }),
      schema: z.object({
        title: z.string(),
        // ... matching schema
      }),
    });
    
    export const collections = { newType, /* ... */ };
  3. Create directory:

    mkdir src/content/newType
  4. Query in components:

    import { getCollection } from "astro:content";
    const entries = await getCollection("newType");

Content Component System

Custom Markdoc components (Spotify, YouTube, Twitter) follow a three-part pattern:

  1. Keystatic Definition (form fields in editor):

    // In keystatic.config.ts
    components: {
      YouTube: block({
        label: "YouTube Video",
        schema: {
          url: fields.text({ label: "Video URL" }),
        },
      }),
    }
  2. Markdoc Registration (tag rendering):

    // In markdoc.config.mjs
    tags: {
      YouTube: {
        render: component('./src/components/YouTube.astro'),
        attributes: { url: { type: String } },
      },
    }
  3. Astro Component (actual implementation):

    ---
    // src/components/YouTube.astro
    const { url } = Astro.props;
    ---
    <iframe src={embedUrl} />

To add new components, update all three files following this pattern.

Learn more: Keystatic Documentation

Development Workflow

MCP Tools for Documentation

When building components or implementing features, always use the available MCP tools to search documentation:

  1. Astro Documentation: Use mcp__astro-docs__search_astro_docs to search official Astro framework documentation

    • Use for: routing, components, content collections, layouts, data fetching, SSR/SSG patterns
  2. DaisyUI Documentation: Use mcp__context7__resolve-library-id and mcp__context7__get-library-docs to search DaisyUI component documentation

    • Use for: UI components, theming, component props, styling patterns
    • First resolve the library ID, then fetch the documentation with specific topics

Important: Always consult these documentation sources before implementing features to ensure best practices and correct API usage.

Working with Markdoc

Blog Content Format

Blog posts use Markdoc format (.md extension) which supports:

  • Standard markdown syntax
  • Custom tags for media embeds
  • Image optimization via Astro assets

Available Media Components

Three media components are available in blog posts via Markdoc tags:

Spotify

Embed Spotify tracks, albums, playlists, or podcasts:

{% Spotify url="https://open.spotify.com/track/..." /%}

YouTube

Embed YouTube videos using ID or URL:

{% YouTube id="video-id" /%}
{% YouTube url="https://youtube.com/watch?v=..." /%}

Twitter

Embed tweets using URL or ID+username:

{% Twitter url="https://x.com/username/status/..." /%}
{% Twitter id="tweet-id" username="username" /%}

Component Configuration Architecture

Media components follow Keystatic's content component system with a three-layer architecture:

  1. Keystatic Layer (keystatic.config.ts)

    • Defines component form fields shown in the editor
    • Uses block() helper to create content components
    • Example:
      Spotify: block({
        label: "Spotify Embed",
        schema: {
          url: fields.text({
            label: "Spotify URL",
            validation: { isRequired: true },
          }),
        },
      })
  2. Markdoc Layer (markdoc.config.mjs)

    • Registers tags for rendering in .mdoc files
    • Maps tags to Astro components
    • Example:
      Spotify: {
        render: component('./src/components/Spotify.astro'),
        attributes: {
          url: { type: String, required: true },
        },
      }
  3. Component Layer (src/components/)

    • Astro components that render the actual HTML
    • Receive attributes as props
    • Example: Spotify.astro, YouTube.astro, Twitter.astro

Component Limitations

⚠️ Important Limitations:

  • Markdoc components (Spotify, YouTube, Twitter) only work in the blog collection currently
  • Files must use the .mdoc extension (not .md) for components to render
  • Standard .md files in the blog collection will not render components
  • Other collections (projects, hackathons, work, education) do not have components enabled

Adding New Content Components

To add a new Markdoc component (e.g., Instagram embed):

  1. Create Astro component (src/components/Instagram.astro):

    ---
    interface Props {
      url: string;
    }
    const { url } = Astro.props;
    const embedUrl = /* transform URL */;
    ---
    <iframe src={embedUrl} />
  2. Register in Keystatic (keystatic.config.ts):

    // In blog collection's content field
    components: {
      Instagram: block({
        label: "Instagram Post",
        schema: {
          url: fields.text({
            label: "Instagram URL",
            validation: { isRequired: true },
          }),
        },
      }),
    }
  3. Register in Markdoc (markdoc.config.mjs):

    tags: {
      Instagram: {
        render: component('./src/components/Instagram.astro'),
        attributes: {
          url: { type: String, required: true },
        },
      },
    }

Extending Components to Other Collections

To enable media components in other collections (e.g., projects, hackathons):

  1. Update Collection Schema in keystatic.config.ts:

    projects: collection({
      label: "Projects",
      path: "src/content/projects/*",
      format: { contentField: "content" },  // Must specify content field
      schema: {
        title: fields.slug({ name: { label: "Title" } }),
        // ... other fields
        content: fields.markdoc({
          label: "Content",
          components: {
            // Add components you want available in projects
            Spotify: block({
              label: "Spotify Embed",
              schema: {
                url: fields.text({ label: "Spotify URL" }),
              },
            }),
            YouTube: block({
              label: "YouTube Video",
              schema: {
                url: fields.text({ label: "YouTube URL" }),
              },
            }),
          },
        }),
      },
    });
  2. File Extension: Ensure files use .mdoc extension (e.g., my-project.mdoc)

  3. Markdoc Config: Component registration in markdoc.config.mjs is global - no changes needed

  4. Usage: Components will now appear in Keystatic's editor for that collection

Important Notes:

  • Component names must match across all three layers (Keystatic, Markdoc, Astro)
  • Only collections with fields.markdoc() content fields support components
  • Components appear in Keystatic's content editor as insertable blocks
  • Changes to component schemas require dev server restart

Learn more: Keystatic Content Components