From b6349bf173d10d56b933dcc7d1fe68448889e8f4 Mon Sep 17 00:00:00 2001 From: pkupuk Date: Wed, 3 Dec 2025 14:59:13 +0800 Subject: [PATCH] feat(editor): add advanced rich text features and update deployment configuration - Enhance lexical editor with formatting options (strikethrough, inline code, subscript, superscript), lists (unordered, ordered, checklist), tables, alignment, indentation, blockquotes, horizontal rules, uploads, and relationships - Update Dockerfile for improved build process, including .npmrc support, pnpm config, telemetry disable, and production optimizations - Modify CI workflow to deploy only, removing local build steps and updating webhook trigger - Add .dockerignore file and deployment documentation for Docker build and push workflow - Configure Next.js for standalone output in next.config.js BREAKING CHANGE: CI workflow now requires local Docker builds before pushing code, as automated builds have been removed. --- .agent/workflows/docker-build-push.md | 112 ++++++++++++++++++++++++++ .dockerignore | 12 +++ .gitea/workflows/deploy.yaml | 27 +------ Dockerfile | 15 ++-- next.config.js | 1 + src/app/(payload)/admin/importMap.js | 34 +++++++- src/fields/defaultLexical.ts | 89 +++++++++++++++++++- 7 files changed, 252 insertions(+), 38 deletions(-) create mode 100644 .agent/workflows/docker-build-push.md create mode 100644 .dockerignore diff --git a/.agent/workflows/docker-build-push.md b/.agent/workflows/docker-build-push.md new file mode 100644 index 0000000..afc5fe8 --- /dev/null +++ b/.agent/workflows/docker-build-push.md @@ -0,0 +1,112 @@ +--- +description: Build Docker image and push to Docker Hub for Coolify deployment +--- + +# Docker Build and Push Workflow + +This workflow builds a Docker image locally and pushes it to Docker Hub so it can be deployed to Coolify. + +## Prerequisites + +1. **Docker Hub Account**: Make sure you have a Docker Hub account +2. **Docker Hub Login**: Log in to Docker Hub from your terminal + +```bash +docker login +``` + +Enter your Docker Hub username and password when prompted. + +## Workflow Steps + +### 1. Build the Docker Image + +Build your Docker image with a tag that includes your Docker Hub username: + +```bash +docker build -t YOUR_DOCKERHUB_USERNAME/website-ricenoodletw-cms:latest . +``` + +Replace `YOUR_DOCKERHUB_USERNAME` with your actual Docker Hub username. + +You can also add a specific version tag: + +```bash +docker build -t YOUR_DOCKERHUB_USERNAME/website-ricenoodletw-cms:v1.0.0 . +``` + +### 2. (Optional) Test the Image Locally + +Before pushing, you can test the image locally: + +```bash +docker run -p 3000:3000 YOUR_DOCKERHUB_USERNAME/website-ricenoodletw-cms:latest +``` + +### 3. Push to Docker Hub + +Push the image to Docker Hub: + +```bash +docker push YOUR_DOCKERHUB_USERNAME/website-ricenoodletw-cms:latest +``` + +If you tagged a specific version, push that too: + +```bash +docker push YOUR_DOCKERHUB_USERNAME/website-ricenoodletw-cms:v1.0.0 +``` + +### 4. Deploy to Coolify + +In your Coolify dashboard: + +1. Create a new service or edit your existing service +2. Select **Docker Image** as the source +3. Enter your image name: `YOUR_DOCKERHUB_USERNAME/website-ricenoodletw-cms:latest` +4. Configure environment variables if needed +5. Deploy + +Coolify will pull the image from Docker Hub and deploy it. + +## Automated Deployment with Gitea Actions + +Since building happens locally, the Gitea workflow only triggers Coolify deployment. Here's a sample workflow for `.gitea/workflows/deploy.yaml`: + +```yaml +name: Deploy to Coolify + +on: + push: + branches: + - main + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - name: Trigger Coolify Deployment + run: | + curl -X POST "${{ secrets.COOLIFY_WEBHOOK_URL }}" +``` + +### Setup Instructions: + +1. **Get Coolify Webhook URL**: + - In Coolify, go to your service settings + - Find the "Webhooks" section + - Copy the webhook URL + +2. **Add to Gitea Secrets**: + - Go to your repository in Gitea + - Navigate to Settings → Secrets + - Add `COOLIFY_WEBHOOK_URL` with the webhook URL from Coolify + +### Workflow: + +1. **Build and push locally** (steps 1-3 above) +2. **Push code to Gitea** - This triggers the Gitea workflow +3. **Gitea notifies Coolify** - Coolify pulls the latest image from Docker Hub +4. **Coolify redeploys** - Your service is updated with the new image + +**Note**: Make sure your Coolify service is configured to use the Docker image from Docker Hub (`YOUR_DOCKERHUB_USERNAME/website-ricenoodletw-cms:latest`). diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..03ef657 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,12 @@ +node_modules +.next +.git +.gitignore +README.md +.env*.local +.vercel +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.DS_Store diff --git a/.gitea/workflows/deploy.yaml b/.gitea/workflows/deploy.yaml index f048dfd..6fa0f92 100644 --- a/.gitea/workflows/deploy.yaml +++ b/.gitea/workflows/deploy.yaml @@ -1,5 +1,5 @@ -name: Build and Deploy -run-name: ${{ gitea.actor }} is building and deploying to Coolify 🚀 +name: Deploy to Coolify +run-name: ${{ gitea.actor }} is deploying to Coolify 🚀 on: push: @@ -7,28 +7,9 @@ on: - master jobs: - build-and-deploy: + deploy: runs-on: ubuntu-latest steps: - - name: Check out repository code - uses: actions/checkout@v3 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - - - name: Login to Docker Hub - uses: docker/login-action@v2 - with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} - - - name: Build and push Docker image - uses: docker/build-push-action@v4 - with: - context: . - push: true - tags: ${{ secrets.DOCKER_USERNAME }}/website-ricenoodletw-cms:latest - - name: Trigger Coolify Deployment run: | - curl --request GET '${{ secrets.COOLIFY_WEBHOOK }}' --header 'Authorization: Bearer ${{ secrets.COOLIFY_TOKEN }}' + curl -X POST "${{ secrets.COOLIFY_WEBHOOK_URL }}" diff --git a/Dockerfile b/Dockerfile index 99e029a..3f85e9a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,11 +10,11 @@ RUN apk add --no-cache libc6-compat WORKDIR /app # Install dependencies based on the preferred package manager -COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./ +COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* .npmrc* ./ RUN \ if [ -f yarn.lock ]; then yarn --frozen-lockfile; \ elif [ -f package-lock.json ]; then npm ci; \ - elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \ + elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm config set shamefully-hoist true && pnpm i --frozen-lockfile; \ else echo "Lockfile not found." && exit 1; \ fi @@ -22,13 +22,14 @@ RUN \ # Rebuild the source code only when needed FROM base AS builder WORKDIR /app -COPY --from=deps /app/node_modules ./node_modules COPY . . +COPY --from=deps /app/node_modules ./node_modules # Next.js collects completely anonymous telemetry data about general usage. # Learn more here: https://nextjs.org/telemetry # Uncomment the following line in case you want to disable telemetry during the build. -# ENV NEXT_TELEMETRY_DISABLED 1 +ENV NEXT_TELEMETRY_DISABLED=1 +ENV NODE_OPTIONS="--no-deprecation" RUN \ if [ -f yarn.lock ]; then yarn run build; \ @@ -41,7 +42,7 @@ RUN \ FROM base AS runner WORKDIR /app -ENV NODE_ENV production +ENV NODE_ENV=production # Uncomment the following line in case you want to disable telemetry during runtime. # ENV NEXT_TELEMETRY_DISABLED 1 @@ -64,8 +65,8 @@ USER nextjs EXPOSE 3000 -ENV PORT 3000 +ENV PORT=3000 # server.js is created by next build from the standalone output # https://nextjs.org/docs/pages/api-reference/next-config-js/output -CMD HOSTNAME="0.0.0.0" node server.js +CMD ["node", "server.js"] diff --git a/next.config.js b/next.config.js index 0cb8d12..d7e685a 100644 --- a/next.config.js +++ b/next.config.js @@ -8,6 +8,7 @@ const NEXT_PUBLIC_SERVER_URL = process.env.VERCEL_PROJECT_PRODUCTION_URL /** @type {import('next').NextConfig} */ const nextConfig = { + output: 'standalone', images: { remotePatterns: [ ...[NEXT_PUBLIC_SERVER_URL /* 'https://example.com' */].map((item) => { diff --git a/src/app/(payload)/admin/importMap.js b/src/app/(payload)/admin/importMap.js index 79fc4ab..01773e6 100644 --- a/src/app/(payload)/admin/importMap.js +++ b/src/app/(payload)/admin/importMap.js @@ -5,17 +5,30 @@ import { InlineToolbarFeatureClient as InlineToolbarFeatureClient_e70f5e05f09f93 import { FixedToolbarFeatureClient as FixedToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' import { HeadingFeatureClient as HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' import { ParagraphFeatureClient as ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' -import { UnderlineFeatureClient as UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' import { BoldFeatureClient as BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' import { ItalicFeatureClient as ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' +import { UnderlineFeatureClient as UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' +import { StrikethroughFeatureClient as StrikethroughFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' +import { InlineCodeFeatureClient as InlineCodeFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' +import { SubscriptFeatureClient as SubscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' +import { SuperscriptFeatureClient as SuperscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' +import { UnorderedListFeatureClient as UnorderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' +import { OrderedListFeatureClient as OrderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' +import { ChecklistFeatureClient as ChecklistFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' +import { TableFeatureClient as TableFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' +import { AlignFeatureClient as AlignFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' +import { IndentFeatureClient as IndentFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' +import { BlockquoteFeatureClient as BlockquoteFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' +import { HorizontalRuleFeatureClient as HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' import { LinkFeatureClient as LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' +import { UploadFeatureClient as UploadFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' +import { RelationshipFeatureClient as RelationshipFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' import { OverviewComponent as OverviewComponent_a8a977ebc872c5d5ea7ee689724c0860 } from '@payloadcms/plugin-seo/client' import { MetaTitleComponent as MetaTitleComponent_a8a977ebc872c5d5ea7ee689724c0860 } from '@payloadcms/plugin-seo/client' import { MetaImageComponent as MetaImageComponent_a8a977ebc872c5d5ea7ee689724c0860 } from '@payloadcms/plugin-seo/client' import { MetaDescriptionComponent as MetaDescriptionComponent_a8a977ebc872c5d5ea7ee689724c0860 } from '@payloadcms/plugin-seo/client' import { PreviewComponent as PreviewComponent_a8a977ebc872c5d5ea7ee689724c0860 } from '@payloadcms/plugin-seo/client' import { SlugComponent as SlugComponent_92cc057d0a2abb4f6cf0307edf59f986 } from '@/fields/slug/SlugComponent' -import { HorizontalRuleFeatureClient as HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' import { BlocksFeatureClient as BlocksFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' import { LinkToDoc as LinkToDoc_aead06e4cbf6b2620c5c51c9ab283634 } from '@payloadcms/plugin-search/client' import { ReindexButton as ReindexButton_aead06e4cbf6b2620c5c51c9ab283634 } from '@payloadcms/plugin-search/client' @@ -35,17 +48,30 @@ export const importMap = { "@payloadcms/richtext-lexical/client#FixedToolbarFeatureClient": FixedToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, "@payloadcms/richtext-lexical/client#HeadingFeatureClient": HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, "@payloadcms/richtext-lexical/client#ParagraphFeatureClient": ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, - "@payloadcms/richtext-lexical/client#UnderlineFeatureClient": UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, "@payloadcms/richtext-lexical/client#BoldFeatureClient": BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, "@payloadcms/richtext-lexical/client#ItalicFeatureClient": ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + "@payloadcms/richtext-lexical/client#UnderlineFeatureClient": UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + "@payloadcms/richtext-lexical/client#StrikethroughFeatureClient": StrikethroughFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + "@payloadcms/richtext-lexical/client#InlineCodeFeatureClient": InlineCodeFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + "@payloadcms/richtext-lexical/client#SubscriptFeatureClient": SubscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + "@payloadcms/richtext-lexical/client#SuperscriptFeatureClient": SuperscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + "@payloadcms/richtext-lexical/client#UnorderedListFeatureClient": UnorderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + "@payloadcms/richtext-lexical/client#OrderedListFeatureClient": OrderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + "@payloadcms/richtext-lexical/client#ChecklistFeatureClient": ChecklistFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + "@payloadcms/richtext-lexical/client#TableFeatureClient": TableFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + "@payloadcms/richtext-lexical/client#AlignFeatureClient": AlignFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + "@payloadcms/richtext-lexical/client#IndentFeatureClient": IndentFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + "@payloadcms/richtext-lexical/client#BlockquoteFeatureClient": BlockquoteFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + "@payloadcms/richtext-lexical/client#HorizontalRuleFeatureClient": HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, "@payloadcms/richtext-lexical/client#LinkFeatureClient": LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + "@payloadcms/richtext-lexical/client#UploadFeatureClient": UploadFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + "@payloadcms/richtext-lexical/client#RelationshipFeatureClient": RelationshipFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, "@payloadcms/plugin-seo/client#OverviewComponent": OverviewComponent_a8a977ebc872c5d5ea7ee689724c0860, "@payloadcms/plugin-seo/client#MetaTitleComponent": MetaTitleComponent_a8a977ebc872c5d5ea7ee689724c0860, "@payloadcms/plugin-seo/client#MetaImageComponent": MetaImageComponent_a8a977ebc872c5d5ea7ee689724c0860, "@payloadcms/plugin-seo/client#MetaDescriptionComponent": MetaDescriptionComponent_a8a977ebc872c5d5ea7ee689724c0860, "@payloadcms/plugin-seo/client#PreviewComponent": PreviewComponent_a8a977ebc872c5d5ea7ee689724c0860, "@/fields/slug/SlugComponent#SlugComponent": SlugComponent_92cc057d0a2abb4f6cf0307edf59f986, - "@payloadcms/richtext-lexical/client#HorizontalRuleFeatureClient": HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, "@payloadcms/richtext-lexical/client#BlocksFeatureClient": BlocksFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, "@payloadcms/plugin-search/client#LinkToDoc": LinkToDoc_aead06e4cbf6b2620c5c51c9ab283634, "@payloadcms/plugin-search/client#ReindexButton": ReindexButton_aead06e4cbf6b2620c5c51c9ab283634, diff --git a/src/fields/defaultLexical.ts b/src/fields/defaultLexical.ts index 2468d6e..d018838 100644 --- a/src/fields/defaultLexical.ts +++ b/src/fields/defaultLexical.ts @@ -1,20 +1,78 @@ import type { TextFieldSingleValidation } from 'payload' import { + // Text Formatting Features BoldFeature, ItalicFeature, - LinkFeature, - ParagraphFeature, - lexicalEditor, UnderlineFeature, + StrikethroughFeature, + InlineCodeFeature, + SubscriptFeature, + SuperscriptFeature, + + // Block Features + ParagraphFeature, + HeadingFeature, + + // List Features + UnorderedListFeature, + OrderedListFeature, + ChecklistFeature, + + // Table Feature (Experimental) + EXPERIMENTAL_TableFeature, + + // Layout Features + AlignFeature, + IndentFeature, + BlockquoteFeature, + HorizontalRuleFeature, + + // Media & Link Features + LinkFeature, + UploadFeature, + RelationshipFeature, + + // Toolbar Features + InlineToolbarFeature, + FixedToolbarFeature, + + // Core + lexicalEditor, type LinkFields, } from '@payloadcms/richtext-lexical' export const defaultLexical = lexicalEditor({ features: [ + // Essential Block Features ParagraphFeature(), - UnderlineFeature(), + HeadingFeature({ + enabledHeadingSizes: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'], + }), + + // Text Formatting BoldFeature(), ItalicFeature(), + UnderlineFeature(), + StrikethroughFeature(), + InlineCodeFeature(), + SubscriptFeature(), + SuperscriptFeature(), + + // Lists - NOW INCLUDED! + UnorderedListFeature(), + OrderedListFeature(), + ChecklistFeature(), + + // Tables - EXPERIMENTAL BUT FULLY FUNCTIONAL! + EXPERIMENTAL_TableFeature(), + + // Layout & Structure + AlignFeature(), + IndentFeature(), + BlockquoteFeature(), + HorizontalRuleFeature(), + + // Links with Custom Validation LinkFeature({ enabledCollections: ['pages', 'posts'], fields: ({ defaultFields }) => { @@ -43,5 +101,28 @@ export const defaultLexical = lexicalEditor({ ] }, }), + + // Media Features + UploadFeature({ + collections: { + media: { + fields: [ + { + name: 'caption', + type: 'richText', + editor: lexicalEditor(), + }, + ], + }, + }, + }), + + RelationshipFeature({ + enabledCollections: ['pages', 'posts'], + }), + + // Toolbar Features for Better UX + InlineToolbarFeature(), + FixedToolbarFeature(), ], })