ESP32-S3 Bench Guide
ESP32-S3 N16R8 bench guide: live pinout, lessons, simulators and the bench log.
翻譯準備中
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證正在載入完整 README。 以下為已儲存的摘錄。
TOEIC語彙ラボ is a comprehensive web application for learning essential TOEIC vocabulary, featuring AI-generated explanations, pronunciation support, and efficient caching strategies. • Content Error Reports and Social Metadata: Every available word explanation includes an email report link with the word and canonical URL prefilled, plus a contact-page fallback. Home Open Graph and Twitter titles/descriptions match its page metadata and retain share images. robots.txt keeps crawl rules and the sitemap URL without the unused Host directive. (Updated: 2026-10-02)檢視完整 README ↗
TOEIC語彙ラボ is a comprehensive web application for learning essential TOEIC vocabulary, featuring AI-generated explanations, pronunciation support, and efficient caching strategies.
Content Error Reports and Social Metadata: Every available word explanation includes an email report link with the word and canonical URL prefilled, plus a contact-page fallback. Home Open Graph and Twitter titles/descriptions match its page metadata and retain share images. robots.txt keeps crawl rules and the sitemap URL without the unused Host directive. (Updated: 2026-10-02)
Study Audio Controls: Study/review word and example-sentence audio buttons use 44×44px targets, 20px icons, blue styling, and visible keyboard focus. The example button sits beside the sentence instead of inline, so wrapped sentences keep even line spacing.
Consistent Difficulty Labels: Level UI uses 最重要 / 中級 / 上級 and shared blue / violet / rose styles across home, search, daily picks, and word details. Labels, score guidance, and styling come from the shared level definitions.
Accessible Favorites Confirmation: Clear-all uses a native modal dialog with initial focus on Cancel, Escape dismissal, and browser-managed background interaction blocking. Cancel restores focus to the trigger; successful deletion focuses the empty-state message.
Inline Audio Recovery: Word details and study/review cards show audio fetch/playback errors near the affected control with an explicit retry button. New playback clears the error; stale requests are ignored after switching audio, cards, or pages. Study word requests use the canonical slug (including uppercase terms such as CEO), and scope changes clear loading/error/retry state so revisiting a card remains playable.
Consistent Term Search: Home, all-words, and favorites share NFKC/case/whitespace normalization and exact-match-first prefix ordering. Home and all-words also share * wildcard matching, including full-width *; favorites retain prefix-only search.
Home Search Recovery: Empty word-search results show spelling/prefix guidance, a clear button that restores input focus and the first page, and a link to the meaning-search tab. (Updated: 2026-09-26)
Initial-load resources: The UI uses system fonts without Geist downloads/preloads. A8 banners and their tracking pixels use native lazy loading, retaining their dimensions to reserve layout space.
Curated Word Lists: Access 1,300+ important TOEIC vocabulary words classified into three levels (Important / Medium / High), sourced from Vercel Blob in production. Blob fetch failures throw (instead of returning an empty list) so a transient outage never gets cached as an empty word list — the last good cache keeps serving.
SEO-Optimized Learning Hub: /words combines a score/letter/prefix search explorer with a server-rendered complete index. The complete index keeps every word available through a crawlable <a href> while nesting level → initial-letter groups in native <details> disclosure panels, so the mobile page is scannable without sacrificing the internal-link graph.
Google Search Site Name: Google-facing site-name signals use TOEIC語彙ラボ (WebSite.name, og:site_name, and application-name), while page titles keep the established TOEIC重要単語 product name. The homepage uses the factual 2026年版 label rather than claiming unverified “latest test trends.”
Snappy List → Detail Navigation: Word links use prefetch={false} (Vercel Edge Request reduction) and the detail route is fully SSG, which together cause the static RSC payload to render with the Suspense boundary already resolved (so loading.tsx never visibly fires). Interactive explorer cards wrap WordLinkPending (useLinkStatus) for instant click feedback; the 1,300+ links in the complete SEO index remain plain Server Component links to avoid creating a client boundary for every word.
AI-Powered Explanations: Detailed definitions, synonyms, and examples generated by Google Gemini (gemini-2.5-flash-lite).
Adaptive Word-Detail Disclosures: A word's primary Japanese translation always stays visible, while dense supporting content can be collapsed. Every native <details> panel is expanded on initial page display and starts with an explicit 「閉じる」action; after the user closes one, its one-line preview, item count, and 「見る」action keep the hidden content discoverable. Short sections remain permanently visible without adding an unnecessary interaction. The pure length/count rules live in src/lib/word-detail-disclosure.ts and keep the behavior consistent across AI-generated payloads.
Smart Caching:
"use cache" + cacheLife("max"), revalidated on demand via /api/revalidate/*) for instant access.Semantic Search(意味で探す): The /words explorer has a second search tab that finds words by meaning — Japanese queries like「延期する」or「感謝を伝えるメールで使う単語」work. Each word's AI-generated WordDetails (definitions, synonyms, examples) is embedded as one document with gemini-embedding-001 (768 dims, L2-normalized) and stored in Upstash Vector; queries are embedded the same way and matched via cosine top-K (POST /api/search/semantic). Candidates below the configurable similarity threshold (default 0.82) are omitted. Results are cached in Redis for 7 days using both the normalized-query hash and a Vector-index generation; bulk or per-word vector updates advance the generation so stale results stop being addressable immediately. Requests are rate-limited per IP and results link straight to /words/[slug]. The TOP page has a lightweight launcher card (SemanticSearchLauncher, titled「意味で探す」and placed below the daily picks, above the spelling-based word list) that transfers the query through same-tab sessionStorage and navigates with only an opaque launch ID (/words?mode=meaning&launch=...#word-explorer). The query text therefore stays out of Vercel request URLs and GA4 page_location; GA4 custom events also receive only query length, never the text. useSearchParams keeps the explorer synchronized with browser history, while an in-flight request map prevents React Strict Mode from issuing duplicate semantic-search requests.
Study Modes:
*) search. /words initially renders 36 compact result cards with “load more”, while the complete A–Z index stays available below./review — including by bookmark — get the shared FavoritePracticeLoginGate instead of a session; there is no guest fallback to the full favorites list.Today’s 6 Picks:
getTodayRecommendedSelection() as a Cache Component ('use cache' + cacheLife('max')), which returns the picks together with their date key and an 8-character word-corpus version. The cache entry is tagged with both today-recommended-words and word-list, so a word-list revalidation also marks the derived daily selection stale. TOP / /today-words / /today-words/listen receive the same cached 6 words, refreshed once per day at JST 7:05 (UTC 22:05) via Vercel Cron (/api/revalidate/today-words). The pick count is the shared constant TODAY_WORDS_COUNT. getTodayRecommendedWords() is a thin uncached wrapper for callers that only need the words — the cache boundary stays on the selection function so there is only ever one cache entry.<Suspense> boundary. The page shell no longer awaits the full word list before streaming, and each async block has a stable skeleton fallback to reduce layout shift.cacheLife('max') never self-expires, the daily rotation depends solely on the Vercel Cron firing; the cron should be monitored, since a stalled cron freezes the same 6 words indefinitely. A time-based fallback ('days') is intentionally not combined with it to avoid double revalidation.picks query. resolveTodayNavigation() validates the bounded, unique list against known words and preserves it through every previous/next link, even when the cached corpus version differs. Invalid or unavailable selections hide both navigation links and offer a return to /today-words; they never widen to the full word list. Valid legacy date/version links are upgraded to snapshots. No daily cache dependency is added to pre-rendered word pages.Listen-and-Repeat Mode (Beta) (/today-words/listen — open to everyone, /favorites/listen — sign-in required):
FavoritesListenClient is a thin state gate (guest → login gate, loading → skeleton, error → retry) and mounts FavoritesListenPlayer only once Supabase favorites are ready, so no TTS request is issued for a signed-out visitor.onended race conditions, and prefetches the current and next word's WordDetails for instant example previews.Favorites: Save difficult words for later review. Persisted in local storage via FavoritesContext for guests; synced to Supabase for signed-in users. Adding and browsing favorites never requires an account; only the favorites-powered review and listen modes do. On /favorites, signed-out visitors with at least one favorite see FavoritePracticeLoginCard (a single card linking to /login?next=/mypage) in place of the review/listen buttons.
/favorites has a prefix search box that filters the list by English term. Matching lives in the pure, unit-tested src/lib/word-search.ts (filterWordsByTermPrefix): input is NFKC-normalized, whitespace-collapsed, trimmed, and lowercased so full-width IME input still matches; exact matches come first and the newest-first order is otherwise kept. Substring and wildcard (*) matching are intentionally not supported here. The query never enters the URL or analytics. The bulk-delete button is labeled 「すべて削除」 so it is not mistaken for clearing the search, and it still removes every favorite behind the existing confirmation modal.src/lib/favorites-store.ts + useSyncExternalStore) rather than being loaded through a mount effect, so no setState happens inside an effect and SSR/hydration stays consistent via getServerSnapshot(). If browser storage is unavailable, an in-memory fallback keeps favorites usable for the current tab.Optional Sign-In & Favorites Sync (Supabase): Google sign-in (/login) stays optional for the core experience — browsing, search, favorites (add/list), Study Mode, and the daily picks all work without an account. The favorites-powered review and listen modes and My Page require sign-in. The login page provides a TOP-page return button in both signed-in and signed-out states. While LOGIN_MAINTENANCE in src/lib/auth-maintenance.ts is true, signed-out visitors see a maintenance notice instead of the Google button and existing sessions keep working (see §10 of docs/operations/phase1-login-favorites-sync-setup.md). The TOP header uses one authentication control: it shows ログイン while signed out and automatically becomes マイページ after authentication, with sign-out beside it instead of rendering a separate “logged in” badge and duplicate My Page button. The login link keeps prefetch={false} and uses useLinkStatus for immediate inline feedback; /login/loading.tsx and the page-level <Suspense> boundary reuse a stable login-card skeleton through route loading, hydration, and the initial auth check. Signing in stores favorites in Supabase Postgres (RLS-protected favorites table) so they sync across devices. On first login, locally stored favorites are merged into the account (upsert with ignoreDuplicates), and local storage is cleared only after a successful merge. On sign-out, the account's favorites are copied back to local storage so they stay visible, tagged with the owner's user id so that a different account signing in on the same browser never inherits them. Auth state is resolved client-side (onAuthStateChange) to keep pages statically cacheable; the session cookie exchange happens in /auth/callback.
My Page & Review Flow (Sign-In Required) (/mypage): A personal hub whose only job is to answer “what should I review today, and why now?”. Account information is deliberately minimal (avatar, display name, sign-out); the screen is dominated by the review flow for favorite words. Signed-in users get a Leitner-style schedule (1 / 3 / 7 / 14 / 30-day intervals) stored in Supabase (word_review_progress, learning_streaks, both RLS-protected), so the same due queue and streak follow them across devices. The page shows today's due count with a progress ring, a 🔥 streak / best-streak / weekly-reviewed row, a four-segment retention breakdown (untouched / learning / familiar / mastered), the most-forgotten words, and links into the existing favorites, listen, study, and daily-picks screens. A filtered review snapshots at most 10 slugs when the session starts, removes each graded slug without refilling from the backlog, and ends on a completion screen. Authenticated sessions use queue- and user-scoped sessionStorage keys, and the answer’s “詳しい解説を見る” link carries the original queue so returning from the word detail keeps the same session context. Grading is optimistic, writes a user-scoped retry outbox to local storage before sending, serializes sends per user so rapid grades cannot finish out of order, and removes pending data only after Supabase accepts it; failed writes are merged and retried on the next review mount or grade. Next-due dates are snapped to the start of a later JST day so a nightly learner does not drift by a day. Favorites are read strictly from Supabase for authenticated review flows: /mypage and signed-in review sessions wait for favoritesStatus === "ready"; a signed-in favorites read failure shows a retry state instead of exposing a possibly stale local snapshot. Guests hitting /mypage, /review, or /favorites/listen get a login gate whose sign-in flow always returns to /mypage, with links to everything that stays free; only a signed-in progress-fetch error still degrades /review?queue=due|weak to the full favorites list with a notice. /review session storage keys are therefore always user- and epoch-scoped. All scheduling rules are pure functions in src/lib/review-schedule.ts (unit-tested), with Supabase access isolated in src/lib/review-progress-repo.ts and src/hooks/useReviewProgress.ts. Auth stays client-side (the proxy matcher is unchanged) so the page shell remains statically prerendered.
Single-Grade Review Detail Flow: Review Mode uses the same reveal-then-grade card as Study Mode, so each word is graded exactly once on the card and grading never leaves the session. Opening “詳しい解説を見る” before grading keeps the card ungraded; the detail-page return action “復習に戻る” brings the learner back to that same revealed card. Detail pages opened from Review Mode (from=review, with any queue or none) hide the previous/next word links, so learners cannot preview cards still waiting to be graded in the same session.
Analytics Events: GA4 tracks settled word-explorer queries with the recommended search event (700 ms debounce, 2+ characters, duplicate suppression), semantic searches (semantic_search with query length and result count; the query text is not sent), level/letter/search-mode filters (words_filter), and detail opens (word_detail_open, with source: semantic_search for meaning-search results) in addition to existing favorite/share events. My Page adds mypage_view (due count, favorite count, streak), mypage_login_gate_view (login-gate exposure for guests), and mypage_cta_click (which review/learning entry point was taken). The favorites review/listen gate adds favorite_practice_gate_view and favorite_practice_gate_cta, both carrying feature (review / listen / card).
Social Share: Share word details via Twitter, Facebook, and LINE (react-share), with GA4 event tracking.
PWA Ready: Installable on mobile and desktop via manifest.ts.
Dynamic OGP: Per-word Open Graph images generated on-the-fly via src/app/words/[word]/opengraph-image.tsx.
CDN-Cached OG Images (Active CPU control): Under cacheComponents: true, metadata image routes are never prerendered — even a route with no data access stays dynamic (ƒ), export const revalidate is inert, and generateStaticParams does not help; the build's prerender-manifest.json lists zero OG routes. The word OG route therefore carries no generateStaticParams / dynamicParams = false: keeping them only doubled the build's static-generation set (1,422 → 2,798 pages) with per-word prerender attempts that emit nothing, measurably degrading local builds (static generation 9.1s → 28.2s, whole build 17s → 35s, macOS compressed memory 4.7 GB → 11.4 GB, swapins 14k → 201k). Unknown slugs are rejected inside the Function by the getWordBySlug check, which returns a bare 404 with OG_IMAGE_NOT_FOUND_CACHE_CONTROL instead of rasterizing a "Not Found" image. Since each image costs a Satori layout pass plus a resvg 1200×630 PNG rasterization, leaving this unaddressed burns Vercel Fluid Active CPU in proportion to request volume. Both OG routes therefore send OG_IMAGE_CACHE_CONTROL (src/lib/og-utils.ts), letting the CDN hold the PNG so the Function runs roughly once per edge instead of once per request. s-maxage is 30 days to match the word-detail cache (WORD_CACHE_TTL_DAYS, cacheLife("max")), with stale-while-revalidate covering expiry. loadGoogleFont is wrapped in 'use cache' and fetches one stable full-TTF URL shared by every OGP. It deliberately omits Google Fonts' text= parameter: that parameter produced a different cache key per word while returning the same roughly 7 MB font, duplicating identical data across 1,300+ entries and exhausting build memory.
Outbound Timeouts & Selective Retries: External-call policy is centralized in src/lib/http-retry.ts, and every server/CLI outbound path has a timeout. fetchWithRetry keeps response decoding (json / text / arrayBuffer) inside the retry boundary and retries only statuses a resend can fix (408/425/500/502/503/504); 429 is deliberately excluded because Gemini and Google Cloud TTS are metered. Gemini SDK retries are disabled because SDK 1.50.x includes 429 and erases the final HTTP status; calls instead use retryWithTimeout, preserving ApiError.status and a fresh signal per attempt. Redis keeps one SDK network retry plus a short command timeout but does not replay ambiguous HTTP failures such as INCR. Upstash Vector uses a custom public requester so idempotent query/upsert/delete calls can retry selected HTTP 5xx while retaining their status. Vercel Blob list() is bounded separately with an AbortController. Route Handlers structurally classify SDK status / statusCode errors (429 → 429, 5xx → 502, timeout/abort → 504).
Lean Proxy Matching: src/proxy.ts refreshes the Supabase Auth session, with the matcher minimized to /login and /auth/* to keep Vercel Middleware/Proxy execution usage low (auth state elsewhere is handled client-side).
Static Pages: Includes About, Privacy, Terms, and Contact pages for better user trust and SEO. The Privacy Policy explicitly discloses Google AdSense / third-party ad vendors' Cookie usage and Google Analytics tracking, with opt-out links, in line with AdSense program policy requirements.
Learning Guide Articles (/guide): 16 article records, of which 15 are currently published. The May 2026 public-test retrospective is retained for source review but excluded from the guide index, static params, sitemap, related links, and search indexing. Published guides use visible Rain bylines, optional linked references, and Article + BreadcrumbList JSON-LD; unsupported score guarantees, fixed forgetting percentages, and unverifiable frequency claims have been removed or qualified. The index lists articles newest-first.
Cookie Consent: First-visit consent banner aligned with GA4 tracking. Consent is stored in a cookie and read server-side via CookieConsentGate; GoogleAnalyticsGate loads GA4 only after explicit acceptance. Both gates are wrapped in <Suspense> so the rest of the root layout remains cacheable.
AdSense Preparation: Ownership meta tag and ads.txt are retained; Google ad scripts are disabled. Affiliate banners are independently opt-in via NEXT_PUBLIC_AFFILIATE_ADS_ENABLED=true. Search indexing does not depend on advertising. Draft guides return not-found. Fifteen guides include revised explanations and nine original exercises. Editorial word corrections take precedence over Redis and generation; review hashes distinguish AI checks from human approval. See reapplication plan and review procedure.
Responsive Design: Optimized for desktop and mobile devices. Below 640px, word-detail audio/favorite buttons and home/favorites pagination have at least 44×44px tap targets; icon sizes and desktop sizing stay compact. (Updated: 2026-09-23)
Stronger Page Backgrounds: The TOP, word index, sign-in, favorites, daily picks, listening, study, and review screens share a more visible blue radial background, while word-detail pages retain their distinct teal hue with increased saturation.
reactCompiler: true, cacheComponents: true)babel-plugin-react-compiler) for automatic memoization@google/genai / GoogleGenAI) - models: gemini-2.5-flash-lite (word details), gemini-embedding-001 (semantic search embeddings)@upstash/redis)@upstash/vector) - 768-dim cosine index for semantic word search@supabase/supabase-js + @supabase/ssr) — Google sign-in and RLS-protected favorites sync@vercel/blob) - for word lists@next/third-parties), Vercel Analytics & Speed Insightspackage.json requires >=24; CI uses Node.js 24)Clone the repository
git clone https://github.com/Rainnnnnnnn-maker/toeic_website_ver2.0.git
cd toeic_website_ver2.0
Install dependencies
npm ci
Environment Variables
Copy .env.example to .env.local and configure the following variables:
cp .env.example .env.local
| Variable | Description |
|---|---|
GEMINI_API_KEY | Google Gemini API Key for generating word details. |
TTS_API_KEY | Google Cloud Text-to-Speech API Key for audio. |
UPSTASH_REDIS_REST_URL | Upstash Redis REST URL. |
UPSTASH_REDIS_REST_TOKEN | Upstash Redis REST Token. |
UPSTASH_VECTOR_REST_URL | Upstash Vector REST URL (semantic search). The index must be created with 768 dimensions / cosine metric. |
UPSTASH_VECTOR_REST_TOKEN | Upstash Vector REST Token (semantic search). |
SEMANTIC_SEARCH_MIN_SCORE | (Optional) Minimum cosine-similarity score returned by semantic search (default: 0.82, range: 0–1). |
BLOB_READ_WRITE_TOKEN | Vercel Blob Token (for fetching word lists in production). |
BLOB_URL_IMPORTANT | (Optional) Direct URL for word.txt in Blob. Skips the Blob list call when set. |
BLOB_URL_MEDIUM | (Optional) Direct URL for word_mid.txt in Blob. |
BLOB_URL_HIGH | (Optional) Direct URL for word_high.txt in Blob. |
The project-level .mcp.json configures Claude Code integrations for Next.js DevTools, Playwright, GitHub, and Upstash. GitHub and Upstash require the following environment variables in the shell that launches Claude Code; values in .env.local are not exported to the parent shell automatically.
| Variable | Description |
|---|---|
GITHUB_MCP_PAT | GitHub personal access token. Grant only the repository permissions required for the intended MCP operations. |
UPSTASH_EMAIL | Email address associated with the Upstash account. |
UPSTASH_API_KEY | Upstash account API key. Prefer a read-only key unless resource changes are explicitly required. |
For example, export the values before starting Claude Code:
export GITHUB_MCP_PAT=<your-github-pat>
export UPSTASH_EMAIL=<your-upstash-email>
export UPSTASH_API_KEY=<your-upstash-api-key>
claude
Claude Code asks for approval before starting project-scoped MCP servers for the first time. Never commit credential values to .mcp.json or another tracked file.
npm run dev: Starts the development server.npm run build: Builds the application for production.npm run start: Starts the production server.npm run lint: Runs ESLint.npm run test: Runs Vitest unit tests. These cover pure logic only (word selection/parsing, Gemini response normalization, TTS cache keys & example allowlist, listen-mode example fallback, favorites sync/auth-session selection, audit-report validation, study-mode word selection & persisted-state parsing, semantic-search query normalization / index-versioned cache keys / score filtering / embedding-document building / result parsing, stale-vector reconciliation guards, and outbound retry policy / timeout detection / upstream-failure status mapping) and run in CI alongside npm run lint. Cypress E2E tests cover guest favorites persistence and today-word navigation; other integration and UI changes still require manual smoke testing.npm run test:e2e:ci: Builds an isolated production app with local Blob/Redis HTTP fixtures, starts it, runs Cypress, and cleans up. No app secrets or local word files are needed. GitHub Actions runs this on Chrome.npm run test:e2e: Runs the Cypress browser tests against a running local server. npm run test:e2e:open opens the interactive runner; npm run typecheck:e2e checks the isolated Cypress TypeScript project. See E2E setup for prerequisites and scope.npm run embed:words: Embeds all WordDetails into Upstash Vector for semantic search (tsx --env-file=.env.local scripts/embed-words.ts). The word list is loaded through the same shared loader as the site (src/lib/word-source.ts), so the indexed corpus matches what is actually served — it defaults to Vercel Blob and only reads __words__/*.txt when NODE_ENV=development. Reads WordDetails from Redis first and generates missing ones with Gemini. Supports -- --dry-run, -- --limit=N, -- --only-cached, and -- --source=local|blob (explicit corpus override). After a complete successful Blob backfill, it ranges the Vector index and deletes word vectors whose IDs are no longer in the served corpus; pruning is skipped for limited, cached-only, local, or failed runs. It then advances the semantic-index generation, invalidating all prior result-cache keys without a Redis key scan. This is a standalone Node/tsx CLI, so it must not import Next.js's marker. Run once for the initial backfill and re-run after bulk word-list changes.*.sql skip the CI workflow. SQL-only pushes to non-main branches also skip the Vercel Preview workflow. A push containing any non-SQL source change still runs the workflows normally.git.deploymentEnabled: true). Pushes to the Vercel Production Branch automatically deploy to Production. The existing Preview push workflow and manual Production workflow also remain available, so Git integration and Actions can both deploy.nodejs-e2e CI job runs the two Cypress flows on an isolated production build without service secrets. Failures upload build/server logs, screenshots, and videos for 7 days. Other CI jobs skip the Cypress binary download.paths-ignore rules do not disable Vercel Git deployments.Triggers on-demand revalidation for cached word lists (tag: word-list). Useful when updating word lists in Vercel Blob.
Endpoint: GET /api/revalidate/words
Authentication: Requires token query parameter matching REVALIDATION_TOKEN.
Usage:
curl "https://<your-site>/api/revalidate/words?token=<REVALIDATION_TOKEN>"
Clears L1 (Next.js Data Cache) for a specific word. To also clear L2 (Upstash Redis) cache, add the upstash=true parameter. This forces a fresh AI generation on the next request if both caches are cleared.
Endpoint: GET /api/revalidate/word
Authentication: Requires token query parameter matching REVALIDATION_TOKEN.
Parameters:
slug (The word to revalidate)upstash (Optional, set to true to also clear Upstash Redis cache)vector (Optional, set to true to re-embed the word and upsert it into Upstash Vector after the caches are cleared, keeping semantic search in sync with the regenerated content. A successful upsert also advances the semantic-index generation so cached search results are invalidated immediately. Combine with upstash=true to force a full Gemini regeneration first.)Usage:
curl "https://<your-site>/api/revalidate/word?token=<REVALIDATION_TOKEN>&slug=additional&upstash=true&vector=true"
Deletes a specific Upstash Redis key for word details (word:<slug>). This endpoint does not touch the Next.js Data Cache (L1).
Endpoint: GET /api/revalidate/upstash-word
Authentication: Requires token query parameter matching REVALIDATION_TOKEN.
Parameters:
key (Must start with word:. Example: word:ability)Usage:
curl "https://<your-site>/api/revalidate/upstash-word?token=<REVALIDATION_TOKEN>&key=word:ability"
Generates audio for words or sentences.
POST /api/ttsContent-Type: application/jsonX-App-Source: toeic-client{ "text": "word or sentence", "language": "en" | "ja", "wordSlug": "<slug>" }
wordSlug is required for multi-word text or any Japanese text.en-US-Wavenet-C (English), ja-JP-Neural2-B (Japanese).TTS_API_KEY to Google using the x-goog-api-key request header; it is never included in the request URL or returned to the browser.WordDetails.toeicExamples or meanings[].detailedMeanings[].example entries for the given wordSlug. This blocks Referer-spoofing attacks from synthesizing arbitrary text against the Google Cloud TTS quota.Finds vocabulary words by meaning (Japanese or English queries) via embedding similarity.
POST /api/search/semanticContent-Type: application/jsonX-App-Source: toeic-client{ "query": "延期する" } (max 100 chars after normalization){ "results": [{ "slug", "term", "level", "japaneseTranslation", "score" }], "cached": boolean } — up to 10 results in cosine-similarity order after applying the minimum-score filter.SEMANTIC_SEARCH_MIN_SCORE are omitted (default 0.82). The UI deliberately does not label raw vector scores as percentages.semsearch:v3:<indexVersion>:<sha256_16> of the NFKC-normalized query, 7-day TTL), and the current minimum-score filter is reapplied on every read so threshold changes take effect without a cache purge. npm run embed:words and successful /api/revalidate/word?...&vector=true updates increment semsearch:index-version, making earlier cache keys unreachable immediately. Cached queries bypass the rate limiter; an empty candidate set is not cached. If the index version cannot be read, the API bypasses the result cache.semantic_search GA4 event sends only query length and result count—not the query text.rl:semsearch).UPSTASH_VECTOR_REST_URL / UPSTASH_VECTOR_REST_TOKEN are not configured.Returns sitemap.xml containing all static pages and all word detail URLs.
GET /api/sitemap/sitemap.xml → /api/sitemap (configured in next.config.ts).noindex pages (/favorites, /review, /login, */listen) are excluded from the sitemap — listing a URL in the sitemap while its page says noindex sends Google contradictory signals.lastmod policy: Static pages and guide articles emit lastmod. URLs whose real modification date is not tracked (all /words/* detail pages, and the daily-rotating /today-words) omit lastmod — emitting one fixed, identical date trains Google to distrust the lastmod signal, so it is left out rather than faked. (changefreq/priority are kept but are ignored by Google.)robots.txt: robots.txt is generated by src/app/robots.ts (App Router metadata route), which points at /sitemap.xml. The former static public/robots.txt was removed to avoid a stale, conflicting second source.src/app: Next.js App Router pages (e.g., /, /study, /favorites, /favorites/listen, /review, /mypage, /today-words, /today-words/listen, /words, /words/[word], /login, /auth/callback, and static /about, /privacy, /terms, /contact, /donate) and API routes.src/proxy.ts: Supabase Auth session refresh, with the matcher minimized to /login and /auth/* to keep Vercel Middleware usage low.src/actions: Server Actions (e.g., fetchWordDetail wrapping the cached getWordDetail).src/components: React components organized by feature (features/words, features/today-words, auth, favorites, mypage, review, study, sns) and common UI (common/ — Footer, TabNavigation, CookieConsent / CookieConsentGate).src/lib: Utility functions and API clients (Upstash Redis, Upstash Vector, Gemini embeddings, Redis-backed word cache, Supabase browser/server clients, favorites-sync / semantic-search / review-schedule pure logic, Supabase review-progress access, JSON-LD, OG utils).src/types: TypeScript type definitions (e.g., WordDetails).src/data: Data fetching logic — words.ts (Local vs Blob switching, getAllWords / getTodayRecommendedWords / getRelatedWords) and word-detail.ts (multi-layer cached getWordDetail, Gemini generation).src/hooks: Custom React hooks (e.g., useTTS, useReviewProgress).src/context: React Contexts (AuthContext, FavoritesContext, ShareTargetContext).docs: Project documentation, with the current architecture in docs/architecture.md, operational runbooks in docs/operations/, specifications in docs/specs/, and completed plans in .REVIEW_SESSION_LIMIT(現在10語、対象が少なければ対象数)とし、到達後は達成メッセージと「もう N 語復習する」を表示する。期限到来の残件数は別に表示する。WEAK_WORDS_LIMIT(現在7語)まで表示する。× は「覚えていない」の累計回数と明記し、開始時の上限を適用した対象語数をボタンに表示する(途中再開時は保存済みセッションが優先)。getRetentionLevel を集計と共用し、詳細へのリンクは from=mypage を付ける。ヒーローには学習モードとお気に入りの2つのボタンだけを置き、すぐ下に今日のおすすめ単語を表示します。収録語数はヒーローの小さなバッジで示します。
表示順は、ヒーロー → 今日のおすすめ単語 → 意味で探す(AI) → レベル別単語一覧(つづり検索)→ 全単語一覧 → はじめての方へ → 学習ガイド最新記事 → このサイトについて(できること・おすすめルーティン・運営者メッセージ)→ FAQ → 解説記事 → SNSシェアです。2つの検索は見出しと入力欄の文言で「意味で探す」「つづりで探す」と役割を分けています。初回学習の案内のボタンは、モバイルで高さ44px以上のタップ領域を確保しています。
REVALIDATION_TOKEN | Secret token for validating ISR revalidation requests. |
WORD_CACHE_TTL_DAYS | (Optional) Cache duration in days (default: 30). |
NEXT_PUBLIC_GA_ID | Google Analytics 4 Measurement ID (G-XXXXXXXXXX). |
NEXT_PUBLIC_AFFILIATE_ADS_ENABLED | Optional: true enables A8 banners; default off. Independent of indexing; changes require redeployment. |
NEXT_PUBLIC_SUPABASE_URL | Supabase project URL (optional sign-in / favorites sync). |
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY | Supabase publishable key (safe for client exposure; data access is protected by RLS). |
Data Setup (Local Development)
For local development (NODE_ENV=development), the app reads word lists from the __words__ directory by default.
Ensure the following files exist in the project root's __words__ folder (one word per line):
__words__/word.txt (Important words: TOEIC 600 level)__words__/word_mid.txt (Medium words: 730–800 level)__words__/word_high.txt (High words: 800+ level)Note: In production, the app automatically switches to fetching these files from Vercel Blob (
words-file/word.txt,words-file/word_mid.txt,words-file/word_high.txt). SettingBLOB_URL_*env vars bypasses the Bloblistcall for faster fetches.
Run the development server
npm run dev
Open http://localhost:3000 with your browser.
server-onlynpm run audit:ci: Dependency-vulnerability gate used by CI. npm audit cannot exclude individual advisories, so scripts/check-audit.mjs skips only explicitly accepted ones (each with a reason and a review deadline) and fails on any other high/critical finding. Severity is checked on each root advisory: a moderate finding sharing a parent dependency with an accepted high finding does not block CI. Audit API errors, unsupported/malformed reports, unresolved high/critical advisory paths, and unaccepted root advisories with unknown severity also fail closed. Production dependencies are gated separately and strictly via npm audit --audit-level=high --omit=dev.docs/archive/.agents/skills: Canonical repository-specific agent skills. .claude/skills and ~/.codex/skills point here through symlinks.supabase/migrations: Version-controlled SQL applied through the Supabase SQL Editor (currently word_review_progress / learning_streaks for the review flow). Manual setup steps live in docs/operations/mypage-review-flow-setup.md.__words__: Local storage for word lists during development.