NextPressで20分でNext.jsアプリにヘッドレスブログを追加

20分 5つのファイル。 ツイート create-next-app プロジェクトはWordPressから投稿をWPGraphQLにインストールし、エディタはすべての段落を所有しています。 next build 静的なページを出荷する revalidate 必要な数にセットします。 下記のポイントを通り抜ける woographqldemo.wpengine.com — エンドで自分のドメインを交換し、同じ6つのステップが適用されます。

箱にあるもの

@axistaylor/nextpress 1つのnpmパッケージと、約6,000行のTypeScriptです。 次の.js 16アプリに3枚のワイヤを転送します。

  • withWCR ・・・ NextConfig WP逆プロキシドメインとフォワードを設定するラッパー /wp-content, /wp-admin/graphql 次のランタイムを通して。
  • <Content> — 生の HTML の WordPress が返し、実際の React ノードとしてレンダリングする React コンポーネントと、各々の 1 つのニーズごとに per-block CSS をそれぞれ取得します。
  • nextImageParser — あらゆるものを交換するパーサー <img> WPコンテンツで next/image お問い合わせ width, heightsizes ブロック属性から引き出される。

バックエンドの要件: WordPress と WPグラフQL アクティブ。 すべてのホストは、WPエンジン、パンテオン、ブルーホスト、セルフマネージドVPS – 限り /graphql お問い合わせ


ステップ1 – 次のアプリをブートストラップ

株式について create-next-app TypeScript テンプレートで、次に追加します。 @axistaylor/nextpress npmから:

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

ステップ2 — next.config.ts

withWCR ベース・ネクスト・コンフィグ、WordPressの起源、およびパブリック・フェース・フロントエンド・オリジンの3つの引数を取ります。 wrapper は、エディタがフロントエンドの相対パスに承認したすべての絶対的な WordPress URL を書き換えるので、次のルートにレンダリングされた投稿コンテンツのランドバック中にリンクします。

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",
  },
);

ステップ3 — src/proxy.ts

次 16 名前を変更しました middleware.ts お問い合わせ proxy.ts; 役割と一致行動が変更されなかった。 ハンドラは2つのことをします。 /wp-* パススルー proxyByWCR そのため、ブラウザはWordPressの起源に触れず、設定しません。 x-uri すべての非プロキシリクエストのヘッダなので、サーバーコンポーネントのダウンストリームは、 URI が、エンキューされたスクリプトとスタイルシートのバックエンドを尋ねるかどうかを知っています。

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|.*\\.).*)",
  ],
};

ステップ4 — src/lib/wp.ts

3つの非同期機能、1つの共有 fetch ラッパー。 fetchPosts インデックスページをフィードします。 fetchPostBySlug 投稿ページのフィード fetchPostSlugs ドライブ generateStaticParams. ザ・ next: { revalidate: 60 } ヒント fetch 必要な唯一のキャッシュ制御です。次は残りの処理を処理します。

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;
}

ステップ5 — /blog インデックス

サーバーコンポーネント、1 await、顧客のホック無し。 投稿配列は 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>
  );
}

ステップ6 — /blog/[slug]

generateStaticParams 公開されたすべてのスラグをビルド時のルートに変換します。 レンダーは2行です。ヘディングとヘディング <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>
  );
}

5つのファイル、2つのドメイン、1つのnpx。 エディタは移動しません。 配信層はそれの下に変化します。

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.

新着情報 <Content> お問い合わせ

ポストボディは、WordPressブロックHTMLの文字列です。 それを dangerouslySetInnerHTML それをレンダリングしますが、プロセスの3つの事を失います。 <Content> それらを扱います:

  • HTML を React ツリーにパースします。 お問い合わせ <p>, <figure>, <blockquote> パーサでオーバーライドできるReactノードです。 nextImageParser スワップ <img> お問い合わせ next/image マークアップがゼロで上流に変化します。
  • 各ブロックのCSSを応答に引きます。 Block- はインラインルール、 theme.json パレットトークン、プラグイン・インジェクトシートをサポートしており、ページでインラインを出荷しているため、水分補給後のレイアウトシフトはありません。
  • 続ける <script type="application/json"> blobs 不正確な. つまり、インタラクティブブロック(モーションエンジン、アコーディオン、ビュースクリプト付き何でも)ハンド状態を、別のAPIの往復なしでクライアント側コードにする方法です。

次の動き

同じ6つのファイルは、いくつかの明らかな指示で拡張されます。 カテゴリーとタグが1つあります WPGraphQL where 引数 fetchPosts. 同じクエリでコメントやパジネーションに乗る。 ヒット検索 search 引数 と 発送 /search?q=... 別の20分のルート。

完全なソース GitHubで. パッケージ docs /docs/ネクストプレス. WordPressを保ちながら、次へ.jsを配信レイヤーとして追加する建築を望むなら、その作品が来ています。



Leave a Reply

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