This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
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.
# 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- 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
/keystaticroute (auto-generated by@keystatic/astro) - Configuration in
keystatic.config.ts - Local storage mode (stores content in
src/content/) - Integrates with Astro Content Collections
- Admin UI at
The styling architecture uses Tailwind CSS 4.x with the new CSS-first configuration:
- Global styles are defined in
src/styles/global.cssusing@import "tailwindcss"and@plugin "daisyui" - The Tailwind Vite plugin is configured in
astro.config.mjs - DaisyUI components are available project-wide through the plugin system
- Component-scoped styles can be added in
<style>tags within.astrofiles
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)
- 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
-
Layout Usage: Pages should import and wrap content in
Layout.astro:--- import Layout from "../layouts/Layout.astro"; --- <Layout> <!-- Page content --> </Layout>
-
Global CSS Import: Only import
global.cssin the main layout to avoid duplicate Tailwind imports -
Styling Priority: Use Tailwind utility classes first, then DaisyUI components, and component-scoped
<style>tags for custom styling only when necessary -
TypeScript: Astro's strict TypeScript config is enabled - expect type checking on component props and imports
Bloomfolio is designed as a portfolio website template with the following sections and components:
- Title
- Description
- Avatar
- Title
- Description
- Company Name
- Position
- Position Description
- Period (e.g., May 2012 - Feb 2020)
- University Name
- Course Name
- Description
- Period (e.g., May 2012 - Feb 2020)
- Link to college website
- Image
- Title
- Period (e.g., May 2012 - Feb 2020)
- Description
- Skills
- Link Demo
- Link Source
- Period (e.g., Nov 23rd - 25th, 2018)
- Title
- Location
- Description
- Link Source
Contact information
- Format: Markdoc (
.mdfiles) - Image
- Title
- Publish Date
- Content (supports Markdoc tags for Spotify, YouTube, Twitter embeds)
Bloomfolio uses Keystatic - a Git-based headless CMS that provides a visual content editor while keeping content as files in the repository.
βββββββββββββββββββββββ
β 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 β
βββββββββββββββββββββββββββββββ
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
.mdocfiles
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 */ }),
},
}),
},
});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
Development:
npm run dev
# Access at: http://localhost:4321/keystaticThe /keystatic route is auto-generated by the Keystatic Astro integration. No manual page creation needed.
Requirements:
- Server mode must be enabled (
output: "server"inastro.config.mjs) @keystatic/astromust be in integrations array
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:
- Create GitHub OAuth App: https://github.com/settings/developers
- Copy
.env.exampleto.env - Fill in environment variables
- Restart dev server
- Sign in at
/keystaticwith GitHub
Learn more: Keystatic GitHub Mode
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.
To add a new content type:
-
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 }, }), }, });
-
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, /* ... */ };
-
Create directory:
mkdir src/content/newType
-
Query in components:
import { getCollection } from "astro:content"; const entries = await getCollection("newType");
Custom Markdoc components (Spotify, YouTube, Twitter) follow a three-part pattern:
-
Keystatic Definition (form fields in editor):
// In keystatic.config.ts components: { YouTube: block({ label: "YouTube Video", schema: { url: fields.text({ label: "Video URL" }), }, }), }
-
Markdoc Registration (tag rendering):
// In markdoc.config.mjs tags: { YouTube: { render: component('./src/components/YouTube.astro'), attributes: { url: { type: String } }, }, }
-
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
When building components or implementing features, always use the available MCP tools to search documentation:
-
Astro Documentation: Use
mcp__astro-docs__search_astro_docsto search official Astro framework documentation- Use for: routing, components, content collections, layouts, data fetching, SSR/SSG patterns
-
DaisyUI Documentation: Use
mcp__context7__resolve-library-idandmcp__context7__get-library-docsto 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.
Blog posts use Markdoc format (.md extension) which supports:
- Standard markdown syntax
- Custom tags for media embeds
- Image optimization via Astro assets
Three media components are available in blog posts via Markdoc tags:
Embed Spotify tracks, albums, playlists, or podcasts:
{% Spotify url="https://open.spotify.com/track/..." /%}
Embed YouTube videos using ID or URL:
{% YouTube id="video-id" /%}
{% YouTube url="https://youtube.com/watch?v=..." /%}
Embed tweets using URL or ID+username:
{% Twitter url="https://x.com/username/status/..." /%}
{% Twitter id="tweet-id" username="username" /%}
Media components follow Keystatic's content component system with a three-layer architecture:
-
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 }, }), }, })
-
Markdoc Layer (
markdoc.config.mjs)- Registers tags for rendering in
.mdocfiles - Maps tags to Astro components
- Example:
Spotify: { render: component('./src/components/Spotify.astro'), attributes: { url: { type: String, required: true }, }, }
- Registers tags for rendering in
-
Component Layer (
src/components/)- Astro components that render the actual HTML
- Receive attributes as props
- Example:
Spotify.astro,YouTube.astro,Twitter.astro
β οΈ Important Limitations:
- Markdoc components (Spotify, YouTube, Twitter) only work in the blog collection currently
- Files must use the
.mdocextension (not.md) for components to render- Standard
.mdfiles in the blog collection will not render components- Other collections (projects, hackathons, work, education) do not have components enabled
To add a new Markdoc component (e.g., Instagram embed):
-
Create Astro component (
src/components/Instagram.astro):--- interface Props { url: string; } const { url } = Astro.props; const embedUrl = /* transform URL */; --- <iframe src={embedUrl} />
-
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 }, }), }, }), }
-
Register in Markdoc (
markdoc.config.mjs):tags: { Instagram: { render: component('./src/components/Instagram.astro'), attributes: { url: { type: String, required: true }, }, }, }
To enable media components in other collections (e.g., projects, hackathons):
-
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" }), }, }), }, }), }, });
-
File Extension: Ensure files use
.mdocextension (e.g.,my-project.mdoc) -
Markdoc Config: Component registration in
markdoc.config.mjsis global - no changes needed -
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