When the Codes Meet the Vibes 🚀
VibeDev ID adalah komunitas vibrant developer, AI enthusiasts, dan tech innovators Indonesia yang punya visi dan passion yang sama untuk bikin produk digital yang keren. Kami menghubungkan creator yang sepikiran untuk kolaborasi, belajar, dan berkembang bareng.
Indonesia's premier community for developers, vibe coders, and AI enthusiasts. Showcase projects, collaborate, network, and level up your skills in web, mobile, and AI development.
- 🔐 User Authentication - Secure login with Better Auth (email + Google/GitHub OAuth)
- 👤 Developer Profiles - Customizable profiles with bio, skills, and social links
- 📝 Project Showcase - Share dan showcase project keren lo
- 💬 Community Interaction - Comments, likes, dan diskusi project
- 🤝 Networking & Collaboration - Connect sama developer yang sepikiran
- 💬 Join Community - Tombol Join Community di homepage (hero dan CTA) membuka
https://wa.vibedevid.com - 📰 Blog System - Rich text editor dengan Novel/TipTap untuk artikel teknis
- 📊 Views Tracking - Session-based analytics untuk project insights
- 🤖 AI Leaderboard - Ranking dan showcase AI tools favorit komunitas
- 🗓️ Community Calendar - Event list at
/event/list./calendarredirects there. - 🎉 Events System - Submit dan browse community events dengan approval workflow
- 💬 Testimonials - Unlisted
/testimonialform, admin approve/reject/unpublish, lalu tampil di section review landing page - 📈 User Dashboard - Personal dashboard untuk manage projects dan activity
- 🛠️ Admin Dashboard - Full admin panel dengan moderation tools
- ⌨️ Command Palette - Quick navigation dan search (cmdk)
- 🎬 Vibe Videos - Video content section untuk tutorial dan highlights
- 🌙 Dark/Light Mode - UI theme yang nyaman mata
- 📱 Responsive Design - Perfect di semua device
- 🏷️ Project Categories - Personal Web, SaaS, Landing Page, dan lainnya
- ❤️ Like System - Like project yang lo suka
- 🔍 Discovery - Filter dan cari project berdasarkan kategori
- 🖼️ Progressive Image Loading - Blur placeholders dengan lazy loading
- 🌍 Internationalization - Full support English dan Indonesia (react-i18next). Locale comes from the
NEXT_LOCALEcookie, notnavigator.language. The resolved locale is also written into SSR HTML (window.__VIBEDEV_LOCALE__) so the client bundle matches the first paint without waiting for a clientbeforeLoad. - 📱 In-app browsers - Public pages load without the blank TanStack "Something went wrong!" screen. Route errors show a recovery page (reload / back to home). The leftover Next.js service worker is unregistered on purpose;
/sw.jsis an uninstall shim so already-installed clients drop it. - 🛡️ Spam Protection - Email domain whitelist dan bot protection
- 📊 Analytics Dashboard - Charts dan data visualization (recharts)
- ❓ FAQ System - Frequently asked questions management
- 🚨 Content Moderation - Report dan moderation system
- 🔎 SEO - SSR meta/Open Graph, JSON-LD, dynamic sitemap, robots.txt, canonical, dan
noindexdi halaman privat
- Framework: TanStack Start (Vite + Nitro) with
@tanstack/react-routerfile-based routing - Build/Dev: Vite 8 + Nitro server output
- Language: TypeScript 5.x
- Database: Neon Postgres in
aws-ap-southeast-1(Drizzle ORM). See Neon Postgres. - Authentication: Better Auth (
/api/auth/*) with email/password + Google/GitHub OAuth - Styling: Tailwind CSS v4
- UI Components: Radix UI + shadcn/ui (50+ components)
- Animations: Motion (Framer Motion) — shared scroll-reveal primitives in
components/ui/motion-wrapper.tsx(ScrollReveal,StaggerContainer/StaggerItem,ScaleIn) applied across the landing page and the/project/list,/blog,/event/list, and/[username]profile pages (header/stats entrance reveal, staggered project/blog card grids, scaled tab-content reveals); Radix dropdown menus (e.g. avatar menu) and the FAQ accordion animate open/close via Framer Motion, all withprefers-reduced-motionsupport - Rich Text: Novel + TipTap
- Icons: Lucide React + Tabler Icons + LobeHub Icons
- Internationalization: react-i18next
- Forms: React Hook Form + Zod
- AI Integration: AI SDK + OpenRouter Provider
- File Uploads: UploadThing + Better Upload
- Command Palette: cmdk
- Charts: Recharts
- Dates: date-fns + date-fns-tz
- Testing: Playwright (E2E) + Vitest (unit)
- Code Quality: Biome (linter + formatter)
- Hosting: Cloudflare Workers (Nitro
cloudflare_modulepreset) - Toast: Sonner
Note: this app was migrated from Next.js 16 App Router to TanStack Start. Some
app/subfolders still use Next.js-style names (page.tsx,[slug],(admin)) but are now plain view/component modules imported by route files inapp/routes/. A full rename of those folders is high-churn with little runtime benefit, so they stay as live view modules.
| Leftover | Why it stays |
|---|---|
NEXT_LOCALE cookie |
Live locale source. Public pages and tests still read this name. |
NEXT_PUBLIC_SITE_URL |
Live server/SEO site URL. Also a Worker secret. VITE_SITE_URL is the browser twin. |
next-themes |
Theme library. It is not the Next.js framework. |
public/sw.js + root unregister script |
Uninstall shim for the old Next.js service worker. Already-installed clients still need it. |
Next-named folders under app/ (except app/routes/) |
Live view modules. Routes import them. |
This path is for a local clone. You apply the schema to your Neon project, then load demo rows. Do not point .env.local at production.
- Node.js 18+ or Bun (recommended)
- Vite+ (
vpCLI) — this project uses the Vite+ unified toolchain - A free Neon Postgres project (not the production database)
OAuth, UploadThing, Resend, and OpenRouter are optional. You can browse the app and log in with seed email accounts without those keys.
- Clone the repository:
git clone https://github.com/julianromli/vibedevid.git
cd vibedevid- Install dependencies:
vp installVite+ ships
vite/vitestindevDependenciesas npm aliases to@voidzero-dev/vite-plus-core/@voidzero-dev/vite-plus-test, and theoverridesfield inpackage.jsonpins both to those same alias specs. Thevitestoverride must match thedevDependencies.vitestspec exactly (i.e. thenpm:@voidzero-dev/vite-plus-test@latestalias, not a plain version like4.1.9). Bun tolerates a mismatch, but npm/npx(used bynpx wrangler deployon Cloudflare) rejects it withEOVERRIDE: Override for vitest conflicts with direct dependency. If you upgradevite-plusandvp installchanges the alias, re-syncoverrides.vitestto matchdevDependencies.vitest. Do not route this pin through acatalog:reference.
-
Create a Neon project at neon.tech (free plan). Copy the pooled connection string (
-poolerin the host) and the direct connection string.Optional throwaway database:
npx neon-newcreates a Claimable Neon project without an account. Unclaimed projects expire after 72 hours. -
Copy env and set the required local values:
cp .env.example .env.localDATABASE_URL=postgresql://user:pass@ep-xxx-pooler.ap-southeast-1.aws.neon.tech/neondb?sslmode=require
DATABASE_URL_UNPOOLED=postgresql://user:pass@ep-xxx.ap-southeast-1.aws.neon.tech/neondb?sslmode=require
BETTER_AUTH_SECRET=replace-with-openssl-rand-base64-32
BETTER_AUTH_URL=http://localhost:3000
VITE_BETTER_AUTH_URL=http://localhost:3000
NEXT_PUBLIC_SITE_URL=http://localhost:3000
VITE_SITE_URL=http://localhost:3000Keep VITE_SITE_URL and NEXT_PUBLIC_SITE_URL on localhost. The seed script refuses vibedevid.com.
- Apply the schema and load demo data:
bun run db:setupThis runs migrate:schema then db:seed. You can run the steps apart:
bun run migrate:schema
bun run db:seed
# bun run db:seed -- --reset # delete seed-owned rows, then insert againdb:seed is idempotent. It upserts seed rows only and does not wipe rows you create after that.
One-time Supabase → Neon import is not part of this path. See docs/migrations/neon-better-auth.md.
- Start the app:
bun run devOpen http://localhost:3000. Log in with a seed account below. New sign-up still needs email verification. Without Resend, the verification link prints in the server log.
Shared password: VibeDevLocal1!
| Role | Profile | |
|---|---|---|
| Admin | seed.admin@gmail.com |
/seedadmin |
| Moderator | seed.moderator@gmail.com |
/seedmoderator |
| Member | seed.sarah@gmail.com |
/sarahchen |
| Member | seed.marcus@gmail.com |
/marcusrodriguez |
Admin dashboard: http://localhost:3000/dashboard.
The seed includes categories, projects, published and draft posts, comments, likes, events (including one pending), FAQs, vibe videos, testimonials, and one comment report. Images use files in public/optimized/, so UploadThing is not required to browse.
# Install dependencies
vp install
# Apply schema + demo rows to your local Neon project
bun run db:setup
bun run db:seed
bun run db:seed -- --reset
# Development server (vite dev, port 3000)
bun run dev
# Build for production (vite build — Nitro server output in .output/)
# Pre-generates responsive AVIF/WebP image variants via scripts/optimize-images.mjs first.
# Needs the sharp build-time dep (devDependency). sharp does not run in the Worker.
bun run build
# Regenerate optimized image variants only (public/optimized/). Needs sharp.
bun run optimize:images
# Recompress existing UploadThing project/video images (dry run by default;
# add --apply to write). Needs DATABASE_URL + UPLOADTHING_TOKEN.
bun run backfill:images
# Start the production server
bun run start
# Type checking (required for type safety)
bunx tsc --noEmit
# Linting & Formatting (Biome)
bun run lint # changed files only
bun run lint:all # full repo
bun run format
# Unit tests (Vitest)
bun run test
bun run test:watch
# E2E tests (Playwright)
bun run test:e2e
bun run test:e2e:headed # see browser
bun run test:e2e:debug # step through
# Run a single Playwright spec / test by name
bunx playwright test tests/views-tracking.spec.ts
bunx playwright test -g "should track views when visiting project page"| Variable | Description | Required for local |
|---|---|---|
DATABASE_URL |
Neon pooled URL (hostname includes -pooler) |
Yes |
DATABASE_URL_UNPOOLED |
Neon direct URL for drizzle-kit, migrate:schema, and db:seed |
For schema/seed |
BETTER_AUTH_SECRET |
Random secret for Better Auth (min 32 chars) | Yes |
BETTER_AUTH_URL |
Public app URL for auth callbacks | Yes |
VITE_BETTER_AUTH_URL |
Same URL, exposed to browser | Yes |
NEXT_PUBLIC_SITE_URL |
Canonical site URL (server + SEO) | Yes |
VITE_SITE_URL |
Same URL, exposed to browser | Yes |
GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET |
Google OAuth | Optional |
GITHUB_CLIENT_ID / GITHUB_CLIENT_SECRET |
GitHub OAuth | Optional |
UPLOADTHING_TOKEN |
UploadThing API token (uploads only) | Optional |
OPENROUTER_API_KEY |
AI blog features | Optional |
RESEND_API_KEY |
Resend API key for auth verification/reset | Optional |
EMAIL_FROM |
Verified sender address for Resend | Optional |
The app deploys to Cloudflare Workers using the Nitro cloudflare_module preset
(configured in vite.config.ts) plus wrangler.jsonc at the repo root.
Production site: https://vibedevid.com.
# 1. Build with production client URLs baked in (VITE_* are compile-time)
NEXT_PUBLIC_SITE_URL=https://vibedevid.com \
VITE_SITE_URL=https://vibedevid.com \
VITE_BETTER_AUTH_URL=https://vibedevid.com \
bun run build
# 2. Preview locally on Workers runtime (copy .env.local → .dev.vars)
bunx wrangler dev
# 3. Set Worker secrets (runtime — see list below), then deploy
bun run scripts/sync-wrangler-secrets.ts # reads .env.local → wrangler secret bulk
bunx wrangler deployNotes:
compatibility_flags: ["nodejs_compat"]and compatibility date2024-09-19(required for Workers Static Assets) are set inwrangler.jsonc.- Server-only secrets must be read via
getServerRuntimeSecrets()(lib/server/runtime-secrets.ts), notprocess.envdirectly. On Workers, bindings are exposed onglobalThis.__env__per request. VITE_*values are inlined at build time — rebuild before deploy wheneverVITE_BETTER_AUTH_URLorVITE_SITE_URLchanges.- Worker secrets (runtime): pooled
DATABASE_URL(Singapore-poolerhost),BETTER_AUTH_SECRET,BETTER_AUTH_URL,GOOGLE_CLIENT_ID,GOOGLE_CLIENT_SECRET,GITHUB_CLIENT_ID,GITHUB_CLIENT_SECRET,NEXT_PUBLIC_SITE_URL,UPLOADTHING_TOKEN,OPENROUTER_API_KEY,RESEND_API_KEY, andEMAIL_FROM. Do not putDATABASE_URL_UNPOOLEDon the Worker. Keep it in local.env.localfor schema tools. - For
wrangler dev, create a gitignored.dev.varsfile with the same keys. - Add OAuth redirect URLs in Google/GitHub developer consoles:
https://vibedevid.com/api/auth/callback/googlehttps://vibedevid.com/api/auth/callback/github
- After cutover, remove legacy Supabase secrets from the Worker if still present.
Production data is Neon Postgres in aws-ap-southeast-1 (Singapore). All server
reads and writes go through Drizzle getDb() plus Better Auth checks
(requireUser, requireAdminOrModeratorUser).
- App / Workers use the pooled
DATABASE_URL. The host includes-pooler. - Schema tools use
DATABASE_URL_UNPOOLED(direct).drizzle.config.ts,bun run migrate:schema, andbun run db:seedprefer this URL when it is set. - Local demo data —
bun run db:seedupserts seed-owned rows (users, projects, posts, events, and related tables). It refusesvibedevid.comsite URLs. Use--resetto delete seed-owned rows and insert them again. - Branch policy is in
neon.ts: new non-default branches get a 7-day TTL, 0.25–1 CU, and a 5-minute suspend. - Harden migration
scripts/migrations/neon/03_harden_schema.sqlruns after01_schema.sql. It is idempotent. It adds unique likes and post slugs, parent XOR checks, extra indexes, expired session/verification cleanup, and dropssupabase_auth_staging. - Homepage cache (
lib/server/short-ttl-cache.ts) stores active categories and home vibe videos for 5 minutes (Cloudflare Cache API on Workers, memory locally). Admin vibe-video mutations clear the video cache key. - Auth cleanup —
getServerSessiondeletes expiredsessionandverificationrows at most once per isolate every 10 minutes. - One-time Supabase import scripts live in
scripts/migrate-to-neon.ts. Staging schemasupabase_auth_stagingholds password hashes. Drop it aftermigrate:userssucceeds.
Workspace link file .neon is gitignored. Do not commit connection strings.
users - Extended auth profiles with role system (0=admin, 1=moderator, 2=user), social links, bio, and location
projects - Project showcase with slug-based URLs for SEO, tags, and category
comments - Unified comments for Blog and Projects. XOR check: exactly one parent.
likes - User likes. Unique per (user_id, project_id) or (user_id, post_id). XOR check: exactly one parent.
views - Session-based views tracking with 30-minute timeout, IP + User Agent fingerprinting. XOR check: exactly one parent.
posts - Blog posts with rich text content (JSON), featured flag, read time, unique slug
post_tags - Blog tag categorization
blog_post_tags - Many-to-many relationship between posts and tags
events - Community events with approval workflow, status (upcoming/past), location details
categories - Project categories with icon and color
faqs - FAQ content with sort order and active status
blog_reports - Comment/report moderation system
vibe_videos - Video content for tutorials and highlights
- Authorization enforced in application code (Drizzle queries +
requireUser/requireAdminOrModeratorUser) — no Supabase RLS on Neon - Authenticated insert/update untuk data milik user sendiri
- Guest comments diizinkan dengan
author_namefield - Admin/moderator role diperlukan untuk moderation
- Email domain whitelist untuk registration
├── app/
│ ├── routes/ # TanStack Router file-based routes (source of truth)
│ │ ├── __root.tsx # Root layout, head/meta, providers
│ │ ├── index.tsx # Homepage
│ │ ├── $username.tsx # Dynamic user profile pages (skips reserved slugs)
│ │ ├── testimonial.tsx # Unlisted public testimonial form
│ │ ├── project.list.tsx / project.$slug.tsx / project.submit.tsx
│ │ ├── blog.tsx (layout) / blog.index.tsx / blog.$slug.tsx / blog.editor*.tsx
│ │ ├── event.list.tsx / event.$slug.tsx
│ │ ├── _admin/ # Protected admin route group (role-gated layout)
│ │ ├── admin.tsx # Standalone admin page (role-gated via beforeLoad)
│ │ ├── api/ # API route handlers (server.handlers blocks)
│ │ └── auth.callback.ts
│ ├── routeTree.gen.ts # Generated route tree (do not hand-edit)
│ ├── router.tsx # Router factory
│ ├── start.ts # TanStack Start instance + request middleware
│ └── <feature>/ # Legacy Next.js-named folders, now view/component
│ # modules imported by routes (blog, project,
│ # event, [username], (admin), user/auth, ...)
├── components/
│ ├── logo.tsx # VibeDev V mark (currentColor SVG for small wells)
│ ├── ui/ # 50+ shadcn/ui components
│ ├── sections/ # Page sections (hero, showcase, faq)
│ ├── blog/ # Blog-specific components
│ ├── project/ # Project-specific components
│ ├── admin-panel/ # Admin dashboard components
│ ├── event/ # Event-specific components
│ └── profile/ # Profile-specific components
├── hooks/ # Custom React hooks
├── lib/
│ ├── actions/ # Server data/mutations + *.functions.ts (createServerFn)
│ │ # rule: foo.functions.ts wraps ONLY foo.ts or its
│ │ # read partner in lib/server/
│ │ # Dashboard client boards import *.functions.ts only.
│ │ # Direct imports of action modules pull
│ │ # @tanstack/react-start/server into the browser
│ │ # chunk and throw AsyncLocalStorage is not a constructor.
│ ├── db/ # Drizzle schema + `getDb()` (Neon serverless)
│ ├── auth/ # Better Auth server/client config
│ ├── server/ # Server-only utilities (auth, runtime secrets,
│ │ # short-ttl-cache.ts);
│ │ # public read modules: project-public.ts,
│ │ # blog-public.ts (list + detail reads)
│ ├── routes/ # Route helpers (server locale/translations)
│ │ # locale: lib/locale.ts owns registry + cookie
│ ├── uploadthing.ts # UploadThing server router
│ ├── uploadthing-client.ts # Client upload helpers
│ ├── uploadthing-router.ts # Client-safe router types
│ └── ai/ # AI integration (OpenRouter)
├── i18n/ # react-i18next config (index.ts, routing.ts)
├── types/ # TypeScript type definitions
├── neon.ts # Neon branch compute policy
├── scripts/ # migrate-to-neon.ts + scripts/migrations/neon/
├── tests/ # Vitest unit tests + Playwright E2E tests
├── messages/ # i18n messages (en.json, id.json)
├── docs/ # Documentation (security, database, deployment)
├── plans/ # Self-contained React improvement plans (improve-react)
├── biome.json # Biome configuration
├── vite.config.ts # Vite + TanStack Start + Nitro configuration
└── tsconfig.json # TypeScript configuration
plans/ holds one implementation plan per confirmed finding from the 2026-08-29 React audit (commit c829824). All 17 plans are DONE. See plans/README.md for status.
Aturan validasi field Project (batas panjang, jumlah tag/gambar, format URL, kategori aktif)
hidup di satu modul: lib/project-submission.ts. Modul ini murni zod + helper tanpa
dependensi server, jadi form submit (submit-project-form) dan form edit
(ProjectEditClient) bisa menjalankan schema yang sama persis dengan yang
di-enforce server (lib/actions/projects.ts) — pesan error klien dan server tidak
bisa drift karena keduanya membaca satu sumber.
PROJECT_LIMITS— satu-satunya tempat konstanta min/max field hidup.PROJECT_FORM_FIELDS— nama field FormData yang melintasi seam (snake_case wire keys).buildProjectSubmissionSchema(activeCategoryNames?)— schema lengkap; kategori di-check terhadap daftar kategori aktif pada submit maupun edit (bila query kategori gagal, edit menurun ke tanpa-check kategori, sama seperti create).readProjectFormData/buildProjectFieldErrors/formatProjectFieldErrors— baca FormData, flatten zod issues ke wire contract, dan format ke string error. Klien memvalidasi per-step saat navigasi plus full-gate saat submit; server selalu mengeksekusi schema penuh.
Project list (/project/list) dan homepage memakai fetchProjectsWithSorting
(lib/server/project-public.ts) lewat server function fetchProjectsWithSortingFn.
Filter kategori bersifat resilient terhadap dua representasi nilai yang
tersimpan di kolom projects.category:
- Project baru menyimpan category
name(slug, mis.landing-page). - Project lama / seed menyimpan display text (mis.
Landing Page).
Nilai filter dari UI di-resolve ke kedua bentuk lalu dimatch dengan in(...),
sehingga semua project pada satu kategori tetap muncul.
Sorting tersedia dalam tiga mode:
newest— urutcreated_atdesc (limit langsung di SQL). Default.top— all-time best, murni berdasarkan total likes (tiebreak terbaru).trending— likes diberi bobot recency (likes / umur-hari).
Untuk top/trending, likes dihitung terpisah dan tidak bisa di-order di
SQL, jadi query mengambil candidate window yang lebih lebar dulu, lalu sort +
truncate ke limit di JS agar project lama dengan banyak likes tidak terpotong.
Pilihan filter & sort di UI tidak mengubah URL. Nilai awal tetap di-seed dari search params saat load pertama (deep link tetap jalan), tapi mengganti dropdown setelahnya hanya mengubah state lokal tanpa menyentuh query string.
Event submission via submitEventFn (lib/actions/events.functions.ts) →
submitEvent (lib/actions/events.ts). Server read path canonical:
lib/server/events-public.ts (detail + list). Slugs memakai slug dari form
(fallback: diturunkan dari nama) lalu di-uniquify dengan retry saat 23505
(maksimal 100 percobaan) — dua event dengan slug sama tidak lagi bentrok.
The public form lives at /testimonial. It is a form route, not a profile URL.
/$username skips reserved first-segment slugs (testimonial, calendar,
admin, and the other static app paths in lib/reserved-profile-slugs.ts).
Sign-up suffixes a reserved base name (testimonial_1); profile edit rejects
a reserved username. Git push does not publish the Worker — after merge, run
bun run build then bunx wrangler deploy.
The form is not in the navbar, footer, or sitemap. robots.txt disallows the
path and the route emits noindex. Anyone with the URL can submit (no login).
Fields: full name, role, testimonial text, required photo. The server accepts
FormData, checks JPEG/PNG/WebP magic bytes (not the client MIME string),
uploads the photo with UploadThing UTApi, and inserts a pending row. If
the insert fails, the uploaded file is deleted. Server-side UTApi must
receive UPLOADTHING_TOKEN from getServerRuntimeSecrets() (same source as
/api/uploadthing). Cloudflare Workers do not expose that secret on
process.env, so new UTApi() with no options fails the submit. The value
must be the UploadThing dashboard token (base64 JSON with apiKey, appId,
and regions), not a placeholder and not a raw sk_live_ API key.
Admin dashboard tab testimonials (/dashboard?tab=testimonials) lets role-0
admins approve, reject, or unpublish. Reject and unpublish set status=rejected
and keep the row. Approve sets status=approved and refreshes approved_at.
The landing reviews section shows approved rows (newest first). Each marquee
column gets the full list, rotated, so a small set still shows on mobile
(columns 2 and 3 stay hidden below md / lg). If approved count is 0, it
falls back to the i18n seed in messages/*/reviews.testimonials. Schema:
testimonials table + scripts/migrations/neon/04_testimonials.sql. Apply with
bun run migrate:schema.
loadDashboardBoardData (app/(admin)/dashboard/dashboard-data.ts) returns a
discriminated union DashboardBoardData (kind: "projects" | "blog" | ...), so
DashboardTabPanel narrows per tab — no any payload crosses the seam.
Tambahan board = satu member union + satu case dengan narrowing.
Dashboard board UI modules call mutations through *.functions.ts
(createServerFn). They must not import lib/actions/admin/*.ts or
lib/actions/events.ts as values. Those modules import
@tanstack/react-start/server, which constructs AsyncLocalStorage at
load time and crashes the browser dashboard chunk.
Centralized comments component yang works untuk both Blog dan Project.
import { CommentSection } from "@/components/ui/comment-section";
<CommentSection
entityType="post" // or "project"
entityId={id}
initialComments={comments}
isLoggedIn={!!user}
/>;Session-based analytics dengan:
- 30-minute session timeout
- IP + User Agent fingerprinting
- Bot protection (user agent filtering)
- Unique visitor counting
- SVG blur placeholders
- Intersection Observer lazy loading
- AVIF/WebP automatic optimization
- Client-safe processing (no sharp in client bundle)
Homepage performance is tuned for Core Web Vitals (LCP/TBT):
- Build-time responsive images -
scripts/optimize-images.mjs(sharp) pre-generates AVIF + WebP variants of large public images intopublic/optimized/at multiple widths. Runs automatically beforebun run build.sharpis a required build-timedevDependencyso Cloudflarebun install --frozen-lockfilecan resolve it. It is not imported from app or Worker code. The hero (the LCP element) drops from a ~660KB 2880×1800 PNG to ~90KB at its 1200px breakpoint. OptimizedImagecomponent (components/ui/optimized-image.tsx) - Renders a<picture>with AVIF/WebPsrcsetpointing at the generated variants. The hero usespriority(eager load +fetchpriority="high") and is preloaded in the home routehead().- Right-sized remote avatars - GitHub avatars request
?s=64; testimonial avatars use the 128px optimized variants instead of full-size source PNGs. - Code-split below-the-fold sections - The homepage lazy-loads non-critical sections (video showcase, community features, AI tools, reviews, FAQ, CTA, footer) with
React.lazy+Suspenseso they no longer block initial hydration (reduces Total Blocking Time). - No service worker cache - The app does not register a service worker. The previous worker precached
/and used cache-first for.js, which served stale HTML/chunks in in-app browsers after deploys. Visiting any page unregisters existing workers and clears Cache Storage. - Long-lived asset caching -
routeRulesinvite.config.tsemitcache-controlheaders (written to the generated.output/public/_headers) for/optimized/*and/fonts/*(immutable). Only non-overlapping directory rules are used: Cloudflare_headerswildcards match across/and concatenate every matching rule's value, so broad extension rules like/*.avifare avoided (they would corrupt the/optimized/*header). - Faster server response (TTFB) - The homepage previously made several redundant per-request DB/auth roundtrips.
getServerSession()(lib/server/auth.ts) is now memoized per request (keyed on the request object via aWeakMap), so the session resolves once instead of being re-fetched by the rootbeforeLoad, route loaders, andgetBatchLikeStatus. The home route loader also reuses the user already resolved in the rootbeforeLoadinstead of re-querying it, and verbose per-requestconsole.logcalls in the hot data path were removed. - Short-TTL homepage cache - Active categories, home vibe videos, and approved testimonials are cached for 5 minutes (
lib/server/short-ttl-cache.ts) so list pages do not scan those tables on every request. - SQL aggregates -
getBatchLikeStatusand category project counts useGROUP BY/count()in Postgres instead of loading full like or project rows into the Worker. - Client-side upload compression - User-uploaded project images go through
lib/image-compression.ts(onBeforeUploadBeginon the UploadThing buttons), which downscales to 1600px and re-encodes to WebP in the browser before upload. UploadThing (ufs.sh) has no on-the-fly resizing andsharpcannot run on Cloudflare Workers, so compressing client-side is the only no-cost option — it typically turns a 380KB+ upload into ~80-120KB. Falls back to the original file if compression fails or does not reduce size. - Backfill for existing images -
scripts/backfill-image-compression.ts(run withbun run backfill:images, or--applyto write) recompresses already-uploaded project/video images: download → WebP via sharp → re-upload to UploadThing → update the DB row → delete the old file. Runs on Node/Bun (needsDATABASE_URL+UPLOADTHING_TOKEN), defaults to a dry run. Use this once to fix images uploaded before client-side compression was added.
Search-engine optimization is handled at the route level:
- Server-rendered meta - Per-route
head()blocks emit title, description, Open Graph, and Twitter Card tags (rendered in SSR HTML, verifiable with a Googlebot user-agent). - Structured data - Organization + WebSite JSON-LD in the root route.
- Dynamic sitemap -
app/routes/sitemap[.]xml.tsqueries Neon (Drizzle) for published posts, projects, approved events, and public profiles, plus static routes. Auth-gated pages are excluded;lastmoduses real content timestamps with a fallback. - robots.txt -
app/routes/robots[.]txt.tsserves a singleUser-agent: *group, disallows private/API paths, and references the sitemap. Note: if Cloudflare's managed robots.txt is enabled it will shadow this route — keep only one source of truth. - Canonical URLs - Self-referencing canonicals on content and list pages; the homepage and
/project/listconsolidate?filter/?sortvariants onto their clean URLs. - noindex - Admin, dashboard, blog editor, project submit,
/testimonial, and auth routes emitrobots: noindex, nofollowvia the sharedNOINDEX_METAhelper inlib/seo/site-url.ts. - Dynamic OG image -
app/routes/api/og.tsrenders a branded 1200×630 SVG from atitlequery param (dependency-free, Cloudflare Workers-safe). - LCP-friendly images - Below-the-fold images use
loading="lazy"; preload is reserved for the hero/logo (above the fold).
Registration dibatasi ke domain terpercaya:
- gmail.com, yahoo.com, outlook.com
- dan 20+ domain edukasi/tech lain
Built-in AI features using OpenRouter:
- AI-powered content suggestions
- Leaderboard for AI tool rankings
- Integration with Agentic workflow
Full-featured events management:
- Event submission with approval workflow
- Status tracking (upcoming, past)
- Location types (online, offline, hybrid)
- Organizer profiles
- Cover image support
Comprehensive admin tools:
- User management (role: admin, moderator, user)
- Content moderation (comments, reports)
- Event approval workflow
- Analytics dashboard
This project includes AI agent skills for enhanced development:
- motion-design - Motion design principles for emotionally-driven, technically sound animations
See .agents/skills/ for workspace skills.
Kami welcome kontribusi dari semua developer! 🎉
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Make your changes
- Test thoroughly
- Commit your changes (
git commit -m 'feat: Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Submit a pull request
- Follow TypeScript strict mode
- Use Tailwind CSS for styling
- Biome will auto-format on commit
- Write descriptive commit messages (Conventional Commits)
- Test your changes on both desktop and mobile
- Maintain the informal but professional Indonesian tone in UI copy
- 2-space indentation, no semicolons, single quotes
- Biome enforces unified linting + formatting
@/prefix untuk absolute imports- Group imports: React → Third-party → Internal
For detailed security documentation, see:
This project is licensed under the MIT License.
Built with ❤️ by VibeDev ID Community