Skip to content
FrameworkJavaScript

Page block: links to sub-pages

A one-line link to a sub-page, like a sub-page in Notion. It shows the page's icon and title. Each page is its own document, and your app stores it. The block saves only the page id and a display copy of the title and icon.

Blok never loads or saves a page. Your app connects pages through the four config members below.

The block has three special states:

  • New page: the page has no title, so the block shows New page instead. It is still a link.
  • Page not found: resolve returned null.
  • No access: resolve returned { access: 'none' }. The cached title is hidden.

A page that is not found or not accessible is never removed automatically. The block stays, but it stops working as a link.

A plain click calls open. So does Enter when the block is selected from the keyboard. Without open, the link follows href.

Cmd/Ctrl-click, Shift-click and middle click keep their browser meaning, such as opening a new tab. New-tab clicks need href. The link never takes focus, so undo and Escape still work after a click.

page is not in defaultBlockTools. Register it yourself, with a config.

For read-only HTML, call blocksToHtml(data, { pageHref }) from @bloklabs/core/view. It renders the block as an icon and a title, and it is a link only when you pass pageHref. The view reads only the saved cache, so it cannot show the not-found or no-access states. It never renders the page's body.

Import

TypeScript
import { Page } from '@bloklabs/core/tools';

Configuration

OptionTypeDefaultDescription
href(pageId: string) => stringundefined

Builds the page's URL from its id. Blok puts it on the link, so Cmd/Ctrl-click or middle click opens the page in a new tab. Without it the link has no URL. Unsafe schemes such as javascript: are dropped.

open(pageId: string, ctx: { event?: MouseEvent | KeyboardEvent }) => voidundefined

Opens the page. Blok calls it on a plain left click. Without it, a click follows href.

Enter also opens the page when the block is selected from the keyboard (Escape, then the arrow keys). Then ctx.event is that key press.

Blok also calls it once right after a user inserts a new page from the toolbox. Then ctx.event is empty. It is not called for a page inserted through the API, or when create throws.

resolve(pageId: string) => PageInfo | null | undefined | Promise<…>undefined

Returns the page's current title and icon. Blok asks once, when the block renders. It may return a value or a promise:

  • { title, icon }: the cached copy is updated if it changed.
  • null: the page does not exist. The block shows "Page not found".
  • { access: 'none' }: this user may not see the page. The block shows "No access".
  • undefined: nothing is known. The cached copy stays.

Return the full picture. A missing title or icon means the page has none. In read-only mode the fresh copy is shown but not saved. It is saved when editing turns on. If resolve throws, the cached copy stays.

create(init: { pageId: string }) => void | Promise<void>undefined

Makes the page in your app. Blok calls it once with the id it minted, when a page block is inserted without a pageId. That happens when a user picks Page in the toolbox, or when your code inserts one through the API.

It is never called on load, paste, undo, redo or a collaborator's change, or in read-only mode. If it throws, the block stays with its id and shows "Page not found". Blok still asks resolve, so a page your app made after all shows up.

Save Data

TypeScript
interface PageData {
  pageId: string; // Id of the page. Blok mints one for a new page.
  cache?: {       // Display copy, refreshed from resolve(). It can be stale.
    title?: string;
    icon?: { type: 'emoji'; value: string } | { type: 'image'; url: string };
  };
}
// The page's body is NOT here. It is a separate document your app stores.
JSON
{
  "id": "pg001",
  "type": "page",
  "data": {
    "pageId": "p1",
    "cache": {
      "title": "Roadmap",
      "icon": { "type": "emoji", "value": "🗺" }
    }
  }
}

Usage Example

TypeScript
import { Blok } from '@bloklabs/core';
import { Page } from '@bloklabs/core/tools';
import { blocksToHtml } from '@bloklabs/core/view';

const pageUrl = (pageId) => `/pages/${pageId}`;

const editor = new Blok({
  holder: 'editor',
  tools: {
    // Not in defaultBlockTools: register it with your own config.
    page: {
      class: Page,
      config: {
        href: pageUrl,
        open: (pageId) => router.push(pageUrl(pageId)),
        resolve: (pageId) => myApi.getPageInfo(pageId), // { title, icon } | null
        create: ({ pageId }) => myApi.createPage(pageId),
      },
    },
  },
});

// Read-only HTML: pass the same URL builder.
const html = blocksToHtml(savedData, { pageHref: pageUrl });