♻️ 使用 NextJS 重构 #1
| @@ -34,7 +34,7 @@ export default function Home() { | ||||
|         </Grid> | ||||
|         <Grid item xs={12} md={6} sx={{ display: "flex", justifyContent: "end" }}> | ||||
|           <Box> | ||||
|             <Image src="smartsheep.svg" alt="Logo" width={256} height={256} /> | ||||
|             <Image src="/smartsheep.svg" alt="Logo" width={256} height={256} /> | ||||
|           </Box> | ||||
|         </Grid> | ||||
|       </Grid> | ||||
|   | ||||
							
								
								
									
										39
									
								
								app/posts/[id]/page.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								app/posts/[id]/page.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,39 @@ | ||||
| import { Box, Card, CardContent, CardMedia, Divider, Typography } from "@mui/material"; | ||||
| import { getSinglePost } from "@/content/posts"; | ||||
| import Image from "next/image"; | ||||
| import PostContent from "@/components/posts/PostContent"; | ||||
|  | ||||
| export default function PostDetailPage({ params }: { params: { id: string } }) { | ||||
|   const post = getSinglePost(params.id); | ||||
|  | ||||
|   return ( | ||||
|     <Card> | ||||
|       { | ||||
|         post.thumbnail && | ||||
|         <CardMedia sx={{ height: 360, position: "relative" }} title={post.title}> | ||||
|           <Image | ||||
|             fill | ||||
|             src={post.thumbnail} | ||||
|             alt={post.title} | ||||
|             style={{ objectFit: "cover" }} | ||||
|           /> | ||||
|         </CardMedia> | ||||
|       } | ||||
|  | ||||
|       <CardContent sx={{ paddingX: 5, paddingY: 3 }}> | ||||
|         <Box> | ||||
|           <Typography gutterBottom variant="h5" component="h1"> | ||||
|             {post.title} | ||||
|           </Typography> | ||||
|           <Typography color="text.secondary" variant="body2"> | ||||
|             {post.description ?? "No description yet."} | ||||
|           </Typography> | ||||
|         </Box> | ||||
|         <Divider sx={{ my: 5 }} /> | ||||
|         <Box component="article" sx={{ minWidth: 0 }}> | ||||
|           <PostContent content={post.content ?? ""} /> | ||||
|         </Box> | ||||
|       </CardContent> | ||||
|     </Card> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										14
									
								
								app/posts/layout.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								app/posts/layout.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | ||||
| import { Box, Container } from "@mui/material"; | ||||
| import { ReactNode } from "react"; | ||||
|  | ||||
| export default function PostLayout({children}: Readonly<{ | ||||
|   children: ReactNode; | ||||
| }>) { | ||||
|   return ( | ||||
|     <Container sx={{ display: "flex", justifyContent: "center", gap: 4, py: 4 }}> | ||||
|       <Box sx={{ flexGrow: 1, maxWidth: 720 }}> | ||||
|         {children} | ||||
|       </Box> | ||||
|     </Container> | ||||
|   ) | ||||
| } | ||||
							
								
								
									
										40
									
								
								app/posts/page.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								app/posts/page.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,40 @@ | ||||
| import { Button, Card, CardActions, CardContent, CardMedia, Typography } from "@mui/material"; | ||||
| import { getSortedPosts } from "@/content/posts"; | ||||
| import Image from "next/image"; | ||||
| import Link from "next/link"; | ||||
|  | ||||
| export default function PostList() { | ||||
|   const posts = getSortedPosts(); | ||||
|  | ||||
|   return ( | ||||
|     posts.map((post) => ( | ||||
|       <Card key={post.id} sx={{ width: "100%" }}> | ||||
|         { | ||||
|           post.thumbnail && | ||||
|           <CardMedia sx={{ height: 160, position: "relative" }} title={post.title}> | ||||
|             <Image | ||||
|               fill | ||||
|               src={post.thumbnail} | ||||
|               alt={post.title} | ||||
|               style={{ objectFit: "cover" }} | ||||
|             /> | ||||
|           </CardMedia> | ||||
|         } | ||||
|  | ||||
|         <CardContent sx={{ paddingX: 5, paddingY: 3 }}> | ||||
|           <Typography gutterBottom variant="h5" component="h2"> | ||||
|             {post.title} | ||||
|           </Typography> | ||||
|           <Typography variant="body2" color="text.secondary"> | ||||
|             {post.description ?? "No description yet."} | ||||
|           </Typography> | ||||
|         </CardContent> | ||||
|         <CardActions sx={{ paddingX: 4, paddingBottom: 2 }}> | ||||
|           <Link href={`/posts/${post.id}`} passHref> | ||||
|             <Button>Read more</Button> | ||||
|           </Link> | ||||
|         </CardActions> | ||||
|       </Card> | ||||
|     )) | ||||
|   ); | ||||
| } | ||||
| @@ -15,6 +15,7 @@ import { SITE_NAME } from "@/app/consts"; | ||||
| import NavigationDrawer, { DRAWER_WIDTH, AppNavigationHeader, isMobileQuery } from "@/components/NavigationDrawer"; | ||||
| import MenuIcon from "@mui/icons-material/Menu"; | ||||
| import Image from "next/image"; | ||||
| import Link from "next/link"; | ||||
|  | ||||
| function HideOnScroll(props: { | ||||
|   window?: () => Window; | ||||
| @@ -105,11 +106,13 @@ export default function AppShell({ children }: { | ||||
|               aria-label="menu" | ||||
|               sx={{ ml: isMobile ? 0.5 : 0, mr: 2 }} | ||||
|             > | ||||
|               <Image src="smartsheep.svg" alt="Logo" width={32} height={32} /> | ||||
|               <Image src="/smartsheep.svg" alt="Logo" width={32} height={32} /> | ||||
|             </IconButton> | ||||
|  | ||||
|             <Typography variant="h6" component="div" sx={{ flexGrow: 1 }}> | ||||
|               {SITE_NAME} | ||||
|               <Link href="/"> | ||||
|                 {SITE_NAME} | ||||
|               </Link> | ||||
|             </Typography> | ||||
|  | ||||
|             <IconButton | ||||
|   | ||||
| @@ -2,15 +2,12 @@ | ||||
|  | ||||
| import ChevronLeftIcon from "@mui/icons-material/ChevronLeft"; | ||||
| import ChevronRightIcon from "@mui/icons-material/ChevronRight"; | ||||
| import InboxIcon from "@mui/icons-material/MoveToInbox"; | ||||
| import MailIcon from "@mui/icons-material/Mail"; | ||||
| import { | ||||
|   Box, | ||||
|   Divider, | ||||
|   Drawer, | ||||
|   IconButton, | ||||
|   List, | ||||
|   ListItem, | ||||
|   ListItemButton, | ||||
|   ListItemIcon, | ||||
|   ListItemText, | ||||
| @@ -20,6 +17,7 @@ import { | ||||
| import { theme } from "@/app/theme"; | ||||
| import { ReactNode } from "react"; | ||||
| import HomeIcon from "@mui/icons-material/Home"; | ||||
| import ArticleIcon from "@mui/icons-material/Article"; | ||||
| import Link from "next/link"; | ||||
|  | ||||
| export interface NavigationItem { | ||||
| @@ -30,7 +28,8 @@ export interface NavigationItem { | ||||
|  | ||||
| export const DRAWER_WIDTH = 320; | ||||
| export const NAVIGATION_ITEMS: NavigationItem[] = [ | ||||
|   { icon: <HomeIcon />, title: "首页", link: "/" } | ||||
|   { icon: <HomeIcon />, title: "首页", link: "/" }, | ||||
|   { icon: <ArticleIcon />, title: "新闻", link: "/posts" }, | ||||
| ]; | ||||
|  | ||||
| export const AppNavigationHeader = styled("div")(({ theme }) => ({ | ||||
|   | ||||
							
								
								
									
										7
									
								
								components/posts/PostContent.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								components/posts/PostContent.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | ||||
| "use client"; | ||||
|  | ||||
| import MuiMarkdown from "mui-markdown"; | ||||
|  | ||||
| export default function PostContent({ content }: { content: string }) { | ||||
|   return <MuiMarkdown>{content}</MuiMarkdown>; | ||||
| } | ||||
							
								
								
									
										47
									
								
								content/posts.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								content/posts.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,47 @@ | ||||
| import fs from "fs"; | ||||
| import path from "path"; | ||||
| import matter from "gray-matter"; | ||||
|  | ||||
| const postsDirectory = path.join(process.cwd(), "content", "posts"); | ||||
|  | ||||
| export interface Post { | ||||
|   id: string; | ||||
|   title: string; | ||||
|   thumbnail?: string; | ||||
|   description?: string; | ||||
|   content?: string; | ||||
|   date: Date; | ||||
| } | ||||
|  | ||||
| export function getSortedPosts() { | ||||
|   const fileNames = fs.readdirSync(postsDirectory); | ||||
|   const allPostsData: Post[] = fileNames.map((fileName) => { | ||||
|     const id = fileName.replace(/\.md$/, ""); | ||||
|  | ||||
|     const fullPath = path.join(postsDirectory, fileName); | ||||
|     const fileContents = fs.readFileSync(fullPath, "utf8"); | ||||
|  | ||||
|     const matterResult = matter(fileContents); | ||||
|  | ||||
|     return { | ||||
|       id, | ||||
|       ...matterResult.data | ||||
|     } as Post; | ||||
|   }); | ||||
|  | ||||
|   return allPostsData.sort((a, b) => { | ||||
|     return a.date < b.date ? 1 : -1; | ||||
|   }); | ||||
| } | ||||
|  | ||||
| export function getSinglePost(id: string) { | ||||
|   const fullPath = path.join(postsDirectory, id + ".md"); | ||||
|   const fileContents = fs.readFileSync(fullPath, "utf8"); | ||||
|   const matterResult = matter(fileContents); | ||||
|  | ||||
|   return { | ||||
|     id, | ||||
|     content: matterResult.content, | ||||
|     ...matterResult.data, | ||||
|   } as Post; | ||||
| } | ||||
							
								
								
									
										14
									
								
								content/posts/initial.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								content/posts/initial.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | ||||
| --- | ||||
| thumbnail: 'https://images.unsplash.com/photo-1707344088547-3cf7cea5ca49?q=80&w=2970&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDF8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D' | ||||
| title: 'Two Forms of Pre-rendering' | ||||
| date: '2020-01-01' | ||||
| --- | ||||
|  | ||||
| # Woah! | ||||
|  | ||||
| Next.js has two forms of pre-rendering: **Static Generation** and **Server-side Rendering**. The difference is in **when** it generates the HTML for a page. | ||||
|  | ||||
| - **Static Generation** is the pre-rendering method that generates the HTML at **build time**. The pre-rendered HTML is then _reused_ on each request. | ||||
| - **Server-side Rendering** is the pre-rendering method that generates the HTML on **each request**. | ||||
|  | ||||
| Importantly, Next.js lets you **choose** which pre-rendering form to use for each page. You can create a "hybrid" Next.js app by using Static Generation for most pages and using Server-side Rendering for others. | ||||
| @@ -1,4 +1,14 @@ | ||||
| /** @type {import('next').NextConfig} */ | ||||
| const nextConfig = {}; | ||||
| /** @type {import("next").NextConfig} */ | ||||
| const nextConfig = { | ||||
|   pageExtensions: ["js", "jsx", "mdx", "ts", "tsx"], | ||||
|   images: { | ||||
|     remotePatterns: [ | ||||
|       { | ||||
|         protocol: "https", | ||||
|         hostname: "**", | ||||
|       }, | ||||
|     ], | ||||
|   } | ||||
| }; | ||||
|  | ||||
| export default nextConfig; | ||||
|   | ||||
							
								
								
									
										15
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										15
									
								
								package.json
									
									
									
									
									
								
							| @@ -13,23 +13,32 @@ | ||||
|     "@emotion/react": "^11.11.3", | ||||
|     "@emotion/styled": "^11.11.0", | ||||
|     "@fontsource/roboto": "^5.0.8", | ||||
|     "@mdx-js/loader": "^3.0.1", | ||||
|     "@mdx-js/react": "^3.0.1", | ||||
|     "@mui/icons-material": "^5.15.10", | ||||
|     "@mui/material": "^5.15.10", | ||||
|     "@mui/material-nextjs": "^5.15.11", | ||||
|     "@next/mdx": "^14.1.0", | ||||
|     "@types/mdx": "^2.0.11", | ||||
|     "gray-matter": "^4.0.3", | ||||
|     "html-react-parser": "^5.1.7", | ||||
|     "marked": "^12.0.0", | ||||
|     "mui-markdown": "^1.1.13", | ||||
|     "next": "14.1.0", | ||||
|     "react": "^18", | ||||
|     "react-dom": "^18" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "typescript": "^5", | ||||
|     "@tailwindcss/typography": "^0.5.10", | ||||
|     "@types/node": "^20", | ||||
|     "@types/react": "^18", | ||||
|     "@types/react-dom": "^18", | ||||
|     "autoprefixer": "^10.0.1", | ||||
|     "eslint": "^8", | ||||
|     "eslint-config-next": "14.1.0", | ||||
|     "postcss": "^8", | ||||
|     "tailwindcss": "^3.3.0", | ||||
|     "eslint": "^8", | ||||
|     "eslint-config-next": "14.1.0" | ||||
|     "typescript": "^5" | ||||
|   }, | ||||
|   "peerDependencies": { | ||||
|     "react": "^17.0.0 || ^18.0.0", | ||||
|   | ||||
| @@ -6,15 +6,6 @@ const config: Config = { | ||||
|     "./components/**/*.{js,ts,jsx,tsx,mdx}", | ||||
|     "./app/**/*.{js,ts,jsx,tsx,mdx}", | ||||
|   ], | ||||
|   theme: { | ||||
|     extend: { | ||||
|       backgroundImage: { | ||||
|         "gradient-radial": "radial-gradient(var(--tw-gradient-stops))", | ||||
|         "gradient-conic": | ||||
|           "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))", | ||||
|       }, | ||||
|     }, | ||||
|   }, | ||||
|   plugins: [], | ||||
|   plugins: [require("@tailwindcss/typography")], | ||||
| }; | ||||
| export default config; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user