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+Mto toggle,Rto 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 point | Environment | Description |
|---|---|---|
@prototyperai/machine-mode | Both | Barrel re-export of client + server |
@prototyperai/machine-mode/client | Client | Provider, gate, shell, toggle, hooks |
@prototyperai/machine-mode/server | Server | Route handlers, resolver types |
@prototyperai/machine-mode/styles.css | CSS | Machine mode theme tokens and utilities |
Quick Start (CLI)
npx @prototyperai/cli machine-mode initThis scaffolds:
lib/machine-resolver.tsapp/machine/[...slug]/route.tsapp/machine.txt/route.tsapp/layout.tsxwiring withMachineModeProvider+MachineGateglobals.cssimport for@prototyperai/machine-mode/styles.css
Manual Setup
Install package
pnpm add @prototyperai/machine-mode@betanpm install @prototyperai/machine-mode@betayarn add @prototyperai/machine-mode@betabun add @prototyperai/machine-mode@betaImport styles
@import "@prototyperai/machine-mode/styles.css";Wire layout
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.
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
import { createMachineRouteHandler } from "@prototyperai/machine-mode/server"
import { resolveMachineDoc } from "@/lib/machine-resolver"
export const GET = createMachineRouteHandler({ resolveDoc: resolveMachineDoc })import { createMachineIndexHandler } from "@prototyperai/machine-mode/server"
export const GET = createMachineIndexHandler()Resolver Contract
Your resolver receives a normalized pathname and returns either:
- A
MachineDocobject for known routes nullto 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): booleanPathname 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[]): stringExported Types
The client entry also exports these TypeScript types:
MachineViewMode--"human" | "machine"MachineRenderMode--"rendered" | "raw"MachineModeConfig-- Resolved provider configuration (seeuseMachineModeabove)MachineModeProviderProps-- Props forMachineModeProviderMachineMarkdownRenderResult-- Return type ofrenderMachineMarkdown
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 }
): MachineDocExported Types
The server entry also exports these TypeScript types:
MachineDoc-- Machine document shape returned by resolversMachineContentType--"docs" | "curated" | "fallback" | (string & {})MachineDocResolver-- Resolver function signatureMachineRouteHandlerOptions-- Options forcreateMachineRouteHandlerMachineIndexHandlerOptions-- Options forcreateMachineIndexHandler
Keyboard Interactions
| Key | Context | Action |
|---|---|---|
Shift+M | Any non-editable element | Toggle between human and machine mode |
R | Machine mode, non-editable | Switch 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).
| Token | Default | Purpose |
|---|---|---|
--machine-bg | 2.8% 0 0 | Shell background |
--machine-panel | 4.4% 0 0 | Panel/header background |
--machine-panel-raised | 7.2% 0 0 | Raised panel background |
--machine-fg | 98% 0 0 | Primary text |
--machine-fg-muted | 88% 0 0 | Body text |
--machine-fg-subtle | 72% 0 0 | De-emphasized text |
--machine-border | 100% 0 0 / 0.22 | Border color |
--machine-rule | 100% 0 0 / 0.3 | Separator/rule color |
--machine-inverse | 96% 0 0 | Inverse background (active toggle) |
--machine-inverse-fg | 8% 0 0 | Inverse foreground text |
--machine-font-size-base | 0.95rem | Base font size |
--machine-line-height-base | 1.7 | Base line height |
--machine-heading-weight | 640 | Heading font weight |
--machine-code-size | 0.84rem | Code block font size |
--machine-content-max | 76ch | Max content width |
--machine-section-gap | 2.5rem | Gap before h2 headings |
--machine-block-gap | 1rem | Gap between block elements |
--machine-rail-offset | clamp(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
- Install
@prototyperai/machine-mode@beta. - Move app-specific content resolution into
lib/machine-resolver.ts. - Replace local mode provider/gate/shell imports with package imports from
@prototyperai/machine-mode/client. - Replace local route handler logic with
createMachineRouteHandlerandcreateMachineIndexHandlerfrom@prototyperai/machine-mode/server. - Replace local machine CSS with
@import "@prototyperai/machine-mode/styles.css". - 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/clientand/server. Use the sub-path imports when you need tree-shaking or want to be explicit about the environment.