update
This commit is contained in:
15
src/app/[slug]/error.tsx
Normal file
15
src/app/[slug]/error.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
'use client';
|
||||
|
||||
export default function Error() {
|
||||
return (
|
||||
<div className="container mx-auto py-16 max-w-3xl text-center">
|
||||
<h1 className="text-2xl font-semibold mb-4">
|
||||
500 — Сервер временно недоступен
|
||||
</h1>
|
||||
<p className="text-gray-600">
|
||||
Страница не может быть загружена прямо сейчас.
|
||||
Пожалуйста, попробуйте позже.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
47
src/app/[slug]/page.tsx
Normal file
47
src/app/[slug]/page.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
// app/[slug]/page.tsx
|
||||
import { renderPostContent } from '@/components/WPRenderer/WPRenderer';
|
||||
|
||||
// ISR: regenerate every 60 seconds
|
||||
export const revalidate = 60;
|
||||
|
||||
interface PageProps {
|
||||
params: {
|
||||
slug: string;
|
||||
};
|
||||
}
|
||||
|
||||
export default async function PostPage({ params }: PageProps) {
|
||||
const { slug } = await params;
|
||||
const baseUrl =
|
||||
process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000';
|
||||
|
||||
const res = await fetch(`${baseUrl}/api/posts/${slug}`, {
|
||||
next: { revalidate: 60 },
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
console.log(`${baseUrl}/api/posts/${slug}`)
|
||||
// 🚨 THROW — this is critical
|
||||
throw new Error('Failed to fetch post');
|
||||
}
|
||||
|
||||
const post = await res.json();
|
||||
|
||||
return (
|
||||
<div className="container mx-auto py-8 max-w-5xl">
|
||||
<article className="prose lg:prose-xl max-w-none">
|
||||
<h1>{post.post_title}</h1>
|
||||
|
||||
{post.post_type === 'post' && (
|
||||
<div className="text-gray-600 mb-6">
|
||||
<time>
|
||||
{new Date(post.post_date).toLocaleString('ru-RU')}
|
||||
</time>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{renderPostContent(post.post_content)}
|
||||
</article>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
71
src/app/[slug]/page/[page]/page.tsx
Normal file
71
src/app/[slug]/page/[page]/page.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import PostGrid from "@/components/WP/PostGrid/PostGrid.server";
|
||||
import { renderPostContent } from "@/components/WPRenderer/WPRenderer";
|
||||
import parse, { DOMNode } from 'html-react-parser'
|
||||
|
||||
interface PageProps {
|
||||
params: { slug: string; page: string };
|
||||
}
|
||||
|
||||
export function extractPostGridId(content: string): number | null {
|
||||
let gridId: number | null = null;
|
||||
|
||||
parse(content, {
|
||||
replace: (domNode: DOMNode) => {
|
||||
// Only check text nodes
|
||||
if ("data" in domNode && typeof domNode.data === "string") {
|
||||
const match = domNode.data.match(/\[the-post-grid\s+id="(\d+)"/);
|
||||
if (match) {
|
||||
gridId = Number(match[1]);
|
||||
return; // stop parsing
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return gridId;
|
||||
}
|
||||
|
||||
|
||||
export default async function PostPage({ params }: PageProps) {
|
||||
const { slug, page } = await params;
|
||||
const pageNum = Number(page) || 1;
|
||||
|
||||
// 1. Fetch the WP page content (same as page 1)
|
||||
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/pages/${slug}`, {
|
||||
next: { revalidate: 60 },
|
||||
});
|
||||
const pageData = await res.json();
|
||||
|
||||
if (!pageData) return <p>Page not found</p>;
|
||||
|
||||
// 2. Extract grid ID from shortcode
|
||||
const gridId = extractPostGridId(pageData.post_content);
|
||||
|
||||
return (
|
||||
<div className="container mx-auto py-8 max-w-5xl">
|
||||
<article className="prose lg:prose-xl max-w-none">
|
||||
<h1>{pageData.post_title}</h1>
|
||||
|
||||
{pageData.post_type === 'post' && (
|
||||
<div className="text-gray-600 mb-6">
|
||||
<time>
|
||||
{new Date(pageData.post_date).toLocaleString('ru-RU')}
|
||||
</time>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{renderPostContent(pageData.post_content, { page: pageNum })}
|
||||
</article>
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<article>
|
||||
<h1>{pageData.title}</h1>
|
||||
|
||||
{/* {gridId && <PostGrid id={gridId} page={pageNum} />} */}
|
||||
{renderPostContent(pageData.post_content, { page: pageNum })}
|
||||
|
||||
</article>
|
||||
);
|
||||
}
|
||||
16
src/app/error.tsx
Normal file
16
src/app/error.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
// app/error.tsx
|
||||
'use client';
|
||||
|
||||
export default function Error() {
|
||||
return (
|
||||
<div className="container mx-auto py-16 text-center">
|
||||
<h1 className="text-2xl font-semibold mb-4">
|
||||
500 — Сервис временно недоступен
|
||||
</h1>
|
||||
<p className="text-gray-600">
|
||||
Главная страница временно недоступна.
|
||||
Пожалуйста, попробуйте позже.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
38
src/app/layout.tsx
Normal file
38
src/app/layout.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import Header from "@/components/Blocks/Header/Header";
|
||||
import Footer from "@/components/Footer/Footer";
|
||||
import "@/styles/globals.css";
|
||||
import { Montserrat, Roboto, Roboto_Condensed } from 'next/font/google'
|
||||
|
||||
export const montserratFont = Montserrat({
|
||||
subsets: ['latin', 'cyrillic'],
|
||||
})
|
||||
|
||||
const mainFont = Roboto({
|
||||
subsets: ['latin', 'cyrillic'],
|
||||
})
|
||||
|
||||
export const condensedFont = Roboto_Condensed({
|
||||
subsets: ['latin', 'cyrillic'],
|
||||
})
|
||||
|
||||
export const revalidate = 10
|
||||
|
||||
export default function GlobalLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html lang="en" className={mainFont.className}>
|
||||
<body className="relative min-h-screen flex flex-col overflow-auto">
|
||||
<Header />
|
||||
|
||||
<main className="flex-1 px-4 sm:px-0 overflow-x-auto">
|
||||
{children}
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
9
src/app/nothing[...puckPath]/client.tsx
Normal file
9
src/app/nothing[...puckPath]/client.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import type { Data } from "@puckeditor/core";
|
||||
import { Render } from "@puckeditor/core";
|
||||
import puckConfig from "../../../puck.config";
|
||||
|
||||
export function Client({ data }: { data: Data }) {
|
||||
return <Render config={puckConfig} data={data} />
|
||||
}
|
||||
50
src/app/nothing[...puckPath]/page.tsx
Normal file
50
src/app/nothing[...puckPath]/page.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* This file implements a catch-all route that renders the user-facing pages
|
||||
* generated by Puck. For any route visited (with exception of other hardcoded
|
||||
* pages in /app), it will check your database (via `getPage`) for a Puck page
|
||||
* and render it using <Render>.
|
||||
*
|
||||
* All routes produced by this page are statically rendered using incremental
|
||||
* static site generation. After the first visit, the page will be cached as
|
||||
* a static file. Subsequent visits will receive the cache. Publishing a page
|
||||
* will invalidate the cache as the page is written in /api/puck/route.ts
|
||||
*/
|
||||
|
||||
import { Client } from "./client";
|
||||
import { notFound } from "next/navigation";
|
||||
import { Metadata } from "next";
|
||||
import { getPage } from "@/lib/get-page";
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ puckPath: string[] }>;
|
||||
}): Promise<Metadata> {
|
||||
const { puckPath = [] } = await params;
|
||||
const path = `/${puckPath.join("/")}`;
|
||||
|
||||
const data = getPage(path)
|
||||
return {
|
||||
title: data?.root.props?.title,
|
||||
};
|
||||
}
|
||||
|
||||
export default async function Page({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ puckPath: string[] }>;
|
||||
}) {
|
||||
const { puckPath = [] } = await params;
|
||||
const path = `/${puckPath.join("/")}`;
|
||||
const data = await getPage(path);
|
||||
|
||||
if (!data) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
return <Client data={data} />;
|
||||
}
|
||||
|
||||
// Force Next.js to produce static pages: https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#dynamic
|
||||
// Delete this if you need dynamic rendering, such as access to headers or cookies
|
||||
export const dynamic = "force-static";
|
||||
28
src/app/page.tsx
Normal file
28
src/app/page.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
// app/page.tsx
|
||||
import { renderPostContent } from '@/components/WPRenderer/WPRenderer';
|
||||
import { PostData } from '@/types/entities';
|
||||
|
||||
export const revalidate = 10;
|
||||
|
||||
export default async function HomePage() {
|
||||
const baseUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000';
|
||||
|
||||
const res = await fetch(`${baseUrl}/api/home`, { next: { revalidate: 10 }, });
|
||||
|
||||
if (!res.ok) {
|
||||
// 🚨 REQUIRED for stale reuse
|
||||
throw new Error('Failed to fetch home posts');
|
||||
}
|
||||
|
||||
const posts: PostData[] = await res.json();
|
||||
|
||||
return (
|
||||
<div className="w-full flex flex-col items-center">
|
||||
{posts.map(post => (
|
||||
<div key={post.ID} className="max-w-5xl">
|
||||
{renderPostContent(post.post_content)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
27
src/app/proxy.txt
Normal file
27
src/app/proxy.txt
Normal file
@@ -0,0 +1,27 @@
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
import type { NextRequest } from "next/server";
|
||||
|
||||
export async function proxy(req: NextRequest) {
|
||||
const res = NextResponse.next({ request: req });
|
||||
|
||||
if (req.method === "GET") {
|
||||
// Rewrite routes that match "/[...puckPath]/edit" to "/puck/[...puckPath]"
|
||||
if (req.nextUrl.pathname.endsWith("/edit")) {
|
||||
const pathWithoutEdit = req.nextUrl.pathname.slice(
|
||||
0,
|
||||
req.nextUrl.pathname.length - 5
|
||||
);
|
||||
const pathWithEditPrefix = `/puck${pathWithoutEdit}`;
|
||||
|
||||
return NextResponse.rewrite(new URL(pathWithEditPrefix, req.url));
|
||||
}
|
||||
|
||||
// Disable "/puck/[...puckPath]"
|
||||
if (req.nextUrl.pathname.startsWith("/puck")) {
|
||||
return NextResponse.redirect(new URL("/", req.url));
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
20
src/app/puck/[...puckPath]/client.tsx
Normal file
20
src/app/puck/[...puckPath]/client.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
"use client";
|
||||
|
||||
import type { Data } from "@puckeditor/core";
|
||||
import { Puck } from "@puckeditor/core";
|
||||
import puckConfig from "../../../../puck.config";
|
||||
|
||||
export function Client({ path, data }: { path: string; data: Partial<Data> }) {
|
||||
return (
|
||||
<Puck
|
||||
config={puckConfig}
|
||||
data={data}
|
||||
onPublish={async (data) => {
|
||||
await fetch("/puck/api", {
|
||||
method: "post",
|
||||
body: JSON.stringify({ data, path }),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
44
src/app/puck/[...puckPath]/page.tsx
Normal file
44
src/app/puck/[...puckPath]/page.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* This file implements a *magic* catch-all route that renders the Puck editor.
|
||||
*
|
||||
* This route exposes /puck/[...puckPath], but is disabled by middleware.ts. The middleware
|
||||
* then rewrites all URL requests ending in `/edit` to this route, allowing you to visit any
|
||||
* page in your application and add /edit to the end to spin up a Puck editor.
|
||||
*
|
||||
* This approach enables public pages to be statically rendered whilst the /puck route can
|
||||
* remain dynamic.
|
||||
*
|
||||
* NB this route is public, and you will need to add authentication
|
||||
*/
|
||||
|
||||
import "@puckeditor/core/puck.css";
|
||||
import { Client } from "./client";
|
||||
import { Metadata } from "next";
|
||||
import { getPage } from "@/lib/get-page";
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ puckPath: string[] }>;
|
||||
}): Promise<Metadata> {
|
||||
const { puckPath = [] } = await params;
|
||||
const path = `/${puckPath.join("/")}`;
|
||||
|
||||
return {
|
||||
title: "Puck: " + path,
|
||||
};
|
||||
}
|
||||
|
||||
export default async function Page({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ puckPath: string[] }>;
|
||||
}) {
|
||||
const { puckPath = [] } = await params;
|
||||
const path = `/${puckPath.join("/")}`;
|
||||
const data = getPage(path);
|
||||
|
||||
return <Client path={path} data={data || {}} />;
|
||||
}
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
25
src/app/puck/api/route.ts
Normal file
25
src/app/puck/api/route.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { NextResponse } from "next/server";
|
||||
import fs from "fs";
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const payload = await request.json();
|
||||
|
||||
const existingData = JSON.parse(
|
||||
fs.existsSync("database.json")
|
||||
? fs.readFileSync("database.json", "utf-8")
|
||||
: "{}"
|
||||
);
|
||||
|
||||
const updatedData = {
|
||||
...existingData,
|
||||
[payload.path]: payload.data,
|
||||
};
|
||||
|
||||
fs.writeFileSync("database.json", JSON.stringify(updatedData));
|
||||
|
||||
// Purge Next.js cache
|
||||
revalidatePath(payload.path);
|
||||
|
||||
return NextResponse.json({ status: "ok" });
|
||||
}
|
||||
3
src/app/puck/page.tsx
Normal file
3
src/app/puck/page.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export { default, generateMetadata } from "./[...puckPath]/page";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
Reference in New Issue
Block a user