Eclipse Con / Moonfest 2026
July 6, 2026 · View on GitHub
What This Repo Is
This repository contains the official Moonfest 2026 website: a bilingual React SPA for a four-day furry convention in Paipa, Boyaca, Colombia, scheduled for July 10 to July 13, 2026.
The product is designed around one core constraint:
- It must work as a polished modern web app during development.
- It must also ship as a self-contained static artifact for simple hosting and archival deployment.
That requirement shapes nearly everything in this codebase: browser-routing plus static fallback support, aggressive asset inlining, embedded Telegram content, and environment-driven runtime configuration.
Site Analysis
The current site is not a generic landing page. It is a structured event operations surface with strong editorial presentation.
Information architecture
The main convention experience is a single long-form page composed in this order:
- Hero
- About / positioning
- Activities and event framing
- Reservation packages
- Ticketing
- Venue
- Hotel amenities and paid extras
- Travel guidance
- Telegram news feed
- Organizers
- FAQ
- Footer
There is also a separate /registration-tutorial route for the two-step booking flow.
Product goals the site is serving
- Sell the atmosphere of Moonfest as a destination event.
- Explain the reservation model clearly enough to reduce attendee confusion.
- Present hotel, pricing, and travel logistics with enough detail to support purchase decisions.
- Keep event communications fresh through Telegram-fed updates.
- Give the organizers a visible public face.
- Preserve privacy by keeping analytics consent-gated and disabled by default.
What makes the site distinctive
- A dramatic layered visual composition with a sticky hero, starfield sky, sakura particles, and overlapping post-hero content.
- A strong destination-driven content model: venue, thermal amenities, transport, weather, and hotel extras are first-class content, not side notes.
- A multilingual editorial/news layer powered by Telegram exports and translated archives.
- A static build pipeline that inlines external fonts, icons, images, and embedded content into a portable deliverable.
UX patterns already present in the code
- Section-aware URL syncing for deep-linking into the long landing page.
- Clean browser URLs with Cloudflare SPA fallback for direct deep links.
- i18n-first UI strings through
react-i18next. - Consent-gated analytics with necessary vs optional tracking categories.
- Registration guidance split into a dedicated step-by-step tutorial instead of forcing everything into the landing page.
Main User Journeys
Landing-page visitor
The primary visitor path is:
Hero -> Why Moonfest -> What is included -> Reservation pricing -> Venue confidence -> FAQ
That path is optimized for conversion and reassurance.
Attendee needing operational detail
This path is more logistics-heavy:
Venue -> Amenities -> Travel tips -> News -> FAQ -> Reservation tutorial
This is where the site behaves more like an event handbook than a promo page.
Returning community member
Returning users are likely checking:
- new Telegram updates
- organizer changes
- package details
- venue and transport reminders
That makes the embedded news archive and section deep-linking materially important.
Feature Map
Convention landing page
- Hero with branding, event dates, and primary reservation CTA
- About section explaining the event identity and 2026 location shift
- Activities and “what’s included” framing
- Reservation packages with room-based pricing
- Ticketing placeholder flow
- Official venue presentation for Hotel Estelar Paipa
- Amenity cards for spa, thermal waters, restaurants, nautical activities, farm, horses, bikes, tennis, pet hotel, and more
- Travel advice with weather, local attractions, and destination context
- Organizer roster
- FAQ
Registration tutorial route
- Three-step explanation of the booking model
- Explicit separation between hotel reservation and event ticket purchase
- Interactive completion checklist
- Analytics hooks for tutorial progress tracking
News system
- Telegram archive stored in
public/telegram/ - Spanish source feed plus English translated feed
- Many alternate visual renderers for the news section
- Static embedding support so the single-file build does not depend on live requests
Analytics and consent
- Necessary and analytics categories
- Local consent storage
- Optional outbound event delivery
- Sanitized event payloads, allowlists, and privacy filters
- Disabled by default unless explicitly enabled through env overrides
- Lean analytics profile by default so PostHog captures page, funnel, CTA, content, consent, and diagnostics without high-volume clickstream noise
- Cloudflare Web Analytics remains an explicit opt-in
- Google Analytics 4 activates when
VITE_GA_MEASUREMENT_IDis present unlessVITE_GA_MEASUREMENT_ENABLED=falseis set explicitly - Google Tag Manager activates when
VITE_GTM_CONTAINER_IDis present unlessVITE_GTM_ENABLED=falseis set explicitly - GA4 uses regional consent defaults: analytics starts denied in EEA/UK/CH and granted elsewhere until the visitor updates the consent banner; ads-related storage stays denied everywhere
- High-value analytics events are also mirrored into
window.dataLayerso GTM can build free GA4 event tags and marketing reports without duplicating low-signal clickstream noise
Technical Architecture
The project uses a Clean Architecture-inspired feature layout.
src/
app/
layouts/
providers/
router.tsx
features/
analytics/
convention/
registration-tutorial/
shared/
application/
domain/
infrastructure/
presentation/
test/
public/
scripts/
Architectural conventions
src/app/owns shell concerns such as routing, providers, and layouts.src/features/<feature>/containsdomain,application,infrastructure, andpresentation.src/shared/contains cross-feature code using the same layer split.- UI strings live in locale JSON files and are accessed through translation keys.
- Imports use the
@/*alias tosrc/*.
Routing and Delivery Model
The production app uses createBrowserRouter with Cloudflare SPA fallback.
This is intentional. The project is built to support:
- direct deep links on the Cloudflare deployment
- static hosting through the Worker asset fallback
- artifact-style deployments via the separate static build flow
Current top-level routes:
/for the main convention page/registration-tutorialfor the booking walkthrough
Static Build Strategy
pnpm build:static is one of the defining workflows in this repo.
The static pipeline:
- builds the app with Vite
- prepares a static public directory
- optimizes Telegram media assets
- embeds Telegram archives for static consumption
- inlines external fonts and asset URLs into the generated output
- deduplicates repeated data URIs
- emits a portable static artifact in
dist-static/
This is why the project can behave like a rich SPA while still shipping as a nearly self-contained deliverable.
Content Sources
The site mixes authored content, structured local data, and curated external references.
Local product content
- convention copy in
src/shared/infrastructure/i18n/locales/ - organizer cards in
src/features/convention/application/data/guests.ts - navigation definitions in
src/features/convention/application/data/navigation.ts
Generated content
- Telegram exports in
public/telegram/messages.json - translated Telegram feed in
public/telegram/messages.en.json
External sources referenced by the site
- Hotel Estelar Paipa official pages and PDFs
- weather and travel sources
- Telegram community links
- social platforms linked in the footer
Environment Model
Safe defaults live in .env.example.
Use .env.local or .env.development for local overrides and secrets. Both are gitignored.
For release-specific overrides, the release script also supports:
.env.production.env.production.local.env.staging.env.staging.local
Important current rules:
- analytics are off by default
- analytics keys should not live in
.env.example VITE_APP_VERSIONis stored in.env.example- the pre-commit version bump updates
.env.example - static build scripts read the version from
.env.example
Relevant variables
| Variable | Purpose |
|---|---|
VITE_APP_NAME | App display name |
VITE_DEFAULT_LOCALE | Fallback locale |
VITE_SUPPORTED_LOCALES | Comma-separated locale list |
VITE_DEBUG | Debug-mode toggle |
VITE_APP_VERSION | Build/version label used in the footer |
VITE_ANALYTICS_ENABLED | Master analytics switch |
VITE_ANALYTICS_ENDPOINT | Optional custom ingest endpoint |
VITE_GA_MEASUREMENT_ENABLED | Optional GA4 override, default on |
VITE_GA_MEASUREMENT_ID | Optional GA4 measurement ID |
VITE_GTM_ENABLED | Optional GTM override, default on |
VITE_GTM_CONTAINER_ID | Optional GTM container ID |
VITE_POSTHOG_API_KEY | Optional PostHog project key |
VITE_POSTHOG_HOST | Optional PostHog host |
VITE_CF_WEB_ANALYTICS_TOKEN | Optional Cloudflare token |
Local Development
pnpm install
pnpm dev
Dev server default:
http://localhost:5173
Primary scripts:
pnpm dev
pnpm build
pnpm build:static
pnpm release
pnpm release:staging
pnpm deploy:cloudflare:dry-run
pnpm deploy:cloudflare
pnpm preview
pnpm typecheck
pnpm lint
pnpm format
pnpm format:check
pnpm test
pnpm test:watch
pnpm test:coverage
pnpm test:e2e
Telegram Workflows
Telegram content is part of the site, not an afterthought.
Use these scripts when updating the news archive:
pnpm fetch:telegram
pnpm translate:telegram
pnpm sync:telegram
These depend on local credentials configured outside the tracked env example.
pnpm translate:telegram supports TRANSLATE_PROVIDER=codex for Codex CLI,
and TRANSLATE_PROVIDER=claude_code now falls back to Codex CLI when Claude
CLI access fails due to auth/subscription issues.
Quality Gates
Before committing, the expected baseline is:
pnpm typecheck && pnpm lint && pnpm build
The repo also uses Husky and lint-staged to enforce checks during commit.
Cloudflare Deployment
The production deployment target is a Cloudflare Worker with static assets served from dist/.
- Config lives in
wrangler.toml - Build artifact for Cloudflare is
dist/ - Route is
moonfest.furrycolombia.com/* - Apex domain
furrycolombia.comis attached as a Worker Custom Domain - Cloudflare SPA fallback handles browser-history routes for direct deep links
Recommended release flow:
pnpm release
Recommended staging validation flow:
pnpm release:staging
Notes:
pnpm releaseloads.env.example, then.env.local, then.env.productionand.env.production.local, then the current shell envpnpm releaseauto-commits dirty local changes withchore: release production, pushes the current branch toorigin, then deploys the top-level Wrangler production environment with an explicit empty--envtargetpnpm releasepromotes project env into Cloudflare too:VITE_*keys are sent as Worker vars, and other project keys such asOPENAI_*,AZURE_*,TELEGRAM_*, andTRANSLATE_*are uploaded as Worker secretspnpm release:stagingkeeps the existing staging safety checks by running the local browser-routing Playwright spec before deploy and the staging browser-routing check after deploypnpm release:stagingloads.env.example, then.env.local, then.env.stagingand.env.staging.local, then the current shell env- use
pnpm release -- --message="feat: your message"to override the auto-generated commit message - use
pnpm release -- --dry-runto run validation plus the Cloudflare dry-run without mutating git state or doing a live deploy - use
pnpm release -- --skip-commitorpnpm release -- --skip-pushwhen you only want the deploy orchestration public/robots.txt,public/sitemap.xml, and social metadata inindex.htmlall point at the live production domain- staging uses the separate Worker environment
stagingand the hostnamestaging-moonfest.furrycolombia.com - the staging hostname must exist in Cloudflare DNS before the Worker route can serve traffic
pnpm deploy:cloudflare:stagingnow deploys first and then runs the staging browser-routing E2E againsthttps://eclipse-con-staging.furrycolombia.workers.devby defaultpnpm deploy:cloudflare:stagingalso runs the same browser-routing E2E locally before deployment, then reruns it against staging after deployment- override the E2E target with
PLAYWRIGHT_BASE_URLif you want to validate the custom staging hostname instead furrycolombia.comis intentionally configured as a Worker Custom Domain instead of a plain Worker route because that allowed Cloudflare to provision the hostname directly fromwrangler deploy- if
furrycolombia.comappears broken locally while public resolvers already answer it, the usual issue is stale local DNS; test withnslookup furrycolombia.com 1.1.1.1or flush the local DNS cache
Testing
Unit testing
- Vitest
- Testing Library
- colocated
*.test.ts/*.test.tsxfiles where practical
End-to-end testing
- Playwright
- accessibility coverage through
@axe-core/playwright
Repository Guidelines
Naming and style
- TypeScript strict mode
- function components only
- PascalCase for components and types
- camelCase for hooks and utilities
- kebab-case for config files
- prefer
import type
Translation discipline
- UI strings should go through
useTranslation() - use dot-notation translation keys
- avoid hardcoded user-facing strings in components
Where to Look First
If you are new to the repo, start here:
src/features/convention/presentation/ConventionPage.tsxsrc/features/convention/application/data/navigation.tssrc/shared/infrastructure/i18n/locales/en.jsonsrc/shared/infrastructure/config/environment.tsvite.config.tsscripts/build-static.mjs
That set gives you the product shape, content model, runtime config, and deployment model fast.
Current Reality of the Product
This repo is best understood as a hybrid of:
- destination-event marketing site
- attendee logistics handbook
- community news surface
- privacy-conscious static web artifact
That combination is what makes it unusual. If you change the README, the build model, or the content structure later, preserve that framing. It is the real identity of the project.