diff --git a/.env.example b/.env.example index d8f292f7..5fadf3b6 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,6 @@ -WORDPRESS_URL="https://wordpress.com" -WORDPRESS_HOSTNAME="wordpress.com" +NEXT_PUBLIC_WORDPRESS_URL="https://sysblok.ru" +NEXT_PUBLIC_URL="https://next.sysblok.team" # If using the revalidate plugin # You can generate by running `openssl rand -base64 32` in the terminal -WORDPRESS_WEBHOOK_SECRET="your-secret-key-here" +NEXT_WORDPRESS_WEBHOOK_SECRET="your-secret-key-here" diff --git a/.gitignore b/.gitignore index fd3dbb57..5eb489df 100644 --- a/.gitignore +++ b/.gitignore @@ -25,8 +25,9 @@ npm-debug.log* yarn-debug.log* yarn-error.log* -# local env files +# env files .env*.local +.env # vercel .vercel diff --git a/CLAUDE.md b/CLAUDE.md index 3e6a28d1..c84efc52 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -59,9 +59,8 @@ This is a headless WordPress starter using Next.js 15 App Router with TypeScript ## Environment Variables Required environment variables (see `.env.example`): -- `WORDPRESS_URL` - Full URL of WordPress site -- `WORDPRESS_HOSTNAME` - Domain for image optimization -- `WORDPRESS_WEBHOOK_SECRET` - Secret for webhook validation +- `NEXT_PUBLIC_WORDPRESS_URL` - Full URL of WordPress site +- `NEXT_WORDPRESS_WEBHOOK_SECRET` - Secret for webhook validation ## Key Dependencies - Next.js 15.3.3 with React 19.1.0 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..78cb5a31 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,75 @@ +# syntax=docker.io/docker/dockerfile:1 +# ============================================ +# Base Stage: Use a Lightweight Node.js Image +# ============================================ + +# Use an official Node.js Alpine image +ARG NODE_VERSION=22.14.0-alpine +FROM node:${NODE_VERSION} AS base + +# Set the working directory +WORKDIR /app + +# ============================================ +# Stage 2: Install dependencies +# ============================================ +FROM base AS deps + +# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. +RUN apk add --no-cache libc6-compat + +# Copy package related files +COPY package.json pnpm-lock.yaml ./ + +# Enable pnpm +RUN corepack enable pnpm + +# Install dependencies +RUN pnpm i --frozen-lockfile + +# ============================================ +# Stage 3: Build Next.js app +# ============================================ +FROM base AS builder + +# Copy node modules from dependencies +COPY --from=deps /app/node_modules ./node_modules + +# Copy source code into the container +COPY . . + +# Next.js collects completely anonymous telemetry data about general usage. +# Learn more here: https://nextjs.org/telemetry +# Following line disables telemetry. +ENV NEXT_TELEMETRY_DISABLED=1 + +# Build the application +RUN npm run build + +# ============================================ +# Stage 4: Create Production Image +# ============================================ +FROM base AS runner + +ENV NODE_ENV=production +ENV NEXT_TELEMETRY_DISABLED=1 + +RUN addgroup -g 1001 -S nodejs +RUN adduser -S nextjs -u 1001 + +# Copy standalone build files +COPY --from=builder --chown=nextjs:nodejs /app/public ./public +COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static + +# Set the port and hostname for the Next.js standalone server +ENV PORT=3000 +ENV HOSTNAME="0.0.0.0" + +# Use nextjs user for security best practices +USER nextjs + +# Expose port 3000 +EXPOSE 3000 + +CMD ["node", "server.js"] \ No newline at end of file diff --git a/Dockerfile.dev b/Dockerfile.dev new file mode 100644 index 00000000..7fba77e7 --- /dev/null +++ b/Dockerfile.dev @@ -0,0 +1,32 @@ +# syntax=docker.io/docker/dockerfile:1 +# Use an official Node.js Alpine image +ARG NODE_VERSION=22.14.0-alpine +FROM node:${NODE_VERSION} AS base + +# Set the working directory +WORKDIR /app + +# Copy package related files +COPY package.json pnpm-lock.yaml ./ + +# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. +RUN apk add --no-cache libc6-compat + +# Enable pnpm +RUN corepack enable pnpm + +# Install dependencies +RUN pnpm i --frozen-lockfile + +# Copy source code into the container +COPY . . + +# Next.js collects completely anonymous telemetry data about general usage. +# Learn more here: https://nextjs.org/telemetry +# Following line disables telemetry. +ENV NEXT_TELEMETRY_DISABLED=1 + +# Expose port 3000 +EXPOSE 3000 + +CMD ["npm", "run", "dev"] \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..ed1a4317 --- /dev/null +++ b/Makefile @@ -0,0 +1,90 @@ +# Variables +IMAGE_NAME = wp-nextjs +IMAGE_TAG = latest +CONTAINER_NAME = wp-nextjs-container +HOST_PORT = 3000 +CONTAINER_PORT = 3000 +DOCKERFILE = Dockerfile +NODE_VERSION = 22.14.0-alpine +NODE_ENV=production + +# Default target +.PHONY: help +help: + @echo "Available commands:" + @echo " make build - Build the production Docker image" + @echo " make build-dev - Build the development Docker image and create container" + @echo " make run - Run the production Docker container" + @echo " make run-dev - Build and run the development Docker container" + @echo " make start-dev - Start the development Docker container" + @echo " make build-run - Build and run the production Docker container" + @echo " make stop - Stop the production Docker container" + @echo " make stop-dev - Stop the development Docker container" + @echo " make down-dev - Stop and remove the development Docker container" + @echo " make restart - Restart the production Docker container" + @echo " make restart-dev - Restart the development Docker container" + @echo " make logs - Show production container logs" + @echo " make clean - Remove production Docker image and container" + +# Build the production Docker image +build: + docker build \ + --build-arg NODE_VERSION=$(NODE_VERSION) \ + -f $(DOCKERFILE) -t $(IMAGE_NAME):$(IMAGE_TAG) . + +# Build the development Docker image and create container +build-dev: + docker compose build + docker compose create + +# Run the production Docker container +run: + @docker rm -f $(CONTAINER_NAME) 2>/dev/null || true + docker run --name $(CONTAINER_NAME) -p $(HOST_PORT):$(CONTAINER_PORT) $(IMAGE_NAME):$(IMAGE_TAG) + +# Build and run the development Docker container +run-dev: + docker compose up --build + +# Start the development Docker container +start-dev: + docker compose start + +# Build and run the production Docker container in one step +build-run: build run + +# Stop the production Docker container +stop: + docker stop $(CONTAINER_NAME) + +# Stop the development Docker container +stop-dev: + docker compose stop + +# Clean the development Docker container +down-dev: + docker compose down + +# Restart the production Docker container +restart: stop run + +# Restart the development Docker container +restart-dev: + docker compose restart + +# Show logs from the production Docker container +logs: + docker logs -f $(CONTAINER_NAME) + +# Clean up by removing production Docker image and container +clean: + -docker rm -f $(CONTAINER_NAME) + -docker rmi $(IMAGE_NAME):$(IMAGE_TAG) + +# Clean up by removing production Docker container +clean-container: + docker rm -f $(CONTAINER_NAME) + +# Clean up by removing production Docker image and container +clean-image: + docker rmi $(IMAGE_NAME):$(IMAGE_TAG) \ No newline at end of file diff --git a/README.md b/README.md index cdd43b2e..befd44c8 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,27 @@ -# Next.js Starter for WordPress Headless CMS +# Next.js frontend for sysblok -> [Watch the Demo Video](https://www.youtube.com/watch?v=JZc1-BcOvYw) -> -> [Need a headless theme? Use 761](https://github.com/9d8dev/761) +This is Next.js application that fetches data from a WordPress site using the WordPress REST API. It includes functions for fetching posts, categories, tags, authors, and featured media from a WordPress site and rendering them in a Next.js application. -![CleanShot 2025-01-07 at 23 18 41@2x](https://github.com/user-attachments/assets/8b268c36-eb0d-459f-b9f1-b5f129bd29bc) +`sysblok-next` is built with [Next.js 15](https://nextjs.org/docs), [React](https://react.dev/), [Typescript](https://www.typescriptlang.org/docs/), [Tailwind](https://tailwindcss.com/), [shadcn/ui](https://ui.shadcn.com/docs), and [brijr/craft](https://github.com/brijr/craft). It pairs nicely with [brijr/components](https://components.bridger.to/) for a rapid development experience. Built by Cameron and Bridger at [9d8](https://9d8.dev). -[![Deploy with Vercel](https://vercel.com/button)]() +## Make usage -This is a starter template for building a Next.js application that fetches data from a WordPress site using the WordPress REST API. The template includes functions for fetching posts, categories, tags, authors, and featured media from a WordPress site and rendering them in a Next.js application. + * `make build` - Build the production Docker image + * `make build-dev` - Build the development Docker image and create container + * `make run` - Run the production Docker container + * `make run-dev` - Build and run the development Docker container + * `make start-dev` - Start the development Docker container + * `make build-run` - Build and run the production Docker container + * `make stop` - Stop the production Docker container + * `make stop-dev` - Stop the development Docker container + * `make down-dev` - Stop and remove the development Docker container + * `make restart` - Restart the production Docker container + * `make restart-dev` - Restart the development Docker container + * `make logs` - Show production container logs + * `make clean` - Remove production Docker image and container + + To start dev server without docker - `npm run dev` -`next-wp` is built with [Next.js 15](https://nextjs.org/docs), [React](https://react.dev/), [Typescript](https://www.typescriptlang.org/docs/), [Tailwind](https://tailwindcss.com/), [shadcn/ui](https://ui.shadcn.com/docs), and [brijr/craft](https://github.com/brijr/craft). It pairs nicely with [brijr/components](https://components.bridger.to/) for a rapid development experience. Built by Cameron and Bridger at [9d8](https://9d8.dev). ## Table of Contents @@ -67,8 +78,7 @@ The following environment variables are required in your `.env.local` file: ```bash WORDPRESS_URL="https://wordpress.com" -WORDPRESS_HOSTNAME="wordpress.com" -WORDPRESS_WEBHOOK_SECRET="your-secret-key-here" +NEXT_WORDPRESS_WEBHOOK_SECRET="your-secret-key-here" ``` You can find the example of `.env.local` file in the `.env.example` file (and in Vercel). @@ -132,7 +142,7 @@ const defaultFetchOptions = { #### Media -- `getFeaturedMediaById(id: number)`: Retrieves featured media (images) with size information. +- `getMediaById(id: number)`: Retrieves featured media (images) with size information. ### Error Handling @@ -393,7 +403,7 @@ The `components/posts/post-card.tsx` file contains the `PostCard` component, whi ### Functionality -1. The component fetches the featured media, author, and category associated with the post using the `getFeaturedMediaById`, `getAuthorById`, and `getCategoryById` functions from `lib/wordpress.ts`. +1. The component fetches the featured media, author, and category associated with the post using the `getMediaById`, `getAuthorById`, and `getCategoryById` functions from `lib/wordpress.ts`. 2. It formats the post date using the `toLocaleDateString` method with the specified options. @@ -586,7 +596,7 @@ This granular system ensures that when content changes, only the relevant cached 2. **Configure Next.js:** - - Add `WORDPRESS_WEBHOOK_SECRET` to your environment variables (same secret as in WordPress plugin) + - Add `NEXT_WORDPRESS_WEBHOOK_SECRET` to your environment variables (same secret as in WordPress plugin) - The webhook endpoint at `/api/revalidate` is already set up - No additional configuration needed diff --git a/app/api/revalidate/route.ts b/app/api/revalidate/route.ts index 7f0ac216..a801ad79 100644 --- a/app/api/revalidate/route.ts +++ b/app/api/revalidate/route.ts @@ -14,7 +14,7 @@ export async function POST(request: NextRequest) { const requestBody = await request.json(); const secret = request.headers.get("x-webhook-secret"); - if (secret !== process.env.WORDPRESS_WEBHOOK_SECRET) { + if (secret !== process.env.NEXT_WORDPRESS_WEBHOOK_SECRET) { console.error("Invalid webhook secret"); return NextResponse.json( { message: "Invalid webhook secret" }, @@ -22,7 +22,7 @@ export async function POST(request: NextRequest) { ); } - const { contentType, contentId } = requestBody; + const { contentType, contentId, contentSlug } = requestBody; if (!contentType) { return NextResponse.json( @@ -35,19 +35,19 @@ export async function POST(request: NextRequest) { console.log( `Revalidating content: ${contentType}${ contentId ? ` (ID: ${contentId})` : "" + }${ + contentSlug ? ` (slug: ${contentSlug})` : "" }` ); - // Revalidate specific content type tags - revalidateTag("wordpress"); - if (contentType === "post") { revalidateTag("posts"); - if (contentId) { - revalidateTag(`post-${contentId}`); - } - // Clear all post pages when any post changes - revalidateTag("posts-page-1"); + if (contentId) revalidateTag(`post-${contentId}`); + if (contentSlug) revalidateTag(`post-${contentSlug}`); + } else if (contentType === "page") { + revalidateTag("pages"); + if (contentSlug) revalidateTag(`page-${contentSlug}`); + if (contentId) revalidateTag(`page-${contentId}`); } else if (contentType === "category") { revalidateTag("categories"); if (contentId) { @@ -66,6 +66,12 @@ export async function POST(request: NextRequest) { revalidateTag(`posts-author-${contentId}`); revalidateTag(`author-${contentId}`); } + } else if (contentType === "media") { + if (contentId) revalidateTag(`media-${contentId}`); + else revalidateTag(`media`); + } else if (contentType !== "menu") { + // revalidate all wordpress requests + revalidateTag("wordpress"); } // Also revalidate the entire layout for safety diff --git a/app/favicon.ico b/app/favicon.ico index 718d6fea..072646f1 100644 Binary files a/app/favicon.ico and b/app/favicon.ico differ diff --git a/app/layout.tsx b/app/layout.tsx index 0507cbe1..775ff5ed 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -25,9 +25,8 @@ const font = FontSans({ }); export const metadata: Metadata = { - title: "WordPress & Next.js Starter by 9d8", - description: - "A starter template for Next.js with WordPress as a headless CMS.", + title: "Системный Блокъ - Онлайн-журнал о влиянии цифровых технологий на культуру, человека и общество", + description: "Онлайн-журнал о влиянии цифровых технологий на культуру, человека и общество", metadataBase: new URL(siteConfig.site_domain), alternates: { canonical: "/", @@ -45,8 +44,7 @@ export default function RootLayout({