Añadir un blog sin cabeza a tu app Next.js en 20 minutos con NextPress

20 minutos. Cinco archivos. A create-next-app proyecto tira publicaciones de una instalación de WordPress sobre WPGraphQL, el editor todavía posee cada párrafo, y next build naves páginas estáticas con revalidate con el número que quieras. El paseo por debajo de los puntos woographqldemo.wpengine.com — intercambio en su propio dominio al final y se aplican los mismos seis pasos.

Qué hay en la caja

@axistaylor/nextpress es un paquete de npm y aproximadamente 6.000 líneas de TipoScript. Lleva tres piezas que alambre en una aplicación Next.js 16:

  • withWCR a NextConfig envoltorio que establece el dominio WP reverso-proxy y hacia adelante /wp-content, /wp-admin, y /graphql a través de la próxima carrera.
  • <Content> — un componente React que toma el WordPress HTML crudo devuelve y lo hace como verdadero Nodos React, más el CSS per-block cada uno necesita.
  • nextImageParser – un parser que cambia cada uno <img> en contenido WP para un next/image con width, height, y sizes De los atributos del bloque.

Requerimiento de backend: WordPress con WPGraphQL activo. Cualquier host funciona — WP Engine, Pantheon, Bluehost, un VPS autogestionado— siempre y cuando /graphql responde.


Paso 1 – Bootstrap la siguiente aplicación

Stock create-next-app con la plantilla TipoScript, a continuación, añadir @axistaylor/nextpress de npm:

npx create-next-app@latest nextpress-quickstart
cd nextpress-quickstart
npm install @axistaylor/nextpress

Paso 2 – next.config.ts

withWCR toma tres argumentos: la base Next config, el origen de WordPress, y el origen de frontend público. El envoltorio reescribirá cada URL de WordPress absoluto que el editor autorizó en un camino relacionado con el frontend, por lo que los enlaces dentro de la tierra de contenido post renderizado en sus Siguientes rutas.

next.config.ts
import type { NextConfig } from "next";
import { withWCR } from "@axistaylor/nextpress/withWCR";

const wpDomain = "woographqldemo.wpengine.com";
const wpProtocol = "https";

const nextConfig: NextConfig = {
  images: {
    formats: ["image/avif", "image/webp"],
    remotePatterns: [{ protocol: "https", hostname: wpDomain }],
  },
  env: {
    GRAPHQL_ENDPOINT: `${wpProtocol}://${wpDomain}/graphql`,
  },
};

export default withWCR(
  nextConfig,
  {
    wpDomain,
    wpProtocol,
    wpHomeUrl: `${wpProtocol}://${wpDomain}`,
    wpSiteUrl: `${wpProtocol}://${wpDomain}`,
  },
  {
    frontendDomain: "localhost:3000",
    frontendProtocol: "http",
  },
);

Paso 3 – src/proxy.ts

Siguiente 16 renombrado middleware.ts a proxy.ts; el comportamiento del rol y del matcher no cambió. El manejador hace dos cosas – adelante el /wp-* caminos a través de proxyByWCR así que el navegador nunca toca el origen de WordPress, y establecer un x-uri header on every non-proxied request so server components downstream know which URI to ask the backend for enqueued scripts and stylesheets:

src/proxy.ts
import { NextResponse, NextRequest } from "next/server";
import { proxyByWCR, isProxiedRoute } from "@axistaylor/nextpress/proxyByWCR";

export const proxy = async (request: NextRequest) => {
  const pathname = request.nextUrl.pathname;

  if (isProxiedRoute(pathname)) {
    return proxyByWCR(request);
  }

  const headers = new Headers(request.headers);
  headers.set("x-uri", pathname);
  return NextResponse.next({ request: { headers } });
};

export const config = {
  matcher: [
    "/atx/:instance/proxiee",
    "/atx/:instance/wp",
    "/atx/:instance/wc",
    "/atx/:instance/wp-internal-assets/:path*",
    "/atx/:instance/wp-assets/:path*",
    "/atx/:instance/wp-json/:path*",
    "/((?!_next|api|favicon.ico|sw.js|.*\\.).*)",
  ],
};

Paso 4 – src/lib/wp.ts

Tres funciones asinc, una compartida fetch Envoltorio. fetchPosts alimenta la página índice, fetchPostBySlug alimenta la página de correo, fetchPostSlugs unidades generateStaticParamsThe next: { revalidate: 60 } la pista fetch es el único control de caché que necesitas — Siguiente maneja el resto:

src/lib/wp.ts
const ENDPOINT = process.env.GRAPHQL_ENDPOINT!;

async function gql<T>(query: string, variables: Record<string, unknown> = {}): Promise<T> {
  const res = await fetch(ENDPOINT, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ query, variables }),
    next: { revalidate: 60 },
  });
  if (!res.ok) throw new Error(`WPGraphQL ${res.status}`);
  const { data, errors } = await res.json();
  if (errors?.length) throw new Error(errors[0].message);
  return data as T;
}

export interface PostSummary {
  slug: string;
  title: string;
  date: string;
  excerpt: string | null;
  featuredImage: { sourceUrl: string; altText: string } | null;
}

export interface Post extends PostSummary {
  content: string;
  contentCssClasses: string;
}

export async function fetchPosts(first = 20): Promise<PostSummary[]> {
  const data = await gql<{ posts: { nodes: PostSummary[] } }>(`
    query Posts($first: Int!) {
      posts(first: $first, where: { status: PUBLISH, orderby: { field: DATE, order: DESC } }) {
        nodes {
          slug title date excerpt
          featuredImage { node { sourceUrl altText } }
        }
      }
    }`, { first });
  return data.posts.nodes;
}

export async function fetchPostSlugs(): Promise<string[]> {
  const data = await gql<{ posts: { nodes: { slug: string }[] } }>(`
    { posts(first: 100, where: { status: PUBLISH }) { nodes { slug } } }`);
  return data.posts.nodes.map(n => n.slug);
}

export async function fetchPostBySlug(slug: string): Promise<Post | null> {
  const data = await gql<{ post: Post | null }>(`
    query Post($slug: ID!) {
      post(id: $slug, idType: SLUG) {
        slug title date excerpt
        content(format: RENDERED)
        contentCssClasses
        featuredImage { node { sourceUrl altText } }
      }
    }`, { slug });
  return data.post;
}

Paso 5 – /blog índice

Un componente servidor, uno awaitNo hay ganchos de cliente. The post array is just JSX:

src/app/blog/page.tsx
import Link from "next/link";
import { fetchPosts } from "@/lib/wp";

export const metadata = { title: "Blog" };

export default async function BlogIndex() {
  const posts = await fetchPosts(20);
  return (
    <ul>
      {posts.map((post) => (
        <li key={post.slug}>
          <Link href={`/blog/${post.slug}`}>{post.title}</Link>
          <time>{new Date(post.date).toLocaleDateString()}</time>
        </li>
      ))}
    </ul>
  );
}

Etapa 6 – /blog/[slug]

generateStaticParams convierte cada slug publicado en una ruta de tiempo de construcción. El render es dos líneas: una partida y una <Content>.

src/app/blog/[slug]/page.tsx
import { notFound } from "next/navigation";
import { Content, nextImageParser } from "@axistaylor/nextpress";
import { fetchPostBySlug, fetchPostSlugs } from "@/lib/wp";

export async function generateStaticParams() {
  const slugs = await fetchPostSlugs();
  return slugs.map((slug) => ({ slug }));
}

export default async function PostPage({ params }: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;
  const post = await fetchPostBySlug(slug);
  if (!post) notFound();

  return (
    <article>
      <h1>{post.title}</h1>
      <Content
        content={post.content}
        contentCssClasses={post.contentCssClasses}
        parsers={[nextImageParser()]}
      />
    </article>
  );
}

Cinco archivos, dos dominios, un npx. El editor nunca se mueve; la capa de entrega cambia debajo de ella.

The WordPress block editor on the left and the same content rendered through Next.js on the right — paragraph, heading, and list blocks come across identically with no template duplication.

¿Qué? <Content> lo hace por ti

El cuerpo de correo es una cadena de WordPress bloque HTML. Pastarlo en un dangerouslySetInnerHTML lo haría, pero perderías tres cosas en el proceso. <Content> los maneja:

  • Pare el HTML en un árbol de reacción. Cada uno <p>, <figure>, <blockquote> es un verdadero nodo React que puede anular con un analizador – así es como nextImageParser swaps <img> para next/image con cero cambios de marcado arriba.
  • Tira el CSS de cada bloque en la respuesta. Bloques soporta reglas en línea, tema.json palette tokens, hojas inyectadas por plugins — envían inline con la página por lo que no hay cambio de diseño después de la hidratación.
  • Mantiene <script type="application/json"> Los bloques intactos. Así es como bloques interactivos (motores de movimiento, acordeones, cualquier cosa con una vista-script) a su código lado cliente sin una ida y vuelta API independiente.

Siguientes movimientos

Los mismos seis archivos se extienden en algunas direcciones obvias. Categorías y etiquetas son una más WPGraphQL where argumentación fetchPosts. Comentarios y paseo por paginación en la misma consulta. La búsqueda golpea el search argumento y usted envía un /search?q=... ruta en otros veinte minutos.

Fuente completa GitHub. Docs de paquetes en /docs/nextpress. Si quieres la arquitectura por qué guardar WordPress y añadir Next.js como una capa de entrega, esa pieza está llegando.



Leave a Reply

Your email address will not be published. Required fields are marked *