# 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. 如何實現更豐富的即時預覽功能
```