Hallo, wie geht es dir? Hier ist Vítor, der mit einem neuen Projekt zurückkommt, das Ihnen dabei helfen soll, Ihre Programmierkenntnisse zu verbessern. Es ist schon eine Weile her, seit ich das letzte Mal ein Tutorial veröffentlicht habe. In den letzten Monaten habe ich mir etwas Zeit genommen, um mich auszuruhen und mich auf andere Aktivitäten zu konzentrieren. In dieser Zeit habe ich ein kleines Webprojekt entwickelt: einen Blog, der zum Schwerpunkt dieses Tutorials wurde.
In dieser Anleitung erstellen wir das Frontend einer Blog-Seite, die Markdown rendern kann. Die Anwendung umfasst öffentliche und private Routen, Benutzerauthentifizierung und die Möglichkeit, Markdown-Text zu schreiben, Fotos hinzuzufügen, Artikel anzuzeigen und vieles mehr.
Sie können Ihre Anwendung jederzeit nach Ihren Wünschen anpassen – ich ermutige Sie sogar dazu.
Sie können hier auf das Repository für diese Anwendung zugreifen:
npm i npm run start
Sie können den Server Ihrer Anwendung auf dem Server finden
Dieses Tutorial beinhaltet auch das Schreiben des Node.js-Servers, der in diesem Handbuch verwendet wird:
Ich hoffe es gefällt euch.
Viel Spaß beim Codieren!
Hier ist eine Zusammenfassung der in diesem Projekt verwendeten Bibliotheken:
Wir werden die neueste Version des Next.js-Frameworks verwenden, die zum Zeitpunkt der Erstellung dieses Tutorials Version 13.4 ist.
Führen Sie den folgenden Befehl aus, um das Projekt zu erstellen:
npm i npm run start
Wählen Sie während der Installation die Vorlageneinstellungen aus. In diesem Tutorial verwende ich TypeScript als Programmiersprache und das Tailwind CSS-Framework zum Gestalten unserer Anwendung.
Jetzt installieren wir alle Bibliotheken, die wir verwenden werden.
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
Bereinigen Sie dann die ursprüngliche Struktur Ihrer Installation, indem Sie alles entfernen, was wir nicht verwenden werden.
Dies ist die endgültige Struktur unserer Bewerbung.
npm i react-icons @types/react-icons
Im Stammverzeichnis des Projekts, in der Datei next.config.js, konfigurieren wir die Domänenadresse, von der aus wir auf die Bilder für unsere Artikel zugreifen. Für dieses Tutorial oder wenn Sie einen lokalen Server verwenden, verwenden wir localhost.
Stellen Sie sicher, dass Sie diese Konfiguration einschließen, um das korrekte Laden von Bildern in Ihrer Anwendung sicherzustellen.
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
Erstellen Sie im Stammordner der Anwendung src/ eine middleware.ts, um den Zugriff auf private Routen zu überprüfen.
const nextConfig = { images: { domains: ["localhost"], }, };
Um mehr über Middlewares und alles, was Sie damit machen können, zu erfahren, schauen Sie sich die Dokumentation an.
Erstellen Sie im Ordner /app eine Datei mit dem Namen route.ts in api/auth/[...nextauth]. Es enthält die Konfiguration für unsere Routen und stellt über den CredentialsProvider eine Verbindung zu unserer Authentifizierungs-API her.
Der CredentialsProvider ermöglicht Ihnen die Anmeldung mit beliebigen Anmeldeinformationen wie Benutzername und Passwort, Domäne, Zwei-Faktor-Authentifizierung, Hardwaregerät usw.
Erstellen Sie zunächst im Stammverzeichnis Ihres Projekts eine .env.local-Datei und fügen Sie ein Token hinzu, das als unser Geheimnis verwendet wird.
npm i npm run start
Als nächstes schreiben wir unser Authentifizierungssystem, in dem dieses NEXTAUTH_SECRET zu unserem Geheimnis in der Datei src/app/auth/[...nextauth]/routes.ts hinzugefügt wird.
npx create-next-app myblog
Lassen Sie uns einen Authentifizierungsanbieter erstellen, einen Kontext, der die Daten unserer Benutzer auf den Seiten unserer privaten Route teilt. Wir werden es später verwenden, um eine unserer layout.tsx-Dateien zu verpacken.
Erstellen Sie eine Datei in src/context/auth-provider.tsx mit folgendem Inhalt:
npm i markdown-it @types/markdown-it markdown-it-style github-markdown-css react-markdown
Insgesamt werden wir in unserer Anwendung Tailwind CSS verwenden, um unser Styling zu erstellen. An einigen Stellen werden wir jedoch benutzerdefinierte CSS-Klassen zwischen Seiten und Komponenten teilen.
remark remark-gfm remark-react
Jetzt schreiben wir die Layouts, sowohl privat als auch öffentlich.
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
Unsere Anwendung führt mehrere Aufrufe an unsere API durch, und Sie können diese Anwendung so anpassen, dass sie jede externe API verwendet. In unserem Beispiel verwenden wir unsere lokale Anwendung. Wenn Sie das Backend-Tutorial und die Servererstellung noch nicht gesehen haben, schauen Sie sich das an.
In src/services/ schreiben wir die folgenden Funktionen:
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"], }, };
export { default } from "next-auth/middleware"; export const config = { matcher: ["/", "/newArticle/", "/article/", "/article/:path*"], };
.env.local NEXTAUTH_SECRET = SubsTituaPorToken
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 };
'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> ) };
Als nächstes schreiben wir jede Komponente, die in der Anwendung verwendet wird.
Eine einfache Komponente mit zwei Navigationslinks.
/*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; }
Eine einfache Ladekomponente, die verwendet wird, während auf den Abschluss von API-Aufrufen gewartet wird.
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> ); }
Eine Paginierungskomponente, die auf unserer Seite verwendet wird und alle unsere Artikel auf unserem privaten Weg anzeigt. Einen ausführlicheren Artikel zum Schreiben dieser Komponente finden Sie hier
npm i npm run start
Eine Kartenkomponente zum Anzeigen geschriebener Artikel.
Diese Komponente enthält auch einen Link, der sowohl zur Artikelanzeigeseite als auch zur Seite zum Bearbeiten eines zuvor verfassten Artikels führt.
npx create-next-app myblog
Eine Komponente, die dafür verantwortlich ist, API-Aufrufe durchzuführen und die Antwort anzuzeigen.
Hier verwenden wir zwei API-Aufrufe über die von uns geschriebenen Funktionen:
Wir werden die zuvor geschriebene Pagination.tsx-Komponente verwenden, um die Anzahl der Artikel auf Seiten aufzuteilen.
npm i markdown-it @types/markdown-it markdown-it-style github-markdown-css react-markdown
Als nächstes gehen wir jede unserer Seiten durch, unterteilt nach ihren jeweiligen Routen.
Dies ist die Homepage unserer Bewerbung. Es handelt sich um eine einfache Seite, die Sie nach Belieben ändern können. Auf dieser Seite verwenden wir die Anmeldefunktion der Next-Auth-Navigationsbibliothek.
In der Datei src/app/pages/public/login/page.tsx.
remark remark-gfm remark-react
Um die Seite zum Lesen von Artikeln zu erstellen, entwickeln wir eine dynamische Seite.
Jede Blog-Plattform, die Sie besucht haben, verfügt wahrscheinlich über eine eigene Seite zum Lesen von Artikeln, auf die über eine URL zugegriffen werden kann. Der Grund hierfür ist eine dynamische Seitenweiterleitung. Glücklicherweise macht Next.js dies mit seiner neuen AppRouter-Methode einfach und macht unser Leben viel einfacher.
Zuerst müssen wir die Route in unserer Struktur erstellen, indem wir einen [id]-Ordner hinzufügen. Dies führt zu folgender Struktur: seiten/(public)/articles/[id]/pages.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
Zweitens: Verwenden Sie die MarkdownIt-Bibliothek, um der Seite die Anzeige von Text im Markdown-Format zu ermöglichen.
npm i react-icons @types/react-icons
Und schließlich
Sobald die Seite fertig ist, können Sie den Artikel mit der angegebenen ID anzeigen, indem Sie beispielsweise im Browser auf localhost:3000/articles/1 zugreifen.
In unserem Fall wird die ID durch die Navigation weitergegeben, wenn auf eine der ArticleCards.tsx-Komponenten geklickt wird, die auf der Hauptseite unserer privaten Route gerendert wird.
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
Hier sind unsere privaten Seiten, auf die nur zugegriffen werden kann, wenn der Benutzer in unserer Anwendung authentifiziert ist.
Wenn in unserem Ordner app/pages/ eine Datei in () deklariert wird, bedeutet dies, dass die Route / entspricht.
In unserem Fall bezieht sich der (Home-)Ordner auf die Homepage unserer privaten Route. Es ist die erste Seite, die der Benutzer sieht, wenn er sich beim System authentifiziert. Auf dieser Seite wird die Liste der Artikel aus unserer Datenbank angezeigt.
Die Daten werden von unserer ArticlesList.tsx-Komponente verarbeitet. Wenn Sie diesen Code noch nicht geschrieben haben, schauen Sie noch einmal im Abschnitt „Komponenten“ nach.
In app/(pages)/(private)/(home)/page.tsx.
npm i npm run start
Dies ist eine der wichtigsten Seiten unserer Bewerbung, da sie uns die Registrierung unserer Artikel ermöglicht.
Auf dieser Seite kann der Benutzer:
Die Seite verwendet mehrere Hooks:
Dazu verwenden wir zwei Komponenten:
Beim Erstellen dieser Seite verwenden wir unsere API:
Wir werden auch den useSession-Hook verwenden, der von der Next-Auth-Bibliothek bereitgestellt wird, um das Authentifizierungstoken des Benutzers zu erhalten, das zur Registrierung des Artikels auf dem Server verwendet wird.
Dies umfasst drei verschiedene API-Aufrufe.
In app/pages/(private)/newArticle/page.tsx.
"Client verwenden"; import React, { ChangeEvent, useCallback, useState } from „react“; import { useSession } from „next-auth/react“; import { redirect } from „next/navigation“; postArtical aus „@/services/postArticle“ importieren; import { AiOutlineFolderOpen } aus „react-icons/ai“; import { RiImageEditLine } aus „react-icons/ri“; Bild aus „next/image“ importieren; TextEditor aus „@/components/textEditor“ importieren; Vorschau aus „@/components/PreviewText“ importieren; import { AiOutlineSend } aus „react-icons/ai“; import { BsBodyText } aus „react-icons/bs“; Standardfunktion exportieren NewArticle(params:any) { const { data: session }: any = useSession({ erforderlich: wahr, onUnauthenticated() { weiterleiten("/login"); }, }); const [imageUrl, setImageUrl] = useState<object>({}); const [previewImage, setPreviewImage] = useState<string>(""); const [previewText, setPreviewText] = useState<boolean>(false); const [title, setTitle] = useState<string>(""); const [doc, setDoc] = useState<string>("# Escreva o seu texto... n"); const handleDocChange = useCallback((newDoc: any) => { setDoc(newDoc); }, []); if (!session?.user) return null; const handleArticleSubmit = async (e:any) => { e.preventDefault(); const token: string = session.user.token; versuchen { const res = warte auf postArtical({ id: session.user.userId.toString(), Token: Token, imageUrl: imageUrl, Titel: „Titel“, doc: doc, }); console.log('re--->', res); umleiten('/success'); } Catch (Fehler) { console.error('Fehler beim Senden des Artikels:', Fehler); // Fehler bei Bedarf behandeln Wurffehler; } }; const handleImageChange = (e: React.ChangeEvent<HTMLInputElement>) => { if (e.target.files && e.target.files.length > 0) { const file = e.target.files[0]; const url = URL.createObjectURL(file); setPreviewImage(url); setImageUrl(file); } }; const handleTextPreview = (e: any) => { e.preventDefault(); setPreviewText(!previewText); }; zurückkehren ( <section className="w-full h-full min-h-screen relative py-8"> {previewText && ( <div className="absolute right-16 top-5 p-5 border-2 border-slate-500 bg-slate-100 abgerundet-xl w-full max-w-[33em] z-30"> <Vorschau doc={doc} Titel={Titel} PreviewImage={previewImage} onPreview={() => 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 abgerundet-md bg-slate-50 Drop-Shadow-XL Flex Flex-Col Gap-2 "> {" "} <div className="flex justify-between items-center"> <-Taste className="border-b-2 abgerundet-md border-slate-500 p-2 flexible items-center gap-2 hover:border-slate-400 hover:text-slate-800" onClick={handleTextPreview} > <BsBodyText /> Vorschau </button>{" "} <-Taste className="group border border-b-2 border-slate-500 abgerundet-md p-2 flexible items-center gap-2 hover:border-slate-400 hover:text-slate-800 " onClick={handleArticleSubmit} > Senden Sie den Text <AiOutlineSend className="w-5 h-5 group-hover:text-red-500" /> </button> </div> <div className="header-wrapper flex flex-col gap-2 "> <div className="image-box"> {previewImage.length === 0 && ( <div className="select-image"> <Label htmlFor="image" className="p-4 border-dashed border-4 border-slate-400 Cursor-Pointer Flex Flex-Col Items-Center Justify-Center" > <AiOutlineFolderOpen className="w-7 h-7" /> Drang-and-Drop-Bild </label> <Eingabe > <h4> Artikel bearbeiten </h4> <p>Eine Seite ähnlich wie <em>Neuer Artikel</em> (newArticle), mit einigen Unterschieden.</p> <p>Zuerst definieren wir eine dynamische Route, bei der wir eine ID als Navigationsparameter erhalten. Dies ist sehr ähnlich zu dem, was auf der Seite zum Lesen von Artikeln durchgeführt wurde. <br> app/(pages)/(private)/editArticle/[id]/page.tsx<br> </p><pre class="brush:php;toolbar:false">"Client verwenden"; import React, { useState, useEffect, useCallback, useRef, ChangeEvent } from „react“; import { useSession } from „next-auth/react“; import { redirect } from „next/navigation“; Bild aus „next/image“ importieren; import { IArticle } from „@/interfaces/article.interface“; import { AiOutlineEdit } aus „react-icons/ai“; import { BsBodyText } aus „react-icons/bs“; import { AiOutlineFolderOpen } aus „react-icons/ai“; import { RiImageEditLine } aus „react-icons/ri“; Vorschau aus „@/components/PreviewText“ importieren; TextEditor aus „@/components/textEditor“ importieren; import Loading from '@/components/Loading'; editArtical aus „@/services/editArticle“ importieren; Standardfunktion exportieren EditArticle({ params }: { params: any }) { const { data: session }: any = useSession({ erforderlich: wahr, onUnauthenticated() { weiterleiten("/login"); }, }); const id: number = params.id; const [article, setArticle] = useState<IArticle | null>(null); const [imageUrl, setImageUrl] = useState<object>({}); const [previewImage, setPreviewImage] = useState<string>(""); const [previewText, setPreviewText] = useState<boolean>(false) const [title, 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) => { versuchen { const Antwort = Warten auf Abruf( `http://localhost:8080/articles/getById/${id}`, ); const jsonData = waiting Response.json(); setArticle(jsonData); } fangen (irrt) { console.log("etwas ist schiefgelaufen:", err); } }; useEffect(() => { if (Artikel !== null || Artikel !== undefiniert) { fetchArticle(id); } }, [Ausweis]); useEffect(()=>{ if(article != null && Article.content){ setDoc(article.content) } if(article !=null && Article.image){ setPreviewImage(`http://localhost:8080/` Article.image) } },[Artikel]) const handleArticleSubmit = async (e:any) => { e.preventDefault(); const token: string = session.user.token; versuchen{ const res = warte auf editArtical({ id: id, Token: Token, imageUrl:imageUrl, Titel: Titel, doc: doc, }); console.log('re--->',res) Rückkehr res; } Catch(Fehler){ console.log("Fehler:", Fehler) } }; 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 file = e.target.files[0]; const url = URL.createObjectURL(file); setPreviewImage(url); setImageUrl(file); } }; const handleTextPreview = (e: any) => { e.preventDefault(); setPreviewText(!previewText); console.log('Hallo aus der Vorschau!') }; if(!article) return <Loading/> if(Artikel?.Inhalt) zurückkehren ( <section className='w-full h-full min-h-screen relative py-8'> {previewText && ( <div className="absolute right-16 top-5 p-5 border-2 border-slate-500 bg-slate-100 abgerundet-xl w-full max-w-[33em] z-30"> <Vorschau doc={doc} Titel={Titel} PreviewImage={previewImage} onPreview={() => 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 abgerundet-md bg-white drop- Shadow-MD Flex Flex-Col Gap-2'> <form className='relative mx-auto max-w-[700px] h-full min-h-[90%] w-full p-2 border-2 border-slate-200 abgerundet-md bg-slate-50 drop-shadow-md flex flex-col Gap-2 '> {" "} <div className='flex justify-between items-center'> <-Taste className='border-b-2 abgerundet-md border-slate-500 p-2 flexible items-center Gap-2 hover:border-slate-400 hover:text-slate-800' onClick={handleTextPreview} > <BsBodyText /> Vorschau </button>{" "} <-Taste className='group border border-b-2 border-slate-500 abgerundet-md p-2 flexible items-center gap-2 hover:border-slate-400 hover:text-slate-800 ' onClick={handleArticleSubmit} > Edite Artigo <AiOutlineEdit className='w-5 h-5 group-hover:text-red-500' /> </button> </div> <div className='header-wrapper flex flex-col gap-2 '> <div className='image-box'> {previewImage.length === 0 && ( <div className='select-image'> <Label htmlFor='image' className='p-4 border-dashed border-4 border-slate-400 Cursor-Pointer Flex Flex-Col Items-Center Justify-Center' > <AiOutlineFolderOpen className='w-7 h-7' /> Drang-and-Drop-Bild </label> <Eingabe > <h2> Abschluss </h2> <p>Zunächst möchte ich Ihnen dafür danken, dass Sie sich die Zeit genommen haben, dieses Tutorial zu lesen, und ich möchte Ihnen auch zum Abschluss gratulieren. Ich hoffe, es hat Ihnen gute Dienste geleistet und die Schritt-für-Schritt-Anleitung war leicht zu befolgen.</p> <p>Zweitens möchte ich ein paar Punkte zu dem hervorheben, was wir gerade gebaut haben. Dies ist die Grundlage eines Blog-Systems, und es gibt noch viel hinzuzufügen, z. B. eine öffentliche Seite, auf der alle Artikel angezeigt werden, eine Benutzerregistrierungsseite oder sogar eine benutzerdefinierte 404-Fehlerseite. Wenn Sie sich während des Tutorials über diese Seiten gewundert haben und sie verpasst haben, wissen Sie, dass dies Absicht war. Dieses Tutorial hat Ihnen genügend Erfahrung vermittelt, um diese neuen Seiten selbst zu erstellen, viele andere hinzuzufügen und neue Funktionen zu implementieren.</p> <p>Vielen Dank und bis zum nächsten Mal. o/</p>
Das obige ist der detaillierte Inhalt vonErstellen eines dynamischen Blog-Dashboards mit Next.js. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!