首頁 >web前端 >js教程 >使用 Next.js 建立動態部落格儀表板

使用 Next.js 建立動態部落格儀表板

Barbara Streisand
Barbara Streisand原創
2024-12-08 17:04:10705瀏覽

介紹

你好,你好嗎?我是 Vítor,帶著一個新專案回來了,可以幫助您提高程式設計技能。自從我上次發布教程以來已經有一段時間了。在過去的幾個月裡,我花了一些時間休息並專注於其他活動。在此期間,我開發了一個小型網路專案:博客,它成為本教程的重點。

在本指南中,我們將建立能夠渲染 Markdown 的部落格頁面的前端。該應用程式將包括公共和私人路由、用戶身份驗證以及編寫 Markdown 文字、新增照片、顯示文章等功能。

隨意自訂您的應用程序,無論您喜歡什麼——我甚至鼓勵這樣做。

您可以在此處存取此應用程式的儲存庫:

Building a Dynamic Blog Dashboard with Next.js 岡德拉克08 / 部落格平台

使用 Next.js/typescript 製作的部落格平台。

部落格平台

  • 文字教學

成分

  • next-auth - Next.js 的autenticação 圖書館
  • github.com/markdown-it/markdown-it - markdown biblioteca。
  • github.com/sindresorhus/github-markdown-css- Para dar estilo ao nosso markdown 編輯器。
  • github.com/remarkjs/react-markdown - Biblioteca para renderizar markdown em nosso 元件react。
  • github.com/remarkjs/remark-react/tree/4722bdf - React 中 Markdown 轉換插件。
  • codemirror.net - 網路編輯器元件。
  • react-icons - 反應圖示庫。

科莫美國

npm i
npm run start

伺服器

você pode encontrar o server dessa aplicação em server


在 GitHub 上查看


本教學還包括本指南中將使用的 Node.js 伺服器的編寫:

希望您喜歡。

編碼愉快!

圖書館

以下是此項目中使用的庫的摘要:

  • next-auth - Next.js 的驗證庫
  • github.com/markdown-it/markdown-it - Markdown 函式庫。
  • github.com/sindresorhus/github-markdown-css - 用於設計我們的 Markdown 編輯器。
  • github.com/remarkjs/react-markdown - 用於在 React 元件中渲染 Markdown 的函式庫。
  • github.com/remarkjs/remark-react/tree/4722bdf - 將 Markdown 轉換為 React 的插件。
  • codemirror.net - Web 元件編輯器。
  • react-icons - React 的圖示庫。

建立 React 項目

我們將使用最新版本的 Next.js 框架,在撰寫本教學時,版本為 13.4。

執行以下命令建立專案:

npm i
npm run start

安裝過程中,選擇模板設定。在本教程中,我將使用 TypeScript 作為程式語言,並使用 Tailwind CSS 框架來設計我們的應用程式。

配置

現在讓我們安裝我們將使用的所有函式庫。

降價
npx create-next-app myblog
反應備註
npm i  markdown-it @types/markdown-it markdown-it-style github-markdown-css react-markdown
程式碼鏡像
remark remark-gfm remark-react
圖示
npm @codemirror/commands @codemirror/highlight @codemirror/lang-javascript @codemirror/lang-markdown @codemirror/language @codemirror/language-data @codemirror/state @codemirror/theme-one-dark @codemirror/view

然後透過刪除我們不會使用的所有內容來清理安裝的初始結構。

建築學

這是我們應用程式的最終結構。

npm i react-icons @types/react-icons

第一步

配置next.config

在專案根目錄的 next.config.js 檔案中,讓我們來設定用於存取文章影像的網域位址。對於本教學課程,或者如果您使用本機伺服器,我們將使用 localhost。

確保包含此配置以確保在應用程式中正確載入映像。

src-
  |- app/
  |    |-(pages)/
  |    |      |- (private)/
  |    |      |       |- (home)
  |    |      |       |- editArticle/[id]
  |    |      |       |
  |    |      |       |- newArticle
  |    |      | - (public)/
  |    |              | - article/[id]
  |    |              | - login
  |    |
  |   api/
  |    |- auth/[...nextAuth]/route.ts
  |    |- global.css
  |    |- layout.tsx
  |
  | - components/
  | - context/
  | - interfaces/
  | - lib/
  | - services/
middleware.ts

配置中介軟體

在應用程式 src/ 的根資料夾中,建立一個 middleware.ts 以驗證對私有路由的存取。

const nextConfig = {
   images: {
    domains: ["localhost"],
  },
};

要了解有關中間件以及可以使用它們執行的所有操作的更多信息,請查看文件。

配置認證路由

在 /app 資料夾內,在 api/auth/[...nextauth] 中建立一個名為 Route.ts 的檔案。它將包含我們的路由配置,使用 CredentialsProvider 連接到我們的身份驗證 API。

CredentialsProvider 可讓您處理使用任意憑證的登錄,例如使用者名稱和密碼、網域、雙重認證、硬體設備等。

首先,在專案的根目錄中,建立一個 .env.local 檔案並新增一個令牌,該令牌將用作我們的秘密

npm i
npm run start

接下來,讓我們來寫我們的驗證系統,這個 NEXTAUTH_SECRET 將會被加入到 src/app/auth/[...nextauth]/routes.ts 檔案中的秘密中。

npx create-next-app myblog

認證提供者

讓我們建立一個身份驗證提供程序,一個上下文,它將在我們的私有路由的頁面上共享使用者的資料。稍後我們將使用它來包裝我們的layout.tsx 檔案之一。

在 src/context/auth-provider.tsx 中建立一個包含以下內容的檔案:

npm i  markdown-it @types/markdown-it markdown-it-style github-markdown-css react-markdown

全球風格

總的來說,在我們的應用程式中,我們將使用 Tailwind CSS 來建立我們的樣式。但是,在某些地方,我們將在頁面和元件之間共用自訂 CSS 類別。

remark remark-gfm remark-react

佈局

現在讓我們來寫私有和公有的佈局。

應用程式/佈局.tsx

npm @codemirror/commands @codemirror/highlight @codemirror/lang-javascript @codemirror/lang-markdown @codemirror/language @codemirror/language-data @codemirror/state @codemirror/theme-one-dark @codemirror/view

頁面/layout.tsx

npm i react-icons @types/react-icons

API呼叫

我們的應用程式將多次呼叫我們的 API,您可以調整此應用程式以使用任何外部 API。在我們的範例中,我們使用本機應用程式。如果你還沒有看過後端教學和伺服器創建,請查看。

在 src/services/ 中,我們寫以下函數:

  1. authService.ts:負責在伺服器上驗證使用者身分的函數。
src-
  |- app/
  |    |-(pages)/
  |    |      |- (private)/
  |    |      |       |- (home)
  |    |      |       |- editArticle/[id]
  |    |      |       |
  |    |      |       |- newArticle
  |    |      | - (public)/
  |    |              | - article/[id]
  |    |              | - login
  |    |
  |   api/
  |    |- auth/[...nextAuth]/route.ts
  |    |- global.css
  |    |- layout.tsx
  |
  | - components/
  | - context/
  | - interfaces/
  | - lib/
  | - services/
middleware.ts

2.refreshAccessToken.tsx:

const nextConfig = {
   images: {
    domains: ["localhost"],
  },
};
  1. getArticles.tsx:負責取得資料庫中儲存的所有文章的函數:
export { default } from "next-auth/middleware";
export const config = {
  matcher: ["/", "/newArticle/", "/article/", "/article/:path*"],
};
  1. postArticle.tsx:負責將文章資料提交到我們的伺服器的函數。
.env.local
NEXTAUTH_SECRET = SubsTituaPorToken
  1. editArticle.tsx:負責修改資料庫中特定文章的函數。
import NextAuth from "next-auth/next";
import type { AuthOptions } from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
import { authenticate } from "@/services/authService";
import refreshAccessToken from "@/services/refreshAccessToken";

export const authOptions: AuthOptions = {
  providers: [
    CredentialsProvider({
      name: "credentials",
      credentials: {
        email: {
          name: "email",
          label: "email",
          type: "email",
          placeholder: "Email",
        },
        password: {
          name: "password",
          label: "password",
          type: "password",
          placeholder: "Password",
        },
      },
      async authorize(credentials, req) {
        if (typeof credentials !== "undefined") {
          const res = await authenticate({
            email: credentials.email,
            password: credentials.password,
          });
          if (typeof res !== "undefined") {
            return { ...res };
          } else {
            return null;
          }
        } else {
          return null;
        }
      },
    }),
  ],

  session: { strategy: "jwt" },
  secret: process.env.NEXTAUTH_SECRET,
  callbacks: {
    async jwt({ token, user, account }: any) {
      if (user && account) {
        return {
          token: user?.token,
          accessTokenExpires: Date.now() + parseInt(user?.expiresIn, 10),
          refreshToken: user?.tokenRefresh,
        };
      }

      if (Date.now() < token.accessTokenExpires) {
        return token;
      } else {
        const refreshedToken = await refreshAccessToken(token.refreshToken);
        return {
          ...token,
          token: refreshedToken.token,
          refreshToken: refreshedToken.tokenRefresh,
          accessTokenExpires:
            Date.now() + parseInt(refreshedToken.expiresIn, 10),
        };
      }
    },
    async session({ session, token }) {
      session.user = token;
      return session;
    },
  },

  pages: {
    signIn: "/login",
    signOut: "/login",
  },
};

const handler = NextAuth(authOptions);
export { handler as GET, handler as POST };
  1. deleteArticle.tsx:負責從資料庫中刪除特定文章的函數。
'use client';
import React from 'react';
import { SessionProvider } from "next-auth/react";
export default function Provider({
    children,
    session
}: {
    children: React.ReactNode,
    session: any
}): React.ReactNode {
    return (
        <SessionProvider session={session} >
            {children}
        </SessionProvider>
    )
};

成分

接下來,讓我們編寫整個應用程式中使用的每個元件。

組件/Navbar.tsx

一個有兩個導航連結的簡單元件。

/*global.css*/
.container {
  max-width: 1100px;
  width: 100%;
  margin: 0px auto;
}

.image-container {
  position: relative;
  width: 100%;
  height: 5em;
  padding-top: 56.25%; /* Aspect ratio 16:9 (dividindo a altura pela largura) */
}

.image-container img {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

@keyframes spinner {
  0% {
    transform: rotate(0deg);
  }
  100% {
    transform: rotate(360deg);
  }
}

.loading-spinner {
  width: 50px;
  height: 50px;
  border: 10px solid #f3f3f3;
  border-top: 10px solid #293d71;
  border-radius: 50%;
  animation: spinner 1.5s linear infinite;
}

組件/Loading.tsx

一個簡單的載入元件,在等待 API 呼叫完成時使用。

import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
import Provider from "@/context/auth-provider";
import { getServerSession } from "next-auth";
import { authOptions } from "./api/auth/[...nextauth]/route";
const inter = Inter({ subsets: ["latin"] });

export const metadata: Metadata = {
  title: "Markdown Text Editor",
  description: "Created by <@vitorAlecrim>",
};

export default async function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  const session = await getServerSession(authOptions);
  return (
    <Provider session={session}>
      <html lang="en">
        <body className={inter.className}>{children}</body>
      </html>
    </Provider>
  );
}

組件/分頁.tsx

我們頁面上使用的分頁元件,在我們的私有路徑中顯示我們的所有文章。您可以在這裡找到有關如何編寫此組件的更詳細的文章

npm i
npm run start

組件/ArticleCard.tsx

用來顯示書面文章的卡片組件。

該組件還包含一個鏈接,該鏈接將指向文章顯示頁面和編輯先前撰寫的文章的頁面。

npx create-next-app myblog

元件/ArticleList.tsx

負責進行 API 呼叫並顯示回應的元件。

在這裡,我們將透過我們編寫的函數使用兩個 API 呼叫:

  1. getArticles.ts - 傳回將在元件中顯示的所有文章。
  2. removeArticle - 從我們的清單和伺服器中刪除特定的文章。

我們將使用先前編寫的 Pagination.tsx 元件來跨頁面分割文章數量。

npm i  markdown-it @types/markdown-it markdown-it-style github-markdown-css react-markdown

頁數

接下來,我們將按各自的路線劃分瀏覽每個頁面。

公共頁面

登入

這是我們應用程式的主頁。這是一個簡單的頁面,您可以根據需要對其進行修改。在這個頁面中,我們將使用next-auth導航庫提供的登入功能。

在檔案 src/app/pages/public/login/page.tsx 中。

remark remark-gfm remark-react

文章頁

為了建立文章閱讀頁面,我們將開發一個動態頁面。

您造訪過的每個部落格平台可能都有一個用於閱讀文章的專用頁面,可透過 URL 存取。其原因是動態頁面路由。幸運的是,Next.js 透過其新的 AppRouter 方法使這一切變得簡單,使我們的生活變得更加簡單。

首先:我們需要透過新增 [id] 資料夾在結構中建立路由。這將產生以下結構:pages/(public)/articles/[id]/pages.tsx.

  • id 對應於我們導航路線的 slug。
  • params 是透過包含導航 slug 的應用程式樹傳遞的屬性。
npm @codemirror/commands @codemirror/highlight @codemirror/lang-javascript @codemirror/lang-markdown @codemirror/language @codemirror/language-data @codemirror/state @codemirror/theme-one-dark @codemirror/view

第二:使用MarkdownIt函式庫,讓頁面能夠顯示Markdown格式的文字。

npm i react-icons @types/react-icons

最後,

頁面準備好後,例如透過在瀏覽器中存取 localhost:3000/articles/1,您將能夠使用提供的 ID 查看文章。

在我們的例子中,當單擊其中一個 ArticleCards.tsx 元件時,ID 將透過導航傳遞,該元件將呈現在我們的私人路由的主頁上。

src-
  |- app/
  |    |-(pages)/
  |    |      |- (private)/
  |    |      |       |- (home)
  |    |      |       |- editArticle/[id]
  |    |      |       |
  |    |      |       |- newArticle
  |    |      | - (public)/
  |    |              | - article/[id]
  |    |              | - login
  |    |
  |   api/
  |    |- auth/[...nextAuth]/route.ts
  |    |- global.css
  |    |- layout.tsx
  |
  | - components/
  | - context/
  | - interfaces/
  | - lib/
  | - services/
middleware.ts

私人頁面

這是我們的私人頁面,只有使用者在我們的應用程式中通過身份驗證後才能存取。

在我們的app/pages/資料夾中,當在()內宣告一個檔案時,就表示該路由對應於/。

在我們的例子中,(Home) 資料夾指的是我們私人路線的主頁。這是使用者在系統中進行身份驗證後看到的第一個頁面。此頁面將顯示我們資料庫中的文章清單。

資料將由我們的 ArticlesList.tsx 元件處理。如果您還沒有編寫此程式碼,請參閱元件部分。

在應用程式/(頁面)/(私人)/(主頁)/page.tsx。

npm i
npm run start

新文章

這是我們應用程式中最重要的頁面之一,因為它允許我們註冊我們的文章。

此頁面將使用戶能夠:

  1. 以 Markdown 格式寫一篇文章。
  2. 為文章分配圖像。
  3. 在將 Markdown 文字提交到伺服器之前預覽它。

頁面使用了多個鉤子

  1. useCallback - 用於記憶函數。
  2. useState - 允許您為我們的元件新增狀態變數。
  3. useSession - 讓我們檢查使用者是否經過身份驗證並取得身份驗證令牌。

為此,我們將使用兩個組件:

  1. TextEditor.tsx:我們之前寫的文字編輯器。
  2. Preview.tsx:用於顯示 Markdown 格式檔案的元件。

在建立此頁面時,我們將使用我們的 API:

  1. POST:使用我們的函數 postArticle,我們將把文章送到伺服​​器。

我們也將使用 next-auth 函式庫提供的 useSession 鉤子來取得使用者的驗證令牌,該令牌將用於在伺服器上註冊文章。

這將涉及三個不同的 API 呼叫。
在 app/pages/(private)/newArticle/page.tsx.

「使用客戶端」;
從「react」匯入 React, { ChangeEvent, useCallback, useState };
從“next-auth/react”導入{useSession};
從“下一步/導航”導入{重定向};
從“@/services/postArticle”匯入 postArtical;
從“react-icons/ai”導入{AiOutlineFolderOpen};
從“react-icons/ri”導入 { RiImageEditLine };

從“下一個/圖像”導入圖像;
從“@/components/textEditor”導入文字編輯器;
從“@/components/PreviewText”導入預覽;
從“react-icons/ai”導入{AiOutlineSend};
從“react-icons/bs”導入{BsBodyText};

匯出預設函數 NewArticle(params:any) {
  const { 資料:會話 }:任何 = useSession({
    要求:真實,
    onUnauthenticated(){
      重定向(“/登入”);
    },
  });
  const [imageUrl, setImageUrl] = useState<object>({});
  const [previewImage, setPreviewImage] = useState<string>("");
  const [previewText, setPreviewText] = useState<boolean>(false);
  const [標題,setTitle] = useState<string>("");
  const [doc, setDoc] = useState<string>("# Escreva o seu texto... n");
  const handleDocChange = useCallback((newDoc: any) => {
    setDoc(newDoc);
  }, []);

  if (!session?.user) 回傳 null;

  const handleArticleSubmit = async (e:any) =>; {
        e.preventDefault();
    const token: string = session.user.token;
    嘗試 {
      const res = 等待 postArtical({
        id: session.user.userId.toString(),
        令牌:令牌,
        圖片網址: 圖片網址,
        標題:“標題”
        文檔: 文檔,
      });
      console.log('re--->', res);
      重定向('/成功');
    } 捕獲(錯誤){
      console.error('提交文章時發生錯誤:', error);
      // 如果需要,處理錯誤
      拋出錯誤;
    }
  };

  const handleImageChange = (e: React.ChangeEvent<htmlinputelement>) =>; {
    if (e.target.files && e.target.files.length > 0) {
      const 檔 = e.target.files[0];
      const url = URL.createObjectURL(文件);
      設定預覽影像(網址);
      setImageUrl(文件);
    }
  };

  const handleTextPreview = (e: 任意) => {
    e.preventDefault();
    setPreviewText(!previewText);
  };
  返回 (
    <section classname="w-full h-full min-h-screenrelative py-8">
      {預覽文字&&(
        <div classname="absolute right-16 top-5 p-5 border-2 border-slate-500 bg-slate-100 rounded-xl w-full max-w-[33em] z-30">;
          ; setPreviewText(!previewText)}
          >>
        </div>;
      )}

      <form classname="relative mx-auto max-w-[700px] h-full min-h-[90%] w-full p-2 border-2 border-slate-200 rounded-md bg-slate-50 drop -shadow-xl flex flex-col 間隙-2“>
        {“”}
        <div className=" flex justify- between items-center>
          
            <bsbodytext></bsbodytext>>
            預覽
          按鈕>{" "}
          
            恩維亞爾·特克斯托
            <aioutlinesend classname="w-5 h-5 group-hover:text-red-500"></aioutlinesend>
          按鈕>
        ;
        <div classname="header-wrapper flex flex-col gap-2">
          <div classname="image-box">
            {previewImage.length === 0 && (
              <div classname="select-image">
                
                  <aioutlinefolderopen classname="w-7 h-7"></aioutlinefolderopen>
                  拖放影像
                標籤>
                



<h4>
  
  
  編輯文章
</h4>

<p>與<em>新文章</em>(newArticle)類似的頁面,但有一些差異。 </p>

<p>首先,我們定義一條動態路線,在其中接收 id 作為導航參數。這與文章閱讀頁面上所做的非常相似。 <br>
app/(pages)/(private)/editArticle/[id]/page.tsx<br>
</p>
<pre class="brush:php;toolbar:false">「使用客戶端」;
從「react」匯入 React, { useState, useEffect, useCallback, useRef, ChangeEvent };
從“next-auth/react”導入{useSession};
從“下一步/導航”導入{重定向};
從“下一個/圖像”導入圖像;

從“@/interfaces/article.interface”導入{IArticle};
從“react-icons/ai”導入{AiOutlineEdit};
從“react-icons/bs”導入{BsBodyText};
從“react-icons/ai”導入{AiOutlineFolderOpen};
從“react-icons/ri”導入 { RiImageEditLine };

從“@/components/PreviewText”導入預覽;
從“@/components/textEditor”導入文字編輯器;
從'@/components/Loading'導入載入;
從“@/services/editArticle”導入 editArtical;

匯出預設函數 EditArticle({ params }: { params: any }) {
 const { 資料:會話 }:任何 = useSession({
    要求:真實,
    onUnauthenticated(){
      重定向(“/登入”);
    },
  });
  const id: 數字 = params.id;
  const [文章,setArticle] = useState<iarticle>(空);
  const [imageUrl, setImageUrl] = useState<object>({});
  const [previewImage, setPreviewImage] = useState<string>("");
  const [previewText, setPreviewText] = useState<boolean>(false)
  const [標題,setTitle] = useState<string>("");
  const [doc, setDoc] = useState<string>('');
  const handleDocChange = useCallback((newDoc: any) => {
    setDoc(newDoc);
  }, []);
  const inputRef= useRef<htmlinputelement>(null);

  const fetchArticle = async (id: number) =>; {
    嘗試 {
      常量響應 = 等待獲取(
        `http://localhost:8080/articles/getById/${id}`,
      );
      const jsonData = 等待回應.json();
      setArticle(jsonData);
    } 捕獲(錯誤){
      console.log("出了點問題:", err);
    }
  };
  useEffect(() => {
    if (文章 !== null || 文章 !== 未定義) {
      取得文章(id);
    }
  }, [ID]);

  useEffect(()=>{
    if(文章!= null && 文章.內容){
        setDoc(文章.內容)
    }

    if(文章!=null && 文章.image){
      setPreviewImage(`http://localhost:8080/` 文章.image)
    }
  },[文章])

  const handleArticleSubmit = async (e:any) =>; {
     e.preventDefault();
    const token: string = session.user.token;
    嘗試{
      const res = 等待 editArtical({
      身分證字號: 身分證號,
      令牌:令牌,
      圖片網址:圖片網址,
      標題: 標題,
      文檔: 文檔,
      });
        console.log('re--->',res)
        返回資源;
    } 捕獲(錯誤){
    console.log(“錯誤:”,錯誤)
    }
  };
  const handleImageClick = ()=>{
      console.log('hiii')
    if(inputRef.current){
      inputRef.current.click();
    }
  }const handleImageChange = (e: React.ChangeEvent<htmlinputelement>) =>; {
    if (e.target.files && e.target.files.length > 0) {
      const 檔 = e.target.files[0];
      const url = URL.createObjectURL(文件);
      設定預覽影像(網址);
      setImageUrl(文件);
    }

  };
   const handleTextPreview = (e: 任意) => {
    e.preventDefault();
    setPreviewText(!previewText);
    console.log('預覽版你好!')
  };

  if(!article) return >
  if(文章?.內容)
  返回 (
    <section classname="w-full h-full min-h-screenrelative py-8">
      {預覽文字&&(
        <div classname="absolute right-16 top-5 p-5 border-2 border-slate-500 bg-slate-100 rounded-xl w-full max-w-[33em] z-30">;
          ; setPreviewText(!previewText)}
          >>
        </div>
      )}

      <div classname="relative mx-auto max-w-[700px] h-full min-h-[90%] w-full p-2 border-2 border-slate-200 rounded-md bg-white drop- Shadow -md flex flex-col 間隙-2">
        <form classname="relative mx-auto max-w-[700px] h-full min-h-[90%] w-full p-2 border-2 border-slate-200 rounded-md bg-slate-50 drop -shadow-md flex flex-col 間隙-2 ">
          {“”}
          <div classname="flex justify- Between items-center">;
            
              <bsbodytext></bsbodytext>>
              預覽
            按鈕>{" "}
            
                編輯阿蒂戈
              <aioutlineedit classname="w-5 h-5 group-hover:text-red-500"></aioutlineedit>>
            按鈕>
          </div>;
          <div classname="header-wrapper flex flex-col gap-2">;
            <div classname="image-box">;
              {previewImage.length === 0 && (
                <div classname="select-image">;
                  
                    <aioutlinefolderopen classname="w-7 h-7"></aioutlinefolderopen>>
                    拖放影像
                  標籤>
                  



<h2>
  
  
  結論
</h2>

<p>首先,我要感謝您花時間閱讀本教程,並且我還要祝賀您完成它。我希望它對您有幫助,並且逐步說明很容易遵循。 </p>

<p>其次,我想強調一下關於我們剛剛建立的內容的幾點。這是部落格系統的基礎,還有很多東西需要添加,例如顯示所有文章的公共頁面、用戶註冊頁面,甚至是自訂的 404 錯誤頁面。如果在教程期間您對這些頁面感到好奇並錯過了它們,請知道這是故意的。本教學為您提供了足夠的經驗來自行創建這些新頁面、添加許多其他頁面以及實現新功能。 </p>

<p>非常感謝,下次再見。哦/</p>


          </div>

            
        </div>
</div>
</form>
</div></section></htmlinputelement></htmlinputelement></string></string></boolean></string></object></iarticle>

以上是使用 Next.js 建立動態部落格儀表板的詳細內容。更多資訊請關注PHP中文網其他相關文章!

typescript json css String Boolean Object NULL define if for while select format include Session try throw catch Error Token const auto register using public private Length Interface finally Property pointer JS console number undefined function default this display border transform flex input github database http everything Access Foundation Other
陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
上一篇:如何使用 JavaScript 可靠地檢測 URL 哈希中的變更?下一篇:如何使用 JavaScript 可靠地檢測 URL 哈希中的變更?

相關文章

看更多