An independent index of Chittagong University of Engineering & Technology students: search by name or ID, browse every batch and department, or use the public JSON API.
Site https://cuet.sayed.app · API docs https://cuet.sayed.app/api
- Astro 7 (server-rendered) on Cloudflare Workers via
@astrojs/cloudflare - Cloudflare D1 (
CUET_students): the single source of truth for student data; no data files live in this repo - Tailwind CSS 4, almost no client JavaScript
- Edge cache per deploy (
src/middleware.ts), API rate limit via the Workers rate-limiting binding
src/
lib/departments.ts department table, keyed by the ID's department digits
lib/db.ts all D1 queries
lib/api.ts JSON helpers, rate limiting, API response shapes
pages/ routes (pages + /api endpoints)
components/, layouts/
migrations/ D1 schema and data fixes (data imports are gitignored)
scripts/csv-to-sql.mjs CSV → D1 migration for importing a new batch
scripts/refresh-derived.sql rebuilds class_counts / name_words (see below)
D1 bills every row a query reads, and students has ~8.6K rows (a full scan
counts as ~17K reads), so no request path may scan it. Counts come from class_counts (one row per class) and name
search goes through the indexed name_words table. After changing students
by hand, run npm run db:refresh (imports made with csv-to-sql do it already).
IDs are YY DD RRR: batch, department code, class roll. The site and API take the department from DD, not from the
department text column. Code 12 shows as MSE for batches ≤ 22 and MME after that.
| Page | URL |
|---|---|
| Home | / |
| Search | /search?q=…&batch=25&dept=cse (a full ID redirects to the profile) |
| Batches | /batch, /batch/25, /batch/25/cse |
| Departments | /department, /department/cse |
| Student | /2504001 |
| API docs | /api |
Old URLs (/batch?department=…&batch=…, /search?name=…, sitemap_N.xml) redirect.
Read-only, no key, CORS *, 60 requests/minute per IP, responses cached up to 24 h.
| Endpoint | Returns |
|---|---|
GET /api/v1/students/{id} |
one student |
GET /api/v1/search?q=&batch=&dept=&limit= |
name search, or ID prefix if q is digits |
GET /api/v1/batches |
batches with per-department counts |
GET /api/v1/batches/{batch}/{dept} |
full class list |
GET /api/v1/departments |
departments with their batches |
The legacy /api/student/{id} and /api/student/search?query= endpoints still answer in the old flat format. Full
reference with a live console: /api.
Node 22.12+ (.nvmrc pins 24).
npm install
npm run dev # http://localhost:4321 with a local D1
npm run check # wrangler types + astro check
npm run preview # build and run the real Worker locallyThe local D1 starts empty. Copy production into it:
npx wrangler d1 export CUET_students --remote --output prod.sql
npx wrangler d1 execute CUET_students --local --file prod.sqlCloudflare Workers Builds watches this repo:
- push to
main→npm run build→npx wrangler deploy(production) - push to any other branch (except
redirect) → preview version only
npm run deploy deploys manually. Every deploy also clears the edge cache.
/sitemap.xmlis a sitemap index:/sitemap-pages.xml(home, listings, batch/class/department pages) plus one/sitemap-batch-YY.xmlper batch with every profile. All entries carry<lastmod>from D1'supdated_at.robots.txtallows everything except/search?…(search result pages are alsonoindex).- Pages have canonical URLs, Open Graph tags and JSON-LD (
WebSite+ search action,Person,BreadcrumbList). - Google: the sitemap is submitted in Search Console (property
cuet.sayed.app). - Bing, Yandex, Seznam, Naver:
node scripts/indexnow.mjspushes every sitemap URL via IndexNow;--since YYYY-MM-DDsends only changed pages. Key file:public/1144afef155385c08e4aca5b16ec8642.txt.
- Prepare a CSV with headers
studentid,name,admission_roll,admission_merit. node scripts/csv-to-sql.mjs batch26.csv migrations/0006_import_batch_26.sql(use the next free migration number; the script appendsscripts/refresh-derived.sql, soclass_countsandname_wordsupdate in the same migration)npm run db:migrate(applies new migrations to production D1)- Check the derived tables match; all three numbers must be equal:
If they differ, run
npx wrangler d1 execute CUET_students --remote --command "SELECT (SELECT COUNT(*) FROM students), (SELECT SUM(count) FROM class_counts), (SELECT COUNT(DISTINCT studentid) FROM name_words)"npm run db:refresh. - Push to
mainso the edge cache and the in-memory overview refresh, thennode scripts/indexnow.mjs --since <today>.
Import migrations contain personal data and are gitignored; keep the CSV outside the repo.
Any hand-made change to students (a fix migration, d1 execute, the dashboard) must be
followed by npm run db:refresh, or counts and search go stale:
- Added/removed students or changed batch/session:
db:refreshis enough. - Renamed a student: the refresh only indexes students it hasn't seen, so delete their
old words first:
DELETE FROM name_words WHERE studentid = '2504001';thendb:refresh. - Deleted a student: also
DELETE FROM name_words WHERE studentid = ...(stale rows are harmless for results, but waste reads).
Don't rebuild name_words from scratch (DELETE FROM name_words + refresh): that is
~50K row writes, half the free daily limit.
D1 bills every row a query reads: 5M/day, then all queries fail until 00:00 UTC. A
query without an index on its WHERE reads all ~8.6K students (plus index rows). Before
adding a query, check its plan in wrangler d1 execute --local with EXPLAIN QUERY PLAN:
it must say SEARCH ... USING INDEX, never SCAN students. Usage per database:
dashboard → D1 → CUET_students → Metrics.
Avoid node scripts/indexnow.mjs without --since: it makes crawlers fetch every
profile at once (that, plus full-table scans, caused the Nov 2026 outage).
D1 keeps 30 days of point-in-time history (Time Travel). For an offline copy:
npx wrangler d1 export CUET_students --remote --output cuet-backup.sqlmain: this siteredirect: thecuet.pages.devPages project, which redirects to cuet.sayed.apparchive/*tags: the old Next.js versions
Data was compiled from publicly available sources and may contain errors. Personal use is free; commercial use or integration into another system requires acknowledgment. See privacy and terms. Contact: hello@sayed.page.
Built by Sayed.