Prototyper UI

Machine Mode Package

Add an LLM-friendly plain text view to any Next.js App Router site.

Overview

@prototyperai/machine-mode is an installable runtime that adds a "machine mode" to any Next.js App Router site. It gives every page a URL-driven plain text view that LLMs can read, with a full-page shell UI for humans to preview what machines see.

Features:

  • URL-driven mode switching (?view=machine)
  • Rendered/raw format toggle (?format=rendered|raw)
  • Full-page machine shell UI with utility bar and copy action
  • Server route helpers for /machine/[...slug] and /machine.txt
  • Keyboard shortcuts (Shift+M to toggle, R to switch format)
  • Client-side text caching and markdown rendering

Requirements

  • Next.js 15 or 16 (App Router)
  • React 19+
  • Tailwind CSS v4

Entry Points

The package provides four entry points:

Entry pointEnvironmentDescription
@prototyperai/machine-modeBothBarrel re-export of client + server
@prototyperai/machine-mode/clientClientProvider, gate, shell, toggle, hooks
@prototyperai/machine-mode/serverServerRoute handlers, resolver types
@prototyperai/machine-mode/styles.cssCSSMachine mode theme tokens and utilities

Quick Start (CLI)

npx @prototyperai/cli machine-mode init

This scaffolds:

  • lib/machine-resolver.ts
  • app/machine/[...slug]/route.ts
  • app/machine.txt/route.ts
  • app/layout.tsx wiring with MachineModeProvider + MachineGate
  • globals.css import for @prototyperai/machine-mode/styles.css

Manual Setup

Install package

pnpm add @prototyperai/machine-mode@beta
npm install @prototyperai/machine-mode@beta
yarn add @prototyperai/machine-mode@beta
bun add @prototyperai/machine-mode@beta

Import styles

app/globals.css
@import "@prototyperai/machine-mode/styles.css";

Wire layout

app/layout.tsx
import { Suspense } from "react"
import { MachineGate, MachineModeProvider } from "@prototyperai/machine-mode/client"

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        <Suspense>
          <MachineModeProvider>
            <MachineGate>{children}</MachineGate>
          </MachineModeProvider>
        </Suspense>
      </body>
    </html>
  )
}

MachineModeProvider uses useSearchParams() internally and must be wrapped in a Suspense boundary. It reads ?view=machine from the URL and provides mode state to the tree. MachineGate conditionally renders either your normal page or the MachineShell overlay.

Create a resolver

The resolver maps pathnames to machine-readable content. It can be sync or async.

lib/machine-resolver.ts
import type { MachineDoc } from "@prototyperai/machine-mode/server"

export async function resolveMachineDoc(pathname: string): Promise<MachineDoc | null> {
  if (pathname === "/") {
    return {
      pathname,
      title: "Home",
      sourceUrl: pathname,
      content: "# Home\n\nWelcome to this site in machine mode.",
      contentType: "curated",
      generatedAt: new Date().toISOString(),
    }
  }

  return null
}

Add route handlers

app/machine/[...slug]/route.ts
import { createMachineRouteHandler } from "@prototyperai/machine-mode/server"
import { resolveMachineDoc } from "@/lib/machine-resolver"

export const GET = createMachineRouteHandler({ resolveDoc: resolveMachineDoc })
app/machine.txt/route.ts
import { createMachineIndexHandler } from "@prototyperai/machine-mode/server"

export const GET = createMachineIndexHandler()

Resolver Contract

Your resolver receives a normalized pathname and returns either:

  • A MachineDoc object for known routes
  • null to use the built-in fallback content

The resolver can be synchronous or asynchronous:

type MachineDocResolver = (
  pathname: string
) => MachineDoc | null | undefined | Promise<MachineDoc | null | undefined>
interface MachineDoc {
  pathname: string
  title: string
  sourceUrl: string
  content: string                                    // markdown string
  contentType: "docs" | "curated" | "fallback" | string
  generatedAt: string                                // ISO 8601
}

When the resolver returns null, the route handler uses createFallbackMachineDoc() to generate a placeholder response that links back to the human page, the machine index, and the LLM index.

API Reference

Client Exports

Imported from @prototyperai/machine-mode/client.

MachineModeProvider

Reads ?view=machine and ?format=rendered|raw from the URL and provides mode context to the component tree. Registers the Shift+M keyboard shortcut.

Prop

Type

MachineGate

Renders either children (human mode) or MachineShell (machine mode), with enter/exit transitions.

Prop

Type

MachineShell

Full-page machine view overlay with utility bar, rendered/raw viewport, copy button, and metadata footer. Fetches content from the /machine/[...slug] route.

Prop

Type

ModeToggle

Floating or inline Human/Machine segmented control.

Prop

Type

useMachineMode

Hook that returns the current mode context:

function useMachineMode(): {
  mode: "human" | "machine"
  setMode: (mode: "human" | "machine") => void
  ready: boolean
  config: MachineModeConfig
}

useMode is an alias for useMachineMode.

MachineModeConfig contains the resolved provider options:

interface MachineModeConfig {
  viewQueryParam: string
  formatQueryParam: string
  machineViewValue: string
  defaultRenderMode: "rendered" | "raw"
  machineEndpointBasePath: string
}

renderMachineMarkdown

Parses a markdown string into a styled React node for the machine shell viewport.

function renderMachineMarkdown(markdown: string): {
  node: React.ReactNode
  error: string | null
}

Utility Functions

// Client-side text cache
function getCachedText(key: string): string | undefined
function setCachedText(key: string, value: string): void
function fetchCachedText(url: string, options?: { signal?: AbortSignal }): Promise<string>

// View/render mode parsing
function parseMachineViewMode(value: string | null, machineViewValue?: string): "human" | "machine" | null
function parseMachineRenderMode(value: string | null): "rendered" | "raw" | null
function isEditableElementTarget(target: EventTarget | null): boolean

Pathname Helpers

Shared between client and server:

function normalizeWebsitePathname(pathname: string): string
function normalizeMachineBasePath(basePath: string): string
function toMachineEndpointPath(pathname: string, machineBasePath?: string): string
function machineSegmentsToWebsitePathname(segments: string[]): string

Exported Types

The client entry also exports these TypeScript types:

  • MachineViewMode -- "human" | "machine"
  • MachineRenderMode -- "rendered" | "raw"
  • MachineModeConfig -- Resolved provider configuration (see useMachineMode above)
  • MachineModeProviderProps -- Props for MachineModeProvider
  • MachineMarkdownRenderResult -- Return type of renderMachineMarkdown

Server Exports

Imported from @prototyperai/machine-mode/server.

createMachineRouteHandler

Creates a Next.js route handler for app/machine/[...slug]/route.ts. Calls your resolver with the normalized pathname and returns the content as text/plain.

Prop

Type

Response headers include X-Machine-Title, X-Machine-Source-Url, X-Machine-Content-Type, and X-Machine-Generated-At.

createMachineIndexHandler

Creates a Next.js route handler for app/machine.txt/route.ts. Returns a plain text index of machine-mode URLs and endpoints.

Prop

Type

buildMachineIndexText

Generates the machine index text content without wrapping it in a route handler. Accepts the same options as createMachineIndexHandler (minus cacheControl and buildIndexText).

createFallbackMachineDoc

Creates a fallback MachineDoc for routes the resolver does not handle:

function createFallbackMachineDoc(
  pathname: string,
  options?: { siteUrl?: string; machineIndexPath?: string; llmsIndexPath?: string }
): MachineDoc

Exported Types

The server entry also exports these TypeScript types:

  • MachineDoc -- Machine document shape returned by resolvers
  • MachineContentType -- "docs" | "curated" | "fallback" | (string & {})
  • MachineDocResolver -- Resolver function signature
  • MachineRouteHandlerOptions -- Options for createMachineRouteHandler
  • MachineIndexHandlerOptions -- Options for createMachineIndexHandler

Keyboard Interactions

KeyContextAction
Shift+MAny non-editable elementToggle between human and machine mode
RMachine mode, non-editableSwitch between rendered and raw format

Both shortcuts are disabled when focus is on an <input>, <textarea>, <select>, or contentEditable element.

Styling

The styles from @prototyperai/machine-mode/styles.css define machine-specific design tokens using OKLCH color values. The same tokens apply in both light and dark mode (machine mode is always dark).

TokenDefaultPurpose
--machine-bg2.8% 0 0Shell background
--machine-panel4.4% 0 0Panel/header background
--machine-panel-raised7.2% 0 0Raised panel background
--machine-fg98% 0 0Primary text
--machine-fg-muted88% 0 0Body text
--machine-fg-subtle72% 0 0De-emphasized text
--machine-border100% 0 0 / 0.22Border color
--machine-rule100% 0 0 / 0.3Separator/rule color
--machine-inverse96% 0 0Inverse background (active toggle)
--machine-inverse-fg8% 0 0Inverse foreground text
--machine-font-size-base0.95remBase font size
--machine-line-height-base1.7Base line height
--machine-heading-weight640Heading font weight
--machine-code-size0.84remCode block font size
--machine-content-max76chMax content width
--machine-section-gap2.5remGap before h2 headings
--machine-block-gap1remGap between block elements
--machine-rail-offsetclamp(0.9rem, 2vw, 1.45rem)Horizontal content padding

Override any token in your own CSS to customize the machine mode appearance.

Migration From Local Implementation

  1. Install @prototyperai/machine-mode@beta.
  2. Move app-specific content resolution into lib/machine-resolver.ts.
  3. Replace local mode provider/gate/shell imports with package imports from @prototyperai/machine-mode/client.
  4. Replace local route handler logic with createMachineRouteHandler and createMachineIndexHandler from @prototyperai/machine-mode/server.
  5. Replace local machine CSS with @import "@prototyperai/machine-mode/styles.css".
  6. Keep existing /llms* endpoints untouched -- they are separate from machine mode.

Notes

  • App Router only (Pages Router is not supported).
  • Machine mode is explicit via URL query params -- no bot/user-agent auto-detection.
  • The package is additive; your existing component workflow stays the same.
  • The barrel entry point (@prototyperai/machine-mode) re-exports everything from both /client and /server. Use the sub-path imports when you need tree-shaking or want to be explicit about the environment.

On this page