vibe.j2team.org — A collaborative vibe coding project by J2TEAM Community with 210+ sub-apps. The homepage acts as a launcher linking to sub-apps, where each community member creates their own page. Deployed to Cloudflare Workers (static assets via wrangler.json).
- Vue 3.5 (Composition API with
<script setup>) - TypeScript (strict mode,
noUncheckedIndexedAccess: true) - Vite 7
- Tailwind CSS v4 (via
@tailwindcss/vite) - Vue Router 5
- Pinia 3
- @unhead/vue 2 (document head/meta management)
- @vueuse/core 14 — 200+ Vue composables (useMouse, useClipboard, useDark, useStorage, useIntersectionObserver, useLocalStorage, useMediaQuery, useWindowSize, etc.)
- @iconify/vue 5 — 200,000+ icons from 150+ icon sets via
<Icon icon="icon-set:icon-name" />component - html-to-image — capture DOM nodes as PNG/JPEG/SVG
- shiki 4 — syntax highlighting
pnpm install
pnpm dev # Dev server
pnpm build # Type-check + production build (+ OG pages + sitemap generation)
pnpm test:unit # Unit tests with Vitest
pnpm lint # Lint with oxlint + ESLint (with --fix)
pnpm lint:ci # Lint without --fix (for CI)
pnpm format # Format with oxfmt
pnpm create:page <slug> # Scaffold a new page (interactive or with flags)src/
main.ts # App entry (createPinia + createHead + router)
App.vue # Root component (<RouterView /> + useHead for dynamic meta)
assets/main.css # Tailwind CSS v4 @theme tokens + custom animations
router/index.ts # Vue Router — auto-generates routes from pages-loader
types/page.ts # PageMeta & PageInfo interfaces
data/
pages-loader.ts # Fetches pre-generated pages.json (built by Vite plugin)
categories.ts # Category definitions (game, tool, creative, fun, learn, health, finance, spiritual, connect, other)
authors.ts # Author aggregation from pages (leaderboard, author pages)
homepage.ts # Homepage content data (tech stack, rules, products)
constants.ts # Shared constants
components/
home/ # Homepage section components (HeroSection, PagesGrid, RecentlyViewed, etc.)
AppBreadcrumb.vue # Standardized breadcrumb navigation for core pages
AuthorAvatar.vue # Author avatar (GitHub avatar via useGithubAvatar)
BackToTop.vue
CategoryFilter.vue # Category filter UI for homepage
EdgeToolbar.vue # Slide-out toolbar on sub-pages (source link, bookmark, home, comments)
ErrorBoundary.vue # Error boundary wrapper
FavoriteButton.vue # Bookmark/favorite toggle button
GiscusModal.vue # Giscus comments modal (per-page discussions)
PageCard.vue # Page card component used in grids
composables/
useDraggable.ts # Drag behavior composable
useGithubAvatar.ts # Resolves GitHub avatar URL from author name
useSearchShortcut.ts # Keyboard shortcut for search (Ctrl+K / Cmd+K)
stores/
useFavoritesStore.ts # Bookmark/favorite state (localStorage via VueUse)
usePagesStore.ts # Pages registry (fetches pages.json, provides page list)
useRecentlyViewedStore.ts # Tracks recently visited pages
views/
HomePage.vue # Landing page / launcher
ContentPolicy.vue # Content policy page
LeaderboardPage.vue # Author leaderboard (/leaderboard)
BookmarksPage.vue # User's bookmarked pages (/bookmarks)
AuthorPage.vue # Author profile page (/author/:slug)
NotFound.vue # 404 page
<app-name>/
index.vue # Each sub-page is a directory with index.vue
meta.ts # Page metadata — route auto-generated from folder name
Routes are auto-generated from a pre-built public/data/pages.json file:
- A Vite plugin (
scripts/generate-pages-json.mjs) scans allsrc/views/*/meta.tsfiles and writespublic/data/pages.jsonat build start and whenever ameta.tsfile changes during dev - Post-build scripts generate OG image pages (
scripts/generate-og-pages.mjs) and sitemap with sub-sitemaps + robots.txt (scripts/generate-sitemap.mjs) src/data/pages-loader.tsfetches this JSON at runtime (bypasses Rollup bundling)- Path is derived from folder name (e.g.,
src/views/my-app/→/my-app) - Featured pages are hand-picked in
scripts/generate-pages-json.mjsand pinned to top of homepage - Pages with
hidden: truein theirmeta.tsare excluded from the listing but their routes still work
IMPORTANT: All UI work MUST follow the design system documented in docs/DESIGN_SYSTEM.md.
Key rules:
- Use the custom color tokens defined in
src/assets/main.css(@themeblock) — never use raw Tailwind grays or default colors - DO NOT use purple, green-cyan gradients, or cold grays (
gray-950,gray-900) - Fonts:
font-display(Anybody) for headings,font-body(Be Vietnam Pro) for body text - Cards use sharp corners (no
rounded-xlorrounded-lg) - Use
bg-bg-deepas page background,bg-bg-surfacefor cards,bg-bg-elevatedfor hover states - Accent colors: coral (
accent-coral) as primary, amber (accent-amber) as secondary, sky (accent-sky) as tertiary - Section headings use
//marker prefix with accent color - Use
animate-fade-upwithanimate-delay-{1-7}for page load animations
Read docs/DESIGN_SYSTEM.md before making any visual changes.
Before implementing any browser/DOM/state logic, check if VueUse already has a composable for it. Common composables to use instead of custom code:
- Storage:
useLocalStorage(),useSessionStorage()— notlocalStorage.getItem/setItem - DOM events:
useEventListener()— not manualaddEventListener/removeEventListener - Clipboard:
useClipboard()— notnavigator.clipboarddirectly - Dark mode:
useDark(),useColorMode()— not manual class toggling - Timers:
useInterval(),useTimeout(),useIntervalFn()— not rawsetInterval/setTimeout - Mouse/Touch:
useMouse(),usePointer(),useSwipe()— not manual event handlers - Viewport:
useWindowSize(),useElementSize(),useIntersectionObserver()— not manual resize/scroll listeners - Media:
useMediaQuery()— notwindow.matchMediadirectly - Network:
useFetch(),useOnline()— not rawfetchwith manual reactive state - Remote scripts:
useScriptTag()— not manualdocument.createElement('script'). Auto-cleans up on unmount - Animation:
useTransition(),useRafFn()— not manualrequestAnimationFrame - Reactivity:
watchDebounced(),watchThrottled(),refDebounced()— not custom debounce/throttle implementations
Full list: https://vueuse.org/functions.html
Live reference: See src/views/hello-world/index.vue for interactive demos of the composables listed above.
Use the <Icon> component for all icons instead of inline SVGs, emoji characters, or custom icon components:
<script setup lang="ts">
import { Icon } from '@iconify/vue'
</script>
<template>
<Icon icon="mdi:home" />
<Icon icon="heroicons:arrow-left" class="size-5" />
<Icon icon="lucide:settings" :width="24" />
</template>Preferred icon set: lucide (e.g., lucide:home, lucide:settings, lucide:arrow-left). Only use other sets (mdi, heroicons, ph, tabler, ri, solar, ion) if Lucide doesn't have the needed icon. Browse at https://icon-sets.iconify.design/
Live reference: See src/views/hello-world/index.vue for icon usage examples across multiple icon sets.
- Use
<script setup lang="ts">for all Vue components - Do not use
classin TypeScript unless absolutely necessary - Do not use
anyorunknowntypes - Use Composition API (not Options API)
- Use
pnpmas package manager (not npm/yarn) - Vietnamese text must use diacritics (tiếng Việt có dấu)
Before implementing any new feature or sub-page, agents MUST:
- Check local
src/views/— List existing directories insrc/views/to see if the same or similar page already exists locally - Check existing pages on main branch — Browse https://github.com/J2TEAM/vibe.j2team.org/tree/main/src/views to see if someone has already built the same or similar feature
- Check open Pull Requests — Browse https://github.com/J2TEAM/vibe.j2team.org/pulls to see if someone is already working on it
- Only proceed if no duplicates found — If the feature already exists locally, on main, or in an open PR, report back to the user instead of building a duplicate
-
No database — the project does not use any database in any form
-
Always link back to homepage — every sub-page must have a link back to the homepage (
/) -
Language: Vietnamese (preferred) or English — page content should be in Vietnamese or English
-
No duplicate sub-apps — check existing directories in
src/views/before creating a new page -
Each sub-page is self-contained — only work within your page's directory (
src/views/<app-name>/), do not modify shared files (App.vue,main.css,router/index.ts). Routes are auto-generated from themeta.tsfile in each page directory. Exception: static assets can be placed inpublic/<app-name>/(see "Static Assets Convention" below) -
Responsive — pages must display well on mobile
-
No new dependencies in
package.jsonunless truly needed and approved. The following libraries are already installed — use them freely (see "Leveraging Installed Libraries" section above):@vueuse/core— Vue composables@iconify/vue— Icon componenthtml-to-image— DOM-to-image capture (PNG/JPEG/SVG)shiki— Syntax highlighter
The following are pre-approved and can be added without additional approval:
vue-konva— 2D canvas library for drawing, games, and interactive graphics
-
Folder names must be kebab-case — sub-page directories under
src/views/must use lowercase kebab-case (e.g.,my-app,dev-rpg). PascalCase or mixed-case folder names are not allowed -
Author attribution required — every page must have an
authorfield in itsmeta.tsfile -
External libraries & APIs are welcome — Sub-apps can load third-party JS libraries at runtime via
useScriptTag()from@vueuse/core(e.g., YouTube IFrame API, Tone.js, Matter.js, p5.js). Authors can also call free/public external APIs (e.g., weather, dictionary, trivia, exchange rates) to power their features — just don't hard-code API keys in source code (see rule 8 in PR Checklist) -
No landing pages or promotional content — Pages must provide direct, self-contained value to users (e.g., a game, tool, interactive experience, or educational content). The following are not accepted:
- Landing pages or showcase pages for external products, services, or brands
- Pages whose primary purpose is to redirect users to external websites or services
- Advertising, marketing, or affiliate content
- Portfolio/brochure pages that only display static information about an external entity
For apps with 4+ files, follow this recommended structure inside src/views/<app-name>/:
src/views/<app-name>/
index.vue # Required: page entry point
meta.ts # Required: page metadata
components/ # Recommended: Vue components used by the page
composables/ # Recommended: composition functions (use-*.ts)
types.ts # Recommended: TypeScript type definitions
utils/ # Recommended: pure utility functions
assets/ # Recommended: images, sounds, CSS (processed by Vite)
Simple apps (just a single page) only need index.vue + meta.ts.
src/views/<app-name>/assets/— small images, sounds, CSS that Vite will hash and optimize. Use this for small assets (< 50 kB total).public/<app-name>/— large or numerous assets (sprite sheets, image sets, audio files, videos, JSON data) served as-is without Vite processing. Accessible at/<app-name>/filename.ext. You are allowed and encouraged to createpublic/<app-name>/directories — this is NOT limited to yoursrc/views/folder.public/shared/— assets used by multiple apps (e.g.noise.webp,web-logo.svg). Accessible at/shared/filename.ext.public/data/pages.json— auto-generated app registry (globally shared). Do NOT place app-specific data here — usepublic/<app-name>/instead.
Important: All app-specific public assets (images, sounds, data files) go in public/<app-name>/. Do NOT use public/images/, public/sounds/, or public/data/ for app-specific assets.
Why use public/? Assets imported via import or import.meta.glob in src/ get bundled into JS chunks, increasing initial page load. Files in public/ are served as static files and loaded on demand by the browser.
Vite/Rollup bundles everything that is imported into JS chunks. The following patterns bloat the bundle and must be avoided:
DO NOT — Export large data as a TypeScript/JS module:
// Whether static or dynamic import — Rollup still creates a JS chunk
export const wordList = { ... } // 500 kB
const mod = await import('./data') // still a JS chunkDO — Replace with JSON in public/<app-name>/ + lazy fetch:
// Bypasses Rollup entirely, browser caches it independently
const response = await fetch('/my-app/my-app-data.json')
const data = await response.json()DO NOT — Import a compiled engine (.js Emscripten/WASM) via ?url:
import engineUrl from './engine.js?url' // Vite still copies it to dist/assets/, triggers warningDO — Place in public/<app-name>/ and use a hardcoded URL:
const engineUrl = '/my-app/engine.js' // Rollup never touches itSize thresholds:
| Data type | Threshold | Recommendation |
|---|---|---|
| Dictionary / word list | > 50 kB | public/<app>/data.json + fetch |
| Geo / SVG path data | > 50 kB | public/<app>/data.json + fetch |
| Sprite frames / image sets | > 10 files or > 50 kB total | public/<app>/ + URL strings |
| Compiled engine (Emscripten, asm.js) | any size | public/<app>/ + hardcoded URL |
| Config / small data | < 20 kB | Direct import is fine |
After building, run pnpm build and verify there are no (!) Some chunks are larger than 500 kB warnings.
Reusable code used by 3+ apps can live in the shared layer:
src/components/shared/ # Shared UI components
src/composables/shared/ # Shared composables
src/utils/shared/ # Shared utility functions
Apps can import from these directories but are never required to. Each app remains self-contained by default.
Run the generator script:
# Interactive (prompts for missing fields)
pnpm create:page <slug>
# Non-interactive (all fields via flags — use this in scripts and AI agents)
pnpm create:page <slug> --name "Display Name" --description "Page description" --author "Author" --category game [--facebook "https://..."] [--hide-toolbar] [--hidden]Available categories: game, tool, creative, fun, learn, health, finance, spiritual, connect, other.
This creates src/views/<slug>/index.vue + meta.ts with the correct structure. Any flag not provided will be prompted interactively.
Manual alternative (if not using the script):
- Create a new directory under
src/views/<your-page-name>/ - Add
index.vueas the main component inside that directory - Add
meta.tsexporting aPageMetaobject with:name,description,author,category, and optionallyfacebook,showToolbar,hidden - Available categories:
game,tool,creative,fun,learn,health,finance,spiritual,connect,other - The route is auto-generated from the folder name — no router changes needed
An EdgeToolbar (src/components/EdgeToolbar.vue) is displayed on all sub-pages by default. It slides out from the right edge of the screen on hover and provides:
- View source code — link to the page's source on GitHub
- Bookmark — add/remove the page from favorites (persisted in localStorage)
- Home — navigate back to the homepage
- Dismiss — hide the toolbar for the current session (reappears on page reload)
- The trigger tab is flush to the right edge of the viewport, semi-transparent (
opacity-50) when idle - On hover, it becomes fully opaque and the panel slides out with button labels
- The toolbar is rendered in
App.vueoutside<RouterView>, so it's independent of sub-page content - Uses scoped styles and design system tokens (
bg-bg-elevated,text-text-secondary,border-border-default,font-display)
Authors can disable the toolbar on their page if it interferes with their layout (e.g., a full-screen game). Add showToolbar: false to your meta.ts:
import type { PageMeta } from '@/types/page'
const meta: PageMeta = {
name: 'My Page',
description: '...',
author: 'Author',
category: 'game',
showToolbar: false, // Disable the edge toolbar on this page
}
export default metaDefault is true — the toolbar is shown unless explicitly disabled.
@/resolves tosrc/
- Framework: Vitest + Vue Test Utils + JSDOM
- Test files:
src/__tests__/
- CI must pass — Run
pnpm build(type-check + build) andpnpm lint:cilocally before pushing. Do NOT create a PR with failing CI - No unused code — Remove variables, constants, imports, and type definitions that are not actually used. Do not define constants and then hard-code values instead of using them
- Use
RouterLinkfor internal navigation — Never use raw<a>tags for links within the app. Use Vue Router's<RouterLink :to="...">instead. Refer to existing pages for examples - Do not redefine shared types — Import types like
PageMetafrom the sharedtypes.tsor@/types/pageinstead of redefining them in your files - Only commit
pnpm-lock.yaml— Do not commitpackage-lock.jsonoryarn.lock - UTF-8 encoding — Ensure all Vietnamese text is properly encoded in UTF-8 (no garbled characters)
- Follow
meta.tsstructure — Copy the pattern fromsrc/views/hello-world/meta.tsexactly. ImportPageMetatype, export default with required fields - No exposed API endpoints/secrets — Since this is open source, never hard-code API keys, endpoints, or secrets in the source code
- No large data files in
src/— If your app needs a large data file (> 50 kB), place it inpublic/<app-name>/as JSON and fetch it lazily. Do NOT export it as a TypeScript/JS module. See "Bundle Size — Avoid bloating JS chunks" section above - Dynamic import for user-triggered libraries — Libraries that are only used when the user performs a specific action (e.g., export image, syntax highlight, share) must be dynamically imported inside the function that needs them, not at the top level. A static top-level
importbundles the entire library into the page's JS chunk even when the user never triggers the action. Example:const { toPng } = await import('html-to-image')inside the export handler, notimport { toPng } from 'html-to-image'at the top of the file. This applies tohtml-to-image,shiki, and any similar on-demand library - Clean up side effects on unmount — Every
addEventListener,setInterval,setTimeout,requestAnimationFrame, or any other global side effect registered inonMountedMUST be cleaned up inonUnmounted. Prefer VueUse composables (useEventListener,useIntervalFn,useTimeoutFn,useRafFn) which handle cleanup automatically. Forgetting cleanup causes memory leaks and ghost listeners that persist across route navigations in an SPA
- ESLint + eslint-plugin-vue + @vue/eslint-config-typescript
- Oxlint (Rust-based linter, runs before ESLint) — config in
.oxlintrc.json - Oxfmt for formatting — config in
.oxfmtrc.json(no semicolons, single quotes) - Prettier config exists for compatibility (eslint-config-prettier)
- Commitlint with
@commitlint/config-conventional— commit messages must follow Conventional Commits format (e.g.,feat:,fix:,chore:) - Pre-commit:
simple-git-hooks+lint-stagedruns linters on staged files and auto-optimizes images (.png,.jpg,.jpeg,.webp) viasharp
Do NOT write multiple statements inline in Vue template event handlers (e.g., @click). Oxfmt strips semicolons during formatting, which causes the Vue compiler to fail with a syntax error because the two statements merge into an invalid expression.
<!-- BAD — oxfmt strips semicolons → build error -->
<button @click="doA(); doB()">Click</button>
<!-- GOOD — extract to a function -->
<button @click="handleClick">Click</button>function handleClick() {
doA()
doB()
}