107
122

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Next.js × TypeScript × microCMSでシンプルなコーポレートサイトを作ってみる

Last updated at Posted at 2021-04-25

概要

友人の依頼でちょっとしたコーポレートサイトを作る機会があったのでメモ。

使ったもの

  • Next.js(フレームワーク)
  • TypeScript(言語)
  • Material UI(デザイン)
  • microCMS(CMS)
  • Vercel(デプロイ)

特にmicroCMSは前々から使ってみたいと思っていたサービスなので良い機会でした。

(※筆者はまだTypeScriptに慣れておらず雰囲気で書いている事も多いため、所々で未熟な部分があるかもしれません。あらかじめご了承ください。)

完成系

https://corporate-site-sample.vercel.app

トップページ

FireShot Capture 077 - Home - Corporate Site Sample - corporate-site-sample.vercel.app.png

事業紹介ページ

FireShot Capture 080 - Service - Corporate Site Sample - corporate-site-sample.vercel.app.png

会社情報ページ

FireShot Capture 085 - Company - Corporate Site Sample - corporate-site-sample.vercel.app.png

採用ページ

一覧

FireShot Capture 091 - Recruit - Corporate Site Sample - corporate-site-sample.vercel.app.png

詳細

FireShot Capture 094 - Recruit - Corporate Site Sample - corporate-site-sample.vercel.app.png

ブログページ

一覧

FireShot Capture 097 - Blog - Corporate Site Sample - corporate-site-sample.vercel.app.png

詳細

FireShot Capture 102 - Blog - Corporate Site Sample - corporate-site-sample.vercel.app.png

お問い合わせページ

FireShot Capture 105 - Contact - Sample - corporate-site-sample.vercel.app.png

コーポレートサイトを制作するにあたってどんなページ構成にするかは好みによると思いますが、今回は

  • トップページ
  • 事業紹介ページ
  • 会社情報ページ
  • 採用ページ
  • ブログページ
  • お問い合わせページ

といったシンプルな構成にしてみました。これらはどのコーポレートサイトにおいても非常に良く見かける気がします。

実装

前置きはほどほどに、実装していきたいと思います。

Next.jsプロジェクトを作成

まずは「create-next-app」コマンドを使ってNext.jsプロジェクトを作成しましょう。

$ create-next-app corporate-site-sample
$ cd corporate-site-sample

TypeScriptを導入

今回はTypeScriptで書いていくつもりなので、早めに導入しておきます。

$ touch tsconfig.json
$ yarn add --dev typescript @types/react @types/node

ビルド

$ yarn dev

...

We detected TypeScript in your project and created a tsconfig.json file for you.

event - compiled successfully

「We detected TypeScript in your project and created a tsconfig.json file for you.」というメッセージが返ってくるとともにルートプロジェクトに「next-env.d.ts」というファイルが作成され、tsconfig.jsonの中身も変わっているので確認してください。

./tsconfig.json
{
  "compilerOptions": {
    "target": "es5",
    "lib": [
      "dom",
      "dom.iterable",
      "esnext"
    ],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": false,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve"
  },
  "include": [
    "next-env.d.ts",
    "**/*.ts",
    "**/*.tsx"
  ],
  "exclude": [
    "node_modules"
  ]
}

既存ファイルの拡張子をtsxに変更します。

$ find pages -name "_app.js" -or -name "index.js" | sed 'p;s/.js$/.tsx/' | xargs -n2 mv & \
  find pages/api -name "*.js" | sed 'p;s/.js$/.ts/' | xargs -n2 mv
./pages/index.tsx
const Home: React.FC = () => {
  return (
    <h1>Hello World!!</h1>
  )
}

export default Home

「.pages/index.tsx」を適当にTypeScriptっぽく書き換えて再度ビルド。http://localhost:3000/ にアクセスして「Hello World!!」と返ってくれば無事TypeScriptの導入に成功です。

スクリーンショット 2021-04-21 22.06.09.png

各種ディレクトリ・ファイルを準備

これから大量のディレクトリ・ファイルを取り扱う事になるので、あらかじめ準備しておきましょう。一個一個ぽちぽち作っていくのはさすがに辛いので、シェルスクリプトを用意して一気にまとめて作ってしまいます。

$ cat > provisioning.sh << EOS
#!/bin/bash

mkdir components
mkdir components/blog
mkdir components/company
mkdir components/contact
mkdir components/home
mkdir components/layouts
mkdir components/recruit
mkdir components/service
mkdir components/utils
mkdir data
mkdir lib
mkdir pages/blog
mkdir pages/blog/page
mkdir pages/recruit

touch components/blog/Post.tsx
touch components/blog/Posts.tsx
touch components/company/About.tsx
touch components/contact/Form.tsx
touch components/home/Introductions.tsx
touch components/home/Slider.tsx
touch components/layouts/Footer.tsx
touch components/layouts/Header.tsx
touch components/layouts/PageTemplate.tsx
touch components/recruit/Jobs.tsx
touch components/recruit/Qualifications.tsx
touch components/recruit/Slider.tsx
touch components/service/Features.tsx
touch components/utils/Link.tsx
touch components/utils/ScrollUp.tsx
touch components/utils/ShareButton.tsx
touch components/utils/SocialMedia.tsx
touch components/utils/theme.ts
touch data/routes.ts
touch lib/api.ts

touch "pages/blog/page/[number].tsx"
touch "pages/blog/[id].tsx"
touch "pages/recruit/[job].tsx"
touch pages/_document.tsx
touch pages/company.tsx
touch pages/contact.tsx
touch pages/recruit.tsx
touch pages/service.tsx

touch .env.local

mkdir src
mv components data lib pages styles src
EOS

実行しましょう。

$ sh provisioning.sh

最終的に次のような構成になっていればOKです。

├── .next
├── node_modules
├── public
│   ├── favicon.ico
│   └── vercel.svg
├── src
│   ├── components
│   │   ├── blog
│   │   │   ├── Post.tsx
│   │   │   └── Posts.tsx
│   │   ├── company
│   │   │   └── About.tsx
│   │   ├── contact
│   │   │   └── Form.tsx
│   │   ├── home
│   │   │   ├── Introductions.tsx
│   │   │   └── Slider.tsx
│   │   ├── layouts
│   │   │   ├── Footer.tsx
│   │   │   ├── Header.tsx
│   │   │   └── PageTemplate.tsx
│   │   ├── recruit
│   │   │   ├── Jobs.tsx
│   │   │   ├── Qualifications.tsx
│   │   │   └── Slider.tsx
│   │   ├── service
│   │   │   └── Features.tsx
│   │   └── utils
│   │       ├── Link.tsx
│   │       ├── ScrollUp.tsx
│   │       ├── ShareButton.tsx
│   │       ├── SocialMedia.tsx
│   │       └── theme.ts
│   ├── data
│   │   └── routes.ts
│   ├── lib
│   │   └── api.ts
│   ├── pages
│   │   ├── _app.tsx
│   │   ├── _document.tsx
│   │   ├── api
│   │   │   └── hello.ts
│   │   ├── blog
│   │   │   ├── [id].tsx
│   │   │   └── page
│   │   │       └── [number].tsx
│   │   ├── company.tsx
│   │   ├── contact.tsx
│   │   ├── index.tsx
│   │   ├── recruit
│   │   │   └── [job].tsx
│   │   ├── recruit.tsx
│   │   └── service.tsx
│   └── styles
│       ├── Home.module.css
│       └── globals.css
├── .env.local
├── .gitignores
├── next-env.d.ts
├── package.json
├── provisioning.sh
├── tsconfig.json
└── yarn.lock

Material UIを導入

デザインを整えるためにMateriau UIを導入していきます。

$ yarn add @material-ui/core @material-ui/icons @material-ui/lab
./src/components/utils/theme.ts
import { createMuiTheme } from "@material-ui/core/styles"
import { red } from "@material-ui/core/colors"

const black = "#212121"
const white = "#fafafa"
const blue = "#757ce8"

const theme = createMuiTheme({
  palette: {
    common: {
      black: black,
      white: white
    },
    primary: {
      main: black
    },
    secondary: {
      main: white
    },
    info: {
      main: blue
    },
    error: {
      main: red.A400
    }
  },
  typography: {
    h1: {
      fontSize: "3rem",
      fontWeight: 500,
    },
    h2: {
      fontSize: "2rem",
      fontWeight: 500,
    },
    h3: {
      fontSize: "1.25rem",
      fontWeight: 500,
    },
    h4: {
      fontSize: "1rem",
      fontWeight: 500,
    }
  }
})

export default theme
./src/pages/_document.tsx
import React from "react"
import Document, { Head, Html, Main, NextScript } from "next/document"
import { ServerStyleSheets } from "@material-ui/core/styles"
import theme from "../components/utils/theme"

export default class MyDocument extends Document {
  render() {
    return (
      <Html lang="ja-JP">
        <Head>
          {/* PWA primary color */}
          <meta name="theme-color" content={theme.palette.primary.main} />
          <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" />
        </Head>
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    )
  }
}

MyDocument.getInitialProps = async (ctx) => {
  const sheets = new ServerStyleSheets()
  const originalRenderPage = ctx.renderPage
 
  ctx.renderPage = () =>
    originalRenderPage({
      enhanceApp: (App) => (props) => sheets.collect(<App {...props} />)
    })

  const initialProps = await Document.getInitialProps(ctx)

  return {
    ...initialProps,
    styles: [...React.Children.toArray(initialProps.styles), sheets.getStyleElement()]
  }
}
./src/pages/_app.tsx
import React, { useEffect } from "react"
import Head from "next/head"
import { AppProps } from "next/app"
import { ThemeProvider } from "@material-ui/core/styles"
import CssBaseline from "@material-ui/core/CssBaseline"
import theme from "../components/utils/theme"
import "../styles/globals.css"

export default function MyApp({ Component, pageProps }: AppProps) {

  useEffect(() => {
    // Remove the server-side injected CSS.
    const jssStyles = document.querySelector("#jss-server-side")
    if (jssStyles) {
      jssStyles.parentElement.removeChild(jssStyles)
    }
  }, [])

  return (
    <React.Fragment>
      <Head>
        <title>Corporate Site Sample</title>
        <meta
          name="viewport"
          content="minimum-scale=1, initial-scale=1, width=device-width"
        />
      </Head>
      <ThemeProvider theme={theme}>
        {/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */}
        <CssBaseline />
        <Component {...pageProps} />
      </ThemeProvider>
    </React.Fragment>
  )
}
./src/pages/index.tsx
import Button from "@material-ui/core/Button"

const Home: React.FC = () => {
  return (
    <>
      <h1>Hello World!!</h1>
      <Button variant="contained">Default</Button>
    </>
  )
}

export default Home

スクリーンショット 2021-04-21 22.02.43.png

ちゃんとそれっぽいボタンが表示されていればMaterial UIの導入に成功です。

その他ライブラリをインストール

他にも後に使う予定のライブラリがあるので、まとめてインストールしてしまいましょう。

$ yarn add lightbox-react moment react-items-carousel react-material-ui-carousel react-scroll-to-top react-share
  • lightbox-react
    • 画像クリックで拡大機能が作れる
  • moment
    • 日付データの操作を簡単にしてくれる
  • react-items-carousel
    • カルーセル (スライダー)が作れる
  • react-material-ui-carousel
    • 同上
  • react-scroll-to-top
    -スムーススクロールが作れる
  • react-share
    • SNSシェアボタンが作れる

ルーティングを設定

./src/data/routes.ts
export const routes = [
  { name: "Service", link: "/service" },
  { name: "Company", link: "/company" },
  { name: "Recruit", link: "/recruit" },
  { name: "Blog", link: "/blog/page/1" },
  { name: "Contact", link: "/contact" }
]
./src/components/utils/Link.tsx
import * as React from "react"
import clsx from "clsx"
import { useRouter } from "next/router"
import NextLink, { LinkProps as NextLinkProps } from "next/link"
import MuiLink, { LinkProps as MuiLinkProps } from "@material-ui/core/Link"

interface NextLinkComposedProps
  extends Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, "href">,
    Omit<NextLinkProps, "href" | "as"> {
  to: NextLinkProps["href"]
  linkAs?: NextLinkProps["as"]
  href?: NextLinkProps["href"]
}

export const NextLinkComposed = React.forwardRef<HTMLAnchorElement, NextLinkComposedProps>(
  function NextLinkComposed(props, ref) {
    const {
      to,
      linkAs,
      href,
      replace,
      scroll,
      passHref,
      shallow,
      prefetch,
      locale,
      ...other
    } = props

    return (
      <NextLink
        href={to}
        prefetch={prefetch}
        as={linkAs}
        replace={replace}
        scroll={scroll}
        shallow={shallow}
        passHref={passHref}
        locale={locale}
      >
        <a ref={ref} {...other} />
      </NextLink>
    )
  },
)

export type LinkProps = {
  activeClassName?: string
  as?: NextLinkProps["as"]
  href: NextLinkProps["href"]
  noLinkStyle?: boolean
} & Omit<NextLinkComposedProps, "to" | "linkAs" | "href"> &
  Omit<MuiLinkProps, "href">

const Link = React.forwardRef<HTMLAnchorElement, LinkProps>(function Link(props, ref) {
  const {
    activeClassName = "active",
    as: linkAs,
    className: classNameProps,
    href,
    noLinkStyle,
    role, // Link don"t have roles.
    ...other
  } = props

  const router = useRouter()
  const pathname = typeof href === "string" ? href : href.pathname
  const className = clsx(classNameProps, {
    [activeClassName]: router.pathname === pathname && activeClassName,
  })

  const isExternal =
    typeof href === "string" && (href.indexOf("http") === 0 || href.indexOf("mailto:") === 0)

  if (isExternal) {
    if (noLinkStyle) {
      return <a className={className} href={href as string} ref={ref as any} {...other} />
    }

    return <MuiLink className={className} href={href as string} ref={ref} {...other} />
  }

  if (noLinkStyle) {
    return <NextLinkComposed className={className} ref={ref as any} to={href} {...other} />
  }

  return (
    <MuiLink
      component={NextLinkComposed}
      linkAs={linkAs}
      className={className}
      ref={ref}
      to={href}
      {...other}
      style={{ textDecoration: "none" }}
    />
  )
})

export default Link

共通レイアウトを作成

ヘッダーやフッターは全てのページで共通にしたいので、テンプレートとしてまとめておきます。

./src/components/layouts/Header.tsx
import React, { useState } from "react"
import { useRouter } from "next/router"

import { makeStyles, useTheme, Theme } from "@material-ui/core/styles"
import useMediaQuery from "@material-ui/core/useMediaQuery"

import {
  Grid,
  AppBar,
  Toolbar,
  Typography,
  List,
  ListItem,
  ListItemText,
  SwipeableDrawer,
  IconButton,
} from "@material-ui/core"

import useScrollTrigger from "@material-ui/core/useScrollTrigger"
import MenuIcon from "@material-ui/icons/Menu"

import Link from "../utils/Link"
import { routes } from "../../data/routes"

interface ElevationScrollProps {
  children: React.ReactElement
}

function ElevationScroll(props: ElevationScrollProps) {
  const { children } = props

  const trigger = useScrollTrigger({
    disableHysteresis: true,
    threshold: 0
  })

  return React.cloneElement(children, {
    elevation: trigger ? 4 : 0
  })
}

const useStyles = makeStyles((theme: Theme) => ({
  toolbarMargin: {
    ...theme.mixins.toolbar,
    [theme.breakpoints.down("md")]: {
    },
    [theme.breakpoints.down("xs")]: {
    }
  },
  drawerIconContainer: {
    marginLeft: "auto",
    padding: 0,
    "&:hover": {
      backgroundColor: "transparent"
    }
  },
  drawerIcon: {
    height: "50px",
    width: "50px",
    color: "inherit",
    [theme.breakpoints.down("xs")]: {
      height: "40px",
      width: "40px"
    }
  },
  drawer: {
    backgroundColor: theme.palette.secondary.main,
    padding: "0 6em"
  }
}))

const Header = () => {
  const classes = useStyles()
  const theme = useTheme()
  const iOS = process.browser && /iPad|iPhone|iPod/.test(navigator.userAgent)
  const matches = useMediaQuery(theme.breakpoints.down("sm"))

  const [openDrawer, setOpenDrawer] = useState(false)

  const router = useRouter()
  const path = routes

  const tabs = (
    <>
      <Grid container justify="flex-end" spacing={4}>
        {path.map(({ name, link }) => (
          <Grid item key={link}>
            <Link href={link}>
              <Typography
                style={{
                  color: "inherit",
                  fontWeight: router.pathname.match(link) ? "bold" : "normal",
                  borderBottom: router.pathname.match(link) && "1px solid #757ce8",
                }}
              >
                {name}
              </Typography>
            </Link>
          </Grid>
        ))}
      </Grid>
    </>
  )
  
  const drawer = (
    <>
      <SwipeableDrawer
        disableBackdropTransition={!iOS}
        disableDiscovery={iOS}
        open={openDrawer}
        onClose={() => setOpenDrawer(false)}
        onOpen={() => setOpenDrawer(true)}
        classes={{ paper: classes.drawer }}
        anchor="right"
      >
        <div className={classes.toolbarMargin} />
        <List disablePadding>
          {path.map(({ name, link }) => (
            <ListItem
              key={link}
              divider
              button
              onClick={() => {
                setOpenDrawer(false)
              }}
            >
              <ListItemText disableTypography>
                <Link href={link}>
                  <Typography
                    style={{
                      color:
                        router.pathname === link
                          ? "primary"
                          : "rgb(107 107 107)",
                      fontWeight: router.pathname === link ? "bold" : "normal"
                    }}
                  >
                    {name}
                  </Typography>
                </Link>
              </ListItemText>
            </ListItem>
          ))}
        </List>
      </SwipeableDrawer>
      <IconButton
        onClick={() => setOpenDrawer(!openDrawer)}
        disableRipple
        className={classes.drawerIconContainer}
      >
        <MenuIcon className={classes.drawerIcon} />
      </IconButton>
    </>
  )
  
  return (
    <>
      <ElevationScroll>
        <AppBar color="inherit">
          <Toolbar
            disableGutters
            style={{
              maxWidth: "1280px",
              margin: "0 auto",
              width: "100%",
              padding: matches ? "0 16px" : "24px"
            }}
          >
            <Link href="/">
              <Typography
                style={{
                  color: "inherit",
                  fontWeight: "bold",
                  fontSize: "1.75em",
                  position: "relative",
                  zIndex: 100
                }}
              >
                Sample
              </Typography>
            </Link>
            {matches ? drawer : tabs}
          </Toolbar>
        </AppBar>
      </ElevationScroll>
      <div className={classes.toolbarMargin} />
    </>
  )
}

export default Header
./src/components/layouts/Footer.tsx
import { useRouter } from "next/router"

import { makeStyles, Theme } from "@material-ui/core/styles"
import { Container, Grid, Typography } from "@material-ui/core"

import { routes } from "../../data/routes"
import Link from "../utils/Link"
import SocialMedia from "../utils/SocialMedia"

const useStyles = makeStyles((theme: Theme) => ({
  footer: {
    backgroundColor: theme.palette.primary.main,
    width: `100%`,
    position: "relative",
    overflow: "hidden",
    marginTop: "6em",
    padding: "2em 0 "
  },
  link: {
    fontSize: "1.25em",
    color: "#fff"
  },
  contact: {
    color: "#fff",
    fontSize: "1.5em",
    marginTop: "20px"
  },
  copylight: {
    marginTop: "15px",
    color: "#fff",
    fontSize: "1em"
  }
}))

const Footer = () => {
  const classes = useStyles()
  const path = routes
  const router = useRouter()

  return (
    <div className={classes.footer}>
      <Container maxWidth="lg">
      <Grid container spacing={3} justify="center">
          {path.map(({ name, link }) => (
            <Grid item key={link}>
              <Link href={link}>
                <Typography
                  className={classes.link}
                  style={{
                    fontWeight: router.pathname.match(link) ? "bold" : "normal",
                    borderBottom: router.pathname.match(link) && "1px solid #757ce8"
                  }}
                >
                  {name}
                </Typography>
              </Link>
            </Grid>
          ))}
        </Grid>
        <Grid container direction="column" style={{ margin: "1.5em 0" }}>
          <SocialMedia />
        </Grid>
        <Grid
          item
          container
          justify="center"
        >
          <Typography className={classes.copylight}>
            &copy;{new Date().getFullYear()} Sample
          </Typography>
        </Grid>
      </Container>
    </div>
  )
}

export default Footer
./src/components/utils/ScrollUp.tsx
import ScrollToTop from "react-scroll-to-top"
import KeyboardArrowUpIcon from "@material-ui/icons/KeyboardArrowUp"

const ScrollUp: React.FC = () => {

  return(
    <>
      <ScrollToTop
        smooth
        component={<KeyboardArrowUpIcon />}
        style={{
          borderRadius: "50%"
        }}
      />
    </>
  )
}

export default ScrollUp
./src/components/utils/SocialMedia.tsx
import { makeStyles, Theme } from "@material-ui/core/styles"
import { Grid } from "@material-ui/core"

import MailIcon from "@material-ui/icons/Mail"
import TwitterIcon from "@material-ui/icons/Twitter"
import InstagramIcon from "@material-ui/icons/Instagram"
import FacebookIcon from "@material-ui/icons/Facebook"

const useStyles = makeStyles((theme: Theme) => ({
  snsIcon: {
    width: "30px",
    height: "30px",

    [theme.breakpoints.down("xs")]: {
      width: "25px",
      height: "25px",
    }
  }
}))

interface SocialMediaProps {
  color?: string
}

const SocialMedia = ({ color }: SocialMediaProps) => {
  const classes = useStyles()

  return (
    <Grid item container spacing={2} justify="center">
      <Grid
        item
        component={"a"}
        target="_blank"
        rel="noreferrer noopener"
        href="/contact"
      >
        <MailIcon
          className={classes.snsIcon}
          color={color ? "primary" : "secondary"}
        />
      </Grid>
      <Grid
        item
        component={"a"}
        target="_blank"
        rel="noreferrer noopener"
        href=""
      >
        <TwitterIcon
          className={classes.snsIcon}
          color={color ? "primary" : "secondary"}
        />
      </Grid>
      <Grid
        item
        component={"a"}
        target="_blank"
        rel="noreferrer noopener"
        href=""
      >
        <InstagramIcon
          className={classes.snsIcon}
          color={color ? "primary" : "secondary"}
        />
      </Grid>
      <Grid
        item
        component={"a"}
        target="_blank"
        rel="noreferrer noopener"
        href=""
      >
        <FacebookIcon
          className={classes.snsIcon}
          color={color ? "primary" : "secondary"}
        />
      </Grid>
    </Grid>
  )
}

export default SocialMedia
./src/components/layouts/PageTemplate.tsx
import Head from "next/head"
import React from "react"

import Header from "./Header"
import Footer from "./Footer"
import ScrollUp from "../utils/ScrollUp"

interface PageTemplateProps {
  children: React.ReactElement
  title: string
}

const PageTemplate = ({ children, title }: PageTemplateProps) => {
  return (
    <>
      <Head>
        <title>{title ? title : "Corporate Site Sample"}</title>
      </Head>
      <header>
        <Header />
      </header>
      <main>
        {children}
      </main>
      <ScrollUp />
      <footer>
        <Footer />
      </footer>
      <style jsx global>
        {`
          html,
          body {
            background: #F5F5F5;
            overflow-x: hidden;
            padding: 0 !important;
          }
          #__next {
            min-height: 100vh;
            display: flex;
            flex-direction: column;
            justify-content: space-between;
          }
          main {
            flex: 1;
          }
        `}
      </style>
    </>
  )
}

export default PageTemplate
./src/pages/index.tsx
import { ThemeProvider } from "@material-ui/core/styles"

import PageTemplate from "../components/layouts/PageTemplate"
import theme from "../components/utils/theme"

const Home: React.FC = () => {
  return (
    <>
      <ThemeProvider theme={theme}>
        <PageTemplate title="Home | Corporate Site Sample">
          <h1>Hello World!!</h1>
        </PageTemplate>
      </ThemeProvider>
    </>
  )
}

export default Home

スクリーンショット 2021-04-22 2.19.57.png

こんな感じでヘッダーとフッターが良い感じになっていればOK.

トップページを作成

./src/components/home/Slider.tsx
import React from "react"
import { makeStyles } from "@material-ui/core/styles"
import { Button, Paper, Typography } from "@material-ui/core"
import Carousel from "react-material-ui-carousel"

const useStyles = makeStyles(() => ({
  slider: {
    width: "100%"
  },
  media: {
    position: "relative",
    height: "300px",
    overflow: "hidden",
    padding: "20px",
    color: "white"
  },
  checkButton: {
    marginTop: "40px",
    color: "#fff",
    fontSize: "25px",
    border: "3px solid white",
    textTransform: "capitalize"
  }
}))

interface ItemProps {
  name: string
  description: string
  color: string
}

const Item = ({ name, description, color }: ItemProps) => {
  const classes = useStyles()

  return (
    <Paper
      className={classes.media}
      style={{
        backgroundColor: color
      }}
      elevation={10}
      square
    >
      <h2>{name}</h2>
      <p>{description}</p>

      <Button className={classes.checkButton}>
        Check it out!
      </Button>
    </Paper>
  )
}

const Slider = ({ items }) => {
  const classes = useStyles()

  return (
    <Carousel
      className={classes.slider}
      autoPlay={items.length > 1 ? true : false}
      animation="fade"
      navButtonsAlwaysInvisible={items.length == 1 ? true : false}
      indicators={false}
      timeout={300}
    >
      {
        items.map((item, index) =>  (
          <Item
            key={index}
            name={item.name}
            description={item.description}
            color={item.color}
          />
        ))
      }
    </Carousel>
  )
}

export default Slider
./src/components/home/Introductions.tsx
import { makeStyles, Theme } from "@material-ui/core/styles"
import { Typography, Box, Button } from "@material-ui/core"

const useStyles = makeStyles((theme: Theme) => ({
  linkButton: {
    marginTop: theme.spacing(2),
    textTransform: "none",
    border: "transparent 1px solid",
    borderRadius: 50,
    backgroundColor: "#4F9DF7",
    color: "#fff",
    "&:hover": {
      backgroundColor: "#fff",
      color: "#4F9DF7"
    }
  }
}))

interface IntroductionsProps {
  index: number
  title: string
  description: string
  action: string
  href: string
}

const Introductions = ({ index, title, description, action, href }: IntroductionsProps) => {
  const classes = useStyles()

  return (
    <>
      <Typography variant="h1" gutterBottom align={index % 2 == 0 ? "left" : "right"}>
        {title}
      </Typography>
      <Typography variant="body1" align={index % 2 == 0 ? "left" : "right"} paragraph>
        {description}
      </Typography>
      <Box textAlign={index % 2 == 0 ? "left" : "right"}>
        <Button variant="outlined" color="primary" className={classes.linkButton} href={href}>
          {action}
        </Button>
      </Box>
    </>
  )
}

export default Introductions
./src/pages/index.tsx
import { makeStyles, ThemeProvider } from "@material-ui/core/styles"
import { Container, Grid} from "@material-ui/core"

import Slider from "../components/home/Slider"
import Introductions from "../components/home/Introductions"
import PageTemplate from "../components/layouts/PageTemplate"
import theme from "../components/utils/theme"

const useStyles = makeStyles(() => ({
  container: {
    marginTop: "3rem"
  }
}))

interface Item {
  name: string
  description: string
  color: string
}

interface Introduction {
  title: string
  description: string
  action: string
  href: string
}

const Home: React.FC = () => {
  const classes = useStyles()

  const items: Item[] = [
    {
      name: "Slide1",
      description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
      color: "#64ACC8"
    },
    {
      name: "Slide2",
      description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
      color: "#7D85B1"
    },
    {
      name: "Slide3",
      description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
      color: "#CE7E78"
    }
  ]

  const introductions: Introduction[] = [
    {
      title: "Service",
      description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
      action: "About Service >",
      href: "/service"
    },
    {
      title: "Company",
      description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
      action: "About Company >",
      href: "/company"
    },
    {
      title: "Recruit",
      description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
      action: "About Recruit >",
      href: "/recruit"
    }
  ]

  return (
    <>
      <ThemeProvider theme={theme}>
        <PageTemplate title="Home | Corporate Site Sample">
          <>
            <Slider
              items={items}
            />
            { introductions.map((introduction, index) => (
              <Container key={index} maxWidth="lg" className={classes.container}>
                <Grid container justify={index % 2 == 0 ? "flex-start" : "flex-end"}>
                  <Grid item lg={6} md={6}>
                    <Introductions
                      index={index}
                      title={introduction.title}
                      description={introduction.description}
                      action={introduction.action}
                      href={introduction.href}
                    />
                  </Grid>
                </Grid>
              </Container>
              ))
            }
          </>
        </PageTemplate>
      </ThemeProvider>
    </>
  )
}

export default Home

home.png

こんな感じになっていればOKです。

Serviceページを作成

./src/components/service/Features.tsx
import { Typography } from "@material-ui/core"

interface FeaturesProps {
  title: string
  description: string
}

const Features = ({ title, description }: FeaturesProps) => {
  
  return (
    <>
      <div style={{marginTop: "3rem"}}>
        <Typography variant="h2" align="left" gutterBottom>
          {title}
        </Typography>
        <Typography variant="body1">
          {description}
        </Typography>
      </div>
    </>
  )
}

export default Features
./src/pages/service.tsx
import { makeStyles, ThemeProvider } from "@material-ui/core/styles"
import { Container, Grid, Typography} from "@material-ui/core"

import Features from "../components/service/Features"
import PageTemplate from "../components/layouts/PageTemplate"
import theme from "../components/utils/theme"

const useStyles = makeStyles(() => ({
  container: {
    marginTop: "3rem"
  }
}))

interface Feature {
  title: string
  description: string
}

const Service: React.FC = () => {
  const classes = useStyles()

  const features: Feature[] = [
    {
      title: "Feature1",
      description: "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum."
    },
    {
      title: "Feature2",
      description: "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum."
    },
    {
      title: "Feature3",
      description: "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum."
    }
  ]

  return (
    <>
      <ThemeProvider theme={theme}>
        <PageTemplate title="Service | Corporate Site Sample">
          <>
            <Container maxWidth="lg"className={classes.container}>
              <Grid container justify="center">
                <Grid item>
                  <Typography variant="h1" gutterBottom>
                    Service
                  </Typography>
                </Grid>
              </Grid>
            </Container>
            <Container maxWidth="lg" className={classes.container}>
              { features.map((feature, index) => (
                  <Features
                    key={index}
                    title={feature.title}
                    description={feature.description}
                  />
                ))
              }
            </Container>
          </>
        </PageTemplate>
      </ThemeProvider>
    </>
  )
}

export default Service

service.png

こんな感じになっていればOKです。

Company(企業情報)ページを作成。

./src/components/compamy/About.tsx
import { makeStyles, Theme } from "@material-ui/core/styles"
import Table from "@material-ui/core/Table"
import TableBody from "@material-ui/core/TableBody"
import TableCell from "@material-ui/core/TableCell"
import TableContainer from "@material-ui/core/TableContainer"
import TableRow from "@material-ui/core/TableRow"
import Paper from "@material-ui/core/Paper"

const useStyles = makeStyles((theme: Theme) => ({
  table: {
    minWidth: 650
  }
}))

const createData = (key: string, value: string) => {
  return { key, value }
}

interface AboutProps {
  name: string
  founded: string
  capital: string
  ceo: string
  address: string
  service: string
  mail: string
}

const About = ({ name, founded, capital, ceo, address, service, mail }: AboutProps) => {
  const classes = useStyles()

  const rows = [
    createData("Name", name),
    createData("Founded", founded),
    createData("Capital", capital),
    createData("CEO", ceo),
    createData("Address", address),
    createData("Service", service),
    createData("Mail", mail),
  ]

  return (
    <TableContainer component={Paper}>
      <Table className={classes.table}>
        <TableBody>
          {rows.map((row) => (
            <TableRow key={row.key}>
              <TableCell component="th" scope="row" style={{ fontWeight: "bold"}}>
                {row.key}
              </TableCell>
              <TableCell>{row.value}</TableCell>
            </TableRow>
          ))}
        </TableBody>
      </Table>
    </TableContainer>
  )
}

export default About
./src/pages/company.tsx
import { makeStyles, ThemeProvider } from "@material-ui/core/styles"
import { Container, Grid, Typography} from "@material-ui/core"

import About from "../components/company/About"
import PageTemplate from "../components/layouts/PageTemplate"
import theme from "../components/utils/theme"

const useStyles = makeStyles(() => ({
  container: {
    marginTop: "3rem"
  }
}))

interface About {
  name: string
  founded: string
  capital: string
  ceo: string
  address: string
  service: string
  mail: string
}

const Company: React.FC = () => {
  const classes = useStyles()

  const about: About = {
    name: "ABC Company",
    founded: "2021/01/01",
    capital: "20,000,000 JPY",
    ceo: "Taro Yamada",
    address: "Tokyo Skytree 1 Chome-1-2 Oshiage, Sumida, Tokyo",
    service: "Engineering",
    mail: "abc@example.com"
  }

  return (
    <>
      <ThemeProvider theme={theme}>
        <PageTemplate title="Company | Corporate Site Sample">
          <>
            <Container maxWidth="lg" className={classes.container}>
              <Grid container justify="center">
                <Grid item>
                  <Typography variant="h1" gutterBottom>
                    Company
                  </Typography>
                </Grid>
              </Grid>
            </Container>
            <Container maxWidth="lg" className={classes.container}>
              <About
                name={about.name}
                founded={about.founded}
                capital={about.capital}
                ceo={about.ceo}
                address={about.address}
                service={about.service}
                mail={about.mail}
              />
            </Container>
          </>
        </PageTemplate>
      </ThemeProvider>
    </>
  )
}

export default Company

スクリーンショット 2021-04-22 20.18.55.png

こんな感じになってればOKです。

Recruit(採用)ページを作成

./src/components/recruit/jobs/.tsx
import React from "react"

import { makeStyles, Theme } from "@material-ui/core/styles"
import { Grid, Paper } from "@material-ui/core"

import Link from "../utils/Link"

const useStyles = makeStyles((theme: Theme) => ({
  paper: {
    padding: "1rem",
    height: "100%",
    textAlign: "center",
    color: theme.palette.text.secondary
  },
}))

interface JobProps {
  name: string
}

const Job = ({ name }: JobProps) => {
  const classes = useStyles()

  return (
    <Grid item xs={6}>
      <Link href="/recruit/[job]" as={`/recruit/${name.toLowerCase()}`}>
        <Paper className={classes.paper}>{name}</Paper>
      </Link>
    </Grid>
  )
}

const Jobs = ({ jobs }) => {

  return (
    <>
      { jobs.map((job, index) => (
        <Job
          key={index}
          name={job.name}
        />
      ))}
    </>
  )
}

export default Jobs
./src/components/recruit/Qualifications.tsx
import { makeStyles } from "@material-ui/core/styles"
import { Grid} from "@material-ui/core"
import Card from "@material-ui/core/Card"
import CardHeader from "@material-ui/core/CardHeader"
import List from "@material-ui/core/List"
import ListItem from "@material-ui/core/ListItem"
import ListItemText from "@material-ui/core/ListItemText"

const useStyles = makeStyles(() => ({
  card: {
    height: "100%",
    marginBottom: "0.5rem",
    transition: "all 0.3s",
    "&:hover": {
      boxShadow:
        "1px 0px 20px -1px rgba(0,0,0,0.2), 0px 0px 20px 5px rgba(0,0,0,0.14), 0px 1px 10px 0px rgba(0,0,0,0.12)",
      transform: "translateY(-3px)"
    }
  },
  cardHeader: {
    padding: "1rem 1rem 0.5rem"
  }
}))

interface QualificationsProps {
  requiredSkills: string[]
  welcomeSkills: string[]
  idealImages: string[]
}

const Qualifications = ({ requiredSkills, welcomeSkills, idealImages}: QualificationsProps) => {
  const classes = useStyles()

  return (
    <>
      <Grid item xs={12} sm={6} md={4}>
        <Card className={classes.card}>
          <CardHeader title="Required Skills" className={classes.cardHeader} />
          <List>
            { requiredSkills.map((requiredSkill, index) => (
              <ListItem key={index}>
                <ListItemText
                  secondary={`・${requiredSkill}`}
                />
              </ListItem>
            ))}
          </List>
        </Card>
      </Grid>
      <Grid item xs={12} sm={6} md={4}>
        <Card className={classes.card}>
          <CardHeader title="Welcome Skills" className={classes.cardHeader} />
          <List>
            { welcomeSkills.map((welcomeSkill, index) => (
              <ListItem key={index}>
                <ListItemText
                  secondary={`・${welcomeSkill}`}
                />
              </ListItem>
            ))}
          </List>
        </Card>
      </Grid>
      <Grid item xs={12} sm={6} md={4}>
        <Card className={classes.card}>
          <CardHeader title="Ideal Images" className={classes.cardHeader} />
          <List>
            { idealImages.map((idealImage, index) => (
              <ListItem key={index}>
                <ListItemText
                  secondary={`・${idealImage}`}
                />
              </ListItem>
            ))}
          </List>
        </Card>
      </Grid>
    </>
  )
}

export default Qualifications
./src/components/recruit/Slider.tsx
import React, { useState } from "react"

import ItemsCarousel from "react-items-carousel"
import Lightbox from "lightbox-react"
import "lightbox-react/style.css"

import ArrowBackIosIcon from "@material-ui/icons/ArrowBackIos"
import ArrowForwardIosIcon from "@material-ui/icons/ArrowForwardIos"

const Slider = ({ images }) => {
  const [activeItemIndex, setActiveItemIndex] = useState(0)
  const chevronWidth = 40

  const [photoIndex, setIndex] = useState(0)
  const [isOpen, setisOpen] = useState(false)
  
  return (
    <div style={{ padding: `0 ${chevronWidth}px` }}>
      <ItemsCarousel
        requestToChangeActive={setActiveItemIndex}
        activeItemIndex={activeItemIndex}
        numberOfCards={2}
        infiniteLoop
        gutter={20}
        leftChevron={<ArrowBackIosIcon />}
        rightChevron={<ArrowForwardIosIcon />}
        outsideChevron
        chevronWidth={chevronWidth}
      >
        {images.map((img, index) => {
          return <img 
            key={index}
            src={img}
            style={{ "width": "100%", "height": "100%" }}
            onClick={() => {
              setisOpen(true), setIndex(index)
            }}
          />
        })}

        {isOpen && (
          <Lightbox
            mainSrc={images[photoIndex]}
            nextSrc={images[(photoIndex + 1) % images.length]}
            prevSrc={images[(photoIndex + images.length - 1) % images.length]}
            onCloseRequest={() => setisOpen(false)}
            onMovePrevRequest={() =>
              setIndex((photoIndex + images.length - 1) % images.length)
            }
            onMoveNextRequest={() => setIndex((photoIndex + 1) % images.length)}
            clickOutsideToClose={true}
            enableZoom={false}
            imagePadding={100}
          />
        )}
      </ItemsCarousel>
    </div>
  )
}

export default Slider
./src/pages/recruit/[job].tsx
import { GetStaticPaths, GetStaticProps } from "next"
import { makeStyles, ThemeProvider } from "@material-ui/core/styles"

import Qualifications from "../../components/recruit/Qualifications"
import { Container, Grid, Typography} from "@material-ui/core"
import PageTemplate from "../../components/layouts/PageTemplate"
import theme from "../../components/utils/theme"

const useStyles = makeStyles(() => ({
  container: {
    marginTop: "3rem"
  },
  gridItem: {
    maxWidth: "1260px"
  }
}))

const jobs: string[] = [
  "planner", "engineer", "designer", "marketer"
]

interface JobDetail {
  image: string
  name: string
  description: string
  requiredSkills: string[]
  welcomeSkills: string[]
  idealImages: string[]
}

const jobDetails: JobDetail[] = [
  {
    image: "https://www.pakutaso.com/shared/img/thumb/MAX75_yubisasu20141025120158_TP_V.jpg",
    name: "Plannner",
    description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
    requiredSkills: [
      "Lorem ipsum dolor sit amet.",
      "Ut enim ad minim veniam.",
      "Duis aute irure dolor in reprehenderit.Duis aute irure dolor in reprehenderit."
    ],
    welcomeSkills: [
      "Lorem ipsum dolor sit amet.",
      "Ut enim ad minim veniam.",
      "Duis aute irure dolor in reprehenderit.Duis aute irure dolor in reprehenderit."
    ],
    idealImages: [
      "Lorem ipsum dolor sit amet.",
      "Ut enim ad minim veniam.",
      "Duis aute irure dolor in reprehenderit."
    ]
  },
  {
    image: "https://www.pakutaso.com/shared/img/thumb/PAK85_MBAdesagyou20140312_TP_V.jpg",
    name: "Engineer",
    description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
    requiredSkills: [
      "Lorem ipsum dolor sit amet.",
      "Ut enim ad minim veniam.",
      "Duis aute irure dolor in reprehenderit.Duis aute irure dolor in reprehenderit."
    ],
    welcomeSkills: [
      "Lorem ipsum dolor sit amet.",
      "Ut enim ad minim veniam.",
      "Duis aute irure dolor in reprehenderit.Duis aute irure dolor in reprehenderit."
    ],
    idealImages: [
      "Lorem ipsum dolor sit amet.",
      "Ut enim ad minim veniam.",
      "Duis aute irure dolor in reprehenderit."
    ]
  },
  {
    image: "https://www.pakutaso.com/shared/img/thumb/N112_nekutaiwonaosudansei_TP_V.jpg",
    name: "Designer",
    description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
    requiredSkills: [
      "Lorem ipsum dolor sit amet.",
      "Ut enim ad minim veniam.",
      "Duis aute irure dolor in reprehenderit.Duis aute irure dolor in reprehenderit."
    ],
    welcomeSkills: [
      "Lorem ipsum dolor sit amet.",
      "Ut enim ad minim veniam.",
      "Duis aute irure dolor in reprehenderit.Duis aute irure dolor in reprehenderit."
    ],
    idealImages: [
      "Lorem ipsum dolor sit amet.",
      "Ut enim ad minim veniam.",
      "Duis aute irure dolor in reprehenderit."
    ]
  },
  {
    image: "https://www.pakutaso.com/shared/img/thumb/OOK82_gurafuwoyubisasu20131223_TP_V.jpg",
    name: "Marketer",
    description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
    requiredSkills: [
      "Lorem ipsum dolor sit amet.",
      "Ut enim ad minim veniam.",
      "Duis aute irure dolor in reprehenderit.Duis aute irure dolor in reprehenderit."
    ],
    welcomeSkills: [
      "Lorem ipsum dolor sit amet.",
      "Ut enim ad minim veniam.",
      "Duis aute irure dolor in reprehenderit.Duis aute irure dolor in reprehenderit."
    ],
    idealImages: [
      "Lorem ipsum dolor sit amet.",
      "Ut enim ad minim veniam.",
      "Duis aute irure dolor in reprehenderit."
    ]
  }
]

export const getStaticPaths: GetStaticPaths = async () => {
  const paths = jobs.map((job) => `/recruit/${job}`)

  return { 
    paths,
    fallback: false
  }
}

export const getStaticProps: GetStaticProps = async ({ params }) => {
  const jobDetail = jobDetails[jobs.indexOf(String(params.job))]

  return {
    props: { jobDetail },
    revalidate: 1
  }
}

const RecruitJob = ({ jobDetail }) => {
  const classes = useStyles()

  return (
    <ThemeProvider theme={theme}>
      <PageTemplate title="Recruit | Corporate Site Sample">
        <>
          <Container maxWidth="lg" className={classes.container}>
            <Grid container justify="center">
              <Grid item>
                <Typography variant="h1" gutterBottom>
                  {jobDetail.name}
                </Typography>
              </Grid>
            </Grid>
          </Container>
          <Container maxWidth="lg" className={classes.container}>
            <Grid container justify="center">
              <Grid item className={classes.gridItem}>
                <img src={jobDetail.image} style={{ height: "auto", maxWidth: "100%" }} />
              </Grid>
            </Grid>
          </Container>
          <Container maxWidth="lg" className={classes.container}>
            <Grid container justify="center">
              <Grid item className={classes.gridItem}>
                <Typography variant="h2" gutterBottom>
                  Description
                </Typography>
                <Typography>
                  {jobDetail.description}
                </Typography>
              </Grid>
            </Grid>
          </Container>
          <Container maxWidth="lg"className={classes.container}>
            <Grid container spacing={2}>
              <Qualifications
                requiredSkills={jobDetail.requiredSkills}
                welcomeSkills={jobDetail.welcomeSkills}
                idealImages={jobDetail.idealImages}
              />
            </Grid>
          </Container>
        </>
      </PageTemplate>
    </ThemeProvider>
  )
}

export default RecruitJob
./src/pages/recruit.tsx
import { makeStyles, ThemeProvider } from "@material-ui/core/styles"
import { Container, Grid, Typography} from "@material-ui/core"

import Jobs from "../components/recruit/Jobs"
import Slider from "../components/recruit/Slider"
import PageTemplate from "../components/layouts/PageTemplate"
import theme from "../components/utils/theme"

const useStyles = makeStyles(() => ({
  container: {
    marginTop: "3rem",
    padding: "0 1rem"
  }
}))

interface Job {
  name: string
}

const Recruit: React.FC = () => {
  const classes = useStyles()

  const jobs: Job[] = [
    {
      name: "Planner"
    },
    {
      name: "Engineer"
    },
    {
      name: "Designer"
    },
    {
      name: "Marketer"
    }
  ]

  const images: string[] = [
    "https://www.pakutaso.com/shared/img/thumb/OOK82_gurafuwoyubisasu20131223_TP_V.jpg",
    "https://www.pakutaso.com/shared/img/thumb/PAK85_MBAdesagyou20140312_TP_V.jpg",
    "https://www.pakutaso.com/shared/img/thumb/N112_nekutaiwonaosudansei_TP_V.jpg",
    "https://www.pakutaso.com/shared/img/thumb/MAX75_yubisasu20141025120158_TP_V.jpg"
  ]

  return (
    <>
      <ThemeProvider theme={theme}>
        <PageTemplate title="Recruit | Corporate Site Sample">
          <>
            <Container maxWidth="lg" className={classes.container}>
              <Grid container justify="center">
                <Grid item>
                  <Typography variant="h1" gutterBottom>
                    Recruit
                  </Typography>
                </Grid>
              </Grid>
            </Container>
            <Container maxWidth="lg" className={classes.container}>
              <Grid container spacing={2} justify="center">
                <Grid container item xs={12} spacing={2}>
                  <Jobs
                    jobs={jobs}
                  />
                </Grid>
              </Grid>
            </Container>
            <Container maxWidth="lg" className={classes.container}>
              <Slider images={images} />
            </Container>
            <Container maxWidth="lg" className={classes.container}>
              <Typography align="center">
                Please feel free to contact us.
              </Typography>
            </Container>
          </>
        </PageTemplate>
      </ThemeProvider>
    </>
  )
}

export default Recruit

FireShot Capture 108 - Recruit - Corporate Site Sample - localhost.png

FireShot Capture 116 - Recruit - Corporate Site Sample - localhost.png

一覧ページ、詳細ページともにこんな感じになっていればOKです。

ブログページを作成

今回、ブログに関しては「micro CMS」を利用して配信していきます。

まず、以下の公式マニュアルに沿ってサービスの作成まで進めてください。

スクリーンショット 2021-04-23 2.05.29_censored.jpg

  • API名: 任意(わかりやすければ何でもOK)
  • エンドポイント: 任意(今回はblog)

スクリーンショット 2021-04-23 2.10.59_censored.jpg

  • 型: リスト形式

スクリーンショット 2021-04-23 3.00.59_censored.jpg

  • title(タイトル)
    • テキストフィールド
  • subTitle(サブタイトル)
    • テキストエリア
  • body(本文)
    • リッチエディタ
  • thumbnail(サムネイル画像)
    • 画像

スクリーンショット 2021-04-23 3.03.06_censored.jpg

APIスキーマの作成が終わったら、「コンテンツの追加」から適当に記事を作成して公開します。

スクリーンショット 2021-04-23 3.04.07_censored.jpg

公開できたら、右上の「APIプレビュー」をクリック。

スクリーンショット 2021-04-23 3.04.37_censored.jpg

するとこんな感じでコンテンツを取得するためのcurlコマンドが表示されるので、ターミナルなどから確認してください。また、APIキーはメモなどに控えておきましょう。

./env.local
NEXT_PUBLIC_MICRO_CMS_SERVICE_ID=<micro CMSのサービスID(ドメインみたいな部分)>
NEXT_PUBLIC_MICRO_CMS_API_KEY=<micro CMSのAPIキー>
./src/lib/api.ts
interface Post {
  id: string
  title: string
  subTitle: string
  body: HTMLElement
  thumbnail: string
}

const serviceId: string = process.env.NEXT_PUBLIC_MICRO_CMS_SERVICE_ID
const baseUrl: string = `https://${serviceId}.microcms.io/api/v1`

const apiKey: string = process.env.NEXT_PUBLIC_MICRO_CMS_API_KEY
const writeApiKey: string = process.env.NEXT_PUBLIC_MICRO_CMS_WRITE_API_KEY

const params = (method: string, data?: {}) => {
  if (data) {
    return {
      "method": method,
      "headers": {
        "Content-Type": "application/json; charset=utf-8",
        "X-WRITE-API-KEY": writeApiKey
      },
      "body": JSON.stringify(data)
    }
  } else {
    return {
      "method": method,
      "headers": {
        "X-API-KEY": apiKey
      }
    }
  }
}

// 記事を全件取得
export const fetchAllPosts = async (): Promise<Post[]> => {
  const data = await fetch(`${baseUrl}/blog`, params("GET"))
  .then(res => res.json())
  .catch(() => null)

  if (data.contents) {
    return data.contents
  }
}

// IDから個別の記事を取得
export const fetchPostById = async (id: string): Promise<Post> => {
  const data = await fetch(`${baseUrl}/blog/${id}`, params("GET"))
  .then(res => res.json())
  .catch(() => null)

  if (data) {
    return data
  }  
}

// ページ番号によって記事を取得
export const fetchPostsByPageNumber = async (pageNumber: number, limit: number): Promise<Post[]> => {
  const data = await fetch(`${baseUrl}/blog?offset=${(pageNumber - 1) * 6}&limit=${limit}`, params("GET"))
  .then(res => res.json())
  .catch(() => null)

  if (data.contents) {
    return data.contents
  }
}

// 最新の記事のみを取得
export const fetchLatestPosts = async (limit: number): Promise<Post[]> => {
  const data = await fetch(`${baseUrl}/blog?limit=${limit}`, params("GET"))
  .then(res => res.json())
  .catch(() => null)

  if (data.contents) {
    return data.contents
  }
}

// お問い合わせを作成
export const createContact = async (data: {}) => {
  await fetch(`${baseUrl}/contacts`, params("POST", data))
}
./src/components/blog/Posts.tsx
import { makeStyles } from "@material-ui/core/styles"
import Card from "@material-ui/core/Card"
import CardMedia from "@material-ui/core/CardMedia"
import CardContent from "@material-ui/core/CardContent"
import Typography from "@material-ui/core/Typography"

import Link from "../utils/Link"

const useStyles = makeStyles(() => ({
  card: {
    marginBottom: "0.5rem",
    transition: "all 0.3s",
    "&:hover": {
      boxShadow:
        "1px 0px 20px -1px rgba(0,0,0,0.2), 0px 0px 20px 5px rgba(0,0,0,0.14), 0px 1px 10px 0px rgba(0,0,0,0.12)",
      transform: "translateY(-3px)"
    }
  },
  cardMedia: {
    height: 0,
    paddingTop: "56.25%"
  }
}))

interface PostsProps {
  id: string
  title: string
  subTitle: string
  thumbnail: string
}

const Posts = ({ id, title, subTitle, thumbnail }: PostsProps) => {
  const classes = useStyles()

  return (
    <Link href="/blog/[id]" as={`/blog/${id}`}>
      <Card className={classes.card}>
        <CardMedia className={classes.cardMedia} image={thumbnail} title={title} />
        <CardContent>
          <Typography variant="h2" gutterBottom>
            {title}
          </Typography>
          <Typography variant="body1" color="textSecondary" component="p">
            {subTitle?.length > 140 ? subTitle.substr(0, 140) + "..." : subTitle}
          </Typography>
        </CardContent>
      </Card>
    </Link>
  )
}

export default Posts
./src/pages/blog/page/[number].tsx
import { GetStaticPaths, GetStaticProps } from "next"
import { useRouter } from "next/router"

import React, { useCallback } from "react"

import { makeStyles, ThemeProvider } from "@material-ui/core/styles"
import { Container, Grid, Typography } from "@material-ui/core"
import { Pagination } from "@material-ui/lab"

import Posts from "../../../components/blog/Posts"
import PageTemplate from "../../../components/layouts/PageTemplate"
import theme from "../../../components/utils/theme"
import { fetchAllPosts, fetchPostsByPageNumber } from "../../../lib/api"

const useStyles = makeStyles(() => ({
  container: {
    marginTop: "3rem"
  }
}))

export const getStaticPaths: GetStaticPaths = async () => {
  const allPosts = await fetchAllPosts()
  
  const per_page = 6
  
  const range = (start: number, end: number) => {
    return (
      [...Array(end - start + 1)].map((_, i) => start + i)
    )
  }

  const paths = range(1, Math.ceil(allPosts.length / per_page)).map((number) =>  `/blog/page/${number}`)

  return {
    paths,
    fallback: false
  }
}

export const getStaticProps: GetStaticProps = async ({ params }) => {
  const currentPageNumber: number = Number(params.number)
  const limit: number = 6

  const postsByPageNumber = await fetchPostsByPageNumber(currentPageNumber, limit)
  const allPosts = await fetchAllPosts()

  return { 
    revalidate: 1,
    props: { 
      currentPageNumber,
      postsByPageNumber,
      allPosts
    }
  }
}

interface Post {
  id: string
  title: string
  subTitle: string
  thumbnail: {
    url: string
  }
}

const BlogPage = ({ currentPageNumber, postsByPageNumber, allPosts }) => {
  const classes = useStyles()
  const router = useRouter()

  const handleChangePage = useCallback(
    (_: React.ChangeEvent<unknown>, number: number) => {
      router.push(`${number}`)
    },[router]
  )

  const perPage: number = 6
  
  return (
    <>
      <ThemeProvider theme={theme}>
        <PageTemplate title="Blog | Corporate Site Sample">
          <>
            <Container maxWidth="lg"className={classes.container}>
              <Grid container justify="center">
                <Grid item>
                  <Typography variant="h1" gutterBottom>
                    Blog
                  </Typography>
                </Grid>
              </Grid>
            </Container>
            <Container maxWidth="lg"className={classes.container}>
              <Grid container spacing={4}>    
                {postsByPageNumber?.map((post: Post) => (
                  <Grid item key={post.id} xs={12} sm={6} md={4}>
                    <Grid container>
                      <Posts
                        id={post.id}
                        title={post.title}
                        subTitle={post.subTitle}
                        thumbnail={post.thumbnail.url}
                      />
                    </Grid>
                  </Grid>
                ))}
              </Grid>
            </Container>
            <Container maxWidth="lg"className={classes.container}>
              <Grid container justify="center">
                <Grid item>
                <Pagination
                  count={Math.ceil(allPosts.length / perPage)}
                  variant="outlined"
                  page={currentPageNumber}
                  onChange={handleChangePage}
                />
                </Grid>
              </Grid>
            </Container>
          </>
        </PageTemplate>
      </ThemeProvider>
    </>
  )
}

export default BlogPage

blog1.png

試しにあと何件か記事を追加してみましょう。

pagination.png

だいぶ良い感じになりました。ちゃんとページネーションも機能していますね。

次は個別記事ページを作成します。

./src/components/utils/ShareButton.tsx
import {
  FacebookShareButton,
  FacebookIcon,
  TwitterShareButton,
  TwitterIcon,
  LineShareButton,
  LineIcon,
} from "react-share"

interface ShareButtonProps {
  url: string
}

const ShareButton = ({ url }: ShareButtonProps) => (
  <>
    <FacebookShareButton url={url} style={{ outline: "none" }}>
      <FacebookIcon size="32px" round />
    </FacebookShareButton>
    <TwitterShareButton
      url={url}
      style={{ marginLeft: `15px`, outline: "none" }}
    >
      <TwitterIcon size="32px" round />
    </TwitterShareButton>
    <LineShareButton url={url} style={{ marginLeft: `15px`, outline: "none" }}>
      <LineIcon size="32px" round />
    </LineShareButton>
  </>
)

export default ShareButton
./src/components/blog/Post.tsx
import { makeStyles } from "@material-ui/core/styles"
import { Container, Grid, Typography } from "@material-ui/core"

import ShareButton from "../utils/ShareButton"
import moment from "moment"

const useStyles = makeStyles(() => ({
  container: {
    marginTop: "3rem",
    maxWidth: "800px",
    overflow: "hidden"
  }
}))

interface PostProps {
  id: string
  title: string
  publishedAt: string
  thumbnail: string
  body: HTMLElement
}

const Post = ({ id, title, publishedAt, thumbnail, body }: PostProps) => {
  const classes = useStyles()
  
  return (
    <>
      <Container className={classes.container}>
        <Grid container direction="column" spacing={3}>
          <Grid item>
            <Typography variant="h1">{title}</Typography>
          </Grid>
          <Grid item>
            <Typography color="textSecondary">
              {moment(publishedAt).format("MMMM Do YYYY")}
            </Typography>
          </Grid>
          <Grid item>
            <img src={thumbnail} style={{ height: "auto", width: "100%" }} />
          </Grid>
        </Grid>
      </Container>
      <Container className={classes.container}>
        <Grid container direction="column" alignItems="center">
          <Grid item >
            <ShareButton
              url={`https://<デプロイ後のドメイン>/blog/${id}`} // 適宜変更
            />
          </Grid>
        </Grid>
      </Container>
      <Container className={classes.container}>
        <Grid container direction="column" alignItems="center">
          <Grid item>
            <div
              dangerouslySetInnerHTML={{
                __html: `${body}`
              }}
            />
          </Grid>
        </Grid>
      </Container>
    </>
  )
}

export default Post
./src/pages/blog/[id].tsx
import { GetStaticPaths, GetStaticProps } from "next"
import { ThemeProvider } from "@material-ui/core/styles"

import Post from "../../components/blog/Post"
import PageTemplate from "../../components/layouts/PageTemplate"
import theme from "../../components/utils/theme"

import { fetchAllPosts, fetchPostById } from "../../lib/api"

export const getStaticPaths: GetStaticPaths = async () => {
  const posts = await fetchAllPosts()
  const paths = posts.map(({ id }) => `/blog/${id}`)

  return { 
    paths,
    fallback: false
  }
}

export const getStaticProps: GetStaticProps = async ({ params }) => {
  const id: string = String(params.id)
  const post = await fetchPostById(id)

  return {
    props: { post },
    revalidate: 1
  }
}

interface Post {
  post: {
    id: string
    title: string
    publishedAt: string
    thumbnail: {
      url: string
    }
    body: HTMLElement
  }
}

const BlogId = ({ post }: Post) => {
  return (
    <ThemeProvider theme={theme}>
      <PageTemplate title="Blog | Corporate Site Sample">
        <Post
          id={post.id}
          title={post.title}
          publishedAt={post.publishedAt}
          thumbnail={post.thumbnail.url}
          body={post.body}
        />
      </PageTemplate>
    </ThemeProvider>
  )
}

export default BlogId

post.png

こんな感じになっていればOKです。

あとはついでにトップページにも掲載しておきましょう。

./src/pages/index.tsx
import { GetStaticProps, NextPage } from "next"

import { makeStyles, ThemeProvider } from "@material-ui/core/styles"
import { Container, Grid, Typography } from "@material-ui/core"

import Slider from "../components/home/Slider"
import Introductions from "../components/home/Introductions"
import Posts from "../components/blog/Posts"
import PageTemplate from "../components/layouts/PageTemplate"
import theme from "../components/utils/theme"

import { fetchLatestPosts } from "../lib/api"

const useStyles = makeStyles(() => ({
  container: {
    marginTop: "3rem"
  }
}))

interface Item {
  name: string
  description: string
  color: string
}

interface Introduction {
  title: string
  description: string
  action: string
  href: string
}

interface Post {
  id: string
  title: string
  subTitle: string
  thumbnail: {
    url: string
  }
}

export const getStaticProps: GetStaticProps = async () => {
  const latestPosts = await fetchLatestPosts(3) // トップページは最新の3件取得

  return {
    props: { latestPosts },
    revalidate: 1
  }
}

const Home = ({ latestPosts }) => {
  const classes = useStyles()

  const items: Item[] = [
    {
      name: "Slide1",
      description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
      color: "#64ACC8"
    },
    {
      name: "Slide2",
      description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
      color: "#7D85B1"
    },
    {
      name: "Slide3",
      description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
      color: "#CE7E78"
    }
  ]

  const introductions: Introduction[] = [
    {
      title: "Service",
      description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
      action: "About Service >",
      href: "/service"
    },
    {
      title: "Company",
      description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
      action: "About Company >",
      href: "/company"
    },
    {
      title: "Recruit",
      description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
      action: "About Recruit >",
      href: "/recruit"
    }
  ]

  return (
    <>
      <ThemeProvider theme={theme}>
        <PageTemplate title="Home | Corporate Site Sample">
          <>
            <Slider
              items={items}
            />
            { introductions.map((introduction, index) => (
              <Container key={index} maxWidth="lg" className={classes.container}>
                <Grid container justify={index % 2 == 0 ? "flex-start" : "flex-end"}>
                  <Grid item lg={6} md={6}>
                    <Introductions
                      index={index}
                      title={introduction.title}
                      description={introduction.description}
                      action={introduction.action}
                      href={introduction.href}
                    />
                  </Grid>
                </Grid>
              </Container>
              ))
            }
            <Container maxWidth="lg" className={classes.container}>
              <Typography variant="h1" align="center" style={{ marginBottom: "2rem" }}>
                Topics
              </Typography>
              <Grid container spacing={4}>
                {latestPosts?.map((post: Post) => (
                  <Grid item key={post.id} xs={12} sm={6} md={4}>
                    <Grid container>
                      <Posts
                        id={post.id}
                        title={post.title}
                        subTitle={post.subTitle}
                        thumbnail={post.thumbnail.url}
                      />
                    </Grid>
                  </Grid>
                ))}
              </Grid>
            </Container>
          </>
        </PageTemplate>
      </ThemeProvider>
    </>
  )
}

export default Home

FireShot Capture 074 - Home - Corporate Site Sample - localhost.png

トップページからも記事を取得できました。

Contact(お問い合わせ)ページを作成

ブログ同様、お問い合わせに関してもmicro CMSを利用させていただきます。

スクリーンショット 2021-04-24 18.22.24_censored (1).jpg

  • API名: 任意(わかりやすければ何でもOK)
  • エンドポイント: 任意(今回はcontacts)

スクリーンショット 2021-04-24 17.54.31_censored.jpg

  • 型: リスト形式

スクリーンショット 2021-04-24 17.58.04_censored.jpg

  • name(名前)
    • テキストフィールド
  • email(メールアドレス)
    • テキストフィールド
  • body(本文)
    • テキストエリア

スクリーンショット 2021-04-24 18.00.07_censored.jpg

APIスキーマの作成が終わったら、「API設定」→「HTTPメソッド」からPOSTリクエストを有効化します。

スクリーンショット 2021-04-24 18.02.39_censored.jpg

また、micro CMSにPOSTリクエストを送る際はX-WRITE-API-KEYが必要になるので、画面の指示に従って作成し、「.env.local」ファイルに追記しておいてください。

./.env.local
NEXT_PUBLIC_MICRO_CMS_WRITE_API_KEY=<X-WRITE-API-KEY>
./src/components/contact/Form.tsx
import React, { useState } from "react"

import { createStyles, makeStyles, Theme } from "@material-ui/core/styles"

import Container from "@material-ui/core/Container"
import TextField from "@material-ui/core/TextField"
import Card from "@material-ui/core/Card"
import CardContent from "@material-ui/core/CardContent"
import Button from "@material-ui/core/Button"
import Box from "@material-ui/core/Box"

import Dialog from "@material-ui/core/Dialog"
import DialogActions from "@material-ui/core/DialogActions"
import DialogContent from "@material-ui/core/DialogContent"
import DialogContentText from "@material-ui/core/DialogContentText"
import DialogTitle from "@material-ui/core/DialogTitle"
import Slide from "@material-ui/core/Slide"
import { TransitionProps } from "@material-ui/core/transitions"

import { createContact } from "../../lib/api"

const Transition = React.forwardRef(function Transition(
  props: TransitionProps & { children?: React.ReactElement<any, any> },
  ref: React.Ref<unknown>,
) {
  return <Slide direction="up" ref={ref} {...props} />
})

interface CompletionDialogProps {
  open: boolean
  handleClose: VoidFunction
}

// 送信完了したらダイアログを表示
const CompletionDialog = ({ open, handleClose}: CompletionDialogProps) => {
  return (
    <div>
      <Dialog
        open={open}
        TransitionComponent={Transition}
        keepMounted
        onClose={handleClose}
      >
        <DialogTitle>
          Thank you for contacting us !
        </DialogTitle>
        <DialogContent>
          <DialogContentText>
            Please wait a couple of days for our reply.
          </DialogContentText>
        </DialogContent>
        <DialogActions>
          <Button onClick={handleClose} color="primary">
            Agree
          </Button>
        </DialogActions>
      </Dialog>
    </div>
  )
}

const useStyles = makeStyles((theme: Theme) =>
  createStyles({
    card: {
      padding: "1rem 4rem"
    },
    header: {
      marginTop: "1.5rem"
    },
    submitBtn: {
      margin: theme.spacing(2),
      textTransform: "none"
    }
  })
)

const Form = () => {
  const classes = useStyles()

  const [name, setName] = useState("")
  const [email, setEmail] = useState("")
  const [body, setBody] = useState("")
  const [open, setOpen] = useState(false)

  const handleOpen = () => {
    setOpen(true)
  }

  const handleClose = () => {
    setOpen(false)
  }

  const handleSubmit = (e: any) => {
    e.preventDefault()

    const data: {} = {
      name: name,
      email: email,
      body: body
    }

    createContact(data)
    .then(() => {
      handleOpen()
      setName("")
      setEmail("")
      setBody("")
    })
    .catch((err) => console.log(err))
  }

  return (
    <>
      <Container fixed>
        <form noValidate autoComplete="off" onSubmit={handleSubmit}>
          <Card className={classes.card}>
            <CardContent>
              <TextField
                required
                label="Name"
                value={name}
                onChange={(e: React.ChangeEvent<HTMLInputElement>) => setName(e.target.value)}
              />
              <TextField
                required
                fullWidth
                label="Email"
                value={email}
                onChange={(e: React.ChangeEvent<HTMLInputElement>) => setEmail(e.target.value)}
              />
              <TextField
                required
                fullWidth
                label="Body"
                multiline
                rows={10}
                value={body}
                variant="outlined"
                onChange={(e: React.ChangeEvent<HTMLInputElement>) => setBody(e.target.value)}
                style={{ marginTop: "2rem"}}
              />
            </CardContent>
            <Box p={1} textAlign="center">
              <Button
                type="submit"
                variant="contained"
                size="large"
                color="default"
                disabled={!name || !email || !body ? true : false}
                className={classes.submitBtn}
                onClick={handleSubmit}
              >
                Send
              </Button>
            </Box>
          </Card>
        </form>
        <CompletionDialog
          open={open}
          handleClose={handleClose}
        />
      </Container>
    </>
  )
}

export default Form
./src/pages/contact.tsx
import { makeStyles, ThemeProvider } from "@material-ui/core/styles"
import { Container, Grid, Typography} from "@material-ui/core"

import PageTemplate from "../components/layouts/PageTemplate"
import Form from "../components/contact/Form"
import theme from "../components/utils/theme"

const useStyles = makeStyles(() => ({
  container: {
    marginTop: "3rem",
    padding: 0
  }
}))

const Contact: React.FC = () => {
  const classes = useStyles()

  return (
    <>
      <ThemeProvider theme={theme}>
        <PageTemplate title="Contact | Sample">
          <>
            <Container maxWidth="lg" className={classes.container}>
              <Grid container justify="center">
                <Grid item>
                  <Typography variant="h1" gutterBottom>
                    Contact
                  </Typography>
                </Grid>
              </Grid>
            </Container>
            <Container maxWidth="lg" className={classes.container}>
              <Form />
            </Container>
          </>
        </PageTemplate>
      </ThemeProvider>
    </>
  )
}

export default Contact

contact.png

試しに送信してみましょう。

スクリーンショット 2021-04-24 19.41.40_censored.jpg

micro CMSの管理画面を確認し、先ほどの内容がちゃんと届いていれば成功です。

デプロイ

さて、ある程度の形が完成したので本番環境にデプロイしていきましょう。今回は「Vercel」を使います。

あらかじめ適当なGitHubリポジトリを作成し、先ほど作成したコードをプッシュしておいてください。

スクリーンショット 2021-04-24 21.32.21.png

https://vercel.com/login

↑からGitHubアカウントでログイン。

スクリーンショット 2021-04-24 21.33.26_censored.jpg

右上の「New Project」をクリック。

スクリーンショット 2021-04-24 21.36.23_censored.jpg

該当のリポジトリをインポートします。

スクリーンショット 2021-04-24 21.38.25.png

各種環境変数をセットし、右下の「Deploy」を押せばOKです。ビルドが始まるので終わるまで待ちましょう。

スクリーンショット 2021-04-24 21.40.36.png

しばらくすると「Congratulations!」というメッセージが表示されるはずなので、「Visit」からちゃんと動いているか確認してください。

https://corporate-site-sample.vercel.app

Vercelとmicro CMSを連携

最後に、Vercelとmicro CMSの連携を行なっておきます。注意点として、今のままの状態だとmicroCMS側で記事を更新しても本番環境には適用されません。(静的なサイトであるため、再度ビルドしないと差分が反映されない。)

そこで、Webhookを利用して記事の新規作成や更新時に自動でビルドを走らせるようする必要があります。

スクリーンショット 2021-04-24 21.55.07.png

Vercelのダッシュボードを開き、「settgings」→「Git」へ進むと「Deploy Hooks」という項目があるので

  • Hooks名: 任意(わかりやすければ何でもOK)
  • Branch: main(もしくはmaster)

スクリーンショット 2021-04-24 21.58.15_censored.jpg

それぞれ入力しWebhook URLを発行してください。

スクリーンショット 2021-04-24 22.00.28_censored.jpg

今度はmicro CMSの管理画面から「API設定」→「Webhook」へ進み、「カスタム通知」を選択。先ほどのWebhook URLを設定しましょう。

これにて無事連携完了です。心配な方は試しに新しい記事を作成してみてください。

備考

今回作成したコード: https://github.com/kazama1209/corporate-site-sample

もし正常に動かない場合、どこが間違っているのか確認するのに使ってください。

あとがき

以上、Next.js × TypeScript × microCMSで簡易的なコーポレートサイトを作ってみました。非常にシンプルな構成なので、あとは各々の好みにカスタマイズしていただければと思います。

今回は画像やアニメーションなどをほとんど入れてないので、その辺を工夫してみるとだいぶ変わるかもしれません。自分はデザイン的なセンスには自信が無いため、煮るなり焼くなり好きにしてください。

デザイン以外の部分で言えば、一応、ブログとお問い合わせの機能に関しては割と参考になるのではないかなと思います。

107
122
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
107
122

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?