Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Member-facing overview — what it is, what members use, current status, and how
| Path | Workspace | Role |
| --- | --- | --- |
| `sites/mainweb` | `web` | Public club site + authenticated portal (App Hosting) |
| `sites/hacklytics2027` | `hacklytics2027` | Hacklytics 2027 marketing site, static export (Firebase Hosting) |
| `sites/hacklytics2027` | `hacklytics2027` | Hacklytics 2027 marketing site, Astro static build (Firebase Hosting) |
| `packages/api` | `@query/api` | tRPC routers, middleware, pricing |
| `packages/auth` | `@query/auth` | NextAuth (Google, GitHub, email codes) |
| `packages/db` | `@query/db` | Drizzle schema, client, membership rules |
Expand All @@ -44,7 +44,7 @@ flowchart LR
routes["Route handlers: /api/trpc /api/auth /api/webhooks/stripe /api/resume /api/bootcamp/materials"]
end

hack["sites/hacklytics2027 (static export)"]
hack["sites/hacklytics2027 (Astro static build)"]
api["@query/api (tRPC appRouter)"]
auth["@query/auth (NextAuth)"]
db["@query/db (Drizzle)"]
Expand Down
4 changes: 2 additions & 2 deletions docs/architecture.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Architecture

**query** is a pnpm + Turborepo monorepo. Two Next.js sites share four internal packages. Club operations and hackathon operations share one Postgres database but are modeled as separate domains.
**query** is a pnpm + Turborepo monorepo. Two sites (the Next.js portal and an Astro marketing site) sit beside four internal packages. Club operations and hackathon operations share one Postgres database but are modeled as separate domains.

```
┌─────────────────────────────────────────────────────────────┐
Expand Down Expand Up @@ -182,7 +182,7 @@ The webhook is `sites/mainweb/app/(portal)/api/webhooks/stripe/route.ts`. Linkin
| Surface | Where it runs | Output |
| --- | --- | --- |
| `sites/mainweb` | Firebase App Hosting / Cloud Run (`apphosting.yaml`) | Next `standalone` |
| `sites/hacklytics2027` | Firebase Hosting target `hacklytics` | Static `output: "export"` |
| `sites/hacklytics2027` | Firebase Hosting target `hacklytics` | Astro static build (`astro build` writes `out/`) |

The event site does not talk to the database. Interest and registration live on the portal; the marketing site links to `/login?callbackUrl=/hacklytics`.

Expand Down
2 changes: 1 addition & 1 deletion docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ Turbo runs every workspace `dev` task. The two sites:
| Site | URL | Notes |
| --- | --- | --- |
| Main website + portal | [http://localhost:3001](http://localhost:3001) | Next.js App Router, `output: "standalone"` |
| Hacklytics 2027 | [http://localhost:3000](http://localhost:3000) | Static-export marketing site (`--turbopack`) |
| Hacklytics 2027 | [http://localhost:3000](http://localhost:3000) | Astro static marketing site (`astro dev`, port set in `astro.config.mjs`) |

Useful filters:

Expand Down
64 changes: 46 additions & 18 deletions docs/sites/hacklytics2027.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,45 +2,73 @@

Path: `sites/hacklytics2027`
Workspace name: `hacklytics2027`
Framework: Next.js 16, React 19, Tailwind 4, **static export** (`output: "export"`)
Framework: Astro 7 (static build), React 19 islands, Tailwind 4 (`@tailwindcss/vite`)

Marketing site for Hacklytics 2027 (“Digital Bloom”). It has **no database** and **no tRPC**. Anything dynamic (interest list, registration) lives on the portal.

## Run

```bash
pnpm --filter hacklytics2027 dev # next dev (port 3000)
pnpm --filter hacklytics2027 build # writes static files for Firebase Hosting
pnpm --filter hacklytics2027 dev # astro dev (port 3000)
pnpm --filter hacklytics2027 build # writes static files to out/ for Firebase Hosting
pnpm --filter hacklytics2027 preview # serves out/
pnpm --filter hacklytics2027 typecheck # tsc --noEmit
```

`astro check` is not used: it needs the TypeScript JavaScript API, and the repo's TypeScript 7 ships only the native compiler. `tsc --noEmit` covers `.ts` and `.tsx`; `.astro` files are checked by the build.

Astro 7 needs Vite 8 while the root `package.json` overrides `vite` to 7 for the rest of the repo, so two scoped overrides (`astro>vite`, `@astrojs/react>vite`) give this site Vite 8 and leave every other workspace on Vite 7. Astro 6 is not an option: GHSA-26w7-cxv4-gfx2 (AVIF RCE) affects every version below 7.2.8.

## Structure

| Path | Role |
| --- | --- |
| `app/page.tsx` | Home: a server component around the hero and sections |
| `components/Hero.tsx` | Pixel garden hero and countdown |
| `app/layout.tsx` | Font preloads, metadata, SW registrar |
| `app/fonts.generated.css`, `lib/fonts.generated.ts`, `public/fonts/` | Output of `scripts/subset-fonts.py`, committed |
| `app/not-found.tsx` | 404 |
| `components/HomeSections.tsx` | Below-the-fold sections |
| `components/sections/*` | About, tracks, schedule, prizes/speakers, FAQ, sponsors |
| `components/pixel/*` | Pixel sprites / garden; `season.ts` holds the one bloom count |
| `components/Navbar.tsx`, `Footer.tsx` | Chrome |
| `lib/links.ts` | Portal origin + interest URL |
| `src/pages/index.astro` | Home: hero, then the sections |
| `src/pages/404.astro` | 404 (`out/404.html`) |
| `src/layouts/Layout.astro` | `<head>` (font preloads, metadata, JSON-LD, the no-flash theme script), navbar, footer |
| `src/components/Hero.astro` | Hero. The countdown is an inline script; the garden is a React island |
| `src/components/Navbar.astro`, `Footer.astro`, `ThemeToggle.astro` | Chrome. Behaviour lives in `src/scripts/` |
| `src/components/HomeSections.astro`, `src/components/sections/*` | About, tracks, prizes, schedule, sponsors, FAQ, rendered at build time (React, no directive) |
| `src/components/pixel/*` | Pixel sprites and flower beds; `season.ts` holds the one bloom count, `GardenBeds.tsx` the four React islands |
| `src/scripts/*` | The plain scripts: `navbar`, `theme-toggle`, `bloom`, `service-worker` |
| `src/styles/globals.css`, `fonts.generated.css`, `src/lib/fonts.generated.ts`, `public/fonts/` | Styles; the fonts are output of `scripts/subset-fonts.py`, committed |
| `src/lib/links.ts` | Portal origin + interest URL |
| `public/sw.js` | Service worker (Firebase header: no-cache, `Service-Worker-Allowed: /`) |

Schedule copy lives in `components/sections/Schedule/data.ts`.
Schedule copy lives in `src/components/sections/Schedule/data.ts`.

## JavaScript and hydration

Everything renders to HTML at build time. Only what needs the browser ships code.

| Piece | How it runs | Why |
| --- | --- | --- |
| About, tracks, prizes, schedule, sponsors, FAQ, sprites | Build time, no JS | Nothing to do after load |
| Tracks and schedule bloom | `src/scripts/bloom.ts` (about 300 bytes) | They only need `data-bloom="on"` written when 35% is in view |
| Navbar (menu, hide on scroll, section links) | `src/scripts/navbar.ts` | Plain DOM work; React would add nothing |
| Theme toggle | `src/scripts/theme-toggle.ts`; the icon follows the theme in CSS | Right icon on first paint, even before the script runs |
| Theme with no flash | `is:inline` script in `<head>` | Must run before first paint |
| Countdown | Inline script after the hero markup | Fills in the zeros during parsing, before first paint, so the figures never jump, then ticks |
| Service worker | `src/scripts/service-worker.ts`, production only, after `load` | Keeps it off the critical path |
| Hero flower bed | React island, `client:idle` | In the first screen but not needed to read it; hydrates off the critical path |
| Footer flower bed | React island, `client:visible` | Page bottom; hydrates when scrolled near |
| 404 flower bed | React island, `client:visible` | Same |
| Menu flower bed | React island, `client:media="(max-width: 1023px)"` | The menu only opens below 1024px, so desktop never loads it |

The beds are the one place React stays. How many flowers are in bloom depends on the date, so the build cannot bake it, and a click plants one. The static HTML draws every plant dormant (`useInBloom` returns 0 there) and hydration brings in the real count.

React Compiler is not enabled: the islands are small, and it would add its runtime for no change in output.

## Fonts

Instrument Sans and Silkscreen are subset by hand and committed. `next/font` is not used: it would regenerate the fallback metrics, and a separate family per file would put that fallback between the core and extended faces.
Instrument Sans and Silkscreen are subset by hand and committed. No font loader is used: one would regenerate the fallback metrics, and a separate family per file would put that fallback between the core and extended faces.

Each font has a **core** file (ASCII, curly quotes, dashes, ellipsis, plus `©` and `·`, which most pages use), preloaded on every page, and an **extended** file (the rest of Latin-1 and Latin Extended-A) behind `unicode-range`, which downloads only when a page needs one of its characters. Core and extended share a family name, so the browser treats them as one font.

```bash
pip install -r sites/hacklytics2027/scripts/subset-fonts.requirements.txt
python sites/hacklytics2027/scripts/subset-fonts.py # rebuild the fonts
python sites/hacklytics2027/scripts/subset-fonts.py --check # after next build
python sites/hacklytics2027/scripts/subset-fonts.py --check # after astro build
```

Run `--check` after copy changes: it lists any character outside the core. Rerun the script if a character should join the core, if an axis or OpenType feature in use changes, or when the pinned upstream font moves. The header of the script has the details.
Expand Down Expand Up @@ -82,9 +110,9 @@ Firebase Hosting target `hacklytics`, public dir `sites/hacklytics2027/out` (`fi

Workflow: `.github/workflows/ci.yml`. The `verify` job builds every site; `deploy-hacklytics` ships that build live on `main` and `preview-hacklytics` to channel `pr-N` on PRs. Neither runs unless `verify` passes.

Asset caching: hashed JS and CSS `max-age=31536000, immutable`; fonts and images `max-age=86400, stale-while-revalidate=604800`, including those under `_next/static`, because the later rule wins; HTML `max-age=3600`. See `firebase.json`.
Asset caching: hashed JS and CSS under `/_astro/` are `max-age=31536000, immutable`; fonts and images `max-age=86400, stale-while-revalidate=604800`; HTML `max-age=0, must-revalidate`. See `firebase.json`. The service worker caches `/_astro/` files cache-first, since their names are fingerprinted.

Images are `unoptimized: true` because static export has no image optimizer. React Compiler is on.
Images are plain `<img>` tags over files in `public/`; there is no image pipeline.

## Assets

Expand Down
2 changes: 1 addition & 1 deletion firebase.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"trailingSlash": true,
"headers": [
{
"source": "/_next/static/**",
"source": "/_astro/**",
"headers": [
{
"key": "Cache-Control",
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@
"brace-expansion": "^5.0.9",
"gaxios>uuid": "^11.1.1",
"source-map-js": "^1.2.2",
"minimatch": "^11.0.3"
"minimatch": "^11.0.3",
"astro>vite": "8.3.5",
"@astrojs/react>vite": "8.3.5"
}
}
}
Loading
Loading