diff --git a/.claude/worktrees/seo-fix b/.claude/worktrees/seo-fix deleted file mode 160000 index 9a58b40..0000000 --- a/.claude/worktrees/seo-fix +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 9a58b4094acc2df23f9b8aa31fc2674d7067fedf diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..2444716 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,46 @@ +name: Bug report +description: Something in a visualization or page does not work as expected +labels: [bug] +body: + - type: input + id: topic + attributes: + label: Topic + description: Which topic, and which step if it is step-specific. + placeholder: Event Loop, step 4 + validations: + required: true + + - type: textarea + id: expected + attributes: + label: What you expected + validations: + required: true + + - type: textarea + id: actual + attributes: + label: What happened instead + description: A screen recording is worth more than a paragraph here. + validations: + required: true + + - type: textarea + id: steps + attributes: + label: Steps to reproduce + placeholder: | + 1. Open /javascript/event-loop + 2. Select the async/await example + 3. Press play + validations: + required: true + + - type: input + id: browser + attributes: + label: Browser and OS + placeholder: Firefox 141, macOS 15 + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..4323ce1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: true +contact_links: + - name: Question or idea + url: https://github.com/tornike14/visualizejs/discussions + about: Ask about the project or float an idea before opening an issue. + - name: Security vulnerability + url: https://github.com/tornike14/visualizejs/security/advisories/new + about: Report privately. Please do not open a public issue. diff --git a/.github/ISSUE_TEMPLATE/content_correction.yml b/.github/ISSUE_TEMPLATE/content_correction.yml new file mode 100644 index 0000000..d716994 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/content_correction.yml @@ -0,0 +1,33 @@ +name: Content correction +description: A theory page or step description states something inaccurate +labels: [content, correction] +body: + - type: markdown + attributes: + value: | + Technical accuracy matters more here than any feature. Thanks for taking + the time to report this. + + - type: input + id: location + attributes: + label: Where + description: Page URL, and the section or step number. + placeholder: /react/hooks/theory, "Common Mistakes" section + validations: + required: true + + - type: textarea + id: claim + attributes: + label: What it currently says + validations: + required: true + + - type: textarea + id: correction + attributes: + label: What it should say, and why + description: A link to the spec, MDN, or the React docs settles it fastest. + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/topic_request.yml b/.github/ISSUE_TEMPLATE/topic_request.yml new file mode 100644 index 0000000..fd4e306 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/topic_request.yml @@ -0,0 +1,47 @@ +name: Topic request +description: Propose a new JavaScript or React concept to visualize +labels: [topic request] +body: + - type: input + id: concept + attributes: + label: Concept + placeholder: Web Workers + validations: + required: true + + - type: dropdown + id: category + attributes: + label: Category + options: + - JavaScript + - React + validations: + required: true + + - type: textarea + id: visual + attributes: + label: What would the visualization show + description: > + The concepts that work well here have moving parts worth watching: + state that changes over time, a structure being traversed, or an + ordering that surprises people. Describe the panels and what changes + between steps. + validations: + required: true + + - type: textarea + id: misconception + attributes: + label: What do people usually get wrong about it + validations: + required: true + + - type: checkboxes + id: contributing + attributes: + label: Would you like to build it? + options: + - label: I would like to work on this myself diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..81258a5 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,34 @@ +## What changed + + + +## Type + +- [ ] Bug fix +- [ ] Content correction +- [ ] New topic +- [ ] Improvement to an existing visualization +- [ ] Docs or tooling + +## Verification + +- [ ] `npm run lint` passes +- [ ] `npm run build` passes +- [ ] `npm run check:registries` passes + +## For a new topic + +Confirm each registry was updated, or delete this section. + +- [ ] `src/lib/topics.ts` +- [ ] Route at `src/app///page.tsx` +- [ ] Visualization at `src/components/visualizations//` +- [ ] Theory content at `src/content/theory//.ts` +- [ ] `THEORY_CONTENT_BY_TOPIC_ID` in `src/content/theory/index.ts` +- [ ] `TOPIC_KEYWORDS` and `TOPIC_THEORY_DESCRIPTIONS` in `src/lib/metadata.ts` +- [ ] `SELECTOR_TOOLBAR_TOPIC_IDS` if the topic uses `ExampleSelector` +- [ ] Added to the `relatedTopicIds` of 2 or more existing theory files + +## Screenshots + + diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..b9114ee --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,47 @@ +name: CI + +on: + push: + branches: [main, develop] + pull_request: + branches: [main, develop] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + verify: + name: Lint and build + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Lint + run: npm run lint + + - name: Build + run: npm run build + + registry-check: + name: Registry integrity + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Verify every topic is fully registered + run: node scripts/check-registries.mjs diff --git a/.gitignore b/.gitignore index 5ef6a52..11eae74 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,10 @@ .DS_Store *.pem +# claude code local state +.claude/settings.local.json +.claude/worktrees/ + # debug npm-debug.log* yarn-debug.log* diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..d769a60 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,33 @@ +# Code of Conduct + +## The Short Version + +Be respectful. This project exists to help people learn, and learners ask basic questions. Treat every question as a reasonable one. + +## Expected Behavior + +- Give feedback on the code and the content, not the person who wrote it. +- Assume the other person is acting in good faith. +- Accept that maintainers may decline a contribution, and explain your reasoning when you decline someone else's. +- Correct technical inaccuracies directly and without condescension. Getting the internals right is the point of this project, so corrections are welcome and expected. + +## Unacceptable Behavior + +- Harassment, insults, or personal attacks. +- Discriminatory language or imagery of any kind. +- Publishing other people's private information without permission. +- Sustained disruption of discussions or reviews. + +## Scope + +This applies in issues, pull requests, discussions, and any other space where someone is representing the project. + +## Reporting + +Report a problem through GitHub's [private vulnerability reporting](https://github.com/tornike14/visualizejs/security/advisories/new) or by contacting the maintainer directly through their GitHub profile. Reports stay confidential. + +Maintainers will review and respond. Outcomes range from a warning to a permanent ban from the project's spaces, depending on severity. + +## Attribution + +Adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 2.1. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..ad77255 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,124 @@ +# Contributing to VisualizeJS + +Thanks for wanting to help. This project explains JavaScript and React internals through animations, so contributions range from fixing a typo in a theory page to building a whole new topic. + +## Ways to Contribute + +- **Report a bug.** Open an issue with the topic, the step number, and what you expected. +- **Correct the content.** If a theory page states something inaccurate, that matters more than any feature. Cite a spec or the React docs where you can. +- **Improve an existing visualization.** Clearer steps, better labels, missing edge cases. +- **Add a topic.** The largest kind of contribution. Read the section below before starting. + +For anything beyond a small fix, open an issue first so we can agree on the approach before you spend time on it. + +## Setup + +```bash +npm install +npm run dev +``` + +Node 20 or newer. Before opening a pull request: + +```bash +npm run lint +npm run build +``` + +Both must pass. CI runs the same two commands. + +## Project Conventions + +These are enforced in review. Most exist because inconsistency between 28 topics is more expensive than it looks. + +**Content** + +- No em dashes anywhere in user-facing text. +- No emojis in step descriptions or UI text. +- Write plainly. Avoid marketing language and filler such as "powerful", "seamless", or "it's worth noting". +- Explain the mechanism. "React marks the fiber for work" beats "React handles it efficiently". + +**Code** + +- React components use const form (`const Component = () =>`), not function declarations. +- Visualization components need the `"use client"` directive and named exports. +- Component files target 200 lines, hard limit 300. Split into `components/` when you exceed it. Data files (`data.ts`) are exempt since they hold content. +- Import panel titles and empty states from `src/lib/visualization/uiCopy.ts`. Never hardcode them. +- Wrap `setActiveExampleId` in a `handleExampleChange` callback rather than passing the setter to `onSelect` directly. +- Code line fading uses the guard `isDone && !isActive`. Never fade an active line. + +**Structure** + +Topics use a folder under `src/components/visualizations//`: + +``` +index.tsx Main component, wires playback and layout +types.ts Topic-specific types +data.ts Examples and step definitions +helpers.ts Pure functions used by the panels +components/ Panel components +``` + +Reconciliation is the reference implementation for React topics. Event Loop is the reference for sandbox mode. + +## Adding a Topic + +A topic is not finished when the visualization renders. It is registered in several places, and missing one produces a page that half works. Work through all of these. + +**1. Topic registry** in [`src/lib/topics.ts`](src/lib/topics.ts) + +Add an entry with `id`, `title`, `category`, `route`, `description`, `difficulty`, and `docsUrl`. The `id` is the slug used everywhere else, so pick it carefully. + +**2. Route** at `src/app///page.tsx` + +Copy an existing page. It resolves the topic with `getTopicOrThrow`, lazy-loads the visualization with `next/dynamic`, wraps it in `ErrorBoundary` and `VisualizationPageShell`, and exports `createTopicMetadata(topic)`. + +**3. Visualization** at `src/components/visualizations//` + +Use `useStepPlayback` from `src/hooks/useStepPlayback.ts` for transport controls. Read [`docs/topic-authoring.md`](docs/topic-authoring.md), or [`docs/react-topic-authoring.md`](docs/react-topic-authoring.md) for React topics. + +**4. Theory content** at `src/content/theory//.ts` + +Implements `TopicTheoryContent`: `summary`, `whatItIs`, `howItWorks`, `commonMistakes`, `interviewQuestions`, and `relatedTopicIds`. See [`docs/theory-authoring.md`](docs/theory-authoring.md). + +`relatedTopicIds` must hold 3 to 5 valid topic IDs and must not include the topic's own ID. + +**5. Theory registry** in [`src/content/theory/index.ts`](src/content/theory/index.ts) + +Add the import and the entry in `THEORY_CONTENT_BY_TOPIC_ID`. `VisualizationPageShell` reads this map to render the theory sections beneath the visualization, so a topic missing here renders without any theory. + +**6. SEO metadata** in [`src/lib/metadata.ts`](src/lib/metadata.ts) + +Add entries to both `TOPIC_KEYWORDS` (4 to 8 search phrases) and `TOPIC_THEORY_DESCRIPTIONS` (one or two sentences, under 160 characters). + +**7. Toolbar registry** in [`src/components/layout/VisualizationPageShell.tsx`](src/components/layout/VisualizationPageShell.tsx) + +If your topic uses `ExampleSelector`, add its ID to `SELECTOR_TOOLBAR_TOPIC_IDS`. Skipping this makes the loading skeleton the wrong shape. + +**8. Inbound links** + +Add your topic ID to the `relatedTopicIds` of 2 or more existing theory files. A topic nothing links to is a dead end for readers and for search engines. Keep each list within the 3 to 5 range when you do this, swapping out a weaker link if needed. + +### Before You Open the PR + +```bash +npm run lint +npm run build +``` + +Then check that `/your-route` renders with the theory sections below the visualization, the sitemap at `/sitemap.xml` includes the route, and the related topic links at the bottom of the page resolve. + +## Pull Requests + +- Branch from `develop`, not `main`. +- One topic or one fix per pull request. +- Describe what changed and why. Screenshots or a screen recording help a lot for visualization changes. +- Say which registries you touched when adding a topic. + +## Reporting Bugs + +Include the topic, the browser, the step where it goes wrong, and what you expected instead. A screen recording is worth more than a paragraph of description. + +## Questions + +Open a [discussion](https://github.com/tornike14/visualizejs/discussions) or an issue. Asking before building saves everyone time. diff --git a/README.md b/README.md index 4d880f9..cc9bcb8 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,12 @@ # VisualizeJS -Interactive visualizations for JavaScript and React concepts - step through animations that show exactly what the engine does at each stage. +[![Live site](https://img.shields.io/badge/live-visualizejs.com-3fb950?style=flat-square&labelColor=24292f)](https://visualizejs.com) [![CI](https://img.shields.io/github/actions/workflow/status/tornike14/visualizejs/ci.yml?branch=main&style=flat-square&label=CI&labelColor=24292f)](https://github.com/tornike14/visualizejs/actions/workflows/ci.yml) [![Topics](https://img.shields.io/badge/topics-28-a371f7?style=flat-square&labelColor=24292f)](#topics) [![Next.js](https://img.shields.io/badge/Next.js-16-000000?style=flat-square&logo=nextdotjs&logoColor=white&labelColor=24292f)](https://nextjs.org) [![React](https://img.shields.io/badge/React-19-61DAFB?style=flat-square&logo=react&logoColor=61DAFB&labelColor=24292f)](https://react.dev) [![TypeScript](https://img.shields.io/badge/TypeScript-strict-3178C6?style=flat-square&logo=typescript&logoColor=white&labelColor=24292f)](https://www.typescriptlang.org) [![License](https://img.shields.io/github/license/tornike14/visualizejs?style=flat-square&color=3fb950&labelColor=24292f)](LICENSE) + +Interactive visualizations for JavaScript and React concepts. Step through animations that show what the engine does at each stage, then read the theory behind it. + +28 topics. Each one pairs a step-by-step visualization with theory sections covering how it works, common mistakes, and interview questions, all on a single page. + +[![VisualizeJS homepage](docs/images/homepage.jpg)](https://visualizejs.com) ## Getting Started @@ -11,41 +17,117 @@ npm run dev Open [http://localhost:3000](http://localhost:3000). The app redirects to `/javascript` by default. +Node 20 or newer is required. + ## Scripts | Command | Description | |---|---| | `npm run dev` | Start dev server (webpack mode) | | `npm run build` | Production build | -| `npm run start` | Start production server | +| `npm run start` | Serve the production build | | `npm run lint` | Run ESLint | -## Routes +## Topics -- `/javascript` - JavaScript topic landing -- `/javascript/event-loop` - Event Loop visualization -- `/javascript/hoisting` - Hoisting visualization -- `/javascript/closures` - Closures visualization -- `/javascript/promises` - Promises visualization -- `/javascript/prototypal-inheritance` - Prototypal Inheritance visualization -- `/javascript/this-keyword` - this Keyword visualization -- `/javascript/scope-chain` - Scope Chain visualization -- `/react` - React (coming soon) +Each topic lives at a single route, with the visualization at the top and the theory sections below it. + +### JavaScript (17) + +| Topic | Difficulty | +|---|---| +| [Event Loop](/javascript/event-loop) | Intermediate | +| [Hoisting](/javascript/hoisting) | Beginner | +| [Execution Context](/javascript/execution-context) | Intermediate | +| [Closures](/javascript/closures) | Intermediate | +| [Promises](/javascript/promises) | Intermediate | +| [this Keyword](/javascript/this-keyword) | Intermediate | +| [Scope Chain](/javascript/scope-chain) | Intermediate | +| [Type Coercion](/javascript/type-coercion) | Beginner | +| [Destructuring](/javascript/destructuring) | Beginner | +| [Spread & Rest](/javascript/spread-rest) | Beginner | +| [Prototypal Inheritance](/javascript/prototypal-inheritance) | Advanced | +| [Reference vs Value](/javascript/reference-value) | Beginner | +| [Heap & Stack](/javascript/heap-stack) | Advanced | +| [Garbage Collection](/javascript/garbage-collection) | Advanced | +| [Generators & Iterators](/javascript/generators) | Advanced | +| [Event Delegation](/javascript/event-delegation) | Beginner | +| [Modules & Imports](/javascript/modules-imports) | Intermediate | -## Project Docs +### React (11) -- [`docs/topic-authoring.md`](docs/topic-authoring.md) - JS topic creation workflow -- [`docs/react-topic-authoring.md`](docs/react-topic-authoring.md) - React topic extensions -- [`docs/component-reference.md`](docs/component-reference.md) - design system, reusable components, hooks, animations -- [`docs/theory-authoring.md`](docs/theory-authoring.md) - theory page authoring -- [`docs/sandbox-authoring.md`](docs/sandbox-authoring.md) - sandbox mode guide -- [`docs/architecture.md`](docs/architecture.md) - frontend architecture rules -- [`docs/seo.md`](docs/seo.md) - SEO implementation and deployment checklist +| Topic | Difficulty | +|---|---| +| [Virtual DOM](/react/virtual-dom) | Beginner | +| [Reconciliation](/react/reconciliation) | Intermediate | +| [Context Propagation](/react/context-propagation) | Intermediate | +| [Fiber Tree](/react/fiber-tree) | Advanced | +| [Hooks](/react/hooks) | Intermediate | +| [Render Cycle](/react/render-cycle) | Advanced | +| [Memoization](/react/memoization) | Intermediate | +| [Suspense](/react/suspense) | Intermediate | +| [Server Components](/react/server-components) | Advanced | +| [Error Boundaries](/react/error-boundaries) | Intermediate | +| [useEffect Lifecycle](/react/use-effect-lifecycle) | Beginner | + +Some topics also have a sandbox mode where you can edit the code and watch the visualization respond. Event Loop is the reference implementation. + +## Project Structure + +``` +src/ + app/ Next.js App Router routes + components/ + layout/ Page shell, navigation + visualization-ui/ Shared primitives (NeonPanel, CodeBlock, TransportControls) + visualizations/ One folder per topic + content/theory/ Theory content, one file per topic + hooks/ useStepPlayback, useChangeFlash + lib/ + topics.ts Topic registry, the single source of truth + metadata.ts SEO metadata factory + sandbox/ Sandbox mode infrastructure +``` + +Adding a topic touches several registries. [`CONTRIBUTING.md`](CONTRIBUTING.md) lists all of them. + +## Environment Variables + +All are optional and have working defaults. Copy [`.env.example`](.env.example) to `.env.local` to override. + +| Variable | Default | Purpose | +|---|---|---| +| `NEXT_PUBLIC_SITE_URL` | `https://visualizejs.com` | Canonical URLs, sitemap, Open Graph tags | +| `NEXT_PUBLIC_CREATOR_LINKEDIN_URL` | Maintainer's profile | Credit link in the footer | +| `NEXT_PUBLIC_CREATOR_AVATAR_SRC` | `/personal-image.png` | Avatar in the footer | + +If you fork this, set the last two to your own. + +## Contributing + +Read [`CONTRIBUTING.md`](CONTRIBUTING.md) first. New topics are welcome, and the authoring docs below walk through the process. + +## Documentation + +| Doc | Covers | +|---|---| +| [`docs/topic-authoring.md`](docs/topic-authoring.md) | JavaScript topic creation workflow | +| [`docs/react-topic-authoring.md`](docs/react-topic-authoring.md) | React topic extensions | +| [`docs/component-reference.md`](docs/component-reference.md) | Design system, components, hooks, animations | +| [`docs/theory-authoring.md`](docs/theory-authoring.md) | Theory content authoring | +| [`docs/sandbox-authoring.md`](docs/sandbox-authoring.md) | Sandbox mode | +| [`docs/architecture.md`](docs/architecture.md) | Frontend architecture rules | +| [`docs/seo.md`](docs/seo.md) | SEO implementation | ## Tech Stack -- [Next.js](https://nextjs.org) App Router +- [Next.js 16](https://nextjs.org) App Router - React 19 - TypeScript (strict) - Tailwind CSS v4 -- shadcn/ui primitives +- [shadcn/ui](https://ui.shadcn.com) primitives +- CodeMirror 6 and Acorn for sandbox mode + +## License + +[MIT](LICENSE) diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..2452519 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,23 @@ +# Security Policy + +## Supported Versions + +This project is a statically rendered site with no backend, no database, and no user accounts. Security fixes are applied to the `main` branch only. + +## Reporting a Vulnerability + +Please do not open a public issue for a security problem. + +Report it through GitHub's private vulnerability reporting on the [Security tab](https://github.com/tornike14/visualizejs/security/advisories/new). That channel is private between you and the maintainer. + +Include what you found, the steps to reproduce it, and the impact you think it has. You can expect an initial response within 7 days. + +## Scope + +The site renders trusted content that ships in the repository and runs entirely in the browser. The areas most worth attention: + +- **Sandbox mode.** Several topics parse and interpret user-supplied JavaScript with Acorn to drive the visualization. Code is analyzed rather than executed, so a report showing user input escaping that boundary and reaching real execution is in scope. +- **Dependency vulnerabilities** with a demonstrated path to exploitation in this app. +- **Cross-site scripting** through any rendered content. + +Out of scope: findings that only apply to a self-hosted fork's own deployment configuration, and automated scanner output with no working proof of concept. diff --git a/docs/images/homepage.jpg b/docs/images/homepage.jpg new file mode 100644 index 0000000..f8ae402 Binary files /dev/null and b/docs/images/homepage.jpg differ diff --git a/docs/react-topic-authoring.md b/docs/react-topic-authoring.md index 073b8c6..8b12a09 100644 --- a/docs/react-topic-authoring.md +++ b/docs/react-topic-authoring.md @@ -217,7 +217,8 @@ When adding a new React topic, follow these additional steps beyond the base che 2. Create route page at `src/app/react//page.tsx` 3. Create theory file at `src/content/theory/react/.ts` (if applicable) 4. Add to `SELECTOR_TOOLBAR_TOPIC_IDS` in `VisualizationPageShell.tsx` (if using ExampleSelector) -5. Sitemap and theory routes update automatically (category-agnostic) +5. Sitemap updates automatically (category-agnostic), and the theory sections + render on the topic page once the content is registered --- diff --git a/docs/seo.md b/docs/seo.md index f2950be..70d6fb2 100644 --- a/docs/seo.md +++ b/docs/seo.md @@ -70,8 +70,8 @@ This document describes the SEO work currently implemented in VisualizeJS. - declares sitemap URL - Sitemap route: `src/app/sitemap.ts` - Generates `/sitemap.xml` - - includes static routes (`/`, `/javascript`, `/react`), all topic routes, and theory routes - - Theory routes are generated via `getTheoryTopicsByCategory()` for both JS and React + - includes static routes (`/`, `/javascript`, `/react`) and all topic routes + - Theory content shares its topic's URL, so it needs no separate sitemap entry ## Files Involved diff --git a/docs/theory-authoring.md b/docs/theory-authoring.md index 7d1f1ce..1b0e35f 100644 --- a/docs/theory-authoring.md +++ b/docs/theory-authoring.md @@ -1,6 +1,6 @@ # Theory Authoring Guide -How to add theory pages for visualization topics. Theory pages provide in-depth conceptual explanations, step-by-step breakdowns, common mistakes, and interview questions for each topic. +How to add theory content for visualization topics. Theory sections render on the topic page beneath the visualization, and provide in-depth conceptual explanations, step-by-step breakdowns, common mistakes, and interview questions for each topic. --- @@ -11,10 +11,10 @@ How to add theory pages for visualization topics. Theory pages provide in-depth - Topic content files: - `src/content/theory/javascript/.ts` - `src/content/theory/react/.ts` -- Shared theory page renderer: `src/components/theory/TheoryArticle.tsx` -- Generic routes: - - `src/app/javascript/[topic]/theory/page.tsx` - - `src/app/react/[topic]/theory/page.tsx` +- Shared theory renderer: `src/components/theory/TopicTheorySections.tsx` +- Rendered by `src/components/layout/VisualizationPageShell.tsx`, which pulls + content from the registry and places it below the visualization. There is no + separate theory route. --- @@ -35,9 +35,9 @@ File name must match topic id exactly: 5. Run `npm run lint` and `npm run build`. After registration: -- `///theory` is statically generated. -- The topic visualization header shows the `Theory` button. -- The sitemap includes the new theory URL automatically. +- The theory sections render on `//`, below the visualization. +- The topic page header shows the `Theory` button, which scrolls to those sections. +- No sitemap change is needed, since the content shares the topic's URL. --- diff --git a/docs/topic-authoring.md b/docs/topic-authoring.md index 93e64a4..b064d89 100644 --- a/docs/topic-authoring.md +++ b/docs/topic-authoring.md @@ -50,10 +50,8 @@ src/ app/ javascript/ your-topic/page.tsx # Route page (Step 2) - [topic]/theory/page.tsx # JS theory route react/ your-topic/page.tsx # React topic route - [topic]/theory/page.tsx # React theory route layout.tsx # React category layout components/ visualizations/ diff --git a/eslint.config.mjs b/eslint.config.mjs index 199c0ec..0644715 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -9,6 +9,7 @@ const eslintConfig = defineConfig([ ".next/**", "out/**", "build/**", + ".claude/**", "next-env.d.ts", ]), ]); diff --git a/next.config.ts b/next.config.ts index eeb94ec..7ea6ee1 100644 --- a/next.config.ts +++ b/next.config.ts @@ -3,11 +3,30 @@ import withBundleAnalyzer from "@next/bundle-analyzer"; const isDev = process.env.NODE_ENV === "development"; +// Vercel Analytics and Speed Insights load their scripts from this host in +// development. In production both scripts are served same-origin under +// /_vercel, but the host stays allow-listed so the policy holds either way. +// Beacons are always same-origin (/_vercel/insights/view, /_vercel/speed-insights/vitals), +// so connect-src does not need the host. +const VERCEL_ANALYTICS_HOST = "https://va.vercel-scripts.com"; + const withAnalyzer = withBundleAnalyzer({ enabled: process.env.ANALYZE === "true", }); const nextConfig: NextConfig = { + redirects: async () => [ + { + source: "/javascript/:topic/theory", + destination: "/javascript/:topic", + permanent: true, + }, + { + source: "/react/:topic/theory", + destination: "/react/:topic", + permanent: true, + }, + ], headers: async () => [ { source: "/(.*)", @@ -20,7 +39,7 @@ const nextConfig: NextConfig = { key: "Content-Security-Policy", value: [ "default-src 'self'", - `script-src 'self' 'unsafe-inline'${isDev ? " 'unsafe-eval'" : ""}`, + `script-src 'self' 'unsafe-inline' ${VERCEL_ANALYTICS_HOST}${isDev ? " 'unsafe-eval'" : ""}`, "style-src 'self' 'unsafe-inline'", "img-src 'self' data: blob:", "font-src 'self'", diff --git a/package.json b/package.json index 973ffa2..4b8b0ae 100644 --- a/package.json +++ b/package.json @@ -2,11 +2,35 @@ "name": "visualizejs", "version": "0.1.0", "private": true, + "description": "Interactive step-by-step visualizations for JavaScript and React internals.", + "license": "MIT", + "author": "Tornike Nizharadze", + "homepage": "https://visualizejs.com", + "repository": { + "type": "git", + "url": "git+https://github.com/tornike14/visualizejs.git" + }, + "bugs": { + "url": "https://github.com/tornike14/visualizejs/issues" + }, + "keywords": [ + "javascript", + "react", + "visualization", + "event-loop", + "learning", + "interactive" + ], + "engines": { + "node": ">=20" + }, "scripts": { "dev": "next dev --webpack", "build": "next build --webpack", "start": "next start", - "lint": "eslint" + "lint": "eslint", + "check:registries": "node scripts/check-registries.mjs", + "verify": "npm run lint && npm run check:registries && npm run build" }, "dependencies": { "@codemirror/commands": "^6.10.2", diff --git a/scripts/check-registries.mjs b/scripts/check-registries.mjs new file mode 100644 index 0000000..9724681 --- /dev/null +++ b/scripts/check-registries.mjs @@ -0,0 +1,177 @@ +#!/usr/bin/env node +/** + * Verifies that every topic is registered everywhere it needs to be. + * + * Adding a topic touches eight places. Missing one produces a page that half + * works: a route with no theory link, a theory page absent from the sitemap, a + * loading skeleton of the wrong shape. TypeScript cannot catch these because + * every registry is independently valid on its own. + * + * Run with: node scripts/check-registries.mjs + */ + +import { readFileSync, readdirSync, existsSync } from "node:fs"; +import { join } from "node:path"; + +const problems = []; +const fail = (message) => problems.push(message); + +const read = (path) => readFileSync(path, "utf8"); +const exists = (path) => existsSync(path); + +/* ── Parse the topic registry ── */ + +const topicsSource = read("src/lib/topics.ts"); +const topics = [ + ...topicsSource.matchAll( + /id: "([^"]+)",\s*\n\s*title: "([^"]+)",\s*\n\s*category: "([^"]+)",\s*\n\s*route: "([^"]+)"/g, + ), +].map(([, id, title, category, route]) => ({ id, title, category, route })); + +if (topics.length === 0) { + fail("Could not parse any topics from src/lib/topics.ts"); + report(); +} + +const topicIds = new Set(topics.map((t) => t.id)); + +/* ── Read the registries that reference topics ── */ + +const theoryIndex = read("src/content/theory/index.ts"); +const metadata = read("src/lib/metadata.ts"); +const pageShell = read("src/components/layout/VisualizationPageShell.tsx"); + +const mapKeys = (source, mapName) => { + const block = source.match( + new RegExp(`${mapName}[^=]*=\\s*\\{([\\s\\S]*?)\\n\\};`), + ); + if (!block) { + fail(`Could not locate ${mapName}`); + return new Set(); + } + return new Set( + [...block[1].matchAll(/^\s{2}"?([a-z0-9-]+)"?:/gm)].map((m) => m[1]), + ); +}; + +const theoryRegistry = mapKeys(theoryIndex, "THEORY_CONTENT_BY_TOPIC_ID"); +const keywordRegistry = mapKeys(metadata, "TOPIC_KEYWORDS"); +const descriptionRegistry = mapKeys(metadata, "TOPIC_THEORY_DESCRIPTIONS"); + +const selectorBlock = pageShell.match( + /SELECTOR_TOOLBAR_TOPIC_IDS = new Set\(\[([\s\S]*?)\]\)/, +); +const selectorRegistry = new Set( + selectorBlock + ? [...selectorBlock[1].matchAll(/"([^"]+)"/g)].map((m) => m[1]) + : [], +); +if (!selectorBlock) fail("Could not locate SELECTOR_TOOLBAR_TOPIC_IDS"); + +/* ── Per-topic checks ── */ + +const inbound = new Map(topics.map((t) => [t.id, 0])); + +for (const topic of topics) { + const { id, category, route } = topic; + + const routeFile = `src/app${route}/page.tsx`; + if (!exists(routeFile)) fail(`${id}: missing route at ${routeFile}`); + + const componentDir = `src/components/visualizations/${id}`; + if (!exists(componentDir)) + fail(`${id}: missing visualization at ${componentDir}/`); + + const theoryFile = `src/content/theory/${category}/${id}.ts`; + if (!exists(theoryFile)) { + fail(`${id}: missing theory content at ${theoryFile}`); + continue; + } + + if (!theoryRegistry.has(id)) + fail(`${id}: not registered in THEORY_CONTENT_BY_TOPIC_ID (absent from sitemap)`); + if (!keywordRegistry.has(id)) fail(`${id}: missing from TOPIC_KEYWORDS`); + if (!descriptionRegistry.has(id)) + fail(`${id}: missing from TOPIC_THEORY_DESCRIPTIONS`); + + // ExampleSelector topics need a matching toolbar skeleton variant. + const usesSelector = readdirSync(componentDir, { recursive: true }) + .filter((f) => typeof f === "string" && /\.tsx?$/.test(f)) + .some((f) => read(join(componentDir, f)).includes("ExampleSelector")); + + if (usesSelector && !selectorRegistry.has(id)) + fail(`${id}: uses ExampleSelector but is absent from SELECTOR_TOOLBAR_TOPIC_IDS`); + if (!usesSelector && selectorRegistry.has(id)) + fail(`${id}: listed in SELECTOR_TOOLBAR_TOPIC_IDS but has no ExampleSelector`); + + // Related topics: 3 to 5 valid IDs, no self-reference, no duplicates. + const theorySource = read(theoryFile); + const relatedBlock = theorySource.match(/relatedTopicIds:\s*\[([^\]]*)\]/); + if (!relatedBlock) { + fail(`${id}: theory content has no relatedTopicIds`); + continue; + } + + const related = [...relatedBlock[1].matchAll(/"([^"]+)"/g)].map((m) => m[1]); + + if (related.length < 3 || related.length > 5) + fail(`${id}: relatedTopicIds has ${related.length} entries, expected 3 to 5`); + if (related.includes(id)) fail(`${id}: relatedTopicIds references itself`); + if (new Set(related).size !== related.length) + fail(`${id}: relatedTopicIds contains duplicates`); + + for (const ref of related) { + if (!topicIds.has(ref)) { + fail(`${id}: relatedTopicIds references unknown topic "${ref}"`); + continue; + } + inbound.set(ref, inbound.get(ref) + 1); + } +} + +/* ── Whole-graph checks ── */ + +for (const [id, count] of inbound) { + if (count === 0) + fail(`${id}: no theory page links to it, leaving it orphaned for readers and search`); +} + +for (const id of theoryRegistry) { + if (!topicIds.has(id)) + fail(`THEORY_CONTENT_BY_TOPIC_ID has "${id}", which is not a registered topic`); +} + +/* ── Content conventions ── */ + +const contentFiles = []; +const walk = (dir) => { + for (const entry of readdirSync(dir, { withFileTypes: true })) { + const path = join(dir, entry.name); + if (entry.isDirectory()) walk(path); + else if (/\.tsx?$/.test(entry.name)) contentFiles.push(path); + } +}; +walk("src/content"); +walk("src/components/visualizations"); + +for (const file of contentFiles) { + if (read(file).includes("—")) + fail(`${file}: contains an em dash, which is not allowed in user-facing content`); +} + +/* ── Report ── */ + +function report() { + if (problems.length > 0) { + console.error(`\nRegistry check failed with ${problems.length} problem(s):\n`); + for (const problem of problems) console.error(` - ${problem}`); + console.error(""); + process.exit(1); + } + + console.log( + `Registry check passed. ${topics.length} topics fully registered across all eight registries.`, + ); +} + +report(); diff --git a/src/app/javascript/[topic]/theory/page.tsx b/src/app/javascript/[topic]/theory/page.tsx deleted file mode 100644 index e8dd510..0000000 --- a/src/app/javascript/[topic]/theory/page.tsx +++ /dev/null @@ -1,138 +0,0 @@ -import type { Metadata } from "next"; -import { notFound } from "next/navigation"; -import { TheoryArticle } from "@/components/theory/TheoryArticle"; -import { - getRelatedTopicsFromTheory, - getTheoryContent, - getTheoryTopicsByCategory, -} from "@/content/theory"; -import { SITE_NAME, SITE_URL } from "@/lib/constants"; -import { createTopicTheoryMetadata, getTopicKeywords } from "@/lib/metadata"; -import { getTopicById } from "@/lib/topics"; - -interface TopicTheoryPageProps { - params: Promise<{ topic: string }>; -} - -async function resolveTopicTheory(paramsPromise: TopicTheoryPageProps["params"]) { - const { topic: topicId } = await paramsPromise; - const topic = getTopicById(topicId); - const theory = getTheoryContent(topicId); - - if (!topic || topic.category !== "javascript" || !theory) { - return null; - } - - return { topic, theory }; -} - -export function generateStaticParams() { - return getTheoryTopicsByCategory("javascript").map((topic) => ({ - topic: topic.id, - })); -} - -export async function generateMetadata({ - params, -}: TopicTheoryPageProps): Promise { - const data = await resolveTopicTheory(params); - if (!data) { - return { - robots: { - index: false, - follow: false, - }, - }; - } - - return createTopicTheoryMetadata(data.topic); -} - -export default async function TopicTheoryPage({ params }: TopicTheoryPageProps) { - const data = await resolveTopicTheory(params); - if (!data) { - notFound(); - } - - const { topic, theory } = data; - const canonicalUrl = `${SITE_URL}${topic.route}/theory`; - const relatedTopics = getRelatedTopicsFromTheory(topic.id); - - const articleSchema = { - "@context": "https://schema.org", - "@type": "TechArticle", - headline: `${topic.title} Theory Guide`, - description: theory.summary, - url: canonicalUrl, - inLanguage: "en-US", - keywords: getTopicKeywords(topic).join(", "), - publisher: { - "@type": "Organization", - name: SITE_NAME, - url: SITE_URL, - }, - mainEntityOfPage: canonicalUrl, - }; - - const breadcrumbSchema = { - "@context": "https://schema.org", - "@type": "BreadcrumbList", - itemListElement: [ - { - "@type": "ListItem", - position: 1, - name: "Home", - item: SITE_URL, - }, - { - "@type": "ListItem", - position: 2, - name: "JavaScript Concepts", - item: `${SITE_URL}/javascript`, - }, - { - "@type": "ListItem", - position: 3, - name: topic.title, - item: `${SITE_URL}${topic.route}`, - }, - { - "@type": "ListItem", - position: 4, - name: `${topic.title} Theory`, - item: canonicalUrl, - }, - ], - }; - - const faqSchema = { - "@context": "https://schema.org", - "@type": "FAQPage", - mainEntity: theory.interviewQuestions.map((q) => ({ - "@type": "Question", - name: q.question, - acceptedAnswer: { - "@type": "Answer", - text: q.answer, - }, - })), - }; - - return ( - <> -