Eclipse Con / Moonfest 2026

July 6, 2026 · View on GitHub

Moonfest 2026 • official event website • bilingual static-first SPA

Live site

React TypeScript Vite Tailwind Vitest

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:

  1. Hero
  2. About / positioning
  3. Activities and event framing
  4. Reservation packages
  5. Ticketing
  6. Venue
  7. Hotel amenities and paid extras
  8. Travel guidance
  9. Telegram news feed
  10. Organizers
  11. FAQ
  12. 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
  • 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_ID is present unless VITE_GA_MEASUREMENT_ENABLED=false is set explicitly
  • Google Tag Manager activates when VITE_GTM_CONTAINER_ID is present unless VITE_GTM_ENABLED=false is 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.dataLayer so 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>/ contains domain, application, infrastructure, and presentation.
  • 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 to src/*.

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-tutorial for 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_VERSION is stored in .env.example
  • the pre-commit version bump updates .env.example
  • static build scripts read the version from .env.example

Relevant variables

VariablePurpose
VITE_APP_NAMEApp display name
VITE_DEFAULT_LOCALEFallback locale
VITE_SUPPORTED_LOCALESComma-separated locale list
VITE_DEBUGDebug-mode toggle
VITE_APP_VERSIONBuild/version label used in the footer
VITE_ANALYTICS_ENABLEDMaster analytics switch
VITE_ANALYTICS_ENDPOINTOptional custom ingest endpoint
VITE_GA_MEASUREMENT_ENABLEDOptional GA4 override, default on
VITE_GA_MEASUREMENT_IDOptional GA4 measurement ID
VITE_GTM_ENABLEDOptional GTM override, default on
VITE_GTM_CONTAINER_IDOptional GTM container ID
VITE_POSTHOG_API_KEYOptional PostHog project key
VITE_POSTHOG_HOSTOptional PostHog host
VITE_CF_WEB_ANALYTICS_TOKENOptional 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.com is 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 release loads .env.example, then .env.local, then .env.production and .env.production.local, then the current shell env
  • pnpm release auto-commits dirty local changes with chore: release production, pushes the current branch to origin, then deploys the top-level Wrangler production environment with an explicit empty --env target
  • pnpm release promotes project env into Cloudflare too: VITE_* keys are sent as Worker vars, and other project keys such as OPENAI_*, AZURE_*, TELEGRAM_*, and TRANSLATE_* are uploaded as Worker secrets
  • pnpm release:staging keeps the existing staging safety checks by running the local browser-routing Playwright spec before deploy and the staging browser-routing check after deploy
  • pnpm release:staging loads .env.example, then .env.local, then .env.staging and .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-run to run validation plus the Cloudflare dry-run without mutating git state or doing a live deploy
  • use pnpm release -- --skip-commit or pnpm release -- --skip-push when you only want the deploy orchestration
  • public/robots.txt, public/sitemap.xml, and social metadata in index.html all point at the live production domain
  • staging uses the separate Worker environment staging and the hostname staging-moonfest.furrycolombia.com
  • the staging hostname must exist in Cloudflare DNS before the Worker route can serve traffic
  • pnpm deploy:cloudflare:staging now deploys first and then runs the staging browser-routing E2E against https://eclipse-con-staging.furrycolombia.workers.dev by default
  • pnpm deploy:cloudflare:staging also runs the same browser-routing E2E locally before deployment, then reruns it against staging after deployment
  • override the E2E target with PLAYWRIGHT_BASE_URL if you want to validate the custom staging hostname instead
  • furrycolombia.com is intentionally configured as a Worker Custom Domain instead of a plain Worker route because that allowed Cloudflare to provision the hostname directly from wrangler deploy
  • if furrycolombia.com appears broken locally while public resolvers already answer it, the usual issue is stale local DNS; test with nslookup furrycolombia.com 1.1.1.1 or flush the local DNS cache

Testing

Unit testing

  • Vitest
  • Testing Library
  • colocated *.test.ts / *.test.tsx files 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.tsx
  • src/features/convention/application/data/navigation.ts
  • src/shared/infrastructure/i18n/locales/en.json
  • src/shared/infrastructure/config/environment.ts
  • vite.config.ts
  • scripts/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.