---
# System prepended metadata

title: Next.js建立作品集網站
tags: [前端]

---

# Next.js建立作品集網站
* 網址: https://archielai.github.io/portfolio/
* github repo: https://github.com/ArchieLai/portfolio

## 1.啟動專案
* User story：「身為對建築設計有興趣的使用者，需要瀏覽設計作品圖片，因為想獲得靈感啟發、或進一步瞭解作者。」
* UI 設計：

![截圖 2024-01-27 上午9.54.31](https://hackmd.io/_uploads/Hy-Ozyf5a.png)

## 2.建立Next.js app
```
npx create-next-app@latest
```
### App router與Page router
Next.js在v13以後增加了App router，和過去的Page router一樣以檔案系統(file-system based)作為路由。例如Page router建立新的`about`頁面時，直接在`pages/`資料夾下建立`about.js`；使用App router則是在`app/`下建立`about/`資料夾，如下圖目錄結構。這個專案採用App router。
```
app/
    ∟about/
        ⊢layout.js
        ⊢page.js
        ⊢loading.js
        ∟error.js
```

### 動態路由
要用id為每個作品建立url:`/posts/[id]`，只需要建立`app/posts/[id]`資料夾以及底下的`page.js`，可以避免重複為每一個id建立頁面。
* generateStaticParams()
	* 回傳包含`params`(id)的陣列，value必須是字串型態。
	* 類似Page router中的`getStaticPaths`
* dynamicParams: 設為false，在url輸入不存在的id時會回傳404。

```javascript
// app/posts/[id]/page.js

export function generateStaticParams() {
  return list.map((post) => ({
    id: String(post.id)
  }));
}

export const dynamicParams = false;

export default function Page({ params }) {
  return (
    <p>{params.id}</p>
  );
}
```

### 靜態檔案
將同一個作品id的圖片放在`public/images/posts/[id]`下，需要用一個utililty software取得檔案名稱。
```javascript
export function getImagePath(id) {
  //取得current working directory下存放圖片位置
  const postsDirectory = path.join(process.cwd(), `public/images/posts/${id}`);
  //取得dir內檔案名稱
  const fileNames = fs.readdirSync(postsDirectory);
  return fileNames;
}
```

將圖片加入作品頁面。
```javascript
export default function Page({ params }){
  const imgNames = getImagePath(params.id);
  return (
    {imgNames.map((img) => {
      const imgPath = `/images/posts/${id}/${img}`;
      return (
        <Image 
          key={id}
          src={imgPath}
          width={1070}
          height={580}
        />);
    })}
  );
}
```
### CSS
使用css module，每個路徑的資料夾下建立`page.module.css`。會自動產生唯一的 class 名稱，避免不同route之間 class 名稱衝突的問題。`globals.css`可以套用到每一個路由。

## 3.Material UI (MUI)
MUI是一個React元件函式庫，預設的CSS樣式引擎是使用emotion，需要安裝以下幾個套件。
```
npm install @mui/material-nextjs @emotion/cache @emotion/react @emotion/styled
```
修改`app/layout.js`，匯入`AppRouterCacheProvider`並加入`RootLayout`元件。因為app開發不是使用emotion，需要調整選項。
```javascript
import { AppRouterCacheProvider } from '@mui/material-nextjs/v13-appRouter';

 export default function RootLayout(props) {
   return (
     <html lang="en">
       <body>
-        {props.children}
+        <AppRouterCacheProvider options={{ enableCssLayer: true }}>{props.children}</AppRouterCacheProvider>
       </body>
     </html>
   );
 }
```

### BottomNavigation
在網頁上方nav bar加上一個導航選單。
官網範例：https://mui.com/material-ui/react-bottom-navigation/
* value是用來辨識第幾個BottomNavigationAction，預設是位置的index
* 嘗試把BottomNavigationAction包在Link元件會出現error，根據[issues的回答](https://github.com/mui/material-ui/issues/12206)，BottomNavigationAction必須是BottomNavigation的直接子元件，所以改用useRouter設定連結。

```javascript=
export default function BottomNav() {
  const [value, setValue] = React.useState(3);
  const router = useRouter();
  const handleClick = (e, route) => {
    e.preventDefault();
    router.push(route);
  };
  return (
    <div>
      <BottomNavigation
        value={value}
        onChange={(event, newValue) => {
          setValue(newValue);
        }}
        sx={{background: "rgba(0,0,0,0)"}}
      >
        <BottomNavigationAction 
          icon={<HomeIcon />}          
          onClick={(e) => handleClick(e, "/")} 
        />
        // add more BottomNavigation
      </BottomNavigation>
    </div>
  );
}
```

### Card元件的Hover效果
當滑鼠停在圖片上(Card)時，產生一個半透明Box、並且顯示該作品的標題。

```javascript=
export default function Post(props) {
  const [isHovered, setHovered] = useState(false);
  const handleMouseEnter = () => {
    setHovered(true);
  };
  const handleMouseLeave = () => {
    setHovered(false);
  };

  return (
    <Card 
      className={styles.card}
      sx={{ width: w, height: h}}
      onMouseEnter={handleMouseEnter}
      onMouseLeave={handleMouseLeave}
    >
      <Link href={`/posts/${props.index}`}>
        <Box sx={{position: "relative"}}>
          <CardMedia
            component={() => (
              <Image
                src={props.cover}
                width={w}
                height={h}
              />
            )}
          />
          {isHovered && (
            <Box className={styles.box}>{props.title}</Box>
          )}
        </Box>
      </Link>
    </Card>
  );
}
```

### Carousel (SwiperJS)
MUI沒有類似carousel的元件，官網上[類似的元件](https://mui.com/material-ui/react-button/#complex-button)比較複雜，使用另一個套件SwiperJS。
```
npm i swiper
```
```javascript=
export default function Carousel(props){
  const carousel = props.pathList;
  return (
    <>
      <Swiper
        spaceBetween={30}
        centeredSlides={true}
        autoplay={{
          delay: 3000,
          disableOnInteraction: false,
        }}
        pagination={{
          clickable: true,
        }}
        navigation={true}
        modules={[Autoplay, Pagination, Navigation]}
        className={styles.mySwiper}
      >
        {carousel.map((imgPath, index) => {
          return(
            <SwiperSlide key={index}>
              <Image 
                src={imgPath} 
                width={1000}
                height={600}
                className={styles.carousel}
                alt='carousel'
              />
            </SwiperSlide>
          );
        })}
      </Swiper>
    </>
  );
}
```

## 4.RWD
### 圖片大小調整
因為圖片長寬比例與Image元件設定比例不同，所以加上aspect-ratio。
```css
.image {
  max-width: 100%;
  height: auto;
  aspect-ratio: 640/450;
  object-fit: cover;
  object-position: 50% 50%;
}
```
### 用media query修改grid
gap, padding, margin可以使用相對尺寸(rem, vw)
```css
.container {
  display: grid;
  grid-template-columns: repeat(2, minmax(100px, 1fr));
  padding: 50px 5vw 50px 5vw;
  column-gap: 5vw;
  row-gap: 5vw;
  justify-items: center;

  @media (max-width: 768px) {
    column-gap: 0px;
    padding: 30px 20px 30px 20px;
    grid-template-columns: repeat(1, minmax(100px, 1fr));
  }
}
```
### 使用MUI useMediaQuery
用className套用css在MUI元件上失敗時，可以直接傳參數到MUI元件，以Swiper為例。
```javascript
export default function Carousel(props){
  const matches = useMediaQuery('(min-width:768px)');
  return (
    <>
      <Swiper
        //...
        style={{aspectRatio: matches ? 1000/600 : 640/450}}
      >
        {carousel.map((imgPath, index) => {
          return(
            <SwiperSlide key={index}>
              <Image 
                src={imgPath} 
                width={1000}
                height={600}
                className={styles.carousel}
                alt='carousel'
              />
            </SwiperSlide>
          );
        })}
      </Swiper>
    </>
  );
}
```
### 文字斷行
```css
.intro {
  @media (max-width: 768px) {
    word-wrap: break-word;
    hyphens: auto;
  }
}
```

## 5.建置及部署
流程參考[How to Deploy Next.js Apps to Github Pages
](https://www.freecodecamp.org/news/how-to-deploy-next-js-app-to-github-pages/)
### 修改next.config.mjs
使用Github Actions部署於Github Pages。修改`next.confing.mjs`，basePath為`/REPO_NAME`，`output: export`會在build之後產出一個out資料夾。
```javascript
/** @type {import('next').NextConfig} */
const nextConfig = {
  basePath: "/portfolio",
  output: "export",
  reactStrictMode: true,
};
export default nextConfig;
```
根據[官方文件](https://nextjs.org/docs/app/api-reference/next-config-js/basePath)，next/link不會受到basePath影響，但是next/image必須要修改src路徑。
```javascript=
//page.js
export default function About(){
  return(
    <Image
      priority 
      src="/portfolio/images/about/person.jpg" 
      width={300}
      height={300}
      alt="photo"
      className={styles.image}
    />
  );
}
```

### 本地端測試build
build完後註解掉output: 'export'，才能執行npm run start。
```
npx next build
cd out
npm run start
```
### 修改YAML檔
使用github預設產生的YAML檔會有error，參考[gregrickaby/nextjs-github-pages](https://github.com/gregrickaby/nextjs-github-pages)，移除（註解）以下兩個部分。

```yaml
      - name: Setup Pages
        uses: actions/configure-pages@v4
        with:
          # static_site_generator: next
										
										
    # - name: Static HTML export with Next.js
      # run: ${{ steps.detect-package-manager.outputs.runner }} next export
```
* 根據[actions/configure-pages的source code](https://github.com/actions/configure-pages/blob/main/src/set-pages-config.js)，static_site_generator: next會設定basePath以及關掉server side image optimization，但是在next.config.mjs已經設定過的話可能會衝突，[starter-workflows](https://github.com/actions/starter-workflows/blob/main/pages/nextjs.yml)的註解中建議有手動設定config就可以移除這一行。
* 根據[官方文件](https://nextjs.org/docs/app/building-your-application/upgrading/version-14)，next export在nextjs v14中被output: 'export'取代