npm package: @minissg/page
Description: simple page organization management for Minissg
Latest version: 2.0.2

# @minissg/page

This library provides a feature to manage the set of webpages to be
generated by [Minissg].

## Install

Install the package by

```bash
npm install @minissg/page
```

and import it in the root file of your project as follows:

```js
import { Page } from "@minissg/page";
```

or

```js
import Page from "@minissg/page";
```

## Getting Started

Download a template project.

```bash
tiged uenob/minissg/template/page my_project
```

Change directory to the new directory and install all dependencies
by the `npm` command:

```bash
cd my_project
npm install
```

The following scripts are initially available:
* `npm run build` for static site generation for production.
* `npm run serve` for preview of the build result.
* `npm run dev` for starting Vite's dev server.

## Basic Usage

Each instance of the `Page` class represents a set of webpages.
The typical usage of the `Page` class is as follows:

1. Combined with `import.meta.glob` provided by Vite, create a `Page` object
   of a set of webpage component modules by `Page.create`.
   For example:

   ```js
   const page = Page.create({
     url: 'https://example.com/',
     pages: import.meta.glob('./pages/**/*.{md,mdx}'),
     substitutePath: s => s.slice('./pages/'.length)
   });
   ```

   The `url` parameter indicates the root URL of this website.
   `pages` has the map from file names to dynamic import functions
   provided by `import.meta.glob`.

   `substitutePath` removes `./pages/` prefix from each file name when
   making URL of each webpage.
   The URL of each webpage is obtained by transforming its file name
   after applying `substitutePath`.
   As a result, for example, a given module `pages/foo/bar.mdx` is
   associated with a webpage of URL `foo/bar/`.

2. Set `page.render` to a function indicating how to render a webpage
   component in a HTML file.
   The following example adds `DOCTYPE`, `html`, and `body` tags to
   `module.default` component and renders all of them by
   `pages.jsx`'s renderer.

   ```jsx
   import render from './pages.jsx?renderer'

   page.render = async function (module) {
     const Webpage = () => (
       <html>
         <body>
           <module.default page={this} />
         </body>
       </html>
     );
     return new Blob(['<!DOCTYPE html>\n', await render(Webpage)]);
   };
   ```

   In each MDX file, you can access to the `Page` object dedicated to
   that MDX file through `props.page` property.

3. Export the root page through the `main` function:

  ```js
  export main = () => page;
  ```

The above three fragments of code is placed in the same JSX file, say
`index.html.jsx`.
Set this file to the input of Vite in `vite.config.js` and do `vite build`.
Then you will find your website in `dist` directory.
An example of `vite.config.js` is given below:

```js
import { defineConfig } from 'vite'
import mdx from '@mdx-js/rollup'
import preact from '@preact/preset-vite'
import minissg from 'vite-plugin-minissg'
import minissgPreact from '@minissg/render-preact'

export default defineConfig({
  build: {
    rollupOptions: {
      input: './index.html.jsx'
    }
  },
  plugins: [
    minissg({
      render: {
        '**/*.jsx': minissgPreact()
      },
      plugins: () => [
        preact(),
        mdx({
          mdxExtensions: ['.mdx', '.mdx?MINISSG-COPY'],
          jsxImportSource: 'preact'
        })
      ]
    })
  ]
})
```

## References

This package provides the `Page` class having the following methods.

### Types

#### `Awaitable`

```typescript
type Awaitable<X> = X | PromiseLike<X>;
```

The type of values that can be passed to the `await` construct.

#### `Pairs`

```typescript
type Pairs<X, Y, This = void> =
  | Awaitable<Record<string, Y>>
  | Awaitable<Iterable<Awaitable<PairsItem<X, Y, This>>>>
  | Awaitable<AsyncIterable<Awaitable<PairsItem<X, Y, This>>>>
  | Awaitable<(this: This) => Awaitable<Pairs<X, Y, This>>>;

type PairsItem<X, Y, This> =
  | [X, Y]
  | Record<string, Y>
  | ((this: This) => Pairs<X, Y, This>);
```

`Pairs<Key, Value>` represents a sequence of key-value pairs.
It is either an `array`, `object`, promise of one of them,
or function returning `Pairs<Key, Value>`.
If `array` is given, each element must be either a pair
`[Key, Value]`, `object`, or function returning `Pairs<Key, Value>`.
If `object` is given as a `Pairs<Key, Value>`, each property must be
of type `Value`.

### `List`

```typescript
type List<X, This = void> =
  | Awaitable<Iterable<X>>
  | Awaitable<AsyncIterable<X>>
  | Awaitable<(this: This) => List<X, This>>;

type Awaitable<X> = X | PromiseLike<X>;
```

`List<Value>` represents a sequence of values.
It is either an `Iterable<Value>`, `AsyncIterable<Value>`, or promise
of one of them, or function returning `List<Value>`.

### `RelPath`

```typescript
interface RelPath {
  moduleName: string;
  stem: string;
  variant: string;
  fileName: string;
}
```

`RelPath` is the type of objects of the form
`{ moduleName: string; stem: string; variant: string; fileName: string }`,
each of which represent relative paths between two `Page`s.

`Page` manages the following four kinds of paths:
* `moduleName` is the path of webpages.
  This is used to determine the URL of each webpage.
* `stem` identifies a content of a webpage regardless of its variants, such
as languages and formats.
* `variant` identifies a variants of the content.
* `fileName` is the path of source file.

### `Paginate`

```typescript
interface Paginate<Item, Page> {
  pages: Array<Paginate<Item, Page>>;
  page: Page;
  pageIndex: number;
  itemIndex: number;
  items: Item[];
  numAllItems: number;
}
```

`Paginate<Item>` is a paginated fragment of a list of items of type `Item`.

It consists of the following properties:
* `pages`: the array of all of the paginated fragments.
* `page`: the `Page` object corresponding to this paginated fragment.
* `pageIndex`: the index of this fragment (starting from 0).
* `itemIndex`: the index of the first item of this fragment
  in the given list of items.
* `items`: the array of items in this fragment.
* `numAllItems`: the number of all of the given items.

### `AssetModule`

```typescript
interface AssetModule {
  default: string;
}
```

`AssetModule` is the type of modules representing an asset.
Such modules must have a default export of type `string`, which is the
path of the asset.

### `MainModule`

```typescript
interface MainModule {
  main: (context: Context) => Awaitable<Module>
}
```

This is the type of modules having the `main` function of Minissg.
See [Minissg's document] for details of `main`.

### `Content` and `Module`

They are identical to [Minissg]'s `Content` and `Module` type.
See [Minissg's document] for details.

### `Delay`

`Delay` is a promise that can be used with React Suspense.
See [@minissg/async] for details.

### Class Methods

#### `Page.create(options, args...) => Page`

``` typescript
declare class Page {
  static create<
    Load,
    Base extends Page<unknown, Base> = PageRec<unknown, PageRec>,
    This extends Base = Base,
    Args extends unknown[] = []
  >(
    this: {
      new (...args: Args): This;
      Base: abstract new (...args: never) => Base;
    },
    arg: PagesArg<Load, This> | LoadArg<Load, This>,
    ...args: Args
  ): This;
}

interface PagesArg<Load, This> {
  pages: Pairs<string | RelPath, Loader<This, Load> | MainModule, This>;
  substitutePath?: ((path: string) => Awaitable<string>) | Null;
  assets?: Pairs<string, LoadAsset | string | Null, This> | Null;
  url?: URL | string | Null;
  parsePath?: ((this: This, path: string) => Awaitable<ParsePath>) | Null;
  paginatePath?: ((this: This, index: number) => Awaitable<RelPath>) | Null;
  render?: ((this: This, loaded: Load) => Awaitable<Content>) | Null;
}

interface LoadArg<Load, This> {
  load: Loader<This, Load>;
  url?: URL | string | Null;
  parsePath?: ((this: This, path: string) => Awaitable<ParsePath>) | Null;
  paginatePath?: ((this: This, index: number) => Awaitable<RelPath>) | Null;
  render?: ((this: This, loaded: Load) => Awaitable<Content>) | Null;
}

type Null = null | undefined;
type Loader<This, Load> = (page: This) => Awaitable<Load | MainModule>;
```

`Page.create` creates a new `Page` object with its contents.
The `options` argument specifies the contents and customizations.
The `args` arguments are passed to constructor of `Page` class, which does
not do anything by default and can be defined by creating a subclass.

The `options` object must have one of the following properties:

* `pages` of type `Pairs`.
  Its key must be either `string` or `RelPath`.
  Its value must be either a function returning `object`, `Page`, or
  `MainModule`.
* `load` of function returning `object`, `Page`, `MainModule`, or promise
  of one of them.

They are exclusive: `pages` is ignored if `load` is specified, and vice versa.

The `options` may have one of the following optional properties:

* `substitutePath` of function taking a `string` and returning either
  a `string` or `Promise<string>`.
* `assets` of type `Pairs`.
  Its key must be `string`.
  Its value must be either a `string` or function returning `AssetModule`
  or `Promise<AssetModule>`.
  This can be used only with `pages` property.
* `url` of type either `URL` or `string`.
* `parsePath` method taking a `string` and returning a `ParsePath` or
  `Promise<ParsePath>`.
* `render` method taking a `string` and returning a `Content` or
  `Promise<Content>`.

#### `Page.paginate(options, args...) => Page`

```typescript
declare class Page {
  static paginate<
    Item,
    Load,
    Base extends Page<unknown, Base> = PageRec<unknown, PageRec>,
    This extends Base = Base,
    Args extends unknown[] = []
  >(
    this: {
      new (...args: Args): This;
      Base: abstract new (...args: never) => Base;
    },
    arg: {
      items: List<Item, This>;
      load: (this: This, paginate: Paginate<Item, This>)
              => Awaitable<Load | MainModule>;
      pageSize?: number | Null;
      url?: URL | string | Null;
      parsePath?: ((this: This, path: string) => Awaitable<ParsePath>) | Null;
      paginatePath?: ((this: This, index: number) => Awaitable<RelPath>) | Null;
      render?: ((this: This, loaded: Load) => Awaitable<Content>) | Null;
    },
    ...args: Args
  ): This;
```

`Page.paginate` creates a new `Page` object with its contents by paginating
the given items.
Similarly to `Page.create`, `options` specifies the contents and `args`
are passed to constructor.

The `options` object must have the following properties:

* `items` of type `List`.
  Its value may be arbitrary type.
* `load` method that takes `Paginate` and returns either an `object`,
  `Page`, `MainModule`, or promise of one of them.

The following are options:

* `pageSize` of integer.
  Its default is 10.
* `paginatePath` method taking a `number` and returning a `RelPath` or
  `Promise<RelPath>`.
* `url`, `parsePath`, and `render` can be specified similarly to `Page.create`.

### Customization

The following methods can be overriden by either
specifying the corresponding options in `Page.create`,
overwriting `Page` object properties, or
creating a subclass of `Page`.

#### `Page.prototype.parsePath(fileName) => Awaitable<ParsePath>`

This method translates a relative file path into `ParsePath`, which
consists of three optional properties `moduleName`, `stem`, and `variant`
of type `string`.

```typescript
type ParsePath = Omit<Partial<RelPath>, 'fileName'>;
```

From the combination of `fileName` and `ParsePath`, a `RelPath` is
generated.

#### `Page.prototype.paginatePath(index) => Awaitable<RelPath>`

This mehtod translates the index of a paginated fragment into a `RelPath`.

#### `Page.prototype.render(object) => Awaitable<Content>`

This method transforms `object` into a `Content` , which is the content
of a webpage to be generated.

The argument `object` is the object returned from the function given to
`Page.create` or `Page.paginate` through their `pages` and `load` option.

#### `Page.Base`

`Base` is a constructor of the base class whose instances constitutes a
page tree.
When a Page object searches for its child node in the page tree,
the first object that is instance of `Base` is regarded as the child.

### Instance Methods

#### `Page.prototype.moduleName => Delay<string>`

Returns the URL path to this page.

#### `Page.prototype.stem => Delay<string>`

Returns the stem of URL path of this page.

#### `Page.prototype.variant => Delay<string>`

Returns the set of variants of this page.

#### `Page.prototype.fileName => Delay<string>`

Returns the source file name of this page.

#### `Page.prototype.url => Delay<URL>`

Returns the full URL of this page.

#### `Page.prototype.parent => Delay<Page | undefined>`

Returns the parent node of this page in the page tree.

#### `Page.prototype.root => Delay<Page>`

Return the root node in the page tree.

#### `Page.prototype.loadThis() => Delay<object | undefined>`

Loads the content object associated to this page.

#### `Page.prototype.load() => Delay<object>`

Searches for the content object associated to the path of this page.

#### `Page.prototype.children() => Delay<Array<[RelPath, Page]>>`

Returns the immediate children of this page.

#### `Page.prototype.findByModuleName(path) => Delay<Page | undefined>`

Searches for a page of the given relative path by its module name.

#### `Page.prototype.findByFileName(path) => Delay<Page | undefined>`

Searches for a page of the given relative path by its source file name.

#### `Page.prototype.find(path) => Delay<Page | undefined>`

Try `findByModuleName` and `findByFileName` in this order.

#### `Page.prototype.findByStem(path) => Delay<Set<Page>>`

Searches for all pages having the given stem path.

#### `Page.prototype.variants() => Delay<Set<Base>>`

Searches for all pages having the same stem path as this page.

## License

MIT

[Minissg]: https://github.com/uenoB/minissg
[Minissg's document]: https://github.com/uenoB/minissg/tree/main/packages/vite-plugin-minissg#readme
[@minissg/async]: https://github.com/uenoB/minissg/tree/main/packages/async#readme