Underground 2 Revived
Play Need for Speed: Underground 2 (2004) in Chrome and Firefox with WebGPU graphics, Web Audio, up to 120 fps widescreen, controller support, free roam and Online races with friends. Fan project, not
VRChat에 숨은 신비 아바타 ‘Akyo’ 700종·Akyo 요소 월드 50종을 수록한 도감 사이트, 이름·제작자·카테고리로 검색 가능
VRChatに潜むなぞ生物アバター「Akyo」を700体以上、そしてAkyo要素のあるワールドを50種類以上収録したAkyoコンテンツの図鑑サイト。名前・作者・カテゴリで探せる日本語/英語/韓国語対応の共有データベースで、今日からキミもAkyoファインダーの仲間入り!
게임TypeScript · JavaScriptGitHub @rad-vrc온라인운영자 미확인
써 보셨어요? 한 번만 눌러 주세요. 로그인은 필요 없어요.
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기VRChatに潜むなぞ生物アバター「Akyo」を500体以上収録した図鑑サイト。名前・作者・属性で探せる日本語対応の共有データベースで、今日からキミもAkyoファインダーの仲間入り!
저장소 README에서 가져옴
README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
Akyodex - Next.js 16 + Cloudflare Pages
1. Quick Start
2. Project Overview
3. Glossary
4. Architecture
5. Tech Stack
6. Project Structure
7. Development Setup
8. Deployment Guide
9. Deployment Verification
10. Environment Variables
11. Features
12. API Endpoints
13. Security
14. Troubleshooting
15. Migration History
16. Contributing
Get Akyodex running locally in 5 minutes!
Step 1: Clone and Install (2 minutes)
Step 2: Set Up Environment (1 minute)
The example below uses Bash heredoc syntax. On PowerShell, create .env.local manually and paste the same key/value pairs.README 전문 보기 ↗VRChat Avatar & World Encyclopedia
Get Akyodex running locally in 5 minutes!
# Check Node.js version (need 20.x or later)
node --version
# Check npm version (need 10.x or later)
npm --version
# Clone repository
git clone https://github.com/rad-vrc/Akyodex.git
cd Akyodex
# Install dependencies
npm install
The example below uses Bash heredoc syntax. On PowerShell, create .env.local manually and paste the same key/value pairs.
# Create .env.local file with default credentials
cat > .env.local << 'EOF'
# Admin Authentication (simple access codes)
# Owner password (full access): RadAkyo
# Admin password (limited access): Akyo
ADMIN_PASSWORD_OWNER=RadAkyo
ADMIN_PASSWORD_ADMIN=Akyo
# Session Secret (Development only)
SESSION_SECRET=629de6ec4bc16b1b31a6b0be24a63a9ab32869c3e7138407cafece0a5226c39d8439bd4ac8c21b028d7eb9be948cf37a23288ce4b8eebe3aa6fefb255b9c4cbf
# R2 Base URL (for image fetching)
NEXT_PUBLIC_R2_BASE=https://images.akyodex.com
# App Origin (for CSRF protection)
NEXT_PUBLIC_APP_URL=http://localhost:3000
EOF
# Start dev server
npm run dev
✅ Gallery: http://localhost:3000/zukan
✅ Admin Panel: http://localhost:3000/admin
Default Admin Credentials:
RadAkyo (full access)Akyo (limited access)Akyodex は、VRChatのオリジナルアバター「Akyo」シリーズと関連ワールドを網羅したオンライン図鑑です。
Key terms used in this documentation:
avtr_xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)┌─────────────────────────────────────────────────────────────┐
│ Cloudflare Pages │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ Next.js 16 App (OpenNext Adapter) │ │
│ │ ┌─────────────┐ ┌──────────────┐ ┌─────────────┐ │ │
│ │ │ SSG Pages │ │ API Routes │ │ Middleware │ │ │
│ │ │ (Static) │ │ (Edge/Node) │ │ (i18n+CSP) │ │ │
│ │ └─────────────┘ └──────────────┘ └─────────────┘ │ │
│ └───────────────────────────────────────────────────────┘ │
│ │ │ │ │
│ ├────────────────┼────────────────┤ │
│ ▼ ▼ ▼ │
│ ┌────────────┐ ┌─────────────┐ ┌──────────────┐ │
│ │ R2 Bucket │ │ KV Store │ │ GitHub │ │
│ │ (Images + │ │ (Session + │ │ (CSV Sync) │ │
│ │ CSV/JSON)│ │ Data Cache)│ └──────────────┘ │
│ └────────────┘ └─────────────┘ │
└─────────────────────────────────────────────────────────────┘
Data Source Priority: KV (~5ms) → JSON (~20ms) → CSV (~200ms)
instrumentation.ts) + client-side (instrumentation-client.ts)crypto.timingSafeEqualAkyodex/
├── README.md # This file
├── package.json # Dependencies and scripts
├── next.config.ts # Next.js + Cloudflare config
├── open-next.config.ts # OpenNext Cloudflare adapter config
├── wrangler.toml # Cloudflare Pages / R2 / KV bindings
├── tsconfig.json # TypeScript config
├── eslint.config.mjs # ESLint flat config
├── knip.json # Dead code analysis config
├── postcss.config.mjs # PostCSS config (Tailwind CSS 4)
├── playwright.config.ts # E2E test config
│
├── public/
│ ├── sw.js # Service Worker (6 caching strategies)
│ └── images/ # PWA icons, logos, placeholder
│
├── src/
│ ├── app/ # Next.js App Router
│ │ ├── layout.tsx # Root layout (fonts, Dify chatbot, Sentry)
│ │ ├── page.tsx # Landing page (redirects to /zukan)
│ │ ├── globals.css # Global styles (Tailwind CSS 4)
│ │ ├── manifest.ts # PWA manifest (dynamic)
│ │ ├── sitemap.ts # Dynamic sitemap
│ │ ├── robots.ts # robots.txt
│ │ ├── not-found.tsx # 404 page
│ │ ├── error.tsx # Error boundary
│ │ ├── global-error.tsx # Global error boundary
│ │ ├── offline/ # PWA offline page
│ │ ├── admin/ # Admin panel
│ │ │ ├── page.tsx # Admin server component
│ │ │ └── admin-client.tsx # Admin client logic
│ │ ├── zukan/ # Entry gallery
│ │ │ ├── page.tsx # Gallery page (SSG + ISR)
│ │ │ ├── loading.tsx # Loading skeleton
│ │ │ └── zukan-client.tsx # Gallery client component
│ │ └── api/ # API Routes
│ │ ├── admin/ # Auth APIs
│ │ │ ├── login/ # POST - Login
│ │ │ ├── logout/ # POST - Logout
│ │ │ ├── verify-session/ # GET - Session verification
│ │ │ └── next-id/ # GET - Next available ID
│ │ ├── akyo-data/ # GET - Data retrieval API (Node.js runtime)
│ │ ├── upload-akyo/ # POST - Entry registration
│ │ ├── update-akyo/ # POST - Entry update
│ │ ├── delete-akyo/ # POST - Entry deletion
│ │ ├── categories/ # GET/POST - Category tree, create/rename/merge/delete (one commit)
│ │ ├── check-duplicate/ # POST - Duplicate check
│ │ ├── avatar-image/ # GET - Image proxy (R2 → VRChat page/API → Placeholder)
│ │ ├── vrc-avatar-info/ # GET - VRChat avatar info fetch
│ │ ├── vrc-avatar-image/ # GET - VRChat avatar image fetch
│ │ ├── vrc-world-info/ # GET - VRChat world info fetch (Node.js runtime)
│ │ ├── vrc-world-image/ # GET - VRChat world image fetch (Node.js runtime)
│ │ ├── csv/ # GET - CSV data endpoint
│ │ ├── download-reference/ # GET - Reference image download (R2)
│ │ ├── revalidate/ # POST - On-demand ISR revalidation
│ │ ├── kv-migrate/ # POST - KV data migration
│ │ └── manifest/ # GET - Dynamic manifest
│ │
│ ├── components/ # React Components
│ │ ├── akyo-card.tsx # Entry card (grid view)
│ │ ├── akyo-list.tsx # Entry list (list view)
│ │ ├── akyo-detail-modal.tsx # Detail modal
│ │ ├── filter-panel.tsx # Category/author filter
│ │ ├── search-bar.tsx # Search input
│ │ ├── language-toggle.tsx # Language switcher
│ │ ├── loading-spinner.tsx # Loading indicator
│ │ ├── mini-akyo-bg.tsx # Animated background
│ │ ├── icons.tsx # SVG icon components
│ │ ├── dify-chatbot.tsx # Dify chatbot loader/state handler
│ │ ├── structured-data.tsx # JSON-LD structured data
│ │ ├── web-vitals.tsx # Web Vitals reporting
│ │ ├── service-worker-register.tsx # SW registration
│ │ └── admin/ # Admin components
│ │ ├── admin-header.tsx
│ │ ├── admin-login.tsx
│ │ ├── admin-tabs.tsx
│ │ ├── attribute-modal.tsx # Category management
│ │ ├── edit-modal.tsx # Edit modal
│ │ └── tabs/
│ │ ├── add-tab.tsx # Add entry tab
│ │ ├── edit-tab.tsx # Edit entry tab
│ │ └── tools-tab.tsx # Tools tab
│ │
│ ├── hooks/ # Custom React Hooks
│ │ ├── use-akyo-data.ts # Data loading + language refetch
│ │ └── use-language.ts # Language detection + cookie
│ │
│ ├── lib/ # Utility Libraries
│ │ ├── akyo-data.ts # Unified data module (KV → JSON → CSV)
│ │ ├── akyo-data-json.ts # JSON data source
│ │ ├── akyo-data-kv.ts # KV data source
│ │ ├── akyo-data-server.ts # Server-side CSV data loading
│ │ ├── akyo-data-helpers.ts # Shared helpers (extractCategories, etc.)
│ │ ├── akyo-crud-helpers.ts # CRUD operation helpers
│ │ ├── akyo-entry.ts # Entry normalization (hydrateAkyoDataset)
│ │ ├── api-helpers.ts # API helpers (jsonError, getApiErrorResponse, CSRF, session)
│ │ ├── csv-utils.ts # CSV parsing/stringify + GitHub sync
│ │ ├── github-utils.ts # GitHub API operations
│ │ ├── r2-utils.ts # R2 storage operations
│ │ ├── html-utils.ts # HTML sanitization
│ │ ├── i18n.ts # i18n utilities
│ │ ├── next-id-state.ts # Next ID allocation state
│ │ ├── session.ts # HMAC session management
│ │ ├── sentry-browser.ts # Sentry browser configuration
│ │ ├── vrchat-utils.ts # VRChat avatar/world utilities
│ │ ├── vrchat-world-image.ts # VRChat world image fetch
│ │ ├── vrchat-world-info.ts # VRChat world info fetch
│ │ ├── world-registration.ts # World entry registration helpers
│ │ ├── blur-data-url.ts # Blur placeholder generation
│ │ └── cloudflare-image-loader.ts # Cloudflare Images loader
│ │
│ ├── types/
│ │ ├── akyo.ts # Core types (AkyoData, AkyoEntryType, etc.)
│ │ ├── kv.ts # KV binding types
│ │ ├── env.d.ts # Environment variable types
│ │ ├── cloudflare-workers.d.ts # Cloudflare Workers type declarations
│ │ ├── css.d.ts # CSS module types
│ │ └── sanitize-html.d.ts # sanitize-html type augmentation
│ │
│ └── middleware.ts # Edge middleware (i18n + CSP + nonce)
│
├── instrumentation.ts # Sentry server-side initialization
├── instrumentation-client.ts # Sentry client-side initialization
│
├── scripts/ # Utility scripts (ESLint excluded)
│ ├── csv-to-json.ts # CSV → JSON conversion
│ ├── fix-categories.js # Japanese category fixes
│ ├── fix-categories-en.js # English category fixes
│ ├── category-definitions-ja.js # Japanese category keywords
│ ├── category-definitions-en.js # English category keywords
│ ├── category-translations.js # Loads data/category-translations.json (JA → EN/KO)
│ ├── generate-ko-data.js # Generate KO data from JA source
│ ├── nickname-map-ko.js # KO nickname translation map
│ ├── update-categories-v3.js # Japanese category updater
│ ├── update-categories-en-v3.js # English category updater
│ ├── update-categories-common.js # Shared category logic
│ ├── sync-akyo-data-en-from-ja.js # Sync EN data from JA
│ ├── convert-akyo-data.js # Data conversion utility
│ ├── generate-vectorize-payload.js # Vectorize payload generator
│ ├── prepare-cloudflare-pages.js # Cloudflare Pages build prep
│ └── test-csv-quality.js # CSV data quality tests
│
└── data/
├── akyo-data-ja.csv # Japanese avatar data
├── akyo-data-en.csv # English avatar data
├── akyo-data-ko.csv # Korean avatar data
├── akyo-data-ja.json # Japanese data (JSON cache)
├── akyo-data-en.json # English data (JSON cache)
└── akyo-data-ko.json # Korean data (JSON cache)
The example below uses Bash heredoc syntax. On PowerShell, create .env.local manually and paste the same key/value pairs.
# Clone repository
git clone https://github.com/rad-vrc/Akyodex.git
cd Akyodex
# Install dependencies
npm install
# Create .env.local file for local development
cat > .env.local << 'EOF'
# Admin Authentication (simple access codes)
# Owner password (full access): RadAkyo
# Admin password (limited access): Akyo
ADMIN_PASSWORD_OWNER=RadAkyo
ADMIN_PASSWORD_ADMIN=Akyo
# Session Secret (Development only)
SESSION_SECRET=629de6ec4bc16b1b31a6b0be24a63a9ab32869c3e7138407cafece0a5226c39d8439bd4ac8c21b028d7eb9be948cf37a23288ce4b8eebe3aa6fefb255b9c4cbf
# R2 Base URL (for image fetching)
NEXT_PUBLIC_R2_BASE=https://images.akyodex.com
# App Origin (for CSRF protection)
NEXT_PUBLIC_APP_URL=http://localhost:3000
EOF
# Run development server
npm run dev
Simple Access Codes (same for development and production):
RadAkyo (full access - can delete avatars)Akyo (limited access - can add/edit only)These are simple, easy-to-share access codes for community contributors.
# Development
npm run dev # Start dev server with Turbopack (localhost:3000)
npm run build # Build for Cloudflare Pages (OpenNext + prepare script)
npm run next:build # Next.js build only
npm run start # Start production server (local)
# Quality
npm run lint # Run ESLint
npm run knip # Dead code analysis
# Testing
npm run test # Run Playwright E2E tests
npm run test:playwright # Run Playwright tests (alias)
npm run test:ui # Run Playwright with UI mode
npm run test:headed # Run Playwright with headed browser
npm run test:csv # CSV data quality checks
# Data
npm run data:convert # Convert CSV to JSON (npx tsx scripts/csv-to-json.ts)
akyodex-workers-production; push to main uploads a tagged candidate without changing traffic.activate action in .github/workflows/deploy-cloudflare-workers-production.yml. Regenerated font subsets can use the guarded activate-fonts action automatically; see below.npm run build and .github/workflows/deploy-cloudflare-pages-preview.yml are retained only for isolated PR previews in the akyodex-pr-preview Pages project.Via Cloudflare Dashboard:
akyodex-pr-preview project; do not attach akyodex.comFramework preset: None
Build command: npm ci && npm run build
Build output directory: .open-next
Root directory: /
Bindings are defined in wrangler.toml and configured in Settings → Functions:
| Binding | Type | Purpose | Notes |
|---|---|---|---|
AKYO_BUCKET | R2 Bucket | Avatar images and data/*.json / CSV files | Usually points to akyo-images |
NEXT_INC_CACHE_R2_BUCKET | R2 Bucket | OpenNext incremental cache | Keys are namespaced under incremental-cache/... |
AKYO_KV | KV Namespace | Admin sessions + app data cache | App cache keys use akyo-data:<locale> |
NEXT_TAG_CACHE_KV | KV Namespace | OpenNext tag revalidation cache | Tag keys use <NEXT_BUILD_ID>/<tag> |
AKYO_BUCKET と NEXT_INC_CACHE_R2_BUCKET は同じ bucket を共有できます。AKYO_KV と NEXT_TAG_CACHE_KV も同じ namespace を共有できますが、キー体系が重ならないことを確認してください。
Go to Settings → Environment variables and add:
| Variable | Required | Purpose |
|---|---|---|
ADMIN_PASSWORD_OWNER | Yes | Owner role login code (src/app/api/admin/login/route.ts) |
ADMIN_PASSWORD_ADMIN | Yes | Admin role login code (src/app/api/admin/login/route.ts) |
SESSION_SECRET | Yes | HMAC session signing key (src/lib/session.ts) |
NEXT_PUBLIC_APP_URL | Yes | CSRF allowlist origin (src/lib/api-helpers.ts) |
NEXT_PUBLIC_R2_BASE | Yes | Public base URL for R2-hosted images/data |
GITHUB_TOKEN | Yes | CSV sync and admin-side GitHub writes |
GITHUB_REPO_OWNER | Yes | GitHub repo owner for CSV sync |
GITHUB_REPO_NAME | Yes | GitHub repo name for CSV sync |
GITHUB_BRANCH | Yes | Target branch for CSV sync |
GITHUB_CSV_PATH_JA | Yes | JA CSV path in repo |
REVALIDATE_SECRET | Recommended | Protects /api/revalidate |
NEXT_PUBLIC_DIFY_CHATBOT_TOKEN | Optional | Enables Dify/Udify chatbot widget |
ADMIN_LOGIN_RATE_LIMIT | Optional | Emergency switch: set to off to disable the admin login rate limit (src/lib/admin-login-rate-limit.ts) |
Admin login rate limiting. POST /api/admin/login is throttled per client IP with a Workers Rate Limiting binding (ADMIN_LOGIN_IP_LIMITER, nominal 10 attempts per 60 seconds, keyed by CF-Connecting-IP). A second binding (ADMIN_LOGIN_GLOBAL_LIMITER, nominal 60 per 60 seconds across all clients) is monitor-only: exceeding it is logged but never blocks, so a distributed attack cannot lock legitimate admins out. The nominal values are configuration targets, not guarantees: Cloudflare enforces them per location and eventually consistently, and staging measurements showed roughly 3-4x the nominal count getting through before 429s start. A blocked IP is accepted again within 60 seconds after its own attempts stop. The check fails open: when a binding is missing (local dev, Pages preview) or throws, the request is allowed; on Workers deployments (CLOUDFLARE_DEPLOY_TARGET=workers) a warning is logged so a configuration regression is visible. Emergency switch: create a Worker version with the secret ADMIN_LOGIN_RATE_LIMIT=off (production: npx wrangler versions secret put ADMIN_LOGIN_RATE_LIMIT --config wrangler.workers.production.jsonc, then activate that version; staging: npx wrangler secret put ADMIN_LOGIN_RATE_LIMIT --config wrangler.workers.jsonc) and delete the secret again once the limiter is healthy.
These are for GitHub-hosted workflows, not for the runtime app itself:
| Name | Required | Used by | Notes |
|---|---|---|---|
CLOUDFLARE_API_TOKEN | Yes | Deploy + Preview Gate | Must be able to deploy Pages and inspect deployments |
CLOUDFLARE_ACCOUNT_ID | Yes | Deploy + Preview Gate | Cloudflare account identifier |
CLOUDFLARE_PAGES_PROJECT | Recommended | Deploy + Preview Gate | Primary Pages project candidate; Preview Gate falls back to akyodex |
NEXT_PUBLIC_SITE_URL | Optional | Build workflows | Build-time public URL fallback |
NEXT_PUBLIC_R2_BASE | Optional | Build workflows | Build-time R2 URL fallback |
R2_ACCESS_KEY_ID | Optional | sync-json-data.yml | Required when JSON sync uploads to R2 |
R2_SECRET_ACCESS_KEY | Optional | sync-json-data.yml | Required when JSON sync uploads to R2 |
DEFAULT_ADMIN_PASSWORD_HASH | Optional / legacy | Build workflows only | Legacy fallback still exported by workflow YAML |
DEFAULT_OWNER_PASSWORD_HASH | Optional / legacy | Build workflows only | Legacy fallback still exported by workflow YAML |
DEFAULT_JWT_SECRET | Optional / legacy | Build workflows only | Legacy fallback still exported by workflow YAML |
The current runtime code reads ADMIN_PASSWORD_OWNER, ADMIN_PASSWORD_ADMIN, and SESSION_SECRET. The *_HASH / DEFAULT_JWT_SECRET values above are workflow-side compatibility defaults and are not read by src/app/api/admin/login/route.ts or src/lib/session.ts.
main → deploy-cloudflare-workers-production.yml uploads a zero-traffic Worker versionDeploy Cloudflare Workers Production → activateactivate-fonts, subject to the production-diff guard belowrollback-worker; optionally specify a Worker version-id, and the production route stays on Workersdeploy-cloudflare-pages-preview.yml deploys an isolated, read-only Pages previewPagesはPRの画面確認専用です。本番のsource of truth、activation、rollbackはすべてWorkers workflowです。 Worker rollback cannot cross a Durable Object class lifecycle change; Durable Object migrationを跨ぐ復帰は専用のmigration手順として扱います。
管理画面の変更に手動 activate を要求しない、というのが運用の原則です。Akyo の登録・編集は KV/R2 配信なのでそのまま反映されますが、バンドルに入るファイルは activate しないと出ません。そこで 2 つの経路が自動で activate を投げます。
activate-fonts — Sync JSON Data from CSV が、フォント生成に成功し、WOFF2 のバイト列が変わり、生成コミットが push され、R2 アップロードが完了したときだけ要求します。フォントが変わらない場合や生成に失敗した場合は候補アップロードのみの従来動作に戻ります。フォント生成の失敗はカタログ同期を止めません。activate-colors — Activate category colors が、src/lib/category-colors.json だけが動いた push を拾って要求します。管理画面の「色を変える」は CSV も対訳も動かさず文字も増やさないため、カタログの自動反映にもフォントの自動追従にも乗らないからです。この経路が無いと、色を変えても反映されないまま残り、次のフォント自動追従もそこで止まります。どちらも「ガード付きの Worker リリース」で、R2 への個別アップロードではありません。ビルドやアップロードの前に scripts/guarded-release.js が、稼働中の本番 Worker が返す完全なコミット ID と候補を突き合わせます(直前の同期コミットではありません)。自動 activate には次のすべてが必要です。
activate-fonts なら src/fonts/mplus2-variable.subset.woff2、activate-colors なら src/lib/category-colors.json)。src/fonts/subset-manifest.json, src/lib/category-canonical.json, src/lib/category-colors.json, data/category-translations.json, or data/akyo-data-{ja,en,ko}.{csv,json}. Added/deleted files and all other paths are rejected. 対訳表は実行時に GitHub から読むだけでバンドルに入らないため、色の対応表は書けるのが管理画面だけで内容が色コードに限られる(category-operations.ts が最上位カテゴリと登録済みの色しか通さない)ため、それぞれ許可しています。activate-colors のゲートは WOFF2 が一緒に動いていても通すので、action 名で分岐すると未反映のフォント更新がある状態で色を変えたときに、従来必須だった検証なしでそのフォントまで公開されてしまいます(フォント起因の失敗を検出できないため巻き戻しも働きません)。色替えがコード変更と同じ push に乗っていた場合、Activate category colors は失敗せずに理由を warning と job summary に残して終わります。その変更を activate するのはそのマージを見た人の判断だからです。
本番ワークフローの concurrency は queue: max(cancel-in-progress: false)です。GitHub の既定は「同じグループの pending をキャンセルして新しいものが置き換わる」で、cancel-in-progress: false が守るのは実行中のものだけです。これがないと、待機中の activate 要求を後から来た候補アップロードが取り消してしまい、Activate category colors は dispatch 成功のまま本番は旧色のまま残ります(再要求する経路がありません)。queue: max は本番操作の直列化を保ったまま、最大 100 件を FIFO で待たせます。
Pending application code, dependencies, scripts, configuration, or workflow changes stop the automatic action with an explicit error; they are never silently activated together with a font. Review those changes and use normal manual activation first. The existing locale-ID regression test remains unchanged; automatic font activation does not translate or repair missing EN/KO records.
The action retains production runtime checks, downloads the deployed font to verify byte-for-byte equality, and checks that the live page's stylesheets reference it. A failed post-deploy check restores the recorded previous Worker version. It never attaches, removes, or changes the production route, and never switches to Pages. Open browser tabs pick up the new hashed font after a page reload; they are not forcibly reloaded.
Initial rollout: after reviewing and merging this workflow change, activate it once through the normal manual path. Until that baseline is live, the workflow/script changes themselves correctly block activate-fonts. Subsequent qualifying catalog/font updates no longer need manual activation. A blocked or interrupted run can be retried with activate-fonts on main; the same guards apply.
main Deploy| Check | Where | Expected Result |
|---|---|---|
| Deploy summary | GitHub Actions Deploy to Cloudflare Pages | Deploy Step: success and Health Check: healthy |
| Deployment URL | Workflow output / Step Summary | URL is present and responds with 200, 301, or 302 within the health-check retry window |
| Core routes | /, /zukan, /admin | Landing redirect works, gallery loads, admin login page renders |
| Static/PWA assets | /manifest.webmanifest, /sw.js | Assets return successfully |
| Cloudflare bindings | Pages dashboard or wrangler.toml | All four bindings (AKYO_BUCKET, NEXT_INC_CACHE_R2_BUCKET, AKYO_KV, NEXT_TAG_CACHE_KV) are present |
| Admin flow | /admin | Login succeeds and session cookie is set |
| Data plane | https://images.akyodex.com/data/*.json | Latest JSON is reachable after data sync |
main or develop.Cloudflare Pages Preview Gate / Verify Cloudflare Pages Preview succeeds.Cloudflare Pages completed successfully.CLOUDFLARE_PAGES_PROJECT, the Cloudflare preview deployment logs, and the GitHub Actions run logs together.vars.CLOUDFLARE_PAGES_PROJECT first and then falls back to akyodex.missing_url and skips HTTP verification. Use the Pages dashboard URL in that case.staging runs: the workflow input only changes the GitHub Actions environment label. If you want true staging isolation, you must also provision separate Pages project / bindings / secrets..env.local)| Variable | Description | Example |
|---|---|---|
ADMIN_PASSWORD_OWNER | Owner access code(平文) | RadAkyo |
ADMIN_PASSWORD_ADMIN | Admin access code(平文) | Akyo |
SESSION_SECRET | Secret key for HMAC signing | 629de6ec... (128 chars) |
NEXT_PUBLIC_APP_URL | App origin for CSRF allowlist | http://localhost:3000 |
NEXT_PUBLIC_R2_BASE | R2 bucket base URL | https://images.akyodex.com |
NEXT_PUBLIC_DIFY_CHATBOT_TOKEN | Udify cloud token | (optional, chatbot disabled if unset) |
CSRF_DEV_ALLOWLIST (任意) | Playwright などで localhost を許可する場合 true | true |
| Variable | Description | Example |
|---|---|---|
ADMIN_PASSWORD_OWNER | Owner access code(平文、Secrets経由) | (secret) |
ADMIN_PASSWORD_ADMIN | Admin access code(平文、Secrets経由) | (secret) |
SESSION_SECRET | HMAC signing key、必ず 128 文字以上 | 629de6ec... |
NEXT_PUBLIC_APP_URL | Production URL | https://akyodex.com |
NEXT_PUBLIC_R2_BASE | CDN base | https://images.akyodex.com |
NEXT_PUBLIC_DIFY_CHATBOT_TOKEN | Udify token | (optional) |
GITHUB_TOKEN | CSV 更新用 PAT(repo scope) | ghp_xxx |
GITHUB_REPO_OWNER | GitHub org/user | rad-vrc |
GITHUB_REPO_NAME | Repo name | Akyodex |
GITHUB_BRANCH | Tracking branch | main |
GITHUB_CSV_PATH_JA | Japanese CSV path in repo | data/akyo-data-ja.csv |
REVALIDATE_SECRET | ISR revalidation API key | (secret) |
| Variable / Secret | Description | Example |
|---|---|---|
CLOUDFLARE_API_TOKEN | GitHub Actions から Pages deploy / preview を操作する API token | (secret) |
CLOUDFLARE_ACCOUNT_ID | Cloudflare account ID | (secret) |
CLOUDFLARE_PAGES_PROJECT | Preview Gate が最初に参照する Pages project 名 | akyodex |
NEXT_PUBLIC_SITE_URL | Workflow build-time fallback URL | https://akyodex.com |
NEXT_PUBLIC_R2_BASE | Workflow build-time fallback R2 base | https://images.akyodex.com |
R2_ACCESS_KEY_ID | sync-json-data.yml が R2 に JSON を upload するときの access key | (secret) |
R2_SECRET_ACCESS_KEY | sync-json-data.yml が R2 に JSON を upload するときの secret key | (secret) |
DEFAULT_ADMIN_PASSWORD_HASH | Legacy workflow fallback(runtime 未使用) | (optional) |
DEFAULT_OWNER_PASSWORD_HASH | Legacy workflow fallback(runtime 未使用) | (optional) |
DEFAULT_JWT_SECRET | Legacy workflow fallback(runtime 未使用) | (optional) |
Current runtime code reads ADMIN_PASSWORD_OWNER, ADMIN_PASSWORD_ADMIN, SESSION_SECRET, and NEXT_PUBLIC_APP_URL. The DEFAULT_*_HASH / DEFAULT_JWT_SECRET values are only referenced by the current workflow YAML.
| Binding | Type | Purpose |
|---|---|---|
AKYO_BUCKET | R2 Bucket | Avatar images and data files |
NEXT_INC_CACHE_R2_BUCKET | R2 Bucket | OpenNext incremental cache |
AKYO_KV | KV Namespace | Admin session storage + data cache |
NEXT_TAG_CACHE_KV | KV Namespace | OpenNext tag revalidation cache |
AKYO_KV と NEXT_TAG_CACHE_KV を同じ namespace に割り当てる場合は、キー体系が重ならないことを確認してください。
akyo-data:ja, akyo-data:en (pattern: akyo-data:<locale>)<NEXT_BUILD_ID>/<tag>AKYO_BUCKET と NEXT_INC_CACHE_R2_BUCKET を同じ bucket に割り当てる場合は、OpenNext incremental cache が incremental-cache/... 配下に保存される前提で、画像や data/*.json と衝突しない構成にしてください。
# Session Secret (128 hex characters)
openssl rand -hex 64
# Or use Node.js
node -e "const crypto = require('crypto'); console.log(crypto.randomBytes(64).toString('hex'));"
The admin passwords are simple access codes designed to be easily shared with community contributors:
These are not meant to be highly secure passwords, but rather easy-to-remember codes for trusted community members.
isMobile === true), while desktop keeps it open.avatar: 512, world: 384) to reduce world-card transfer size.Access: /admin (requires authentication)
Cache First (Fonts, Icons)
Network First (HTML, API)
Cache Only (Offline page)
Network Only (Admin, Auth)
Stale While Revalidate (Images, CSS, JS)
Offline Fallback
AKYO_LANG=ja / en / ko)cf-ipcountry)NEXT_PUBLIC_DIFY_CHATBOT_TOKEN から読み込み(必須・未設定ならチャットボットを読み込まない)https://udify.app/embed.min.js)Users can ask questions like:
GET /api/avatar-imageAvatar image proxy with VRChat fallback
Query Parameters:
id (string): Avatar ID (e.g., "0001")avtr (string, optional): VRChat avatar ID (e.g., "avtr_abc123...")w (number, optional): Image width (default: 512, max: 4096)Fallback Priority:
https://images.akyodex.com/images/{id}.webp)avtr provided or found in CSV)Response: Image binary (WebP/PNG/JPEG)
GET /api/vrc-avatar-infoFetch VRChat avatar info
Query Parameters:
avtr (string): VRChat avatar ID (e.g., "avtr_abc123...")Response:
{
"avatarName": "Avatar Name",
"creatorName": "Creator Name",
"description": "Description...",
"fullTitle": "Full OGP Title",
"avtr": "avtr_abc123..."
}
GET /api/vrc-avatar-imageFetch VRChat avatar image
Query Parameters:
avtr (string): VRChat avatar IDResponse: Image binary
GET /api/vrc-world-infoFetch VRChat world info (Node.js runtime)
Query Parameters:
wrld (string): VRChat world ID (e.g., "wrld_abc123...")Response:
{
"success": true,
"data": {
"worldName": "World Name",
"creatorName": "Creator Name",
"description": "Description...",
"wrld": "wrld_abc123..."
}
}
GET /api/vrc-world-imageFetch VRChat world image (Node.js runtime)
Query Parameters:
wrld (string): VRChat world IDw (number, optional): snapped to 96 / 384 / 512 / 800 / 1024 pixelsResponse: Image binary
World thumbnails now follow the avatar path's saved-image-first approach. The
private NEXT_INC_CACHE_R2_BUCKET stores successful VRChat fetches under
world-thumbnails/v1/, separately for world ID, size, and negotiated format.
Registration-time uploads in AKYO_BUCKET are neither read nor overwritten.
after() fetches
current OGP HTML and refreshes the image after responding. Refresh is on demand,
not a scheduled monthly job. Successful refreshes start the next 30-day interval.
Conditional R2 writes prevent a late response from replacing
a newer image; simultaneous stale requests may still duplicate the upstream work.
The real VRChat file version is preserved, never forced to 1.X-World-Image-Fetched-At retains the acquisition
time; X-World-Image-Cache reports HIT / STALE / MISS / BYPASS.No bucket creation, data sync, or modification to existing uploaded thumbnails is
required. Deployment needs a manual site activate; the cache fills on demand.
Local next start without Cloudflare bindings uses live fetches (BYPASS), so local
UI timings do not measure the R2 acceleration. world-thumbnail-runtime.test.ts
tests R2 conditional writes and post-response refresh in local workerd; production
latency and after() completion must also be checked after activation.
GET /api/akyo-dataFetch Akyo data by language (Node.js runtime)
Query Parameters:
lang (string): Language code (ja, en, ko)Response:
{
"success": true,
"data": [/* AkyoData[] */]
}
POST /api/admin/loginAdmin login
Body:
{
"password": "YourPassword"
}
Response:
{
"success": true,
"role": "admin",
"message": "ログインしました"
}
Sets HTTP-only cookie: admin_session (HMAC-signed token, 24h expiry)
POST /api/admin/logoutAdmin logout
Response:
{
"success": true
}
GET /api/admin/verify-sessionVerify admin session
Response:
{
"valid": true,
"role": "admin"
}
GET /api/admin/next-idGet next available avatar ID
Response:
{
"nextId": "0640"
}
POST /api/upload-akyoRegister new avatar
Body (FormData):
id: Avatar ID (4-digit)nickname: NicknameavatarName: Avatar namecategory: Categories (comma-separated)comment: Notes/commentsauthor: Author nameavatarUrl: VRChat avatar URLimageData: Base64 image data (optional)POST /api/update-akyoUpdate existing entry
Body (FormData): Same as upload-akyo
POST /api/delete-akyoDelete entry
Body:
{
"id": "0001"
}
GET /api/categoriesList categories (admin or owner). Reads data/akyo-data-ja.csv, data/category-translations.json and src/lib/category-colors.json from the current main commit.
Response:
{
"head": "<commit sha>",
"categories": [{ "path": "動物/うま", "en": "Animal/Horse", "ko": "동물/말", "count": 12 }],
"colors": { "動物": "#d44335" }
}
POST /api/categoriesCreate / translate / rename / merge / delete a category. create and translate are open to admins; rename, merge and delete are owner-only. Every call is one commit on main (Git Data API) that updates the JA CSV, the translation table and the colour table together; EN/KO CSV/JSON are regenerated by the Sync JSON Data workflow.
Body:
{ "action": "create", "path": "動物/ねこ", "en": "Cat", "ko": "고양이" }
{ "action": "translate", "path": "動物", "en": "Animal", "ko": "동물" }
{ "action": "rename", "from": "動物", "to": "生き物", "en": "Creature", "ko": "생물" }
{ "action": "merge", "from": "乗り物", "into": "機械" }
{ "action": "delete", "path": "動物/うま" }
en / ko are the names of that level only; the parent's translations are prepended. Renaming or deleting a parent applies to every descendant. Booth and the world marker categories cannot be changed. Every action except create must also send head (the commit returned by GET); a different current head, or a push that lands while the commit is being built, returns 409.
POST /api/check-duplicateCheck for duplicates
Body:
{
"field": "nickname" | "avatarName",
"value": "value to check",
"excludeId": "0001" (optional)
}
File: src/app/api/admin/login/route.ts
function timingSafeCompare(a: string, b: string): boolean {
const digestA = createHash('sha256').update(a, 'utf8').digest();
const digestB = createHash('sha256').update(b, 'utf8').digest();
return timingSafeEqual(digestA, digestB);
}
// Always check both passwords to prevent role detection
const isOwner = timingSafeCompare(password, ownerPassword);
const isAdmin = timingSafeCompare(password, adminPassword);
File: src/lib/html-utils.ts
import sanitizeHtml from 'sanitize-html';
// Strip all HTML tags safely
export function stripHTMLTags(html: string): string {
if (!html) return html;
return sanitizeHtml(html, {
allowedTags: [],
allowedAttributes: {}
});
}
File: src/app/api/vrc-avatar-info/route.ts
import { VRCHAT_AVATAR_ID_PATTERN } from '@/lib/akyo-entry';
import { jsonError } from '@/lib/api-helpers';
// Length-limited regex via shared pattern (prevents ReDoS)
if (!VRCHAT_AVATAR_ID_PATTERN.test(avtr)) {
return jsonError('Invalid avtr format (must be avtr_[A-Za-z0-9-]{1,64})', 400);
}
File: src/lib/session.ts
// HMAC-signed sessions with Web Crypto API
export async function createSessionToken(
username: string,
role: AdminRole,
durationMs: number = 24 * 60 * 60 * 1000
): Promise<string> {
// Signs session data with HMAC SHA-256
// Returns base64url-encoded signed token
}
File: src/lib/api-helpers.ts
// Secure cookie configuration
export async function setSessionCookie(token: string, maxAge: number) {
cookieStore.set('admin_session', token, {
httpOnly: true, // Prevent XSS
secure: process.env.NODE_ENV === 'production', // HTTPS only in production
sameSite: 'strict', // CSRF protection
maxAge,
path: '/',
});
}
File: src/middleware.ts
// Nonce-based CSP generated per request
const randomBytes = crypto.getRandomValues(new Uint8Array(16));
const nonce = btoa(String.fromCharCode(...randomBytes));
const cspHeader = `default-src 'self'; script-src 'self' 'nonce-${nonce}' ...`;
✅ Passwords: Server-side comparison, never exposed to client
✅ Sessions: HMAC-signed with HTTP-only cookies (24h expiry)
✅ API Keys: Environment variables only (never in code)
✅ Input: Validated with length-limited regex
✅ HTML: Sanitized with sanitize-html library
✅ Timing Attacks: Constant-time comparison for passwords
✅ CSRF: Origin/Referer validation + SameSite=Strict cookies
✅ XSS: HTML entity decoding + tag stripping + CSP
✅ CSP: Nonce-based Content Security Policy per request
Error: Build failed
Solution: Ensure the build command and output directory are correct:
Build command: npm ci && npm run build
Build output directory: .open-next
Possible Causes:
Solution:
# Check environment variables are set
# ADMIN_PASSWORD_OWNER and ADMIN_PASSWORD_ADMIN must be set
# Regenerate session secret
openssl rand -hex 64
Possible Causes:
AKYO_BUCKET)Solution:
# Check R2 bucket
npx wrangler r2 bucket list
# Re-upload images
npx wrangler r2 object put akyo-images/images/0001.webp --file=path/to/image.webp
Possible Causes:
Solution:
/manifest.webmanifestError: Type 'NextRequest' is not assignable to type 'Request'
Solution: Use standard Web API types:
// ❌ Old pattern
import { NextRequest, NextResponse } from 'next/server';
export async function POST(request: NextRequest) {
return NextResponse.json({ success: true });
}
// ✅ New pattern
export async function POST(request: Request) {
return Response.json({ success: true });
}
When to use NextRequest: Only if you need Next.js-specific features like request.nextUrl. Document the reason in a comment.
Solution: Use the jsonError() helper for all error responses:
import { jsonError } from '@/lib/api-helpers';
// ✅ Correct pattern
return jsonError('Invalid input', 400);
// Returns: { success: false, error: 'Invalid input' }
Solution: Use the cookie helper functions:
import { setSessionCookie, clearSessionCookie } from '@/lib/api-helpers';
// ✅ Set session cookie
await setSessionCookie(token);
// ✅ Clear session cookie
await clearSessionCookie();
Error: Route using Node.js APIs fails on Edge Runtime
Solution: Add the runtime export:
// For routes using csv-parse/sync, GitHub API, or Buffer
export const runtime = 'nodejs';
Request/Response typesjsonError, jsonSuccess, setSessionCookie)attribute → category, notes → comment, creator → authorakyo-data.csv → akyo-data-ja.csv, akyo-data-US.csv → akyo-data-en.csvhydrateAkyoDataset — type inference, display serial allocation)vrc-world-info, vrc-world-image)SourceURL, EntryType, DisplaySerial columns)akyo-data-ko.csv)generate-ko-data.js)# 1. Create feature branch from main
git checkout main
git pull origin main
git checkout -b feature/your-feature-name
# 2. Make changes and commit
git add .
git commit -m "feat: description of changes"
# 3. Push to remote
git push -u origin feature/your-feature-name
# PowerShell in this repo automatically runs the PR conflict check after push.
# Use this instead of plain git push in other shells:
npm run push:check-pr -- -u origin HEAD
# 4. Create Pull Request on GitHub
If the push check exits with code 5, the branch already has a merged PR and you should continue on a new branch / PR.
Exit code 2 means the open PR has merge conflicts, and exit code 4 means GitHub has not finished calculating mergeability yet.
feat: Add new feature
fix: Fix bug
docs: Update documentation
style: Format code
refactor: Refactor code
test: Add tests
chore: Update dependencies
any type prohibited: Use precise type definitionsnpm run lintnpm run build (includes type checking)npm run push:check-pr -- --skip-push to confirm the branch PR is still mergeablenpm run devFor questions or issues:
MIT License - See LICENSE file for details
Last Updated: 2026-03-07
Status: ✅ Production Ready