Revista is a photography portfolio and blog built on Astro v5.13.3. I created it to showcase various photography collections and writing organized into different categories like long-form, short-form, muses, zeitweilig, and my CV. The project prioritizes speed and visual design while using Astro's content collection API to manage everything efficiently.
The project supports multiple deployment targets with optimized builds for each platform, including GitHub Pages with proper base path configuration.
Project Structure Diagram (click to expand)
graph TD
classDef root fill:#f9f7f3,stroke:#333,stroke-width:2px
classDef mainDir fill:#f2e9de,stroke:#333,stroke-width:1px
A["/revista" Root] --> B["📁 src"]
A --> C["📁 public<br>(static assets)"]
A --> D["⚙️ Configuration Files<br>(astro.config, tailwind.config)"]
class A root
class B,C,D mainDir
graph TD
classDef mainDir fill:#f2e9de,stroke:#333,stroke-width:1px
classDef contentDir fill:#e9d8c4,stroke:#333,stroke-width:1px
B["📁 src"] --> E["📁 components<br>(UI building blocks)"]
B --> F["📁 layouts<br>(page templates)"]
B --> G["📁 pages<br>(routes)"]
B --> H["📁 content<br>(markdown collections)"]
B --> I["📁 styles<br>(CSS)"]
B --> J["📁 scripts<br>(client JS)"]
H --> K["📝 long_form<br>(articles)"]
H --> L["📝 short_form<br>(quick posts)"]
H --> M["📝 muses<br>(photography)"]
H --> N["📝 zeitweilig<br>(temporary thoughts)"]
H --> O["📝 authors<br>(contributor info)"]
H --> P["📝 cv<br>(resume data)"]
class B,E,F,G,H,I,J mainDir
class K,L,M,N,O,P contentDir
graph TD
classDef compFile fill:#e0f0e3,stroke:#333,stroke-width:1px
classDef layoutFile fill:#e3e0f0,stroke:#333,stroke-width:1px
classDef pageFile fill:#f0e0e3,stroke:#333,stroke-width:1px
classDef styleFile fill:#f0e3e0,stroke:#333,stroke-width:1px
classDef scriptFile fill:#e0e3f0,stroke:#333,stroke-width:1px
E["📁 components"] --> E1["🧩 BlogPost.astro"]
E --> E2["🧩 Footer.astro"]
E --> E3["🧩 Header.astro"]
E --> E4["🧩 Navigation.astro"]
F["📁 layouts"] --> F1["📄 BaseLayout.astro"]
F --> F2["📄 MarkdownPostLayout.astro"]
G["📁 pages"] --> G1["🌐 index.astro<br>(homepage)"]
G --> G2["🌐 404.astro<br>(error page)"]
G --> G3["🌐 cv.astro<br>(resume)"]
I["📁 styles"] --> I1["🎨 global.css<br>(site-wide styles)"]
I --> I2["🎨 MasonryLayout.css<br>(photo grid styling)"]
J["📁 scripts"] --> J1["⚡ menu.js<br>(mobile navigation)"]
J --> J2["⚡ themetoggle.js<br>(dark/light mode)"]
class E1,E2,E3,E4 compFile
class F1,F2 layoutFile
class G1,G2,G3 pageFile
class I1,I2 styleFile
class J1,J2 scriptFile
src/
: Contains the main source code for the sitecomponents/
: Reusable Astro components (Components Documentation)BlogPost.astro
: Component for rendering individual blog post previewsFooter.astro
: Site-wide footer componentHeader.astro
: Site-wide header componentNavigation.astro
: Navigation menu component
layouts/
: Page layouts used across the site (Layouts Documentation)BaseLayout.astro
: The main layout used by most pagesMarkdownPostLayout.astro
: Layout for rendering Markdown content
pages/
: Astro pages that generate routes (Pages Documentation)index.astro
: The home page404.astro
: Custom 404 error pagecv.astro
: CV page
content/
: Markdown content for blog posts and collections (Content Collections Documentation)- Architecture and implementation documentation:
- Technical Architecture: Component structure, state management, and design patterns
- Performance Optimization: Techniques used for site speed optimization
- Docker Implementation: Container configuration and deployment
- CI/CD Implementation: Build and deployment automation
content.config.ts
: Configuration file for content collections using Astro's glob loader patternstyles/
: CSS files for stylingglobal.css
: Global styles and Tailwind v4 importsMasonryLayout.css
: Styles for the masonry layout used in galleries
scripts/
: JavaScript files for client-side functionalitymenu.js
: Handles mobile menu functionalitythemetoggle.js
: Manages dark/light theme toggling
public/
: Static assets like images and fonts- Configuration files:
astro.config.mjs
: Astro configurationtailwind.config.mjs
: Tailwind CSS configurationtsconfig.json
: TypeScript configuration
-
Multiple Content Collections: The site organizes content into different types (long_form, short_form, muses, zeitweilig, authors, cv), each managed as an Astro content collection using the glob loader pattern. This gives me type-safe content management, explicit file selection, and simplified querying.
-
Responsive Design: The site uses Tailwind CSS for a mobile-first approach. I've customized the breakpoints to match my specific needs at 800px, 1200px, 1900px, 2500px, and 3800px, which ensures the site looks good on everything from phones to ultra-wide monitors.
-
Dark Mode: Users can toggle between light and dark themes with the ThemeToggle component. Theme preference is stored in localStorage so it persists across visits. The dark theme uses a deep charcoal background with light text for comfortable reading at night.
-
Dynamic Routing: Routes are generated from the content collections themselves. Each post and tag gets its own URL automatically, making content organization much simpler.
-
RSS Feeds: Each content collection has its own RSS feed. I use
@astrojs/rss
to generate these dynamically, so readers can subscribe to just the content types they're interested in. -
SEO Optimization: Every page includes customizable meta tags for titles, descriptions, and Open Graph data, which helps with search engine visibility and social sharing.
-
Performance Focus: Astro's static site generation gives the site exceptional loading times. I've also implemented lazy loading for images and prefetching for linked pages to make navigation feel instantaneous.
-
Interactive Elements: The site uses targeted client-side JavaScript for the mobile menu, theme toggle, and image lightbox functionality, keeping the bundle size small while adding important interactivity.
-
Custom 404 Page: I created a unique 404 error page featuring rotating quotes from Ron Burgundy – a little humor to lighten the mood when someone hits a missing page.
-
CV Section: The site includes a dedicated CV page, which shows how this platform works not just for photography and writing but also for personal branding.
All content lives in Markdown files located in the src/content/
directory. Each content type has its own subdirectory.
The project includes custom CLI tools for creating and managing content:
# Development server
bun run dev
# Standard production build
bun run build
# GitHub Pages specific build (includes base path configuration)
bun run build:github-pages
# Preview production build
bun run preview
# Run the content creator
bun run create
# Specify content type directly
bun run create -t muses
# Preview frontmatter without creating a file (dry run)
bun run create --dry-run
# or
bun run create -d
# Show help for all options
bun run create --help
# Non-interactive mode (for scripts or automated workflows)
bun run create --non-interactive --type muses --title "Post Title" --description "Post description" --tags "tag1,tag2" --pub-date "2024-05-19T12:00:00Z" --updated-date "2024-05-20T10:00:00Z"
This interactive tool:
- Dynamically reads schema requirements from content.config.ts
- Provides a user-friendly interface with colored prompts
- Validates input according to schema requirements
- Generates proper filenames using date-slug.mdx pattern (uses pubDate for the filename when provided)
- Supports all content types: muses, short_form, long_form, zeitweilig, authors, cv
# Update an existing post's frontmatter (e.g., add/modify updated date)
bun run update-post --file muses/2025-05-19-commodification.mdx --updated-date "2025-05-20T12:00:00Z"
# Preview changes without writing to file
bun run update-post --file short_form/2025-05-19-the-essence-of-light.mdx --tags "photography,light,art,philosophy" --dry-run
# Update multiple fields at once
bun run update-post --file muses/2025-05-19-commodification.mdx \
--title "New Title" \
--tags "photography,art,economics,critique" \
--updated-date "2025-05-20T08:15:00Z"
This tool allows you to:
- Update publication or update dates
- Change tags or categories
- Update image metadata
- Modify titles or descriptions
- Preview changes before applying them
For detailed documentation on both tools, see scripts/README.md.
Content Management Diagram (click to expand)
graph TD
classDef rootDir fill:#f5f5f5,stroke:#333,stroke-width:2px
classDef contentType fill:#e8f4f8,stroke:#333,stroke-width:1px
A["📁 content/"] --> B["📁 long_form/<br><i>in-depth articles</i>"]
A --> C["📁 short_form/<br><i>brief posts</i>"]
A --> D["📁 muses/<br><i>photo collections</i>"]
A --> E["📁 zeitweilig/<br><i>ephemeral content</i>"]
A --> F["📁 authors/<br><i>contributor profiles</i>"]
A --> G["📁 cv/<br><i>professional info</i>"]
class A rootDir
class B,C,D,E,F,G contentType
graph TD
classDef contentType fill:#e8f4f8,stroke:#333,stroke-width:1px
classDef mdFile fill:#f8f4e8,stroke:#333,stroke-width:1px
B["📁 long_form/"] --> H["📄 iceland-trip.mdx<br><i>frontmatter + markdown</i>"]
B --> I["📄 camera-review.mdx<br><i>frontmatter + markdown</i>"]
C["📁 short_form/"] --> J["📄 new-lens.mdx<br><i>frontmatter + markdown</i>"]
C --> K["📄 photo-tip.mdx<br><i>frontmatter + markdown</i>"]
class B,C contentType
class H,I,J,K mdFile
graph TD
classDef contentType fill:#e8f4f8,stroke:#333,stroke-width:1px
classDef mdFile fill:#f8f4e8,stroke:#333,stroke-width:1px
D["📁 muses/"] --> L["📄 urban-geometry.mdx<br><i>photo gallery post</i>"]
E["📁 zeitweilig/"] --> M["📄 thoughts-on-bw.mdx<br><i>creative exploration</i>"]
F["📁 authors/"] --> N["📄 about-me.mdx<br><i>author bio</i>"]
G["📁 cv/"] --> O["📄 resume.mdx<br><i>professional experience</i>"]
class D,E,F,G contentType
class L,M,N,O mdFile
Each content collection is defined with a specific schema in content.config.ts
using Zod for validation. Here's a simplified example of the frontmatter structure:
// Example collection schema in content.config.ts
const muses = defineCollection({
loader: glob({ pattern: "**\/[^_]*.mdx", base: "./src/content/muses" }),
schema: z.object({
title: z.string(),
tags: z.array(z.string()),
author: z.string(),
description: z.string(),
image: z.object({
src: z.string(),
alt: z.string(),
positionx: z.string().optional(),
positiony: z.string().optional(),
}).optional(),
pubDate: z.coerce.date(),
updatedDate: z.coerce.date().optional(),
}),
});
// Example frontmatter from an actual muses post:
---
title: "Stockholm: Urban Reflections"
tags: ["sweden", "architecture", "street", "reflection"]
author: "Erfi Anugrah"
description: "A winter wander through Stockholm's glass-filled business district"
image:
src: "https://cdn.erfianugrah.com/stockholm-reflections-01.jpg"
alt: "Office building reflection with stark contrast on a winter day"
positionx: "center"
positiony: "top-33"
pubDate: 2024-01-21
---
My weekend explorations took me to Kungsholmen, where the lowering sun
creates dramatic shadows across the sleek glass facades of Stockholm's
business district...
Each Markdown file includes frontmatter with metadata like title, publication date, tags, and image information. I define the content collections in src/content.config.ts
, which specifies the schema using Zod for runtime type checking and uses Astro's glob loader pattern to identify which files belong to each collection.
Revista uses a mix of file-based routing and dynamic route generation:
Routing Diagram (click to expand)
graph TD
classDef rootRoute fill:#f8f8f8,stroke:#333,stroke-width:2px
classDef staticRoute fill:#f0f8ff,stroke:#333,stroke-width:1px
A["🏠 www.erfianugrah.com<br>(Root)"] --> B["❌ /404<br>(Custom error page)"]
A --> C["👤 /authors<br>(Contributor profiles)"]
A --> D["📋 /cv<br>(Resume page)"]
A --> E["📚 /long_form<br>(Article index)"]
A --> F["📝 /short_form<br>(Brief posts index)"]
A --> G["🖼️ /muses<br>(Photography index)"]
A --> H["⏳ /zeitweilig<br>(Ephemeral content)"]
%% RSS feeds
E -.-> E0["📡 /long_form/rss.xml"]
F -.-> F0["📡 /short_form/rss.xml"]
G -.-> G0["📡 /muses/rss.xml"]
H -.-> H0["📡 /zeitweilig/rss.xml"]
class A rootRoute
class B,C,D,E,F,G,H staticRoute
class E0,F0,G0,H0 staticRoute
graph TD
classDef staticRoute fill:#f0f8ff,stroke:#333,stroke-width:1px
classDef dynamicRoute fill:#fff0f5,stroke:#333,stroke-width:1px
classDef tagRoute fill:#f5fff0,stroke:#333,stroke-width:1px
%% Long-form routes
E["📚 /long_form"] --> I["📄 /long_form/[post-slug]<br>(Individual article pages)"]
E --> J["🏷️ /long_form/tags<br>(Tags index)"]
J --> K["🔖 /long_form/tags/[tag]<br>(Articles with specific tag)"]
%% Short-form routes
F["📝 /short_form"] --> L["📄 /short_form/[post-slug]<br>(Individual post pages)"]
F --> M["🏷️ /short_form/tags<br>(Tags index)"]
M --> N["🔖 /short_form/tags/[tag]<br>(Posts with specific tag)"]
class E,F staticRoute
class I,L dynamicRoute
class J,K,M,N tagRoute
graph TD
classDef staticRoute fill:#f0f8ff,stroke:#333,stroke-width:1px
classDef dynamicRoute fill:#fff0f5,stroke:#333,stroke-width:1px
classDef tagRoute fill:#f5fff0,stroke:#333,stroke-width:1px
%% Muses routes
G["🖼️ /muses"] --> O["🖼️ /muses/[post-slug]<br>(Individual gallery pages)"]
G --> P["🏷️ /muses/tags<br>(Tags index)"]
P --> Q["🔖 /muses/tags/[tag]<br>(Galleries with specific tag)"]
%% Zeitweilig routes
H["⏳ /zeitweilig"] --> R["📄 /zeitweilig/[post-slug]<br>(Individual content pages)"]
H --> S["🏷️ /zeitweilig/tags<br>(Tags index)"]
S --> T["🔖 /zeitweilig/tags/[tag]<br>(Content with specific tag)"]
class G,H staticRoute
class O,R dynamicRoute
class P,Q,S,T tagRoute
The routing system combines static and dynamic routes:
- Static routes like
/muses
are defined by files atsrc/pages/muses.astro
- Dynamic routes like
/long_form/camera-review
are handled bysrc/pages/long_form/[...id].astro
- Collection pages use
getStaticPaths()
to generate routes from content collections - Tag pages are automatically generated for each tag used in the content
Each collection follows the same pattern of routes: index, individual posts, tags index, and tag-specific pages.
-
Root and Static Routes:
/
: Home page (src/pages/index.astro
)/404
: Custom 404 error page (src/pages/404.astro
)/authors
: Authors page (src/pages/authors.astro
)/cv
: CV page (src/pages/cv.astro
)
-
Collection Routes: For each collection (long_form, short_form, muses, zeitweilig):
/{collection}
: Index page for the collection (src/pages/{collection}/index.astro
)/{collection}/post-id
: Individual post pages (src/pages/{collection}/[...id].astro
)/{collection}/tags
: Tag index for the collection (src/pages/{collection}/tags/index.astro
)/{collection}/tags/tag-name
: Pages for specific tags (src/pages/{collection}/tags/[tag].astro
)
-
Dynamic Route Generation:
- Post pages (e.g.,
/long_form/post-id
) are generated dynamically based on the content in the respective collection usinggetStaticPaths()
in[...id].astro
. - Tag pages (e.g.,
/long_form/tags/tag-name
) are generated for each unique tag used in the collection, also usinggetStaticPaths()
in[tag].astro
.
- Post pages (e.g.,
-
RSS Feeds:
- Each collection has an RSS feed available at
/{collection}/rss.xml
, generated byrss.xml.js
files in each collection's directory.
- Each collection has an RSS feed available at
The site uses Tailwind CSS v4.0.8 for styling, with carefully configured settings in tailwind.config.mjs
to create a cohesive design system:
-
Typography System
- Custom Fonts: The site uses two variable fonts for better performance and flexibility:
- "Overpass Mono Variable": A monospace font for code, technical details, and headers
- "Inconsolata Variable": A secondary monospace used for specific UI elements
- These fonts were chosen for their:
- Technical, precise aesthetic that complements photography
- Excellent readability at different sizes
- Variable font support for optimal performance
- Wide character set support
- Custom Fonts: The site uses two variable fonts for better performance and flexibility:
-
Color System
- Base Light Theme: Clean white background (#f5f5f5) with deep charcoal text (#333333)
- Dark Theme: Rich dark background (#222125) with high-contrast light text (#f5f5f5)
- Accent Colors: Minimal use of accent colors, focusing on photography as the visual focus
- Photography-Optimized: The color scheme is designed to enhance rather than compete with images
-
Layout System
- Photography-Specific Breakpoints: Custom breakpoints designed for optimal image viewing:
// tailwind.config.mjs screens: { 'sm': '800px', // Small devices (tablets) 'md': '1200px', // Medium devices (laptops) 'lg': '1900px', // Large devices (desktops) 'xl': '2500px', // Extra large (large monitors) '2xl': '3800px', // Ultra-wide displays }
- These breakpoints are significantly different from Tailwind defaults, prioritizing photography display over conventional web design breakpoints
- Photography-Specific Breakpoints: Custom breakpoints designed for optimal image viewing:
-
Component Styling
- Custom Utilities: Extended Tailwind with utilities for:
extend: { objectPosition: { 'center-60': 'center 60%', 'top-33': 'center 33%', // Additional custom position values }, // Other extended utilities }
- Typography Plugin: The
@tailwindcss/typography
plugin provides rich styling for long-form content
- Custom Utilities: Extended Tailwind with utilities for:
-
Dark Mode Strategy
- Class-based Implementation: Using Tailwind's
class
strategy for theme switchingdarkMode: "class"; // Toggle with JavaScript using ThemeToggle.tsx
- Implementation:
ThemeToggle.tsx
React component controls the theme, adding/removing thedark
class on the document
- Class-based Implementation: Using Tailwind's
-
CSS Organization
-
Global Styles:
src/styles/global.css
contains:/* Base imports and Tailwind directives */ @import "tailwindcss/base"; @import "tailwindcss/components"; @import "tailwindcss/utilities"; /* Global custom styles */ :root { /* Custom CSS variables */ } /* Dark mode specific overrides */ .dark { /* Dark mode CSS variables */ }
-
Component-specific CSS:
MasonryLayout.css
: Custom grid-based implementationcv-print.css
: Print-specific styles for the CV pageglightbox.css
: Customized styling for the image lightbox
-
-
CSS-in-JS Integration
- The project uses minimal CSS-in-JS, primarily in the React components like
ThemeToggle.tsx
andHeroImage.tsx
, where dynamic styling is needed
- The project uses minimal CSS-in-JS, primarily in the React components like
- Component-First Approach: Styles are primarily applied using Tailwind utility classes in components
- Minimal Custom CSS: Custom CSS is only used for complex layouts that Tailwind can't easily handle
- Consistent Color Variables: Color references use CSS variables for theme consistency
- Media Query Standardization: All responsive designs use the custom breakpoint system
- Print Considerations: Special styling for PDF/print versions of content (especially CV)
Client-side JavaScript lives in the src/scripts/
directory, providing essential interactivity while maintaining a focus on performance:
-
themetoggle.js
: Manages dark/light theme switching with the following features:- Persists user preference in localStorage
- Respects user's system preference via
prefers-color-scheme
media query - Adds/removes the
dark
class on the document for Tailwind's dark mode - Updates all necessary UI elements when theme changes
-
menu.js
: Controls the mobile navigation menu:- Toggles visibility of the mobile menu overlay
- Handles animation timings for smooth transitions
- Manages accessibility attributes like
aria-expanded
- Implements touch events for mobile devices
-
lightbox.js
: Implements image gallery lightbox functionality using GLightbox:- Enables fullscreen image viewing
- Supports keyboard navigation
- Provides zoom and pan capabilities for images
- Implements swipe navigation on touch devices
- Shows image captions when available
-
getrandomimage.js
: Helper utility used by components to select random featured images- Used in both the homepage and tag pages
- Ensures images don't repeat in the same view
- Handles empty image arrays gracefully
-
burgundy.js
: Creates the dynamic quote system for the 404 page:- Stores a collection of Ron Burgundy quotes
- Randomly selects and displays a different quote on each page load
- Sets up a rotating quote system with fade transitions
-
rss.js
: Manages RSS subscription features:- Conditionally shows/hides RSS links based on the current page
- Updates RSS link URLs dynamically
- Provides visual feedback when subscription options are available
-
homePage.js
: Powers the dynamic homepage content:- Selects featured content from different collections
- Implements a weighted random selection algorithm for better variety
- Ensures fresh content appears on each page load
remark-reading-time.mjs
: MDX plugin that calculates and adds reading time estimates to postsremark-modified-time.mjs
: MDX plugin that extracts and normalizes file modification timestampsupdateImageLinks.js
: Build-time utility for processing and optimizing image references
All scripts are designed to be minimal, focused, and non-blocking to maintain the site's excellent performance profile.
I've optimized the site in several ways:
-
Image Processing: Using Astro's
getImage
function to convert images to efficient formats and appropriate dimensions. -
Lazy Loading: Images load on demand using the
loading="lazy"
attribute, which prevents initial page load delays. -
Preloading and Prefetching: Astro's
prefetch
feature loads linked pages before the user clicks, making navigation feel instant. -
Efficient Bundling: Astro v5.6.0 includes improved bundling and tree-shaking to minimize client-side code, with enhanced hydration strategies and faster component rendering.
-
Cloudflare CDN: The site uses Cloudflare's CDN with custom cache headers to serve content from edge locations worldwide.
-
Tailwind Optimizations: Tailwind CSS v4.0.8's improved performance and lighter bundle size help pages load quickly.
The site includes search powered by Pagefind, integrated into the Navigation.astro
component through the Pagefind.astro
component. This search implementation provides:
-
Comprehensive Content Indexing: Automatically indexes all site content during the build process (via a postbuild script defined in package.json)
-
Modal Search Interface: A clean, accessible modal dialog that appears when users click the search button
-
Dark Mode Support: Custom CSS variables in the Pagefind component ensure the search UI respects the site's dark/light theme setting
-
Sub-Results Display: Shows nested results for more detailed content exploration with the
showSubResults: true
option -
Keyboard Navigation: Supports keyboard focus and navigation for accessibility
-
Responsive Design: Adapts to different screen sizes with custom widths for mobile and desktop
The search functionality is implemented with minimal JavaScript and maintains the site's performance focus by loading the search UI assets only when needed.
// Example simplified implementation from Pagefind.astro
<button id="searchButton" class="text-lg pl-[30px] h-[50px]">Search</button>
<div id="myModal" class="modal">
<div class="modal-content bg-[rgb(245,245,245)] dark:bg-[rgb(34,33,37)]">
<span id="closeButton" class="close">×</span>
<div id="search" class="m-8" transition:persist></div>
</div>
</div>
<script>
document.addEventListener("astro:page-load", () => {
// Modal control logic
// ...
// Initialize Pagefind UI
new PagefindUI({
element: "#search",
showSubResults: true,
resetStyles: false,
});
});
</script>
While the site is currently in English, I've structured it with future translation in mind:
- The RSS feeds include language tags (
<language>en-us</language>
) - The content structure would easily support localized content in additional languages
-
Cloudflare:
- Handles hosting and CDN services
- The CI/CD pipeline includes cache purging to ensure visitors see the latest content
-
Deno Deploy:
- Provides a secondary deployment target
- Shows how the site can adapt to different hosting environments
-
GitHub Pages:
- Provides an additional deployment target using GitHub's native hosting
- Uses a separate build process with correct base path (
/revista-3
) configuration - Includes proper permissions and environment configuration for Pages deployment
- Maintains compatibility with other deployment targets through environment-specific builds
-
Bun:
- Works as both the JavaScript runtime and package manager
- Significantly faster than Node.js and npm, especially on M-series Macs
- All scripts in
package.json
run through Bun
-
TypeScript:
- The project uses TypeScript v5.7.2 throughout
- Astro's built-in TypeScript support with
@astrojs/check
v0.9.4 catches type errors during build
-
Prettier:
- Code formatting with Prettier v3.4.2 ensures consistent style
- The Astro Prettier plugin (prettier-plugin-astro v0.14.1) properly formats .astro files
-
Tailwind CSS v4:
- The latest Tailwind CSS v4.0.8 with better performance and smaller bundles
- Configured with the typography plugin for long-form content
The GitHub Actions workflow in .github/workflows/deploy.yml
handles deployment:
-
Build Process:
- Uses Bun for faster dependency installation and builds
- Implements dependency caching to speed up subsequent builds
- Includes retry logic in case of transient errors
-
Deno Deployment:
- Pushes the built site to Deno Deploy
-
Cloudflare Deployment:
- Deploys to Cloudflare Pages via Wrangler
-
GitHub Pages Deployment:
- Uses a dedicated build process with environment-specific configuration
- Builds independently with the correct site URL and base path for GitHub Pages
- Employs the
build:github-pages
npm script for proper routing - Maintains full compatibility with other deployment platforms
-
Docker Handling:
- Builds a multi-architecture Docker image for broader compatibility
- Pushes to Docker Hub for container-based deployments
- Signs the image with Cosign for security verification
-
Cache Management:
- Purges Cloudflare's edge cache after each deployment
The project includes Docker support for containerized deployment. For detailed information, see README.Docker.md.
The project's Dockerfile is straightforward:
# Using the lightweight Alpine variant of Caddy for better performance
FROM caddy:2.8.4-alpine
# Set the working directory for the site files
WORKDIR /usr/share/caddy
# Copy the Astro-built static files (from the 'dist' directory after 'bun run build')
COPY ./dist .
# Copy our custom Caddy configuration
COPY Caddyfile /etc/caddy/Caddyfile
# Set proper ownership and permissions for security
RUN chown -R root:root /usr/share/caddy && \
chmod -R 755 /usr/share/caddy && \
# Create Caddy-specific directories with proper permissions
mkdir -p /data/caddy /config/caddy && \
chmod 700 /data/caddy /config/caddy
# Expose the HTTP port (HTTPS is handled by Cloudflare in production)
EXPOSE 80
# Run Caddy with our custom config
CMD ["caddy", "run", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"]
My Caddyfile is quite simple, as I'm using Cloudflare as my edge CDN:
# Basic Caddyfile for the Revista site
:80 {
# Enable gzip compression
encode gzip
# Set cache control headers for better performance
header /* {
# Cache static assets for 1 week
Cache-Control "public, max-age=604800, must-revalidate"
# Security headers
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
X-Content-Type-Options "nosniff"
X-Frame-Options "DENY"
Referrer-Policy "strict-origin-when-cross-origin"
}
# Special cache settings for images
header /assets/* {
Cache-Control "public, max-age=2592000, must-revalidate"
}
# Serve the static site from the container's working directory
root * /usr/share/caddy
file_server
}
This setup:
- Uses Caddy as the web server on Alpine Linux for a small footprint
- Sets up proper permissions for security
- Configures caching and security headers
- Exposes port 80 (Cloudflare handles the HTTPS in production)
-
Docker Image Signing: The CI/CD pipeline signs Docker images with Cosign to prevent tampering.
-
Content Security: The RSS feed generation uses
sanitize-html
to prevent XSS vulnerabilities. -
Secure Hosting: Cloudflare provides DDoS protection, SSL, and other security features.
For local development, you'll need:
- Bun (latest version)
- Node.js (LTS version)
- Git
- VS Code with the Astro extension is recommended
To start working with this project:
-
Clone the repository:
git clone https://github.com/your-username/revista.git cd revista
-
Install dependencies:
bun install
This installs:
- Astro v5.13.3
- Tailwind CSS v4.0.8
- React v19.0.0
- MDX v4.2.4 and other dependencies
-
Run the development server:
bun run dev
-
Build for production:
# Standard build (for Cloudflare, Deno, Docker) bun run build # GitHub Pages specific build (with base path) bun run build:github-pages
Both commands include Pagefind indexing for search functionality.
-
Preview the production build:
bun run preview
The project supports several deployment methods:
- Cloudflare Pages (primary)
- Deno Deploy
- GitHub Pages
- Docker container (deployable to any container platform)
When contributing:
- Get familiar with Astro's content collections and routing
- Follow the existing code style and use Tailwind for styling
- Test your changes on various screen sizes
- Update or add tests for new features
- Update documentation when necessary
- Use Bun for running scripts and managing dependencies
If you run into problems:
- Make sure all dependencies are installed (
bun install
) - Try clearing the Astro cache (
.astro
directory) for build errors - Check the Astro Discord for help with common issues
- Verify that Bun is up to date
This project is licensed under the MIT License - see the LICENSE file for details.
Note: The blog content (posts, articles, images, etc.) is not covered by the MIT License. All rights to the content are reserved by the respective authors unless otherwise specified.
- The Astro community for building such a great static site generator
- Tailwind CSS for their utility-first approach
- Cloudflare for reliable hosting and CDN services
- Deno Deploy for providing an additional deployment option
- All contributors who have helped improve this project
For questions about this project, please open an issue on the GitHub repository.
Some ideas I'm considering for future updates:
- Full multilingual support
- Enhanced search with filtering options
- Integration with a headless CMS
- Automated image optimization workflow
- More interactive gallery views
The CV page uses a dedicated component system in src/components/cv/
to create a professional, interactive resume:
-
Component-Based Architecture: The CV is built from specialized components:
Section.astro
: Base container for each CV sectionCompany.astro
: Displays company information with logo and detailsTimeline.astro
: Visualizes position duration with color-coded barsSkillBar.astro
: Shows skill proficiency with visual indicatorsEducationTimeline.astro
: Specialized timeline for educational historyContact.astro
: Presents contact information with iconsColorLegend.astro
: Explains the timeline color coding system
-
Single Source of Truth: All CV content is maintained in a single file (
src/content/cv/resume.mdx
) with a structured schema:--- title: "Erfi Anugrah" description: "Photographer | Writer | Customer Solutions Engineer" contacts: - type: email value: [email protected] url: mailto:[email protected] companies: - name: "Cloudflare" positions: - title: "Senior Customer Solutions Engineer" dateRange: { start: "2024-10", end: "Present" } skills: - name: "HTML/CSS" level: "expert" education: - institution: "Nanyang Technological University" degree: "Bachelor of Business (Marketing)" ---
-
Print Optimization: Special CSS rules in
cv-print.css
ensure the CV looks professional when printed or exported as PDF -
Responsive Design: The CV layout adapts seamlessly from mobile to desktop with Tailwind's responsive utilities
For detailed implementation information, see src/components/cv/README.md
.
The photo gallery displays use a custom masonry layout implementation:
-
CSS Grid-Based Masonry: Instead of using a library, the site implements a modern CSS Grid approach to masonry layouts:
.masonry { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); grid-gap: 16px; grid-auto-flow: dense; } .image-container:nth-child(3n) { grid-row: span 2; } .image-container:nth-child(4n) { grid-column: span 2; }
-
Image Optimization: The
Masonry.astro
component uses Astro's built-in image optimization:const imageAssets = await Promise.all( images.map(async (image) => { if (image) { return await getImage({ src: image.src, alt: image.alt, width: 3840, height: 2160, format: "avif", loading: "lazy", }); } }) );
-
Responsive Breakpoints: The masonry layout adapts to screen sizes with custom media queries:
@media (max-width: 768px) { .masonry { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); } }
-
Lightbox Integration: The masonry gallery integrates with GLightbox for fullscreen viewing:
<a href={imageAsset.src} class="image-link glightbox"> <img src={imageAsset.src} alt={imageAsset.attributes.alt} loading="lazy" /> </a>
-
Animation Effects: Subtle hover animations enhance the user experience:
.image-container:hover { transform: scale(1.01); } .image-container:hover .image { transform: scale(1.005); }
This approach provides an elegant, performant solution for displaying photography portfolios with minimal client-side JavaScript.
While not explicitly documented, I expect all contributors to be respectful and inclusive in all interactions.
This README will continue to evolve as the project does. Feel free to suggest improvements!