Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,20 +36,20 @@

## What is this repo?

[Nodejs.org](https://nodejs.org/) by the [OpenJS Foundation](https://openjsf.org/) is the official website for the Node.js® JavaScript runtime. This repo is the source code for the website. It is built using [Next.js](https://nextjs.org), a React Framework.
[Nodejs.org](https://nodejs.org/), maintained by the [OpenJS Foundation](https://openjsf.org/), is the official website for the Node.js® JavaScript runtime. This repo is the source code for the website. It is built using [Next.js](https://nextjs.org), a React Framework.

```bash
pnpm install --frozen-lockfile
pnpm dev

# listening at localhost:3000
# Listening at http://localhost:3000
```

## Contributing

This project adopts the Node.js [Code of Conduct][].

Any person who wants to contribute to the Website is welcome! Please read [Contribution Guidelines][] and see the [Figma Design][] to understand better the structure of this repository.
Anyone who wants to contribute to the website is welcome! Please read [Contribution Guidelines][] and see the [Figma Design][] to understand better the structure of this repository.

> \[!IMPORTANT]\
> Please read our [Translation Guidelines][] before contributing to Translation and Localization of the Website
Expand Down
12 changes: 12 additions & 0 deletions apps/site/app/api/site.json/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { NextResponse } from 'next/server';

import { siteConfig } from '#site/next.json.mjs';

export const GET = () =>
NextResponse.json(siteConfig, {
headers: {
'Cache-Control': 'public, max-age=300, stale-while-revalidate=3600',
},
});

export const dynamic = 'force-static';
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ How is `setImmediate()` different from `setTimeout(() => {}, 0)` (passing a 0ms

A function passed to `process.nextTick()` is going to be executed on the current iteration of the event loop, after the current operation ends. This means it will always execute before `setTimeout` and `setImmediate`.

A `setTimeout()` callback with a 0ms delay is very similar to `setImmediate()`. The execution order will depend on various factors, but they will be both run in the next iteration of the event loop.
A `setTimeout()` callback with a 0ms delay is very similar to `setImmediate()`. The execution order will depend on various factors, but they will be both run in the next iteration (the first one) of the event loop when called from the main module. When scheduled inside an I/O callback, setImmediate is guaranteed to run in the current iteration's **check** phase, while setTimeout must wait for the **timers** phase of the subsequent iteration.

A `process.nextTick` callback is added to `process.nextTick queue`. A `Promise.then()` callback is added to `promises microtask queue`. A `setTimeout`, `setImmediate` callback is added to `macrotask queue`.

Expand Down
45 changes: 22 additions & 23 deletions apps/site/pages/en/learn/test-runner/mocking.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,30 +149,27 @@ This leverages [`mock`](https://nodejs.org/api/test.html#class-mocktracker) from

```mjs
import assert from 'node:assert/strict';
import { before, describe, it, mock } from 'node:test';
import { describe, it, mock } from 'node:test';

describe('foo', { concurrency: true }, () => {
describe('foo', { concurrency: true }, async () => {
const barMock = mock.fn();
let foo;

before(async () => {
const barNamedExports = await import('./bar.mjs')
// discard the original default export
.then(({ default: _, ...rest }) => rest);

// It's usually not necessary to manually call restore() after each
// nor reset() after all (node does this automatically).
mock.module('./bar.mjs', {
defaultExport: barMock,
// Keep the other exports that you don't want to mock.
namedExports: barNamedExports,
});

// This MUST be a dynamic import because that is the only way to ensure the
// import starts after the mock has been set up.
({ foo } = await import('./foo.mjs'));
const barNamedExports = await import('./bar.mjs')
// discard the original default export
.then(({ default: _, ...rest }) => rest);

// It's usually not necessary to manually call restore() after each
// nor reset() after all (node does this automatically).
mock.module('./bar.mjs', {
defaultExport: barMock,
// Keep the other exports that you don't want to mock.
namedExports: barNamedExports,
});

// This MUST be a dynamic import because that is the only way to ensure the
// import starts after the mock has been set up.
const { foo } = await import('./foo.mjs');

it('should do the thing', () => {
barMock.mock.mockImplementationOnce(function bar_mock() {
/* … */
Expand Down Expand Up @@ -258,17 +255,19 @@ Note the use of time-zone here (`Z` in the time-stamps). Neglecting to include a
import assert from 'node:assert/strict';
import { describe, it, mock } from 'node:test';

import ago from './ago.mjs';
describe('whatever', { concurrency: true }, async () => {
mock.timers.enable({ now: new Date('2000-01-01T00:02:02Z') });

describe('whatever', { concurrency: true }, () => {
it('should choose "minutes" when that\'s the closet unit', () => {
mock.timers.enable({ now: new Date('2000-01-01T00:02:02Z') });
const { default: ago } = await import('./ago.mjs');

it('should choose "minutes" when that\'s the closest unit', () => {
const t = ago('1999-12-01T23:59:59Z');

assert.equal(t, '2 minutes ago');
});
});
```

`ago` **must** be imported dynamically _after_ `mock.timers` is enabled. As with all module dependency mocking, this is necessary so that the `ago` module receives the mock before the `ago` module is executed (if the mocking does not occur before, it will be too late).

This is especially useful when comparing against a static fixture (that is checked into a repository), such as in [snapshot testing](https://nodejs.org/api/test.html#snapshot-testing).
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ New to contributing? Start here:
## Technical Documentation

- **[Technologies](./technologies.md)** - Overview of the tech stack and architecture decisions
- **[Site Configuration](./site-config.md)** - How to update `site.json` (banners, badges, RSS feeds, metadata)
- **[Downloads Page](./downloads-page.md)** - How to add installation methods and package managers
- **[Package Publishing](./package-publishing.md)** - Guidelines for publishing packages in our monorepo
- **[Cloudflare build and deployment](./cloudflare-build-and-deployment.md)** - Overview and useful information about the Cloudflare build and deployment
Expand Down
127 changes: 127 additions & 0 deletions docs/site-config.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
# Site Configuration (`site.json`)

`apps/site/site.json` is a manually maintained JSON file that controls global site metadata, RSS feeds, and time-sensitive UI elements (banners and badges).

It is imported via `apps/site/next.json.mjs` and exposed as a read-only API endpoint at `/api/site.json`.

This endpoint is also consumed externally by the [doc-kit](https://github.com/nodejs/doc-kit) to display dynamic banners inside the API docs, for example security announcements or EOL notices, without requiring a doc-kit release.

## Structure

### Top-level metadata

| Field | Description |
| ------------- | ------------------------------------------- |
| `title` | Site title used in `<title>` and OG tags |
| `description` | Default meta description |
| `favicon` | Path to the favicon (relative to `/public`) |
| `accentColor` | Primary accent color (hex) |

### `twitter`

Social card metadata for Twitter/X.

| Field | Description |
| ---------- | -------------------------------------------------- |
| `username` | Twitter handle (e.g. `@nodejs`) |
| `card` | Card type (`summary`, `summary_large_image`, etc.) |
| `img` | Path to the card image |
| `imgAlt` | Alt text for the card image |

### `rssFeeds`

Array of RSS feed definitions. Each feed is statically generated at `/:locale/feed/:file`.

| Field | Description |
| ---------- | --------------------------------------------------------------- |
| `title` | Human-readable feed title |
| `file` | Output filename (e.g. `blog.xml`) |
| `category` | Blog category to include (`all`, `release`, `vulnerability`, …) |

Adding a new feed here automatically makes it available; no code changes needed.

### `websiteBanners`

A map of keys to banner definitions. Banners appear at the top of the target surface within the given date range.

Keys are either a **page slug** (for nodejs.org pages) or a **major version string** (for API doc pages). The doc-kit fetches this endpoint and matches banners by version key (e.g. `"v24"`) or a special `"all"` key that applies to every API docs version.

```json
"websiteBanners": {
"index": {
"startDate": "2026-01-13T00:00:00.000Z",
"endDate": "2026-01-20T00:00:00.000Z",
"text": "January Security Release is available",
"link": "https://nodejs.org/en/blog/vulnerability/…",
"type": "warning"
},
"all": {
"startDate": "2026-01-13T00:00:00.000Z",
"endDate": "2026-01-20T00:00:00.000Z",
"text": "January Security Release affects all active versions",
"link": "https://nodejs.org/en/blog/vulnerability/…",
"type": "warning"
},
"v20": {
"startDate": "2026-04-30T00:00:00.000Z",
"endDate": "2027-04-30T00:00:00.000Z",
"text": "Node.js 20 is End-of-Life",
"link": "https://nodejs.org/en/about/previous-releases",
"type": "error"
}
}
```

| Field | Description |
| ----------------------- | --------------------------------------------------------------------------------------- |
| key | Page slug (e.g. `index`), major version (e.g. `v20`), or `all` for all API doc versions |
| `startDate` / `endDate` | ISO 8601 timestamps; the banner is hidden outside this range |
| `text` | Banner message |
| `link` | URL the banner links to |
| `type` | Visual style: `warning` (orange) or `error` (red) |

### `websiteBadges`

A map of page slugs to badge definitions. Badges appear as small promotional labels near the page title within the given date range.

```json
"websiteBadges": {
"index": {
"startDate": "2025-10-30T00:00:00.000Z",
"endDate": "2025-11-15T00:00:00.000Z",
"kind": "default",
"title": "Discover",
"text": "New migration guides",
"link": "https://nodejs.org/en/blog/migrations"
}
}
```

| Field | Description |
| ----------------------- | ----------------------------------------------------------- |
| key | Page slug |
| `startDate` / `endDate` | ISO 8601 timestamps; the badge is hidden outside this range |
| `kind` | Badge style variant (e.g. `default`) |
| `title` | Short label shown before the text |
| `text` | Badge body text |
| `link` | URL the badge links to |

## How to update

1. Edit `apps/site/site.json` directly. No code changes are required for banners, badges, or new RSS feeds.
2. Date-gated content (banners and badges) activates and deactivates automatically at runtime; no re-deploy is needed once the change is live.
3. After editing, run `pnpm format` to ensure the file remains consistently formatted.

## API endpoint

The full contents of `site.json` are available at:

```
GET /api/site.json
```

The response is `application/json` and is statically cached at build time. It refreshes on each deployment.

### External consumers

The [doc-kit](https://github.com/nodejs/doc-kit) fetches this endpoint asynchronously on page load to inject banners into the Node.js API documentation site. This allows publishing announcements (security releases, EOL notices, etc.) that appear in the API docs without requiring a doc-kit release or a rebuild of the static documentation.
Loading