- Documentation Index:
docs/README.md
wongzhunhao.com is a photography portfolio and creative collection built on Astro v6.0.1. It showcases various photography and creative work organized into different collections: astrophotography, travel photos, studio work (the atelier), creative vignettes, and a CV. The project prioritizes speed and visual design while using Astro's content collection API to manage everything efficiently.
The site deploys to Cloudflare Workers Static Assets via Workers Builds (Cloudflare's dashboard git integration), with no GitHub Actions deploy step.
- Docs Index:
docs/README.md - Architecture:
src/README.md - Performance:
docs/performance.md - CI/CD & Deployments:
.github/CICD.md - Components:
src/components/README.md - Layouts:
src/layouts/README.md - Pages:
src/pages/README.md - Content Collections:
src/content/README.md
Project Structure Diagram (click to expand)
graph TD
A["/revista" Root] --> B["📁 src"]
A --> C["📁 public<br>(static assets)"]
A --> D["⚙️ Configuration Files<br>(astro.config, tailwind.config)"]
graph TD
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["📝 astrophotography<br>(star & sky photos)"]
H --> L["📝 travel_photos<br>(travel collections)"]
H --> M["📝 the_atelier<br>(studio work)"]
H --> N["📝 vignettes<br>(creative sketches)"]
H --> O["📝 authors<br>(contributor info)"]
H --> P["📝 cv<br>(resume data)"]
graph TD
E["📁 components"] --> E1["🧩 BlogPost.astro"]
E --> E2["🧩 Footer.astro"]
E --> E3["🧩 Header.astro"]
E --> E4["🧩 Navigation.astro"]
E --> E5["🧩 Homepage.astro"]
E --> E6["🧩 Masonry.astro"]
E --> E7["🧩 HeroImage.tsx"]
E --> E8["🧩 NextPost.astro"]
E --> E9["🧩 ThemeToggle.tsx"]
F["📁 layouts"] --> F1["📄 BaseLayout.astro"]
F --> F2["📄 MarkdownPostLayout.astro"]
F --> F3["📄 AuthorLayout.astro"]
F --> F4["📄 TagLayout.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["⚡ theme.ts<br>(dark/light mode)"]
J --> J2["⚡ lightbox.ts<br>(image lightbox)"]
-
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
- 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/: TypeScript files for client-side functionalitytheme.ts: Shared theme preference, apply, toggle, and init helperslightbox.ts: Custom image lightbox with keyboard/touch navigationhomePage.ts: Homepage dynamic content and random image selectiongetrandomimage.ts: Random featured image selection for tag pagesburgundy.ts: 404 page quote rotationrss.ts: RSS link visibility and URL managementundici-retry.ts: HTTP fetch retry helper for build-time requestsutils.ts: Sharedshuffle()andformatDate()utilitiescollections.ts: SharedbuildDetailPaths(),buildTagPaths(),generateRss()helpers
-
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 (astrophotography, travel_photos, the_atelier, vignettes, 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/rssto 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
# Preview production build
bun run preview# Run the content creator
bun run create
# Specify content type directly
bun run create -t astrophotography
# 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 astrophotography --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: astrophotography, travel_photos, the_atelier, vignettes, authors, cv
# Update an existing post's frontmatter (e.g., add/modify updated date)
bun run update-post --file astrophotography/2026-03-14-22-degree-halo-25-august-2025.mdx --updated-date "2026-03-15T12:00:00Z"
# Preview changes without writing to file
bun run update-post --file travel_photos/2026-01-10-one-day-in-hong-kong-hong-kong.mdx --tags "travel,asia,photography" --dry-run
# Update multiple fields at once
bun run update-post --file astrophotography/2026-03-14-22-degree-halo-25-august-2025.mdx \
--title "New Title" \
--tags "astrophotography,optics,sky" \
--updated-date "2026-03-15T08: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
A["📁 content/"] --> B["📁 astrophotography/<br><i>stars & sky photos</i>"]
A --> C["📁 travel_photos/<br><i>travel collections</i>"]
A --> D["📁 the_atelier/<br><i>studio work</i>"]
A --> E["📁 vignettes/<br><i>creative sketches</i>"]
A --> F["📁 authors/<br><i>contributor profiles</i>"]
A --> G["📁 cv/<br><i>professional info</i>"]
graph TD
B["📁 astrophotography/"] --> H["📄 22-degree-halo-25-august-2025.mdx<br><i>frontmatter + markdown</i>"]
C["📁 travel_photos/"] --> J["📄 one-day-in-hong-kong-hong-kong.mdx<br><i>frontmatter + markdown</i>"]
C --> K["📄 a-month-in-shanghai-shanghai-china.mdx<br><i>frontmatter + markdown</i>"]
graph TD
D["📁 the_atelier/"] --> L["📄 the-skys-sketchbook.mdx<br><i>studio work</i>"]
E["📁 vignettes/"] --> M["📄 thursday-night-glow.mdx<br><i>creative sketch</i>"]
F["📁 authors/"] --> N["📄 wong-zhun-hao.mdx<br><i>author bio</i>"]
G["📁 cv/"] --> O["📄 cv-export.html<br><i>exported CV from cv-v0</i>"]
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:
// content.config.ts — shared base schema eliminates duplication across collections
const baseSchema = 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(),
});
const astrophotography = defineCollection({
loader: glob({ pattern: "**\/[^_]*.mdx", base: "./src/content/astrophotography" }),
schema: baseSchema,
});
// Example frontmatter from an actual astrophotography post:
---
title: "22 Degree Halo - 25 August 2025"
pubDate: 2026-03-14T12:00:00.000Z
tags: [ 'astrophotography', 'optics' ]
author: "Zhun Hao"
image:
src: "https://www.wongzhunhao.com/astrophotography/halo_2025/22-degree-halo.avif"
alt: "A 22 degree halo surrounding the moon, a luminous ring created by ice crystal refraction in the upper atmosphere."
positionx: "50"
positiony: "50"
description: "Captured a beautiful 22 degree halo around the moon on the evening of August 25, 2025. An optical phenomenon caused by ice crystals in the atmosphere refracting light."
---
The phenomenon occurs when hexagonal ice crystals in cirrus clouds refract moonlight at a specific angle...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
A["🏠 www.wongzhunhao.com<br>(Root)"] --> B["❌ /404<br>(Custom error page)"]
A --> C["👤 /authors<br>(Contributor profiles)"]
A --> D["📋 /cv<br>(Resume page)"]
A --> E["🌟 /astrophotography<br>(Stars & sky photos)"]
A --> F["✈️ /travel_photos<br>(Travel collections)"]
A --> G["🎨 /the_atelier<br>(Studio work)"]
A --> H["✍️ /vignettes<br>(Creative sketches)"]
C -.-> C0["📡 /authors/rss.xml"]
E -.-> E0["📡 /astrophotography/rss.xml"]
F -.-> F0["📡 /travel_photos/rss.xml"]
G -.-> G0["📡 /the_atelier/rss.xml"]
H -.-> H0["📡 /vignettes/rss.xml"]
graph TD
E["🌟 /astrophotography"] --> I["📄 /astrophotography/[post-slug]<br>(Individual photo pages)"]
E --> J["🏷️ /astrophotography/tags<br>(Tags index)"]
J --> K["🔖 /astrophotography/tags/[tag]<br>(Photos with specific tag)"]
F["✈️ /travel_photos"] --> L["📄 /travel_photos/[post-slug]<br>(Individual collection pages)"]
F --> M["🏷️ /travel_photos/tags<br>(Tags index)"]
M --> N["🔖 /travel_photos/tags/[tag]<br>(Collections with specific tag)"]
graph TD
G["🎨 /the_atelier"] --> O["📄 /the_atelier/[post-slug]<br>(Individual studio work pages)"]
G --> P["🏷️ /the_atelier/tags<br>(Tags index)"]
P --> Q["🔖 /the_atelier/tags/[tag]<br>(Studio work with specific tag)"]
H["✍️ /vignettes"] --> R["📄 /vignettes/[post-slug]<br>(Individual vignette pages)"]
H --> S["🏷️ /vignettes/tags<br>(Tags index)"]
S --> T["🔖 /vignettes/tags/[tag]<br>(Vignettes with specific tag)"]
The routing system combines static and dynamic routes:
- Static routes like
/astrophotographyare defined by files atsrc/pages/astrophotography.astro - Dynamic routes like
/astrophotography/22-degree-halo-25-august-2025are handled bysrc/pages/astrophotography/[...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 (astrophotography, travel_photos, the_atelier, vignettes):
/{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.,
/astrophotography/22-degree-halo-25-august-2025) are generated dynamically based on the content in the respective collection usinggetStaticPaths()in[...id].astro. - Tag pages (e.g.,
/astrophotography/tags/astrophotography) 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.tsfiles in each collection's directory.
- Each collection has an RSS feed available at
The site uses Tailwind CSS v4.1.17 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 (#f2f2f2) 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: { 'top-33': 'center top 33.33%', 'top-50': 'center top 50%', }, // Other extended utilities }
- Typography Plugin: The
@tailwindcss/typographyplugin provides rich styling for long-form content
- Custom Utilities: Extended Tailwind with utilities for:
-
Dark Mode Strategy
- Class-based Implementation: The
darkclass on<html>drives Tailwind's dark variant. Theme state is managed bysrc/scripts/theme.tsand toggled via theThemeToggle.tsxReact component.
- Class-based Implementation: The
-
CSS Organization
-
Global Styles:
src/styles/global.csscontains:/* Tailwind v4 single import */ @import "tailwindcss"; @config '../../tailwind.config.mjs'; /* Global custom styles */ :root { /* Custom CSS variables */ } /* Dark mode specific overrides */ .dark { /* Dark mode CSS variables */ }
-
Component-specific CSS:
MasonryLayout.css: Custom grid-based implementationlightbox.css: Custom lightbox styling (fade transitions, overlay, controls)
-
-
CSS-in-JS Integration
- The project uses minimal CSS-in-JS, primarily in the React components like
ThemeToggle.tsxandHeroImage.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:
theme.ts: Shared theme management module with the following features:getThemePreference(): reads from localStorage, falls back toprefers-color-schemeapplyTheme(): adds/removes thedarkclass on the document roottoggleTheme(): cycles the theme and persists to localStorageinitTheme(): inline-safe initialiser used by ThemeToggle.astro to prevent FOUC
-
lightbox.ts: Custom image lightbox (replaced GLightbox - 73 KB -> ~2.4 KB gzipped):- Multi-level zoom: click cycles 2x -> 3.5x -> reset; scroll wheel for cursor-anchored incremental zoom (up to 5x); continuous pinch zoom on touch
- Zoom uses
scale() + translate3d()- a single CSS transform, pure compositor operation, no layout recalculation on any frame - Keyboard navigation (arrow keys, Escape zooms out first then closes)
- Touch swipe navigation at 1x, drag/pan when zoomed
- Fade transitions, prev/next/close/zoom controls, image counter
- Adjacent image preloading, body scroll lock
- Full View Transitions lifecycle support (destroy/reinit on
astro:page-load)
-
getrandomimage.ts: 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.ts: 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.ts: 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.ts: Powers the dynamic homepage content:- Selects featured content from different collections
- Uses Fisher-Yates shuffle (from
utils.ts) to randomize the selection - Ensures fresh content appears on each page load
-
utils.ts: Common helpers shared across scripts:shuffle(): Fisher-Yates array shuffleformatDate(): consistent date formatting using nativeDate.toDateString()
-
collections.ts: Shared content collection helpers:CollectionNametype: union of all content collection keys ("muses" | "short_form" | …), used across layouts and pages for type-safegetCollection()callsbuildDetailPaths(): generatesgetStaticPathsfor[...id].astropagesbuildTagPaths(): generatesgetStaticPathsfortags/[tag].astropagesgenerateRss(): generates RSS feed XML for any collection
remark-reading-time.mjs: MDX plugin that calculates and adds reading time estimates to posts
All scripts are TypeScript (except the remark plugin which remains .mjs), minimal, focused, and non-blocking to maintain the site's performance profile.
prebuild(automatic): Runsscripts/sync-readme-versions.jsto keep version badges in docs in sync withpackage.json.postbuild(automatic): Runs Pagefind indexing over thedist/output.
The astro.config.mjs includes several features worth noting:
- Math Rendering:
remark-math+rehype-katexfor LaTeX-style equations in MDX content - MDX remarkPlugins: The
mdx()integration carries its ownremarkPluginsarray (remarkGfm,remarkMath,remarkReadingTime) because MDX replaces (not merges with) the basemarkdown.remarkPluginswhen it specifies its own. This ensures GFM tables and reading-time estimates work in.mdxfiles. - Markdoc Integration:
@astrojs/markdocavailable alongside MDX for content authoring - Dual Shiki Themes: Syntax highlighting uses
rose-pine-dawn(light) andtokyo-night(dark) withdefaultColor: falseso both themes are emitted and CSS controls which one is visible - Sitemap Generation:
@astrojs/sitemapautomatically generatessitemap-index.xmlduring build - Experimental Client Prerendering:
clientPrerender: trueenables speculative prerendering of linked pages for near-instant navigation - Experimental Fonts API: Fonts (Inconsolata, Overpass Mono) are loaded via Astro's font provider system with
optimizedFallbacks: truefor reduced CLS - undici-retry: Custom Astro integration (
src/scripts/undici-retry.ts) that patches the global fetch with retry logic for build-time HTTP requests
I've optimized the site in several ways:
-
Image Processing: Using Astro's
getImagefunction 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
prefetchfeature loads linked pages before the user clicks, making navigation feel instant. -
Efficient Bundling: Astro v6.0.1 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.1.17'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: trueoption -
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.
<!-- Simplified from Pagefind.astro -->
<button id="searchButton" aria-haspopup="dialog">Search</button>
<dialog id="searchDialog" class="search-dialog">
<div class="dialog-content">
<button id="closeButton" class="close" aria-label="Close search">
×
</button>
<div id="search" class="m-8"></div>
</div>
</dialog>
<script>
document.addEventListener("astro:page-load", () => {
const dialog = document.getElementById("searchDialog");
// dialog.showModal() / dialog.close() for open/close
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 Workers Static Assets: hosts the site at
wongzhunhao.com. Workers Builds (the dashboard's git integration) auto-builds and deploys on push tomain, with atomic per-deploy cache invalidation. www.wongzhunhao.com: separate origin used as a CDN for image source files referenced from MDX frontmatter (image.srcURLs). Astro fetches these at build time for Sharp processing.
-
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.jsonrun through Bun
-
TypeScript:
- The project uses TypeScript v5.9.3 throughout
- Astro's built-in TypeScript support with
@astrojs/checkv0.9.6 catches type errors during build
-
Prettier:
- Code formatting with Prettier v3.7.4 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.1.17 with better performance and smaller bundles
- Configured with the typography plugin for long-form content
- Cloudflare Workers Builds (the dashboard's git integration) is the sole deployment path: it watches
main, runsbun run build, and uploadsdist/to Workers Static Assets. There is no GitHub Actions deploy job. - GitHub Actions (
.github/workflows/ci.yml) is type-check-only — runsastro checkon PRs and pushes tomain. It does not deploy. - Cache invalidation happens automatically per Workers Builds deploy; no separate purge step.
-
Content Security: The RSS feed generation uses
sanitize-htmlto prevent XSS vulnerabilities. -
Secure Hosting: Cloudflare provides DDoS protection, SSL, and other security features.
For local development, you'll need:
- Bun 1.2.21 (lockfile and scripts are generated with this version)
- Node.js 20+ (only needed if you prefer npm/yarn tooling; builds run with Bun)
- 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 installThis installs:
- Astro v6.0.1
- Tailwind CSS v4.1.17
- React v19.2.1
- MDX v4.3.13 and other dependencies
-
Run the development server:
bun run dev -
Build for production:
bun run build
Includes Pagefind indexing for search functionality.
-
(Optional) Run local quality checks before committing:
bun run lint:site # build, HTML validate, and internal link check -
Content workflows: the CLI helpers for creating/editing posts are documented in
scripts/README.md. -
Preview the production build:
bun run preview
The site deploys to Cloudflare Workers Static Assets via Workers Builds (the dashboard's git integration). On every push to main, Cloudflare clones the repo, runs bun run build, and uploads dist/ to the assets store. The custom domain wongzhunhao.com is wired up in wrangler.jsonc. No GitHub Actions secrets or workflows are involved in deployment.
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 (
.astrodirectory) 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
- 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 (src/pages/cv.astro) imports a pre-rendered HTML export from my separate cv-v0 Next.js app rather than building the CV from Astro components:
-
HTML Import Pipeline: At build time,
cv.astroreadssrc/content/cv/cv-export.html(a Puppeteer DOM capture from cv-v0), extracts<style>blocks and<body>content, strips conflictinghtml/bodyrules, and rescopesbody > divselectors to.cv-imported. -
Dark Mode Overrides: The cv-v0 export uses Tailwind utility classes (
.text-gray-900,.text-gray-700, etc.) as real class tokens, so dark mode is handled by targeting those classes directly under.dark .cv-importedwith appropriate slate-palette colors. -
Minimal Shell: The page uses
BaseLayoutwithhideHeaderFooterand just renders a theme toggle above the imported CV content. No nav, no print button, no section scroll-spy. -
Updating: To update the CV, re-export from cv-v0 and replace
src/content/cv/cv-export.html.
The photo gallery displays use a CSS Grid masonry layout with focal-point-aware cropping:
-
CSS Grid with Dense Packing: Editorial-style grid with
nth-childspan rules for visual rhythm:.masonry { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); grid-gap: 12px; grid-auto-flow: dense; } .image-container:nth-child(3n) { grid-row: span 2; } .image-container:nth-child(4n) { grid-column: span 2; }
-
Smart Crop Positioning: Images default to
object-position: center 25%so subjects (faces, upper-third content) stay visible when cropped by the grid. Per-image overrides viapositionx/positionyprops:// Default smart crop — no override needed for most photos { src: "https://image.erfi.io/photo.jpg", alt: "Photo" } // Fine-tune a specific image's crop anchor { src: "https://image.erfi.io/photo.jpg", alt: "Photo", positionx: "30%", positiony: "10%" }
-
Native CSS Masonry (Progressive Enhancement):
@supports (grid-template-rows: masonry)automatically upgrades to true masonry layout when browsers ship CSS Grid Level 3, with no cropping needed. -
Custom Lightbox Integration: Gallery images open in a purpose-built lightbox (~2.4 KB gzipped) with multi-level zoom, cursor-anchored scroll zoom, drag/pan, pinch zoom, keyboard and touch navigation — replacing the 73 KB GLightbox dependency.
-
Image Optimization: All thumbnails are processed through Astro's
getImage()to AVIF format, while lightboxhreflinks point to original CDN images for full-resolution viewing.
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!