diff --git a/.impeccable.md b/.impeccable.md new file mode 100644 index 0000000..5f08573 --- /dev/null +++ b/.impeccable.md @@ -0,0 +1,39 @@ +# Design Context: jscebu.org + +Distilled from the approved specs: +- `docs/superpowers/specs/2026-10-27-jscebu-design-system-design.md` +- `docs/superpowers/specs/2026-10-28-jscebu-site-v1-design.md` + +The brand rules themselves live in `packages/brand` and at https://www.jscebu.org/brand.md. This file covers who the site is for and how it should feel. + +## Audience + +- **Developers in Cebu:** students, juniors and working devs who come to meetups, workshops and Hacktoberfest Cebu. +- **Volunteers, co-leads and partner communities:** they make things for JS Cebu. +- **AI agents:** they make on-brand graphics and pages, given only the brand link. + +## Use cases + +- Find the next event. For now that means Cebby and Facebook. +- See that the community is real and active: real photos, real projects. +- Find where to join. +- Get the logos and brand rules. + +## Personality + +- **The website uses the Terminal personality:** ink background, a faint grid, yellow light. +- **The two signatures carry recognition:** the block cursor at the end of the primary headline, and the logo as a tilted die-cut sticker. +- **Voice:** + - English first, friendly and direct, like a fellow developer. + - Bisaya only lightly, and only approved phrases. +- **Photos:** real and never filtered. +- **No hexagon motifs** beyond the logo itself. + +## Motion + +Added 2026-10-28, because Dorell said "a little bit of animations. right now, it's zero". + +- **A little, and purposeful.** Motion follows the brand's two signatures: text arriving like terminal output, and the sticker being slapped on. +- **Easing and timing:** decelerating curves (ease-out-expo or quart), never bounce. Entrances run 400–700ms; feedback runs 100–250ms. Animate only transform and opacity. +- **One hero moment per page,** plus quiet reveals as sections scroll in and small hover and press feedback on things you can click. +- **Under `prefers-reduced-motion: reduce`, nothing moves,** and the cursor doesn't blink. Content never depends on JavaScript to become visible. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 75f01aa..5493447 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,14 +2,46 @@ Thanks for helping JavaScript Cebu. +## How changes ship + +Every change, from a person or an AI agent, follows the same path: + +1. **Intent.** Open an issue, or start a conversation, that says what should change and why. +2. **Plan.** Larger changes get a written spec and plan under `docs/superpowers/` before anyone writes code. +3. **Approval.** A JS Cebu lead approves the plan. +4. **Implementation.** Build it on a branch and open a pull request. +5. **Review.** + - CI runs `pnpm install --frozen-lockfile && pnpm test && pnpm build` on every pull request. + - A reviewer reads the whole branch. +6. **Merge.** Merging to `main` deploys www.jscebu.org. + +## Deploys + +**Merging to `main` is the deploy.** Cloudflare Workers Builds builds `main` and deploys the `jscebu-org` Worker with these settings: + +| Setting | Value | +|---|---| +| Root directory | repo root | +| Build command | `pnpm install --frozen-lockfile && pnpm test && pnpm --filter website build` | +| Deploy command | `pnpm --filter website exec wrangler deploy` | +| Other branches | `pnpm --filter website exec wrangler versions upload` (a preview URL per branch) | + +Node comes from `.nvmrc` and pnpm from `packageManager` in `package.json`. A failing test or post-build check stops the deploy. + +Don't run `wrangler deploy` by hand. + +Be careful with `routes` in `apps/website/wrangler.jsonc`. www.jscebu.org is attached there as a Custom Domain. Deploys from CI replace any existing DNS record for a Custom Domain they list, so add or change a domain only through a reviewed pull request. + ## Brand changes are releases `packages/brand` is the JS Cebu design system. A change to a color, font, rule, logo or template is a **versioned release** (for example v0.1 → v0.2), not a drive-by edit: -1. Open an issue or PR that says what changes and why. -2. Bump `version` in `packages/brand/src/brand.ts` and add a `changelog` entry describing the change. +1. Bump `version` in `packages/brand/src/brand.ts`. +2. Add a `changelog` entry describing the change. 3. A JS Cebu lead approves it before merge. +Bisaya copy appears only after a lead approves the phrase, which means its `status` is `approved`. + ## Before you open a PR ```sh @@ -18,8 +50,8 @@ pnpm test pnpm build ``` -All tests must pass. CI runs the same commands. +All tests and checks must pass. CI runs the same commands. ## Never commit secrets -This repository is public. No tokens, API keys, form responses or personal data. Deploy credentials live in Cloudflare and GitHub secrets. +This repository is public. Never commit tokens, API keys, form responses or personal data. Deploy credentials live in Cloudflare and GitHub. diff --git a/README.md b/README.md index 6c2faa7..0e5e957 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,34 @@ # jscebu.org -Monorepo for [JavaScript Cebu](https://www.jscebu.org). +The website and design system of [JavaScript Cebu](https://www.jscebu.org), live at . | Path | What it is | |---|---| -| `packages/brand` | The JS Cebu design system: brand data, generators for `tokens.css` and `brand.md`, logos and templates | -| `apps/website` | The Astro site deployed to Cloudflare, starting with [jscebu.org/brand](https://www.jscebu.org/brand) | +| `packages/brand` | The JS Cebu design system: brand data, the generators for `tokens.css`, `brand.md` and `llms.txt`, logos, templates and an HTML checker for the brand's MUST rules | +| `apps/website` | The Astro site: `/`, `/projects`, `/about`, `/links` and `/brand`. It deploys as the Cloudflare Worker `jscebu-org`, which serves www.jscebu.org | -- Brand guidelines: +- Brand guidelines: - For AI agents: -- Design spec: [docs/superpowers/specs/2026-10-27-jscebu-design-system-design.md](docs/superpowers/specs/2026-10-27-jscebu-design-system-design.md) +- Design specs: [design system](docs/superpowers/specs/2026-10-27-jscebu-design-system-design.md) · [site v1](docs/superpowers/specs/2026-10-28-jscebu-site-v1-design.md) + +## How changes ship + +1. Intent +2. Plan +3. Approval +4. Implementation +5. Review +6. Merge + +**Merging to `main` deploys to production automatically** through Cloudflare Workers Builds. See [CONTRIBUTING.md](CONTRIBUTING.md). ## Develop ```sh pnpm install pnpm test -pnpm build +pnpm build # builds the site and runs the post-build checks +pnpm --filter website dev # local preview ``` -Code is MIT-licensed ([LICENSE](LICENSE)). The JS Cebu name and logos are not; see [packages/brand/LICENSE](packages/brand/LICENSE). See [CONTRIBUTING.md](CONTRIBUTING.md) before changing the brand. +Code is MIT-licensed ([LICENSE](LICENSE)). The JS Cebu name and logos are not; see [packages/brand/LICENSE](packages/brand/LICENSE). diff --git a/apps/website/package.json b/apps/website/package.json index c87a454..e052f09 100644 --- a/apps/website/package.json +++ b/apps/website/package.json @@ -14,7 +14,8 @@ "linkedom": "~0.18.14", "tsx": "^4.23.15", "vitest": "^5.0.5", - "wrangler": "^4.153.0" + "wrangler": "^4.153.0", + "yaml": "^2.9.1" }, "dependencies": { "@fontsource-variable/archivo": "^5.3.0", diff --git a/apps/website/public/_redirects b/apps/website/public/_redirects new file mode 100644 index 0000000..c3013be --- /dev/null +++ b/apps/website/public/_redirects @@ -0,0 +1,13 @@ +# Old Notion-era jscebu.org URLs (spec: docs/superpowers/specs/2026-10-28-jscebu-site-v1-design.md §5). +# Targets end in / because folders are served at /name/; a bare /name would redirect twice. +/mission-vision /about/ 301 +/members /about/#organizers 301 +/iniatives /about/ 301 +/initiatives /about/ 301 +/official-links /links/ 301 +/community-projects /projects/ 301 +# Temporary: /events becomes a real page once events come from Cebby. +/events /#events 302 +/promotional-materials /brand/ 301 +/annual-hacktoberfest https://hacktoberfest.jscebu.org/ 301 +/volunteers-training-workshop /about/ 301 diff --git a/apps/website/scripts/check-dist.ts b/apps/website/scripts/check-dist.ts index 91e6ca4..497fb70 100644 --- a/apps/website/scripts/check-dist.ts +++ b/apps/website/scripts/check-dist.ts @@ -1,7 +1,8 @@ /** * Post-build checks over dist/ and src/. Run after `astro build`; exits 1 with a list on any failure. * - The cursor never wraps alone (and HTML compression didn't eat the space before its word). - * - Every /brand link on the page and in brand.md resolves to a built file. + * - Every link to this site, on every page and in brand.md and llms.txt, resolves to a built file. + * - Every redirect in _redirects that points at this site lands on a built file. * - Every rule appears on both the page and brand.md ("one source, two readers"). * - No color or font-family literals in src/: everything comes from --jc-* variables. */ @@ -12,8 +13,6 @@ import { brand, lintHtml, type Brand } from '@jscebu/brand'; import { parseHTML } from 'linkedom'; const SITE = 'https://www.jscebu.org'; -/** Paths this app serves; everything else still belongs to the legacy site. */ -const OURS = /^\/(?:brand(?:[/.]|$)|llms\.txt$|_astro\/)/; export function findCursorViolations(html: string): string[] { const out = lintHtml(html) @@ -38,8 +37,7 @@ export function findMissingLinks(text: string, fileExists: (path: string) => boo const paths = urls .map((u) => (u.startsWith(SITE) ? u.slice(SITE.length) || '/' : u)) .filter((u) => u.startsWith('/') && !u.startsWith('//')) - .map((u) => u.replace(/[?#].*$/, '')) - .filter((p) => OURS.test(p)); + .map((u) => u.replace(/[?#].*$/, '')); return [...new Set(paths)].filter((p) => !fileExists(p)); } @@ -81,6 +79,18 @@ export function parityRules(b: Brand): string[] { ]; } +/** Parse a Cloudflare `_redirects` file: `source destination [status]`, status defaulting to 302. */ +export function parseRedirects(text: string): { from: string; to: string; status: number }[] { + return text + .split('\n') + .map((line) => line.trim()) + .filter((line) => line && !line.startsWith('#')) + .map((line) => { + const [from, to, status] = line.split(/\s+/); + return { from, to, status: status ? Number(status) : 302 }; + }); +} + function walk(dir: string): string[] { return readdirSync(dir).flatMap((name) => { const p = join(dir, name); @@ -92,23 +102,39 @@ function main(): void { const app = fileURLToPath(new URL('..', import.meta.url)); const dist = join(app, 'dist'); const read = (p: string) => readFileSync(join(dist, p), 'utf8'); + /** A URL path is built when it names a file, or ends in / over a folder with index.html. A bare folder path would cost a redirect. */ const exists = (urlPath: string) => { const p = join(dist, decodeURIComponent(urlPath)); - return urlPath.endsWith('/') ? existsSync(join(p, 'index.html')) : existsSync(p) || existsSync(join(p, 'index.html')); + if (urlPath.endsWith('/')) return existsSync(join(p, 'index.html')); + return existsSync(p) && statSync(p).isFile(); }; - const required = ['brand/index.html', 'brand.md', 'brand/tokens.css', 'llms.txt', '404.html', 'brand/logo/sticker.svg', 'brand/templates/event-post.html']; - const html = read('brand/index.html'); + const required = [ + 'index.html', 'projects/index.html', 'about/index.html', 'links/index.html', '404.html', + 'brand/index.html', 'brand.md', 'brand/tokens.css', 'llms.txt', 'sitemap.xml', 'robots.txt', '_redirects', + 'brand/logo/sticker.svg', 'brand/templates/event-post.html', + ]; + const pages = walk(dist).filter((p) => p.endsWith('.html') && !p.startsWith(join(dist, 'brand', 'templates'))).map((p) => relative(dist, p)); + const brandHtml = read('brand/index.html'); const md = read('brand.md'); - const { document } = parseHTML(html); + const redirects = existsSync(join(dist, '_redirects')) ? parseRedirects(read('_redirects')) : []; const src = walk(join(app, 'src')).map((p) => ({ path: relative(app, p), text: readFileSync(p, 'utf8') })); const errors = [ ...required.filter((f) => !existsSync(join(dist, f))).map((f) => `dist/${f} was not built`), - ...findCursorViolations(html).map((m) => `/brand cursor: ${m}`), - ...findMissingLinks(html, exists).map((p) => `/brand links to ${p}, which was not built`), + ...pages.flatMap((page) => { + const html = read(page); + return [ + ...findCursorViolations(html).map((m) => `${page} cursor: ${m}`), + ...findMissingLinks(html, exists).map((p) => `${page} links to ${p}, which was not built`), + ]; + }), ...findMissingLinks(md, exists).map((p) => `brand.md links to ${p}, which was not built`), - ...findParityGaps(document.body.textContent ?? '', md, parityRules(brand)), + ...findMissingLinks(read('llms.txt'), exists).map((p) => `llms.txt links to ${p}, which was not built`), + ...redirects + .filter((r) => r.to.startsWith('/') && !exists(r.to.replace(/#.*$/, ''))) + .map((r) => `_redirects sends ${r.from} to ${r.to}, which was not built`), + ...findParityGaps(parseHTML(brandHtml).document.body.textContent ?? '', md, parityRules(brand)), ...findLiterals(src), ]; @@ -116,7 +142,9 @@ function main(): void { console.error(`check-dist: ${errors.length} problem(s)\n${errors.map((e) => ` - ${e}`).join('\n')}`); process.exit(1); } - console.log(`check-dist: ok (${required.length} files, cursor, links, parity, ${src.length} source files free of literals)`); + console.log( + `check-dist: ok (${required.length} files, ${pages.length} pages: cursor and links; ${redirects.length} redirects; /brand parity; ${src.length} source files free of literals)`, + ); } if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) main(); diff --git a/apps/website/scripts/url-matrix.sh b/apps/website/scripts/url-matrix.sh new file mode 100755 index 0000000..21f7e09 --- /dev/null +++ b/apps/website/scripts/url-matrix.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +# Check a deployed (or `wrangler dev`) jscebu.org against the v1 spec: old Notion-era URLs +# redirect per §5, pages return 200, brand.md keeps its agent-facing headers, misses 404. +# Usage: scripts/url-matrix.sh https://www.jscebu.org +set -uo pipefail + +base=${1:?usage: url-matrix.sh BASE_URL} +base=${base%/} +host=$(printf '%s' "$base" | sed -E 's#^https?://([^/]+).*#\1#') +fail=0 + +# fetch PATH → sets status, location (host-relative when on the same host) and headers +fetch() { + headers=$(curl -s -o /dev/null -D - --max-time 30 "$base$1" | tr -d '\r') + status=$(printf '%s\n' "$headers" | head -1 | awk '{print $2}') + location=$(printf '%s\n' "$headers" | grep -i '^location:' | head -1 | sed -E 's/^[Ll]ocation: *//; s#^https?://'"$host"'##') +} + +report() { # ok? label detail + if [ "$1" = ok ]; then printf 'ok %s\n' "$2"; else printf 'FAIL %s (%s)\n' "$2" "$3"; fail=1; fi +} + +redirect() { # path target status + fetch "$1" + if [ "$status" = "$3" ] && [ "$location" = "$2" ]; then report ok "$1 → $3 $2" + else report fail "$1 → $3 $2" "got $status ${location:-no location}"; fi +} + +page() { # path [content-type prefix] + fetch "$1" + local type + type=$(printf '%s\n' "$headers" | grep -i '^content-type:' | head -1 | sed -E 's/^[Cc]ontent-[Tt]ype: *//') + if [ "$status" = 200 ] && [[ "$type" == ${2:-}* ]]; then report ok "$1 200 ${type}" + else report fail "$1 200 ${2:-}" "got $status $type"; fi +} + +redirect /mission-vision /about/ 301 +redirect /members /about/#organizers 301 +redirect /iniatives /about/ 301 +redirect /initiatives /about/ 301 +redirect /official-links /links/ 301 +redirect /community-projects /projects/ 301 +redirect /events /#events 302 +redirect /promotional-materials /brand/ 301 +redirect /annual-hacktoberfest https://hacktoberfest.jscebu.org/ 301 +redirect /volunteers-training-workshop /about/ 301 + +for p in / /projects/ /about/ /links/ /brand/; do page "$p" text/html; done +page /brand.md 'text/markdown; charset=utf-8' +if printf '%s\n' "$headers" | grep -qi '^access-control-allow-origin: \*'; then report ok '/brand.md CORS *'; else report fail '/brand.md CORS *' 'header missing'; fi +page /llms.txt 'text/plain; charset=utf-8' +page /brand/tokens.css text/css +page /brand/logo/sticker.svg image/svg+xml +page /sitemap.xml application/xml +page /robots.txt text/plain + +fetch /this-page-does-not-exist +if [ "$status" = 404 ]; then report ok '/this-page-does-not-exist 404'; else report fail '/this-page-does-not-exist 404' "got $status"; fi + +exit $fail diff --git a/apps/website/src/assets/projects/cebby.png b/apps/website/src/assets/projects/cebby.png new file mode 100644 index 0000000..3dcbb74 Binary files /dev/null and b/apps/website/src/assets/projects/cebby.png differ diff --git a/apps/website/src/components/Cursor.astro b/apps/website/src/components/Cursor.astro index b5a9f0e..7dc00e4 100644 --- a/apps/website/src/components/Cursor.astro +++ b/apps/website/src/components/Cursor.astro @@ -2,17 +2,33 @@ /** * A headline's text with the brand cursor after the last word. The last word and * the cursor share a .jc-nw wrapper so the cursor can never wrap onto its own line. + * With `animate`, each word rises in turn (styles in Site.astro); the cursor is there from the start. */ interface Props { text: string; blink?: boolean; + animate?: boolean; } -const { text, blink = false } = Astro.props; -const trimmed = text.trim(); -const split = trimmed.lastIndexOf(' '); -const rest = split === -1 ? '' : trimmed.slice(0, split + 1); -const last = trimmed.slice(split + 1); +const { text, blink = false, animate = false } = Astro.props; +const words = text.trim().split(/\s+/); +const last = words.pop()!; +const cursorClass = ['jc-cursor', { 'jc-cursor--blink': blink }]; --- -{rest}{last} +{ + animate ? ( + <> + {words.map((word, i) => ( + <> + {word}{' '} + + ))} + {last} + + ) : ( + <> + {words.length ? `${words.join(' ')} ` : ''}{last} + + ) +} diff --git a/apps/website/src/components/ProjectCard.astro b/apps/website/src/components/ProjectCard.astro new file mode 100644 index 0000000..eeda270 --- /dev/null +++ b/apps/website/src/components/ProjectCard.astro @@ -0,0 +1,76 @@ +--- +import { render, type CollectionEntry } from 'astro:content'; + +interface Props { + entry: CollectionEntry<'projects'>; + /** Show the longer description from the Markdown body, not just the summary. */ + full?: boolean; +} + +const { entry, full = false } = Astro.props; +const { name, url, repo, summary, image, status } = entry.data; +const { Content } = await render(entry); +const host = new URL(url).hostname.replace(/^www\./, ''); +--- + +
+ +
+
+

{name}

+ {status === 'archived' && archived} +
+

{summary}

+ {full &&
} +

+ {host} ↗ + {repo && source ↗} +

+
+
+ + diff --git a/apps/website/src/components/Section.astro b/apps/website/src/components/Section.astro index 97cb915..06c40c5 100644 --- a/apps/website/src/components/Section.astro +++ b/apps/website/src/components/Section.astro @@ -21,7 +21,7 @@ const { id, number, title, tag } = Astro.props; .section { padding: 56px 0; border-top: 1px solid var(--jc-line); - scroll-margin-top: 72px; + scroll-margin-top: 112px; } .head { display: flex; diff --git a/apps/website/src/components/SiteFooter.astro b/apps/website/src/components/SiteFooter.astro new file mode 100644 index 0000000..60b134c --- /dev/null +++ b/apps/website/src/components/SiteFooter.astro @@ -0,0 +1,60 @@ +--- +import { brand } from '@jscebu/brand'; + +const links = [ + ['/projects/', 'Projects'], + ['/about/', 'About'], + ['/links/', 'Links'], + ['/brand/', 'Brand'], + ['/brand.md', 'brand.md'], + ['/llms.txt', 'llms.txt'], + ['https://github.com/javascriptcebu/jscebu.org', 'GitHub'], +]; +const mark = brand.assets.find((a) => a.id === 'mark-color')!.path; +--- + +
+
+ JavaScript Cebu +
    + {links.map(([href, label]) =>
  • {label}
  • )} +
+ +
+
+ + diff --git a/apps/website/src/components/SiteNav.astro b/apps/website/src/components/SiteNav.astro new file mode 100644 index 0000000..7e241de --- /dev/null +++ b/apps/website/src/components/SiteNav.astro @@ -0,0 +1,91 @@ +--- +import { brand } from '@jscebu/brand'; + +interface Props { + current?: 'home' | 'projects' | 'about' | 'links' | 'brand'; +} + +const { current } = Astro.props; +const items = [ + { href: '/projects/', label: 'Projects', key: 'projects' }, + { href: '/about/', label: 'About', key: 'about' }, + { href: '/brand/', label: 'Brand', key: 'brand' }, +] as const; +const lockup = brand.assets.find((a) => a.id === 'lockup-dark')!.path; +const mark = brand.assets.find((a) => a.id === 'mark-color')!.path; +--- + + + + diff --git a/apps/website/src/components/Sticker.astro b/apps/website/src/components/Sticker.astro index 99e7c2a..255646c 100644 --- a/apps/website/src/components/Sticker.astro +++ b/apps/website/src/components/Sticker.astro @@ -1,23 +1,58 @@ --- -/** The logo as a tilted die-cut sticker. The brand allows 6–10° either way. */ +/** The logo as a tilted die-cut sticker. The brand allows 6–10° either way. `slap` sticks it on as the page loads. */ interface Props { size: string; tilt?: number; alt?: string; class?: string; + slap?: boolean; + /** When the slap starts, so it can land after the headline. */ + delay?: number; } -const { size, tilt = -8, alt = '', class: className } = Astro.props; +const { size, tilt = -8, alt = '', class: className, slap = false, delay = 600 } = Astro.props; if (Math.abs(tilt) < 6 || Math.abs(tilt) > 10) { throw new Error(`Sticker tilt must be 6–10° either way, got ${tilt}`); } --- {alt} + + diff --git a/apps/website/src/content.config.ts b/apps/website/src/content.config.ts new file mode 100644 index 0000000..3801b7c --- /dev/null +++ b/apps/website/src/content.config.ts @@ -0,0 +1,10 @@ +import { defineCollection } from 'astro:content'; +import { file, glob } from 'astro/loaders'; +import { linkSchema, organizerSchema, pageSchema, projectSchema } from './content/schemas'; + +export const collections = { + projects: defineCollection({ loader: glob({ pattern: '**/*.md', base: './src/content/projects' }), schema: projectSchema }), + organizers: defineCollection({ loader: file('src/content/organizers.json'), schema: organizerSchema }), + links: defineCollection({ loader: file('src/content/links.json'), schema: linkSchema }), + pages: defineCollection({ loader: glob({ pattern: '**/*.md', base: './src/content/pages' }), schema: pageSchema }), +}; diff --git a/apps/website/src/content/links.json b/apps/website/src/content/links.json new file mode 100644 index 0000000..21f8a6a --- /dev/null +++ b/apps/website/src/content/links.json @@ -0,0 +1,6 @@ +[ + { "id": "facebook", "label": "Facebook", "url": "https://www.facebook.com/JavaScriptCebu/", "handle": "JavaScriptCebu", "order": 1 }, + { "id": "x", "label": "X (Twitter)", "url": "https://twitter.com/javascriptcebu", "handle": "@JavaScriptCebu", "order": 2 }, + { "id": "github", "label": "GitHub", "url": "https://github.com/javascriptcebu/", "handle": "javascriptcebu", "order": 3 }, + { "id": "email", "label": "Email", "url": "mailto:hello@jscebu.org", "handle": "hello@jscebu.org", "order": 4 } +] diff --git a/apps/website/src/content/organizers.json b/apps/website/src/content/organizers.json new file mode 100644 index 0000000..0b76a49 --- /dev/null +++ b/apps/website/src/content/organizers.json @@ -0,0 +1,6 @@ +[ + { "id": "cyrus-hiyas", "name": "Cyrus Hiyas", "role": "Lead", "community": "Angular Cebu", "order": 1 }, + { "id": "dorell-james", "name": "Dorell James", "role": "Lead", "community": "React Cebu", "order": 2 }, + { "id": "harlequin-doyon", "name": "Harlequin Doyon", "role": "Lead", "community": "Vue.js Cebu", "order": 3 }, + { "id": "sieg-balona", "name": "Sieg Balona", "role": "Co-Lead", "community": "React Cebu", "order": 4 } +] diff --git a/apps/website/src/content/pages/about.md b/apps/website/src/content/pages/about.md new file mode 100644 index 0000000..09685f2 --- /dev/null +++ b/apps/website/src/content/pages/about.md @@ -0,0 +1,27 @@ +--- +title: About JS Cebu +description: JavaScript Cebu's mission, vision, history and organizers. +--- + +## Our mission + +JavaScript Cebu aims to empower and unite developers by providing a collaborative platform for learning, sharing, and innovation in JavaScript and related technologies. We strive to enhance the technological landscape of Cebu through community-driven events such as meetups, codecamps, and other educational gatherings. + +## Our vision + +To be the leading catalyst for technological advancement and community collaboration in Cebu, fostering a vibrant ecosystem where developers can grow, innovate, and contribute to the global tech scene. + +## How we measure success + +We use the following metrics time to time to see how we're doing: + +- **Growth in Membership:** Increasing the number of active participants and registered members attending our events. +- **Event Frequency and Diversity:** Successfully hosting a variety of events regularly, including meetups, codecamps, and special tech talks. +- **Community Engagement:** High levels of engagement in community discussions, projects, and collaborations as evidenced by feedback, social media interactions, and community-driven projects. +- **Partnerships and Sponsorships:** Establishing strong partnerships with local and international tech companies and securing sponsorships that support the sustainability and expansion of our events. +- **Success Stories:** Documenting and showcasing success stories of members who have advanced in their careers or launched successful projects as a result of our community support. +- **Positive Remarks:** We always aim to be the type of organization for the people and by the people. + +## History + +JavaScript Cebu's GitHub organization was created in December 2018, and the community launched in 2019, sprouting from Laravel Cebu and Vue.js Cebu. For years it was home to three sub-communities, Angular Cebu, React Cebu and Vue.js Cebu, which ran their own meetups and workshops under the JS Cebu banner. Today JS Cebu runs as one community for everyone building with JavaScript in Cebu, and it co-presents Hacktoberfest Cebu every October. diff --git a/apps/website/src/content/projects/cebby.md b/apps/website/src/content/projects/cebby.md new file mode 100644 index 0000000..49f4abc --- /dev/null +++ b/apps/website/src/content/projects/cebby.md @@ -0,0 +1,12 @@ +--- +name: Cebby +url: https://www.getcebby.com +repo: https://github.com/getcebby/cebby +summary: Cebby is a public discovery layer for Cebu tech events. +image: ../../assets/projects/cebby.png +status: active +featured: true +order: 1 +--- + +Cebby gathers Cebu tech events from Luma, Eventbrite, Meetup and Facebook into one calm, glanceable feed, so you can see what's happening this week without following every page. It's built by the community and open source. diff --git a/apps/website/src/content/schemas.ts b/apps/website/src/content/schemas.ts new file mode 100644 index 0000000..30a3a83 --- /dev/null +++ b/apps/website/src/content/schemas.ts @@ -0,0 +1,36 @@ +import { z } from 'astro/zod'; +import type { SchemaContext } from 'astro:content'; + +/** Takes Astro's schema context so `image()` resolves local images; tests pass a stand-in. */ +export const projectSchema = ({ image }: Pick) => + z.object({ + name: z.string(), + url: z.string().url(), + repo: z.string().url().optional(), + summary: z.string(), + image: image(), + status: z.enum(['active', 'archived']), + featured: z.boolean(), + order: z.number(), + }); + +export const organizerSchema = z.object({ + id: z.string(), + name: z.string(), + role: z.string(), + community: z.string().optional(), + order: z.number(), +}); + +export const linkSchema = z.object({ + id: z.string(), + label: z.string(), + url: z.string(), + handle: z.string().optional(), + order: z.number(), +}); + +export const pageSchema = z.object({ + title: z.string(), + description: z.string(), +}); diff --git a/apps/website/src/layouts/Site.astro b/apps/website/src/layouts/Site.astro new file mode 100644 index 0000000..4367656 --- /dev/null +++ b/apps/website/src/layouts/Site.astro @@ -0,0 +1,149 @@ +--- +import SiteFooter from '../components/SiteFooter.astro'; +import SiteNav from '../components/SiteNav.astro'; +import Terminal from './Terminal.astro'; + +interface Props { + title: string; + description: string; + current?: 'home' | 'projects' | 'about' | 'links' | 'brand'; + index?: boolean; +} + +const { title, description, current, index } = Astro.props; +--- + + + +
+ +
+ +
+ + + + diff --git a/apps/website/src/layouts/Terminal.astro b/apps/website/src/layouts/Terminal.astro index 9a11aaa..d333b68 100644 --- a/apps/website/src/layouts/Terminal.astro +++ b/apps/website/src/layouts/Terminal.astro @@ -8,9 +8,13 @@ import { brand, tokensCss } from '@jscebu/brand'; interface Props { title: string; description: string; + /** False for pages search engines shouldn't list, like the 404 page. */ + index?: boolean; } -const { title, description } = Astro.props; +const { title, description, index = true } = Astro.props; +const canonical = new URL(Astro.url.pathname, Astro.site); +const shareImage = new URL(brand.assets.find((a) => a.id === 'photo')!.path, Astro.site); --- @@ -20,6 +24,14 @@ const { title, description } = Astro.props; {title} + {index ? : } + + + + + {index && } + + diff --git a/apps/website/src/pages/404.astro b/apps/website/src/pages/404.astro index 7847826..c855b82 100644 --- a/apps/website/src/pages/404.astro +++ b/apps/website/src/pages/404.astro @@ -1,27 +1,18 @@ --- import Cursor from '../components/Cursor.astro'; -import Terminal from '../layouts/Terminal.astro'; +import Site from '../layouts/Site.astro'; --- - -
+ +

// 404

-

-

That page doesn't exist. The brand guidelines are at /brand.

-
-
+

+

That page doesn't exist. Try the home page or the brand guidelines.

+ + diff --git a/apps/website/src/pages/about.astro b/apps/website/src/pages/about.astro new file mode 100644 index 0000000..898cff1 --- /dev/null +++ b/apps/website/src/pages/about.astro @@ -0,0 +1,99 @@ +--- +import { getCollection, getEntry, render } from 'astro:content'; +import Cursor from '../components/Cursor.astro'; +import Site from '../layouts/Site.astro'; + +const page = (await getEntry('pages', 'about'))!; +const { Content } = await render(page); +const organizers = (await getCollection('organizers')).sort((a, b) => a.data.order - b.data.order); +--- + + +
+
+

// about

+

+
+
+ +
+
+

Organizers

+
    + { + organizers.map(({ data }, i) => ( +
  • + {data.name} + + {data.role} + {data.community && ` · ${data.community}`} + +
  • + )) + } +
+
+
+
+ + diff --git a/apps/website/src/pages/brand/index.astro b/apps/website/src/pages/brand/index.astro index 0da0615..14e9039 100644 --- a/apps/website/src/pages/brand/index.astro +++ b/apps/website/src/pages/brand/index.astro @@ -7,7 +7,7 @@ import RuleList from '../../components/RuleList.astro'; import Section from '../../components/Section.astro'; import Sticker from '../../components/Sticker.astro'; import Swatch from '../../components/Swatch.astro'; -import Terminal from '../../layouts/Terminal.astro'; +import Site from '../../layouts/Site.astro'; import { asset, fontVar } from '../../lib/view'; const agentUrl = `${brand.site}/brand.md`; @@ -36,29 +36,28 @@ const nav = [ const sample = 'Aa Bb Cc 0123456789'; --- - - + -
+

// brand · v{brand.version}

-

+

Everything you need to make something for JavaScript Cebu: logos, colors, type, rules and templates. Using an AI agent? Give it the brand.md link. It has the same rules in a form agents can follow.

- +
@@ -141,7 +140,7 @@ const sample = 'Aa Bb Cc 0123456789';
{s.id === 'cursor' && {'word'}} {s.id === 'sticker' && The JS Cebu sticker, tilted} - {s.id === 'bisaya' &&

{brand.voice.phrases.map((p) => p.bisaya).join(' · ')}

} + {s.id === 'bisaya' &&

{brand.voice.phrases.map((p) => p.bisaya).join(' · ')}{brand.voice.phrases.some((p) => p.status === 'draft') && drafts, not yet approved}

} {s.id === 'photos' && The JS Cebu community at a meetup}
@@ -216,13 +215,8 @@ const sample = 'Aa Bb Cc 0123456789'; )) }
-
- - -
+ + diff --git a/apps/website/src/pages/index.astro b/apps/website/src/pages/index.astro new file mode 100644 index 0000000..422d930 --- /dev/null +++ b/apps/website/src/pages/index.astro @@ -0,0 +1,247 @@ +--- +import { brand } from '@jscebu/brand'; +import { getCollection } from 'astro:content'; +import Cursor from '../components/Cursor.astro'; +import ProjectCard from '../components/ProjectCard.astro'; +import Sticker from '../components/Sticker.astro'; +import Site from '../layouts/Site.astro'; + +const featured = (await getCollection('projects', ({ data }) => data.featured)).sort((a, b) => a.data.order - b.data.order); +const links = (await getCollection('links')).sort((a, b) => a.data.order - b.data.order); +const photo = brand.assets.find((a) => a.id === 'photo')!.path; + +const work = [ + { title: 'Meetups', text: 'Talks, demos and lightning talks from developers building in Cebu.' }, + { title: 'Workshops', text: 'Hands-on sessions, from automated testing to building with AI.' }, + { title: 'Hacktoberfest Cebu', text: 'A month of open source every October, with partner communities.', href: 'https://hacktoberfest.jscebu.org' }, +]; +--- + + +
+
+
+

// cebu's javascript community

+

+

Meetups, workshops and Hacktoberfest, run by developers in Cebu.

+ +
+ +
+ +
+

What we do

+ +
+ +
+

Upcoming events

+

Our next meetups and workshops, along with every other Cebu tech event, are on Cebby.

+ +
+ +
+

Featured project

+
+ {featured.map((entry) => )} +
+ All projects → +
+ +
+
+ The JS Cebu community at a meetup +
The JS Cebu community at a meetup
+
+
+ +
+

Join us

+

Come to a meetup, follow along online, or say hello.

+ +
+
+
+ + diff --git a/apps/website/src/pages/links.astro b/apps/website/src/pages/links.astro new file mode 100644 index 0000000..7e82bba --- /dev/null +++ b/apps/website/src/pages/links.astro @@ -0,0 +1,69 @@ +--- +import { getCollection } from 'astro:content'; +import Cursor from '../components/Cursor.astro'; +import Site from '../layouts/Site.astro'; + +const links = (await getCollection('links')).sort((a, b) => a.data.order - b.data.order); +--- + + +
+
+

// official links

+

+
+ +
+
+ + diff --git a/apps/website/src/pages/projects.astro b/apps/website/src/pages/projects.astro new file mode 100644 index 0000000..514e8d4 --- /dev/null +++ b/apps/website/src/pages/projects.astro @@ -0,0 +1,30 @@ +--- +import { getCollection } from 'astro:content'; +import Cursor from '../components/Cursor.astro'; +import ProjectCard from '../components/ProjectCard.astro'; +import Site from '../layouts/Site.astro'; + +const projects = (await getCollection('projects')).sort((a, b) => a.data.order - b.data.order); +--- + + +
+
+

// projects

+

+

Things the JS Cebu community builds and runs, in the open.

+
+
+ {projects.map((entry, i) =>
)} +
+
+
+ + diff --git a/apps/website/src/pages/robots.txt.ts b/apps/website/src/pages/robots.txt.ts new file mode 100644 index 0000000..6c12151 --- /dev/null +++ b/apps/website/src/pages/robots.txt.ts @@ -0,0 +1,6 @@ +import type { APIRoute } from 'astro'; + +export const GET: APIRoute = ({ site }) => + new Response(`User-agent: *\nAllow: /\n\nSitemap: ${new URL('/sitemap.xml', site)}\n`, { + headers: { 'Content-Type': 'text/plain; charset=utf-8' }, + }); diff --git a/apps/website/src/pages/sitemap.xml.ts b/apps/website/src/pages/sitemap.xml.ts new file mode 100644 index 0000000..bc01f7c --- /dev/null +++ b/apps/website/src/pages/sitemap.xml.ts @@ -0,0 +1,10 @@ +import type { APIRoute } from 'astro'; + +const pages = ['/', '/projects/', '/about/', '/links/', '/brand/']; + +export const GET: APIRoute = ({ site }) => { + const urls = pages.map((p) => ` ${new URL(p, site)}`).join('\n'); + return new Response(`\n\n${urls}\n\n`, { + headers: { 'Content-Type': 'application/xml; charset=utf-8' }, + }); +}; diff --git a/apps/website/test/check-dist.test.ts b/apps/website/test/check-dist.test.ts index 04d51e5..21e470a 100644 --- a/apps/website/test/check-dist.test.ts +++ b/apps/website/test/check-dist.test.ts @@ -26,8 +26,12 @@ describe('findMissingLinks', () => { expect(findMissingLinks('See https://www.jscebu.org/brand/logo/missing.svg.', exists)).toEqual(['/brand/logo/missing.svg']); }); - it('ignores pages the legacy site still serves and other origins', () => { - expect(findMissingLinks('xy', () => false)).toEqual([]); + it('checks every path on this site, so a link to a redirect source counts as missing', () => { + expect(findMissingLinks('xy', (p) => p === '/about/')).toEqual(['/events']); + }); + + it('ignores other origins, mailto links and in-page anchors', () => { + expect(findMissingLinks('ymj', () => false)).toEqual([]); }); }); diff --git a/apps/website/test/content.test.ts b/apps/website/test/content.test.ts new file mode 100644 index 0000000..d08b4f4 --- /dev/null +++ b/apps/website/test/content.test.ts @@ -0,0 +1,44 @@ +import { readFileSync } from 'node:fs'; +import { z } from 'astro/zod'; +import { parse } from 'yaml'; +import { describe, expect, it } from 'vitest'; +import { linkSchema, organizerSchema, pageSchema, projectSchema } from '../src/content/schemas.ts'; + +const content = new URL('../src/content/', import.meta.url); +const read = (path: string) => readFileSync(new URL(path, content), 'utf8'); +const frontmatter = (md: string) => parse(md.match(/^---\n([\s\S]*?)\n---/)![1]); + +describe('site content', () => { + it('lists the four organizers from /members, in order', () => { + const organizers = z.array(organizerSchema).parse(JSON.parse(read('organizers.json'))); + expect(organizers.map((o) => [o.name, o.role])).toEqual([ + ['Cyrus Hiyas', 'Lead'], + ['Dorell James', 'Lead'], + ['Harlequin Doyon', 'Lead'], + ['Sieg Balona', 'Co-Lead'], + ]); + }); + + it('lists the official channels from /official-links', () => { + const links = z.array(linkSchema).parse(JSON.parse(read('links.json'))); + expect(links.map((l) => l.url)).toEqual( + expect.arrayContaining([ + 'https://www.facebook.com/JavaScriptCebu/', + 'https://twitter.com/javascriptcebu', + 'https://github.com/javascriptcebu/', + 'mailto:hello@jscebu.org', + ]), + ); + }); + + it('features Cebby as the only project', () => { + const cebby = projectSchema({ image: () => z.string() }).parse(frontmatter(read('projects/cebby.md'))); + expect(cebby).toMatchObject({ name: 'Cebby', url: 'https://www.getcebby.com', featured: true, status: 'active' }); + }); + + it('keeps the mission verbatim on the about page', () => { + const about = read('pages/about.md'); + pageSchema.parse(frontmatter(about)); + expect(about).toContain('JavaScript Cebu aims to empower and unite developers'); + }); +}); diff --git a/apps/website/test/motion.test.ts b/apps/website/test/motion.test.ts new file mode 100644 index 0000000..db43fe7 --- /dev/null +++ b/apps/website/test/motion.test.ts @@ -0,0 +1,13 @@ +import { readFileSync } from 'node:fs'; +import { describe, expect, it } from 'vitest'; + +const site = readFileSync(new URL('../src/layouts/Site.astro', import.meta.url), 'utf8'); + +describe('scroll reveal', () => { + it('only hides not-yet-revealed content on screens, so printing or saving as PDF shows everything', () => { + const hide = site.indexOf('.motion [data-reveal] {'); + expect(hide).toBeGreaterThan(0); + const media = site.slice(site.lastIndexOf('@media', hide), hide); + expect(media).toMatch(/^@media screen and \(prefers-reduced-motion: no-preference\)/); + }); +}); diff --git a/apps/website/test/redirects.test.ts b/apps/website/test/redirects.test.ts new file mode 100644 index 0000000..27b0f49 --- /dev/null +++ b/apps/website/test/redirects.test.ts @@ -0,0 +1,30 @@ +import { readFileSync } from 'node:fs'; +import { describe, expect, it } from 'vitest'; +import { parseRedirects } from '../scripts/check-dist.ts'; + +describe('parseRedirects', () => { + it('skips comments and blank lines and defaults the status to 302', () => { + expect(parseRedirects('# old pages\n\n/a /b\n/c https://x.example 301\n')).toEqual([ + { from: '/a', to: '/b', status: 302 }, + { from: '/c', to: 'https://x.example', status: 301 }, + ]); + }); +}); + +describe('public/_redirects', () => { + it('sends every old jscebu.org URL where the v1 spec says (§5)', () => { + const rules = parseRedirects(readFileSync(new URL('../public/_redirects', import.meta.url), 'utf8')); + expect(rules).toEqual([ + { from: '/mission-vision', to: '/about/', status: 301 }, + { from: '/members', to: '/about/#organizers', status: 301 }, + { from: '/iniatives', to: '/about/', status: 301 }, + { from: '/initiatives', to: '/about/', status: 301 }, + { from: '/official-links', to: '/links/', status: 301 }, + { from: '/community-projects', to: '/projects/', status: 301 }, + { from: '/events', to: '/#events', status: 302 }, + { from: '/promotional-materials', to: '/brand/', status: 301 }, + { from: '/annual-hacktoberfest', to: 'https://hacktoberfest.jscebu.org/', status: 301 }, + { from: '/volunteers-training-workshop', to: '/about/', status: 301 }, + ]); + }); +}); diff --git a/apps/website/wrangler.jsonc b/apps/website/wrangler.jsonc index ab5da2b..4ccf7b0 100644 --- a/apps/website/wrangler.jsonc +++ b/apps/website/wrangler.jsonc @@ -9,7 +9,8 @@ "directory": "./dist", "binding": "ASSETS", "not_found_handling": "404-page" - } - // No routes yet. The www.jscebu.org routes are added at cutover (plan Task 8), - // so every other path keeps going to the existing site. + }, + // www.jscebu.org is attached as a Workers Custom Domain (Cloudflare manages its DNS + // record and certificate). The apex jscebu.org still redirects to www via Vercel. + "routes": [{ "pattern": "www.jscebu.org", "custom_domain": true }] } diff --git a/docs/superpowers/plans/2026-10-28-jscebu-site-v1.md b/docs/superpowers/plans/2026-10-28-jscebu-site-v1.md new file mode 100644 index 0000000..366d897 --- /dev/null +++ b/docs/superpowers/plans/2026-10-28-jscebu-site-v1.md @@ -0,0 +1,199 @@ +# jscebu.org v1 Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Ship the jscebu.org landing page plus `/projects` (Cebby), `/about` and `/links` on the existing Astro app, redirect every old Notion URL, and swap www.jscebu.org to the `jscebu-org` Worker. + +**Architecture:** +- The app is the static Astro 7 site in `apps/website`. +- Content lives in Markdown/JSON content collections, validated with zod schemas. +- All pages share a `Site` layout (nav and footer) on top of the existing `Terminal` layout. +- Old URLs are redirected by a `_redirects` file that Workers static assets applies before asset matching. +- `check-dist` grows to cover every built page. +- The swap attaches `www.jscebu.org` to the Worker as a Custom Domain. + +**Tech Stack:** Astro 7.4 (content layer: `glob` and `file` loaders from `astro/loaders`, zod from `astro/zod`), Vitest 5, Wrangler 4, linkedom. + +**Spec:** `docs/superpowers/specs/2026-10-28-jscebu-site-v1-design.md`. It builds on `docs/superpowers/specs/2026-10-27-jscebu-design-system-design.md`. + +## Global Constraints + +- Every page uses the Terminal personality. There must be no hex, `rgb()`, `hsl()` or font-family literals in `apps/website/src`; `check-dist` enforces this. +- The primary headline on each page ends with ``, wrapped in `.jc-nw`. There is one cursor per page, and only the landing hero's cursor blinks. +- No Bisaya copy until Dorell approves the phrases. +- Content comes from the current site: + - Mission and vision, verbatim from `/mission-vision`. + - The four leads, from `/members`. + - The channels, from `/official-links`. + - Cebby's description: "Cebby is a public discovery layer for Cebu tech events", linking https://www.getcebby.com and https://github.com/getcebby/cebby. +- The upcoming-events links are https://www.getcebby.com and https://www.facebook.com/JavaScriptCebu/events. +- There is no blog link, because blog.jscebu.org returns 403. +- Work happens on branch `feat/site-v1`, stacked on `feat/brand-v0.1`. The PR targets `feat/brand-v0.1` until PR #1 merges. +- Commit messages: an imperative subject of about 50 characters; the body has one line per paragraph with no hard wrapping. +- **Outward actions:** + - The workers.dev preview deploy is approved. + - Deleting the www DNS record and attaching the custom domain each need Dorell's explicit "go". + +## Review Focus + +1. **Someone following an old link** (`/members`, `/iniatives`, `/events`, the three Notion "page not found" pages): they must land on the target in spec §5 with its status. A redirect into a 404 is a failure. Owners: Task 4 (`_redirects` table test) and Tasks 5–6 (live URL matrix). +2. **A visitor on a 320px phone:** no horizontal scroll on `/`, `/projects`, `/about`, `/links`, `/brand`. Owner: Task 5 (iframe harness, since headless Chrome won't lay out below 500px). +3. **A nav or footer link to a path that doesn't exist:** the build must fail. Owner: Task 4 (`findMissingLinks` over every page, with no "ours" filter, and redirect sources counting as missing). +4. **An agent fetching `/brand.md` after the swap:** it must still get `text/markdown; charset=utf-8` with CORS. Owner: Task 6. +5. **The swap gap:** between deleting the CNAME and attaching the domain, www is down. The rollback commands must be written down and tested for syntax before the "go". Owner: Task 6. + +--- + +## File Structure + +``` +apps/website/ + src/content.config.ts collections: projects, organizers, links, pages + src/content/schemas.ts zod schemas (shared by config and tests) + src/content/projects/cebby.md src/content/organizers.json src/content/links.json + src/content/pages/about.md + src/assets/projects/cebby.png Cebby icon (copied from getcebby/cebby) + src/layouts/Site.astro Terminal + SiteNav + slot + SiteFooter + src/components/SiteNav.astro SiteFooter.astro ProjectCard.astro + src/pages/index.astro projects.astro about.astro links.astro sitemap.xml.ts robots.txt.ts + public/_redirects + scripts/check-dist.ts all pages; parseRedirects + scripts/url-matrix.sh old/new URL checks against a base URL + test/content.test.ts test/redirects.test.ts +``` + +--- + +### Task 1: Content collections and data + +**Files:** Create `src/content.config.ts`, `src/content/schemas.ts`, the content files above and `src/assets/projects/cebby.png`. Test: `test/content.test.ts`. + +**Interfaces (produces):** +- `projectSchema({ image })`: + - `name: string`, `url: url`, `repo?: url`, `summary: string` + - `image: image()` + - `status: 'active' | 'archived'`, `featured: boolean`, `order: number` +- `organizerSchema`: `{ id, name, role, community?, order }`. The `file()` loader needs an `id` per entry. +- `linkSchema`: `{ id, label, url, handle?, order }`. +- `pageSchema`: `{ title, description }`. +- `collections = { projects: glob('**/*.md', base: 'src/content/projects'), organizers: file('src/content/organizers.json'), links: file('src/content/links.json'), pages: glob('**/*.md', base: 'src/content/pages') }`. + +- [ ] **Step 1: Write the failing test.** `content.test.ts` reads the JSON files and the frontmatter of each Markdown file. It parses them with the schemas; for the image field, a stub `z.string()` stands in. Assert: + - The organizers are exactly Cyrus Hiyas, Dorell James, Harlequin Doyon and Sieg Balona, with roles Lead, Lead, Lead, Co-Lead, in that order. + - The links include `https://www.facebook.com/JavaScriptCebu/`, `https://twitter.com/javascriptcebu`, `https://github.com/javascriptcebu/` and `mailto:hello@jscebu.org`. + - The only project is Cebby (`featured: true`, url `https://www.getcebby.com`). + - `about.md` contains the mission's first sentence verbatim: "JavaScript Cebu aims to empower and unite developers". +- [ ] **Step 2: Run** `pnpm --filter website test`. Expected: FAIL (the schemas module is missing). +- [ ] **Step 3: Implement** the schemas, config and data. + - `about.md` holds the Mission, Vision and "How we measure success" sections verbatim, plus a short History section (spec §3). + - Copy Cebby's icon from `~/Projects/cebby/apps/pwa/public/` (choose the largest PNG app icon). +- [ ] **Step 4: Run** the tests, then `pnpm --filter website build`. Expected: PASS, and Astro reports the 4 collections. +- [ ] **Step 5: Commit:** `feat(website): add content collections for the site`. + +### Task 2: Shared layout, and the /projects, /about and /links pages + +**Files:** Create `src/layouts/Site.astro`, `src/components/{SiteNav,SiteFooter,ProjectCard}.astro`, and `src/pages/{projects,about,links}.astro`. Modify `src/pages/brand/index.astro` and `src/pages/404.astro` to use `Site`. + +**Interfaces:** +- `Site.astro` props: `{ title: string; description: string; current?: 'home' | 'projects' | 'about' | 'links' | 'brand' }`. + - It renders `Terminal`, then `SiteNav`, a `
` slot, and `SiteFooter`. +- `SiteNav`: + - The lockup-dark logo links to `/`. Then the links Projects, About, Brand. + - `aria-current="page"` on the current link. + - It collapses to the logo plus links on one line from 320px up; no hamburger menu. +- `/brand` keeps its section links as a second row under `SiteNav`, and keeps its `brand.md` link. +- `SiteFooter`: + - Links to Projects, About, Links, Brand, `/brand.md`, the GitHub repo and `/llms.txt`. + - The license sentence from `/brand`'s footer. +- `ProjectCard` props: `{ entry: CollectionEntry<'projects'> }`. +- Each page's H1 ends with `` (non-blinking): + - `/projects` "Community projects" + - `/about` "About JS Cebu" + - `/links` "Find JS Cebu" + +- [ ] **Step 1:** Build the layout and pages from the collections (`getCollection`, `render`). +- [ ] **Step 2: Run** `pnpm --filter website build`. Expected: `check-dist` passes, including `/brand` parity. +- [ ] **Step 3: Commit:** `feat(website): add site layout and about, links, projects`. + +### Task 3: The landing page + +**Files:** Create `src/pages/index.astro`. + +- [ ] **Step 1:** Build `/` with `Site` (`current: 'home'`). Sections, in order, with these ids: + - `hero`: + - The label `// cebu's javascript community`. + - The H1 ``. + - One line: "Meetups, workshops and Hacktoberfest, run by developers in Cebu." + - Buttons: "Upcoming events ↓" (`#events`) and "Join us" (`#join`). + - `` on the right on desktop, in the flow on mobile, as on `/brand`. + - `what-we-do`: three cards (Meetups, Workshops, Hacktoberfest Cebu), each one line. Hacktoberfest links to `https://hacktoberfest.jscebu.org`. + - `events`: a heading, one line saying events are listed on Cebby, and buttons to Cebby (primary) and Facebook events. + - `projects`: the featured project's `ProjectCard`, plus a link to `/projects`. + - `community`: the real community photo (`.jc-photo`, unfiltered, `/brand/photos/community-meetup.jpg`), with a mono caption. + - `join`: the `links` collection as buttons. +- [ ] **Step 2: Run** `pnpm --filter website build`. Expected: PASS. +- [ ] **Step 3: Commit:** `feat(website): add the landing page`. + +### Task 4: Every page checked, redirects, sitemap + +**Files:** Modify `scripts/check-dist.ts`. Create `public/_redirects`, `src/pages/{sitemap.xml,robots.txt}.ts` and `test/redirects.test.ts`. Modify `test/check-dist.test.ts`. + +**Interfaces:** +- `parseRedirects(text: string): { from: string; to: string; status: number }[]`. Comments and blank lines are skipped; the status defaults to 302. +- `findMissingLinks(text, fileExists)` checks every root-relative and `https://www.jscebu.org/…` path; the "ours" filter is removed. Redirect sources are not files, so linking to one counts as missing. +- `check-dist` runs the cursor and link checks over every `dist/**/*.html`, `brand.md` and `llms.txt`. Parity stays on `/brand`, and the literal check stays on `src/**`. It also checks that every redirect target that is a path on this site exists. + +- [ ] **Step 1: Write the failing tests.** + - `redirects.test.ts` reads `public/_redirects` and asserts it equals spec §5, row for row: + - `/mission-vision` → `/about` 301 + - `/members` → `/about#organizers` 301 + - `/iniatives` → `/about` 301 + - `/initiatives` → `/about` 301 + - `/official-links` → `/links` 301 + - `/community-projects` → `/projects` 301 + - `/events` → `/#events` 302 + - `/promotional-materials` → `/brand` 301 + - `/annual-hacktoberfest` → `https://hacktoberfest.jscebu.org` 301 + - `/volunteers-training-workshop` → `/about` 301 + - In `check-dist.test.ts`, replace the "ignores pages the legacy site serves" case with: `/events` is reported missing when it isn't a file. +- [ ] **Step 2: Run** `pnpm --filter website test`. Expected: FAIL. +- [ ] **Step 3: Implement.** + - `sitemap.xml` lists `/`, `/projects`, `/about`, `/links` and `/brand/` as absolute URLs. + - `robots.txt` allows everything and points to the sitemap. +- [ ] **Step 4: Run** `pnpm test && pnpm --filter website build`. Expected: PASS. +- [ ] **Step 5: Commit:** `feat(website): redirect old URLs and check every page`. + +### Task 5: Preview on workers.dev (CHECKPOINT: Dorell reviews) + +**Files:** Create `scripts/url-matrix.sh`. + +- [ ] **Step 1:** Write `scripts/url-matrix.sh BASE`. It curls each row of spec §5 without following redirects and checks the status and `Location`. It also checks: + - `/`, `/projects`, `/about`, `/links` and `/brand/` return 200. + - `/brand.md` returns 200 with `text/markdown; charset=utf-8` and `access-control-allow-origin: *`. + - `/nope` returns 404. + - It prints one line per check and exits 1 on any mismatch. +- [ ] **Step 2: Run** it against `npx wrangler dev --local`. Expected: all pass. +- [ ] **Step 3: Deploy the preview.** Run `pnpm --filter website build && pnpm --filter website exec wrangler deploy`. Then run `scripts/url-matrix.sh https://jscebu-org..workers.dev`. Expected: all pass. +- [ ] **Step 4:** Screenshot `/`, `/projects`, `/about` and `/links` at 1280px, and at a true 320px through the iframe harness, with reduced motion forced. Check for no horizontal scroll and a visible cursor. +- [ ] **Step 5:** Push, then open a PR from `feat/site-v1` into `feat/brand-v0.1`. +- [ ] **CHECKPOINT:** share the preview URL. Dorell approves the copy and the pages, or asks for changes. + +### Task 6: Swap www.jscebu.org (CHECKPOINT: explicit "go") + +- [ ] **Step 1: Pre-flight (read-only).** + - `npx wrangler whoami` shows the account. + - Confirm the `jscebu.org` zone is in it, using the Cloudflare dashboard or an API zone listing with that account. + - Record the current DNS records for `www` and the apex in the ledger. + - Write the rollback commands into the ledger. +- [ ] **Step 2: CHECKPOINT.** Get Dorell's "go" for the swap window. +- [ ] **Step 3: Delete the www CNAME** (`cname.vercel-dns.com`). Use the API if wrangler's token has DNS edit; otherwise Dorell deletes it in the dashboard. +- [ ] **Step 4: Attach the domain.** Add `"routes": [{ "pattern": "www.jscebu.org", "custom_domain": true }]` to `wrangler.jsonc` and replace the "no routes yet" comment. Then run `wrangler deploy`. +- [ ] **Step 5: Verify.** Run `scripts/url-matrix.sh https://www.jscebu.org` until it passes. Certificate issuance can take a few minutes. + - Also check that `https://jscebu.org` still returns `308 → https://www.jscebu.org/` from Vercel. + - If www isn't serving within 15 minutes, roll back. +- [ ] **Step 6: Commit:** `feat(website): serve www.jscebu.org from the Worker`. Update project memory with the launch date and the follow-ups: + - Retire Vercel and Notion, and move the apex redirect to Cloudflare. + - Remove www from the Vercel project. + - Workers Builds auto-deploy. + - The Cebby events endpoint. + - Design-system Task 9 against the live `/brand`. diff --git a/docs/superpowers/specs/2026-10-27-jscebu-design-system-design.md b/docs/superpowers/specs/2026-10-27-jscebu-design-system-design.md index 9fcf9c3..c9eba43 100644 --- a/docs/superpowers/specs/2026-10-27-jscebu-design-system-design.md +++ b/docs/superpowers/specs/2026-10-27-jscebu-design-system-design.md @@ -87,7 +87,7 @@ Success means **you send one link, and what comes back is recognizably JS Cebu w | Full-color mark | The source of the sticker | | Flat lockup, light | Mark plus black "JAVASCRIPT / CEBU" wordmark, on light backgrounds | | Flat lockup, dark | Mark plus white wordmark, on dark backgrounds | -| One-color mark and lockup, ink and white | Required on yellow backgrounds. The white separators and ring gap become **see-through**, so the background shows through the "JS" lines (prototyped and approved in the brainstorm). | +| One-color mark and lockup, ink and white | Required on yellow backgrounds. The white separators and ring gap become **see-through**, so the background shows through the "JS" lines (prototyped and approved in the brainstorm). **[Proposed amendment, 2026-10-28, awaiting Dorell's confirmation]** On yellow, use **ink**: white on yellow is only 1.3:1. White one-color is for dark backgrounds and photos. | | Sticker | The full-color mark with a white die-cut outline and an optional soft shadow (§4.3) | **Source: the original PSD.** Dorell provided it on 2026-10-27, at `~/Downloads/js-cebu-logo.psd` (1166×1299, RGBA, sha256 `d7c26803…2ad2dab`). diff --git a/docs/superpowers/specs/2026-10-28-jscebu-site-v1-design.md b/docs/superpowers/specs/2026-10-28-jscebu-site-v1-design.md new file mode 100644 index 0000000..f1e0801 --- /dev/null +++ b/docs/superpowers/specs/2026-10-28-jscebu-site-v1-design.md @@ -0,0 +1,171 @@ +# jscebu.org v1: Design + +- **Date:** 2026-10-28 +- **Owner:** Dorell James +- **Status:** design for approval +- **Scope:** project 2 of 3 (see the design-system spec, "Project order"), first release +- **Builds on:** `docs/superpowers/specs/2026-10-27-jscebu-design-system-design.md` (the brand, `apps/website`, `/brand`) + +Labels: **[Decided]** Dorell's call · **[Proposed]** Claude's call, change freely · **[Builder]** left to whoever builds it. + +## 1. Intent + +Move www.jscebu.org off Notion and onto the new Astro site now, with what exists today, and add events once Cebby can serve them. + +- **[Decided]** "Ship the landing page for now and what's available." +- **[Decided]** Build the site first, then swap all of www.jscebu.org to it. + - This **supersedes Task 8 of the design-system plan**, which was a `/brand`-only route cutover. + - There is no partial cutover. + +## 2. Goals, non-goals, anti-goals + +**Goals** +- A landing page and the few pages the old site really had, all built on the design system. +- www.jscebu.org served by the `jscebu-org` Cloudflare Worker. +- Every old URL lands somewhere sensible. + +**Non-goals for v1** (later releases) +- `/events` pages and the Cebby endpoint (§6). +- A Hacktoberfest page (it has its own site). +- Event recaps, RSS, a blog. +- Moving the apex domain off Vercel. + +**Anti-goals** +- An old link that now 404s without a reason. +- An API token reaching the browser or the repo. +- Hand-typed colors or fonts. The design-system rules and `check-dist` apply to every page. + +## 3. Pages + +All pages use the **Terminal** personality and share one nav and footer. +- **Nav:** the dark flat lockup, then Projects, About, Brand. +- **Footer:** links, the code license and the brand-assets note. + +| Path | Content | Source | +|---|---|---| +| `/` | **Hero:** "Build things. Ship them. Together" with the blinking cursor, the sticker, and a one-line intro. **What we do:** meetups, workshops, Hacktoberfest Cebu. **Upcoming events** (`#events`): buttons to Cebby and to JS Cebu's Facebook events. **Featured project:** Cebby. **Community photo:** real and unfiltered. **Join:** Facebook, GitHub, X, email. | site content (§4) | +| `/projects` | Project cards. Cebby only in v1, as **[Decided]** "Cebby first". | `projects` collection | +| `/about` | Mission and vision, plus "how we measure success" (moved verbatim from `/mission-vision`). History: GitHub org 2018, launched 2019 from Laravel Cebu and Vue.js Cebu, with the Angular, React and Vue Cebu sub-communities as history. Organizers (`#organizers`): the four leads from `/members`, as listed today. | `about.md` + `organizers` collection | +| `/links` | The official channels: Facebook, X, GitHub, website, hello@jscebu.org. | `links` collection | +| `/brand`, `/brand.md`, `/brand/*`, `/llms.txt`, `/404` | Already built. `/brand` switches to the shared nav, keeping its section links. | unchanged | +| `/sitemap.xml`, `/robots.txt` | **[Proposed]** For search engines, generated by a small endpoint rather than an integration. The old sitemap returns 500. | build | + +- **[Proposed]** No Bisaya on these pages until Dorell approves the phrases. Approval is part of the design-system PR review. +- **[Builder]** Final copy, written in the brand voice. Dorell reviews it on the preview. + +## 4. Content model + +- **[Decided]** Markdown in the repo, using Astro content collections ("or any new one today"). + - **[Builder]** Use Astro 7's current content-layer API (loaders) and check its docs at build time. +- **[Proposed]** Collections, with schemas validated at build: + - **`projects`:** one Markdown file per project. Fields: + - `name`, `url`, `repo?`, `summary` + - `image` (a local logo or screenshot) + - `status` (`active` | `archived`) + - `featured: boolean` + - `order` + - Body: a longer description. + - **`organizers`:** one data file. Fields: `name`, `role`, `community?`, `order`. + - **`links`:** one data file. Fields: `label`, `url`, `handle?`, `order`. +- `about.md` is a single Markdown page. +- The landing-page copy lives in the page itself. +- **Cebby's card content:** "Cebby is a public discovery layer for Cebu tech events". It links to https://www.getcebby.com and github.com/getcebby/cebby, with its icon copied from the Cebby repo. + +## 5. Old URLs + +Redirects ship as a `_redirects` file served by Workers static assets. Targets end in `/` because Workers serves `about/index.html` at `/about/` and 307s `/about` to it; a slash-less target would redirect twice. **[Builder]** Confirm the file format against the current Cloudflare docs. + +| Old URL (today) | v1 | Status | +|---|---|---| +| `/mission-vision` | `/about/` | 301 | +| `/members` | `/about/#organizers` | 301 | +| `/iniatives` (sic), `/initiatives` | `/about/` | 301 | +| `/official-links` | `/links/` | 301 | +| `/community-projects` (a 2023 Hacktoberfest repo list) | `/projects/` | 301 | +| `/events` | `/#events` | **302**, because `/events` becomes a real page later | +| `/promotional-materials` (Notion "page not found" today) | `/brand/` | 301 | +| `/annual-hacktoberfest` (Notion "page not found" today) | `https://hacktoberfest.jscebu.org/` | 301 | +| `/volunteers-training-workshop` (Notion "page not found" today) | `/about/` | 301 | +| `/feed` (500 today) | 404 | None. v1 has no feed. | +| `/sitemap.xml` (500 today) | the new sitemap | | +| anything else | the branded 404 page | | + +## 6. Later: events from Cebby + +Recorded so v1 doesn't paint it into a corner. This gets built in its own release. + +- **[Decided]** + - Events come from Cebby's API. + - Cebby gains an endpoint that returns JS Cebu's events, protected by an API token. + - jscebu.org pulls it server-side, so the token never reaches the browser or the repo. +- **What exists today in Cebby** (`getcebby/cebby`, `apps/pwa`): + - Astro 5 on Cloudflare Workers, with server-side rendering. + - Supabase tables `organizations`, `accounts`, `events`, `event_organizers`. + - JS Cebu is in the data (`javascript-cebu`). + - Existing routes live under `src/pages/api/`. +- **[Builder, then]** Fetch at build time with a content loader, or per request with a live collection, which needs the Cloudflare adapter. Decide when building it. +- **[Decided, input for that release]** "We've got lots of events and I don't want to build it in a way that's one list of top to bottom." + - Dorell shared the Facebook past-events page on 2026-10-28. It holds about 50 events, from Jan 2019 to Oct 2026. + - **Series:** React Series: Zero to Hero 1–5 plus a hackathon (2019); six AI4Devs Workshops (Jun 2025); Automated Testing workshops (2024–2025). + - **Hacktoberfest Cebu stages, per edition:** kickoff or opening, online midpoint, deployment day, awarding. Editions ran 2020, 2021, 2023, 2024, 2025 and 2026. + - **Hosts besides JS Cebu:** React Cebu, Vue.js Cebu, DEVCON Cebu, enspace Cebu, PWA Pilipinas, Cebu Tech Communities. + - **Recurring venues:** DOST Cebu PSTO, enspace Cebu, LegalMatch, Zero-Ten Park. + - **Facebook lists some events twice.** + - The browse design (grouping by year, series or edition, filters, highlights) is decided in that release, not v1. + +## 7. Going live + +**Before the swap** +- Confirm the jscebu.org zone is in the Cloudflare account wrangler uses (`@dorelljames`). +- Deploy the Worker to workers.dev and check every page and redirect there. +- **[Proposed]** These are the go/no-go gate. + +**The swap: [Proposed]** +- Record the current DNS records. +- Delete the `www` CNAME to `cname.vercel-dns.com`. +- Attach `www.jscebu.org` to the `jscebu-org` Worker as a **Workers Custom Domain**. Cloudflare creates the DNS record and the certificate, and there is no origin, so the zone's SSL mode doesn't matter. +- Expect a gap of seconds to minutes between the delete and the attach. +- Needs Dorell's explicit "go". + +**After the swap** +- Run the §5 URL matrix against www, plus `/brand.md` headers. + +**Rollback** +- Remove the custom domain. +- Recreate the `www` CNAME to `cname.vercel-dns.com`, DNS-only. + +**Apex** +- `jscebu.org` keeps its working Vercel `308 → www` for now. +- Retiring Vercel is a follow-up. + +**Deploys** +- **[Decided, 2026-10-28]** Merging to `main` deploys, through Cloudflare Workers Builds connected to the GitHub repo. The commands are in `CONTRIBUTING.md`. +- The flow is intent → plan → approval → implementation → review → merge. +- **What actually happened at launch:** Dorell attached `www.jscebu.org` as a Custom Domain in the dashboard after reviewing the preview. `wrangler.jsonc` records it. + +**Branches** +- This work is on `feat/site-v1`, stacked on the design-system PR (#1). +- Its PR targets `feat/brand-v0.1` until #1 merges. + +## 8. Acceptance + +1. Every page in §3 builds. +2. `check-dist` passes on all pages: + - It covers cursor wrapping, internal links (now every path, since the whole site is ours), parity on `/brand`, and no literals. + - The redirect table is tested against `_redirects`. +3. The pages hold at 320px with no horizontal scroll, and the cursor is still under reduced motion. +4. On workers.dev and then on www: + - Every §5 row returns its listed status and target. + - `/brand.md` returns `text/markdown; charset=utf-8`. +5. Dorell looks at the preview and says ship it. +6. After launch, the design-system plan's Task 9 runs against the live `https://www.jscebu.org/brand`: a fresh agent designs a landing page and a social graphic. + +## 9. Left to the builder + +- Final copy. +- The section layout within the Terminal recipe. +- The project card design. +- The Astro collections API details. +- The `_redirects` syntax. +- The sitemap endpoint. +- Cebby's icon file. diff --git a/packages/brand/src/brand.ts b/packages/brand/src/brand.ts index 889beba..490cabb 100644 --- a/packages/brand/src/brand.ts +++ b/packages/brand/src/brand.ts @@ -199,7 +199,7 @@ ${fontLink('interTight', 'jetbrainsMono')} The JS Cebu community at a meetup
The November meetup · Photo: JS Cebu
-

Notes from the November meetup: what worked, what broke, and what we'd do again. Padayon!

+

Notes from the November meetup: what worked, what broke, and what we'd do again.

`, }, ], diff --git a/packages/brand/src/generate/brand-md.ts b/packages/brand/src/generate/brand-md.ts index 0604acc..593e544 100644 --- a/packages/brand/src/generate/brand-md.ts +++ b/packages/brand/src/generate/brand-md.ts @@ -29,7 +29,7 @@ export function brandMd(b: Brand): string { sections.push(`# ${b.name} brand · v${b.version} -> Rules for anyone, person or AI agent, making something for ${b.name}: event graphics, slides, pages, recaps. Follow the MUST checklist exactly and check your work against it before sharing. The same rules, with pictures, are at ${url('/brand')}. +> Rules for anyone, person or AI agent, making something for ${b.name}: event graphics, slides, pages, recaps. Follow the MUST checklist exactly and check your work against it before sharing. The same rules, with pictures, are at ${url('/brand/')}. Start here: diff --git a/packages/brand/src/generate/llms-txt.ts b/packages/brand/src/generate/llms-txt.ts index b8a4a12..828dbe2 100644 --- a/packages/brand/src/generate/llms-txt.ts +++ b/packages/brand/src/generate/llms-txt.ts @@ -9,7 +9,7 @@ export function llmsTxt(b: Brand): string { ## Brand - [Brand rules for agents](${b.site}/brand.md): the MUST checklist, exact tokens, a recipe per personality, voice and asset URLs. Read this before making anything for ${b.name}. -- [Brand guidelines](${b.site}/brand): the same rules for people, with logo downloads. +- [Brand guidelines](${b.site}/brand/): the same rules for people, with logo downloads. - [tokens.css](${b.site}/brand/tokens.css): CSS custom properties and the cursor, sticker and highlighter classes. `; } diff --git a/packages/brand/templates/fb-cover.html b/packages/brand/templates/fb-cover.html index 4d7dc42..37bfdae 100644 --- a/packages/brand/templates/fb-cover.html +++ b/packages/brand/templates/fb-cover.html @@ -55,8 +55,8 @@

Meetup · Nov 21, 2026 · Cebu City

- -

Salamat, Cebu

+ +

Thank you, Cebu

diff --git a/packages/brand/test/generate.test.ts b/packages/brand/test/generate.test.ts index c8a57a7..572a4b6 100644 --- a/packages/brand/test/generate.test.ts +++ b/packages/brand/test/generate.test.ts @@ -73,6 +73,15 @@ describe('brandMd', () => { }); }); +describe('links to the /brand page', () => { + it('use the trailing slash the site serves it at, so agents skip a redirect', () => { + for (const text of [brandMd(brand), llmsTxt(brand)]) { + expect(text).toContain('https://www.jscebu.org/brand/'); + expect(text).not.toMatch(/https:\/\/www\.jscebu\.org\/brand(?![/.])/); + } + }); +}); + describe('llmsTxt', () => { it('points agents to brand.md', () => { const txt = llmsTxt(brand); diff --git a/packages/brand/test/templates.test.ts b/packages/brand/test/templates.test.ts index e9e6ab6..bc23080 100644 --- a/packages/brand/test/templates.test.ts +++ b/packages/brand/test/templates.test.ts @@ -34,3 +34,17 @@ describe('personality examples', () => { expect(p.exampleHtml).toContain(`class="jc-${p.id}`); }); }); + +describe('Bisaya copy', () => { + // Spec §11: Dorell approves all Bisaya copy before it appears anywhere. Agents copy these verbatim. + const drafts = brand.voice.phrases.filter((p) => p.status === 'draft').map((p) => p.bisaya); + const pieces: [string, string][] = [ + ...brand.personalities.map((p) => [`${p.id} example`, p.exampleHtml] as [string, string]), + ['event-post template', read('templates/event-post.html')], + ['fb-cover template', read('templates/fb-cover.html')], + ]; + + it.each(pieces)('keeps draft phrases out of the %s', (_, html) => { + for (const phrase of drafts) expect(html.toLowerCase(), phrase).not.toContain(phrase.toLowerCase()); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index eefa212..e56631f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -51,6 +51,9 @@ importers: wrangler: specifier: ^4.153.0 version: 4.153.0(@types/node@26.6.5) + yaml: + specifier: ^2.9.1 + version: 2.9.1 packages/brand: dependencies: