# Next.js 整合 Tailwind CSS、CKEditor 4 和 Bootstrap 4.6 專案指南 ###### tags: `nextjs`, `tailwind` , `CKeditor`, `Bootstrap` ## 專案概述 本專案成功整合了 Next.js、Tailwind CSS、CKEditor 4 和 Bootstrap 4.6,建立了一個編輯器頁面,使用者可以在 CKEditor 中編輯內容並即時預覽結果。我們還創建了一個使用 Tailwind CSS 的 Header 組件,展示了響應式設計和過渡效果。 ## 完整步驟指南 ### 1. 建立 Next.js 專案 ```bash npx create-next-app@latest nextjs-tailwind-ckeditor-bootstrap4 ``` 選擇以下選項: - 使用 TypeScript:是 - 使用 ESLint:是 - 使用 Tailwind CSS:是 - 使用 `src/` 目錄:是 - 使用 App Router:是 - 自定義導入別名:是(預設 `@/*`) ### 2. 安裝必要依賴 由於 React 19 與 ckeditor4-react 的相容性問題,需要使用 `--legacy-peer-deps` 參數: ```bash cd nextjs-tailwind-ckeditor-bootstrap4 npm install ckeditor4-react@4.2.0 bootstrap@4.6.0 jquery --legacy-peer-deps ``` > 注意:我們選擇 ckeditor4-react@4.2.0,它使用的是 CKEditor 4.22.1,這是最後一個不需要商業許可證的版本。 ### 3. 建立 Bootstrap CSS 檔案 在 `public` 目錄下創建 `bootstrap.min.css` 文件,通過 CDN 引用 Bootstrap 4.6: ```css /* 從 CDN 引入 Bootstrap 4.6 CSS */ @import url('https://cdn.jsdelivr.net/npm/bootstrap@4.6.0/dist/css/bootstrap.min.css'); ``` ### 4. 建立首頁 修改 `src/app/page.tsx`: ```tsx import Link from 'next/link'; export default function Home() { return ( <main className="min-h-screen p-8"> <h1 className="text-3xl font-bold mb-6">Next.js 整合 Tailwind CSS 與 CKEditor 4</h1> <div className="max-w-2xl"> <p className="mb-4 text-gray-700"> 這是一個示範專案,展示如何在 Next.js 中整合 Tailwind CSS、CKEditor 4 和 Bootstrap 4.6。 </p> <div className="bg-blue-50 border-l-4 border-blue-500 p-4 mb-6"> <h2 className="text-lg font-semibold text-blue-800">專案特點</h2> <ul className="list-disc ml-5 mt-2 text-blue-700"> <li>使用 Next.js 最新版本和 App Router</li> <li>整合 Tailwind CSS 進行現代化 UI 設計</li> <li>使用 CKEditor 4 作為富文本編輯器</li> <li>整合 Bootstrap 4.6 樣式到 CKEditor 中</li> <li>實現編輯內容的即時預覽功能</li> </ul> </div> <Link href="/editor" className="inline-block bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded-lg transition-colors duration-300" > 前往編輯器頁面 </Link> </div> </main> ); } ``` ### 5. 建立 Header 組件 在 `src/components` 目錄下創建 `Header.tsx`: ```tsx import Link from 'next/link'; import { useState } from 'react'; export default function Header() { const [isMenuOpen, setIsMenuOpen] = useState(false); return ( <header className="bg-gradient-to-r from-blue-600 to-blue-800 text-white shadow-lg"> <div className="container mx-auto px-4 py-3"> <div className="flex justify-between items-center"> <div className="flex items-center space-x-2"> <svg className="h-8 w-8" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" > <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" /> </svg> <span className="font-bold text-xl">編輯器測試專案</span> </div> {/* 桌面導航 */} <nav className="hidden md:flex space-x-6"> <Link href="/" className="hover:text-blue-200 transition duration-300"> 首頁 </Link> <Link href="/editor" className="hover:text-blue-200 transition duration-300"> 編輯器 </Link> <a href="#" className="hover:text-blue-200 transition duration-300"> 關於 </a> <a href="#" className="hover:text-blue-200 transition duration-300"> 聯絡我們 </a> </nav> {/* 行動裝置選單按鈕 */} <div className="md:hidden"> <button onClick={() => setIsMenuOpen(!isMenuOpen)} className="focus:outline-none" > <svg className="h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" > {isMenuOpen ? ( <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /> ) : ( <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" /> )} </svg> </button> </div> </div> {/* 行動裝置選單 */} {isMenuOpen && ( <div className="md:hidden mt-4 pb-2"> <Link href="/" className="block py-2 hover:bg-blue-700 px-2 rounded transition duration-300" onClick={() => setIsMenuOpen(false)} > 首頁 </Link> <Link href="/editor" className="block py-2 hover:bg-blue-700 px-2 rounded transition duration-300" onClick={() => setIsMenuOpen(false)} > 編輯器 </Link> <a href="#" className="block py-2 hover:bg-blue-700 px-2 rounded transition duration-300" onClick={() => setIsMenuOpen(false)} > 關於 </a> <a href="#" className="block py-2 hover:bg-blue-700 px-2 rounded transition duration-300" onClick={() => setIsMenuOpen(false)} > 聯絡我們 </a> </div> )} </div> </header> ); } ``` ### 6. 建立編輯器頁面 創建 `src/app/editor/page.tsx`: ```tsx 'use client'; import { useState, useEffect } from 'react'; import Link from 'next/link'; import dynamic from 'next/dynamic'; import Header from '@/components/Header'; // 動態導入 CKEditor 組件,避免服務器端渲染問題 const CKEditor = dynamic( async () => { // 在客戶端導入 Bootstrap 和 CKEditor // 使用 await import() 而不是直接 import 語句 if (typeof window !== 'undefined') { await import('bootstrap/dist/css/bootstrap.min.css'); const mod = await import('ckeditor4-react'); // 確保 jQuery 和 Bootstrap JS 只在客戶端加載 window.jQuery = require('jquery'); window.$ = window.jQuery; require('bootstrap/dist/js/bootstrap.bundle.min.js'); // 全局設置 CKEditor 配置,禁用版本檢查 if (window.CKEDITOR) { window.CKEDITOR.disableAutoInline = true; window.CKEDITOR.config.versionCheck = false; } return mod.CKEditor; } return null; }, { ssr: false, // 禁用服務器端渲染 loading: () => <p className="text-gray-500">CKEditor 加載中...</p> } ); export default function EditorPage() { const [content, setContent] = useState('<p>這裡將顯示編輯器內容</p>'); const [editorLoaded, setEditorLoaded] = useState(false); // 確保組件只在客戶端渲染 useEffect(() => { setEditorLoaded(true); }, []); // 處理編輯器內容變更 const handleEditorChange = (evt) => { const data = evt.editor.getData(); setContent(data); }; return ( <> <Header /> <main className="min-h-screen p-8"> <h1 className="text-3xl font-bold mb-6">CKEditor 整合頁面</h1> <div className="mb-8"> <Link href="/" className="text-blue-500 hover:text-blue-700 underline"> 返回首頁 </Link> </div> <div className="grid grid-cols-1 gap-8 md:grid-cols-2"> <div className="border border-gray-300 p-4 rounded-lg shadow-md hover:shadow-lg transition-shadow duration-300"> <h2 className="text-xl font-semibold mb-4">編輯器</h2> <div className="min-h-[300px]"> {editorLoaded && ( <CKEditor config={{ // 禁用版本檢查警告 versionCheck: false, // 啟用 Bootstrap 內容模板 contentsCss: '/bootstrap.min.css', // 允許使用 Bootstrap 類別 allowedContent: true, // 添加 Bootstrap 相關按鈕和功能 extraPlugins: 'div,autolink,autoembed,embedsemantic,autogrow,widget,lineutils,clipboard,dialog,dialogui,elementspath', toolbar: [ { name: 'document', items: [ 'Source' ] }, { name: 'clipboard', items: [ 'Cut', 'Copy', 'Paste', 'PasteText', 'PasteFromWord', '-', 'Undo', 'Redo' ] }, { name: 'basicstyles', items: [ 'Bold', 'Italic', 'Underline', 'Strike', 'Subscript', 'Superscript', '-', 'RemoveFormat' ] }, { name: 'paragraph', items: [ 'NumberedList', 'BulletedList', '-', 'Outdent', 'Indent', '-', 'Blockquote', 'CreateDiv', '-', 'JustifyLeft', 'JustifyCenter', 'JustifyRight', 'JustifyBlock' ] }, { name: 'links', items: [ 'Link', 'Unlink' ] }, { name: 'insert', items: [ 'Image', 'Table', 'HorizontalRule', 'SpecialChar' ] }, { name: 'styles', items: [ 'Styles', 'Format', 'Font', 'FontSize' ] }, { name: 'colors', items: [ 'TextColor', 'BGColor' ] }, { name: 'tools', items: [ 'Maximize', 'ShowBlocks' ] }, ], // 添加 Bootstrap 相關樣式 stylesSet: [ { name: 'Alert Primary', element: 'div', attributes: { 'class': 'alert alert-primary' } }, { name: 'Alert Success', element: 'div', attributes: { 'class': 'alert alert-success' } }, { name: 'Alert Danger', element: 'div', attributes: { 'class': 'alert alert-danger' } }, { name: 'Button Primary', element: 'a', attributes: { 'class': 'btn btn-primary' } }, { name: 'Button Success', element: 'a', attributes: { 'class': 'btn btn-success' } }, { name: 'Button Danger', element: 'a', attributes: { 'class': 'btn btn-danger' } }, { name: 'Card', element: 'div', attributes: { 'class': 'card' } }, { name: 'Card Body', element: 'div', attributes: { 'class': 'card-body' } }, { name: 'Card Header', element: 'div', attributes: { 'class': 'card-header' } }, { name: 'Card Footer', element: 'div', attributes: { 'class': 'card-footer' } }, ] }} data={content} onChange={handleEditorChange} /> )} </div> </div> <div className="border border-gray-300 p-4 rounded-lg shadow-md hover:shadow-lg transition-shadow duration-300"> <h2 className="text-xl font-semibold mb-4">預覽結果</h2> <div className="min-h-[300px] bg-white p-4 rounded border border-gray-200 overflow-auto" dangerouslySetInnerHTML={{ __html: content }} /> </div> </div> </main> </> ); } ``` ### 7. 啟動開發伺服器 ```bash npm run dev ``` ## 主要挑戰與解決方案 ### 1. React 19 與 ckeditor4-react 的相容性問題 **問題**:ckeditor4-react 依賴較舊版本的 React,與 Next.js 15 使用的 React 19 不相容。 **解決方案**:使用 `--legacy-peer-deps` 參數安裝依賴。 ### 2. 避免服務器端渲染 (SSR) 問題 **問題**:CKEditor 是客戶端庫,在服務器端渲染時會出錯。 **解決方案**:使用 Next.js 的動態導入 (`next/dynamic`) 並設置 `ssr: false`。 ### 3. CKEditor 4 許可證問題 **問題**:CKEditor 4.23.0 及以上版本需要商業許可證。 **解決方案**:使用最後一個免費版本 4.22.1 (通過 ckeditor4-react@4.2.0)。 ### 4. 版本檢查警告 **問題**:CKEditor 4.22.1 會顯示版本不安全的警告。 **解決方案**: 1. 在組件配置中設置 `versionCheck: false` 2. 全局設置 `window.CKEDITOR.config.versionCheck = false` ### 5. Bootstrap 樣式整合 **問題**:需要在 CKEditor 中使用 Bootstrap 4.6 樣式。 **解決方案**: 1. 在 `public` 目錄下創建 `bootstrap.min.css` 2. 在 CKEditor 配置中設置 `contentsCss: '/bootstrap.min.css'` 3. 添加 Bootstrap 相關樣式到 `stylesSet` 配置中 ## 提示 (Prompt) 範例 以下是一些可用於創建類似專案的提示範例: ### 建立 Next.js 與 CKEditor 4 整合專案 ``` 建立一個 Next.js 專案,整合 Tailwind CSS、CKEditor 4 和 Bootstrap 4.6。專案需要: 1. 使用 App Router 架構 2. 在編輯器頁面整合 CKEditor 4 和 Bootstrap 4.6 3. 使用動態導入避免 SSR 問題 4. 實現編輯內容的即時預覽功能 5. 使用 Tailwind CSS 設計現代化 UI 6. 創建響應式 Header 組件 ``` ### 解決 CKEditor 4 在 Next.js 中的整合問題 ``` 我正在 Next.js 15 (React 19) 專案中整合 CKEditor 4,遇到以下問題,請提供解決方案: 1. React 版本衝突問題 2. 服務器端渲染錯誤 3. CKEditor 4 版本警告訊息 4. 如何在 CKEditor 中使用 Bootstrap 4.6 樣式 5. 如何實現編輯內容的即時預覽 ``` ### 優化 CKEditor 4 與 Bootstrap 4.6 整合 ``` 我已經在 Next.js 專案中整合了 CKEditor 4 和 Bootstrap 4.6,請提供以下優化建議: 1. 如何添加更多 Bootstrap 元素到 CKEditor 工具列 2. 如何自定義 CKEditor 的外觀以匹配 Tailwind CSS 設計 3. 如何處理 CKEditor 的版本警告 4. 如何提升編輯器的加載性能 5. 如何實現更豐富的即時預覽功能 ```