npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

pagiera

v0.2.0-alpha.42

Published

Full-stack visual website editor with PostgreSQL, Redis and OpenRouter

Readme

Pagiera

Pagiera visual website builder

Website · GitHub · Issues

Pagiera is a full-stack visual website builder for React and Next.js. It ships the editor, responsive canvas, page management, reusable components, templates, AI generation, server-side data fetching, publishing runtime, PostgreSQL persistence, and Redis caching as one installable package.

Design in the editor and render the same document on the server. Published pages use semantic HTML, support dynamic routes and query parameters, and can load external API data before HTML is sent to the browser.

Highlights

  • Framer-style visual editor with an infinite canvas
  • Responsive breakpoints with isolated overrides
  • Free and flow-based layouts
  • Reusable components and variants
  • Layers, assets, templates, icons, and component library
  • Hover, press, entrance, loop, and scroll effects
  • Custom easing curves and spring controls
  • Site-wide fonts provided through next/font
  • AI-assisted site generation through OpenRouter and the AI SDK
  • GET, POST, PUT, PATCH, and DELETE data sources
  • Path parameters, query parameters, headers, and request bodies
  • Server-side Request and Repeat blocks
  • Dynamic routes such as /blog/:slug
  • Draft preview and published page rendering
  • PostgreSQL persistence and optimistic revisions
  • Redis-backed publishing cache and AI rate limiting
  • GitHub-backed template catalog with browser caching and offline fallbacks
  • Semantic output including main, section, nav, header, footer, headings, links, paragraphs, and buttons

Requirements

  • Node.js 20+
  • React 18.3+
  • Next.js App Router for the full-stack integration
  • PostgreSQL
  • Redis
  • OpenRouter API key for AI generation

Installation

npm install pagiera
bun add pagiera

Environment variables

Create .env.local:

PAGIERA_POSTGRES_URL=postgresql://postgres:postgres@localhost:5432/pagiera
PAGIERA_REDIS_URL=redis://localhost:6379
OPENROUTER_API_KEY=sk-or-v1-your-key
OPENROUTER_MODEL=anthropic/claude-sonnet-4.5
# Optional upstream override; the editor-facing endpoint is created by Pagiera
PAGIERA_TEMPLATE_REGISTRY_URL=https://raw.githubusercontent.com/voilabs/pagiera/main/templates/registry.json

Pagiera creates its required PostgreSQL tables when the server initializes. Redis is used for published-page caching, template bundle caching, and AI rate limiting. Template installation sends only the selected template ID to your backend; the backend downloads and validates the configured registry bundle before replacing the project atomically.

Next.js setup

1. Import the editor stylesheet

/* src/app/globals.css */
@import "tailwindcss";
@import "pagiera/full.css";

full.css contains the complete studio UI. Use pagiera/styles.css only with the smaller PagieraEditor API.

Pagiera uses @fontsource-variable/figtree for its editor chrome, including Latin and Latin Extended glyphs. The host application does not need to configure an editor font.

2. Add the font provider

The provider makes next/font fonts available in Typography → Site font. A selection applies to the canvas, preview, and published site.

// src/app/layout.tsx
import { Geist, Geist_Mono } from "next/font/google";
import { PagieraProvider } from "pagiera/provider";
import type { ReactNode } from "react";
import "./globals.css";

const geistSans = Geist({
  variable: "--font-geist-sans",
  subsets: ["latin"],
});

const geistMono = Geist_Mono({
  variable: "--font-geist-mono",
  subsets: ["latin"],
});

export default function RootLayout({ children }: { children: ReactNode }) {
  return (
    <html lang="en">
      <body>
        <PagieraProvider
          fonts={[
            { variable: geistSans.variable, title: "Geist Sans" },
            { variable: geistMono.variable, title: "Geist Mono" },
          ]}
        >
          {children}
        </PagieraProvider>
      </body>
    </html>
  );
}
type PagieraFont = {
  variable: string;
  title: string;
};

3. Mount the backend

// src/app/api/pagiera/[...path]/route.ts
import {
  createPagieraRouteHandlers,
  pagieraConfigFromEnv,
} from "pagiera/server";

export const maxDuration = 120;
export const dynamic = "force-dynamic";

const handlers = createPagieraRouteHandlers(pagieraConfigFromEnv());
export const { GET, POST, PUT, PATCH, DELETE } = handlers;

Open /api/pagiera/health after starting the application to verify PostgreSQL, Redis, and the configured OpenRouter model.

4. Load editor data on the server

// src/lib/editor-bootstrap.ts
import {
  getPagieraServer,
  pagieraConfigFromEnv,
} from "pagiera/server";

export async function editorBootstrap(pageId?: string) {
  const server = await getPagieraServer(pagieraConfigFromEnv());
  const bootstrap = await server.getEditorBootstrap(pageId);

  if (!bootstrap) throw new Error("Editor page not found");
  return bootstrap;
}

5. Mount the studio

The initial document is server-rendered. A client wrapper handles editing and in-editor navigation.

// src/app/editor/pagiera-editor.tsx
"use client";

import { createPagieraClient, editorPanel, editorPath } from "pagiera";
import PagieraStudio from "pagiera/full";
import type { PagieraStudioProps } from "pagiera/full";
import { useRouter } from "next/navigation";
import { useMemo, useState } from "react";

type Bootstrap = {
  page: PagieraStudioProps["page"];
  pages: PagieraStudioProps["pages"];
  library: PagieraStudioProps["library"];
};

export function PagieraEditorClient({ initial, initialPanel }: {
  initial: Bootstrap;
  initialPanel: string;
}) {
  const router = useRouter();
  const client = useMemo(() => createPagieraClient(), []);
  const [bootstrap, setBootstrap] = useState(initial);
  const editorHref = (pageId: string, panel?: string) =>
    editorPath(pageId, editorPanel(panel) ?? "layers");

  return (
    <PagieraStudio
      page={bootstrap.page}
      pages={bootstrap.pages}
      library={bootstrap.library}
      initialPanel={initialPanel}
      adapters={{
        ...client.adapters,
        editorHref,
        navigate: async (pageId, options) => {
          const next = await client.bootstrap(pageId) as Bootstrap;
          setBootstrap(next);
          const panel = window.location.pathname.split("/").filter(Boolean).at(-1);
          const href = editorHref(pageId, panel);
          options?.replace ? router.replace(href) : router.push(href);
        },
        refresh: () => {
          void client.bootstrap(bootstrap.page.id).then((next) => {
            setBootstrap(next as Bootstrap);
          });
        },
        previewHref: (pageId) => `/preview/${encodeURIComponent(pageId)}`,
        publishedHref: (slug) => slug === "home" ? "/" : `/${slug}`,
      }}
    />
  );
}
// src/app/editor/page.tsx
import { editorBootstrap } from "@/lib/editor-bootstrap";
import { editorPath } from "pagiera";
import { redirect } from "next/navigation";

export const dynamic = "force-dynamic";

export default async function EditorPage() {
  const initial = await editorBootstrap();
  redirect(editorPath(initial.page.id));
}
// src/app/editor/[pageId]/[panel]/page.tsx
import { editorBootstrap } from "@/lib/editor-bootstrap";
import { editorPanel, editorPath } from "pagiera";
import { redirect } from "next/navigation";
import { PagieraEditorClient } from "../../pagiera-editor";

export const dynamic = "force-dynamic";

export default async function EditorDocumentPage({ params }: {
  params: Promise<{ pageId: string; panel: string }>;
}) {
  const { pageId, panel: rawPanel } = await params;
  const panel = editorPanel(rawPanel);
  if (!panel) redirect(editorPath(pageId));
  return (
    <PagieraEditorClient
      initial={await editorBootstrap(pageId)}
      initialPanel={panel}
    />
  );
}

Place this server page at src/app/editor/[pageId]/[panel]/page.tsx. Passing initialPanel makes the selected surface part of the server render, so direct links do not flash Layers before opening their requested panel. Pagiera then keeps editor surfaces in shareable paths such as /editor/{pageId}/layers, /editor/{pageId}/pages, and /editor/{pageId}/templates; the old ?tab= form is migrated automatically.

Publishing pages

Load published documents on the server. Request blocks then finish before HTML is returned, making API-backed content available to search engines.

// src/lib/published-page.tsx
import { notFound } from "next/navigation";
import { RenderedPage } from "pagiera/runtime";
import {
  getPagieraServer,
  pagieraConfigFromEnv,
} from "pagiera/server";

type SearchParams = Promise<Record<string, string | string[] | undefined>>;

export async function renderPublishedPage(
  slug: string,
  searchParams: SearchParams,
  params: Record<string, string> = {},
) {
  const rawQuery = await searchParams;
  const query = Object.fromEntries(
    Object.entries(rawQuery).map(([key, value]) => [
      key,
      Array.isArray(value) ? value[0] ?? "" : value ?? "",
    ]),
  );

  const server = await getPagieraServer(pagieraConfigFromEnv());
  const page = await server.getPublishedPage(slug, {
    query,
    params,
    page: { slug },
  });

  if (!page) notFound();

  return (
    <RenderedPage
      elements={page.elements}
      rootStyle={page.rootStyle}
      data={page.data}
    />
  );
}

The home page is permanent and maps to /:

// src/app/page.tsx
import { renderPublishedPage } from "@/lib/published-page";

export const dynamic = "force-dynamic";

export default function HomePage({ searchParams }: {
  searchParams: Promise<Record<string, string | string[] | undefined>>;
}) {
  return renderPublishedPage("home", searchParams);
}

Other pages can use a catch-all route:

// src/app/[...path]/page.tsx
import { renderPublishedPage } from "@/lib/published-page";

export const dynamic = "force-dynamic";

export default async function PublishedPage({ params, searchParams }: {
  params: Promise<{ path: string[] }>;
  searchParams: Promise<Record<string, string | string[] | undefined>>;
}) {
  return renderPublishedPage((await params).path.join("/"), searchParams);
}

Template registry

The existing Pagiera catch-all handler automatically exposes the editor catalog at:

/api/pagiera/templates/registry.json

No second API route, copied JSON, or filesystem setup is required. The package endpoint fetches the public GitHub Raw/CDN registry, caches it through Redis and HTTP caching, serves package-generated thumbnails, and falls back to bundled templates when the upstream catalog is unavailable.

You may still point the studio at a custom browser-facing catalog:

<PagieraStudio
  {...props}
  templateRegistryUrl="https://example.com/pagiera/registry.json"
/>

New templates live under the repository's templates/ directory. They can be released independently from npm package versions. See templates/README.md for the registry schema and contribution workflow.

Dynamic routes and request data

Page slugs may contain named parameters:

blog/:slug

For /blog/1, pass the matched values in the server context:

{
  params: { slug: "1" },
  query: { preview: "true" }
}

Request URLs, headers, query fields, and bodies may reference context values:

https://dummyjson.com/posts/{{params.slug}}
https://api.example.com/search?q={{query.q}}

Request blocks expose one returned object directly to descendants. Repeat blocks iterate array results. Both are resolved on the server for published pages.

An upstream 404 can turn the complete route into a 404 page when the data source uses the page-404 behavior.

Preview routes

// src/app/preview/[pageId]/page.tsx
import { notFound } from "next/navigation";
import { RenderedPage } from "pagiera/runtime";
import {
  getPagieraServer,
  pagieraConfigFromEnv,
} from "pagiera/server";

export const dynamic = "force-dynamic";

export default async function PreviewPage({ params }: {
  params: Promise<{ pageId: string }>;
}) {
  const server = await getPagieraServer(pagieraConfigFromEnv());
  const page = await server.getPreviewPage((await params).pageId);
  if (!page) notFound();

  return (
    <RenderedPage
      elements={page.elements}
      rootStyle={page.rootStyle}
      data={page.data}
    />
  );
}

Server API

import {
  createPagieraRouteHandlers,
  createPagieraServer,
  getPagieraServer,
  pagieraConfigFromEnv,
} from "pagiera/server";

getPagieraServer(config) returns a shared server with:

  • handle(request) — process a Pagiera API request
  • getEditorBootstrap(pageId?) — load editor document, pages, and library
  • getPreviewPage(pageId, context?) — load a draft and resolve data
  • getPublishedDocument(slug) — load published document metadata
  • getPublishedPage(slug, context?) — load published document and SSR data
  • close() — close PostgreSQL and Redis connections
  • pool — PostgreSQL pool
  • redis — Redis client
type PagieraServerConfig = {
  postgresUrl: string;
  redisUrl: string;
  openRouterApiKey: string;
  openRouterModel: string;
  basePath?: string;
  aiRateLimitPerMinute?: number;
  templateRegistryUrl?: string;
};

Client API

import { createPagieraClient } from "pagiera";

const client = createPagieraClient({
  baseUrl: "/api/pagiera",
});

The client includes:

  • bootstrap(pageId?)
  • loadPage(pageId)
  • adapters.save(...)
  • adapters.createPage(...)
  • adapters.renamePage(...)
  • adapters.duplicatePage(...)
  • adapters.deletePage(...)
  • adapters.installTemplate(...)
  • adapters.publishPage(...)
  • adapters.unpublishPage(...)
  • adapters.previewSource(...)
  • adapters.generate(...)

Package exports

| Import | Purpose | | --- | --- | | pagiera | Document utilities, lightweight editor, renderer, client adapters | | pagiera/provider | Provider, next/font integration, font types | | pagiera/full | Complete visual studio component | | pagiera/full.css | Complete studio stylesheet | | pagiera/runtime | Published and preview renderer | | pagiera/data | Server-side page data loader | | pagiera/server | PostgreSQL, Redis, OpenRouter, API, publishing server | | pagiera/styles.css | Lightweight editor stylesheet |

Lightweight editor

"use client";

import { useState } from "react";
import {
  createDocument,
  PagieraEditor,
  type PagieraDocument,
} from "pagiera";
import "pagiera/styles.css";

export function SmallEditor() {
  const [document, setDocument] = useState<PagieraDocument>(() => (
    createDocument()
  ));

  return (
    <PagieraEditor
      value={document}
      onChange={setDocument}
      adapters={{
        save: async (nextDocument) => saveDocument(nextDocument),
      }}
    />
  );
}

Security

The Pagiera API can create, modify, publish, and delete content. Protect /editor, /preview, and write operations under /api/pagiera with your application's authentication and authorization layer before deploying publicly.

Never expose PAGIERA_POSTGRES_URL, PAGIERA_REDIS_URL, or OPENROUTER_API_KEY to client components. These are server-only values.

Troubleshooting

server.getEditorBootstrap is not a function

Make sure all imports resolve to one installed Pagiera version. Remove stale local tarballs or lockfile entries, reinstall, and verify:

node -p "require('pagiera/package.json').version"

CSS says @import must precede other rules

Upgrade Pagiera. Current builds use @font-face for the editor font and do not distribute a Google Fonts @import.

PostgreSQL or Redis fails

Open /api/pagiera/health, then verify both connection URLs are reachable from the Next.js server process.

AI generation fails

Verify the OpenRouter key, model identifier, model availability, and account credits.

Published API data is missing from HTML

Use server.getPublishedPage(...) or loadPageData(...) in a Server Component before rendering RenderedPage. Client-only fetching does not provide the same SEO behavior.

Local package example

This repository includes apps/example, a standalone workspace consumer of the package.

cd apps/example
cp .env.example .env.local
bun install
bun run dev

Open:

  • /editor — visual editor
  • /preview/:pageId — draft renderer
  • / — published home page
  • /api/pagiera/health — backend diagnostics

License

MIT