概要
友人の依頼でちょっとしたコーポレートサイトを作る機会があったのでメモ。
使ったもの
- Next.js(フレームワーク)
- TypeScript(言語)
- Material UI(デザイン)
- microCMS(CMS)
- Vercel(デプロイ)
特にmicroCMSは前々から使ってみたいと思っていたサービスなので良い機会でした。
(※筆者はまだTypeScriptに慣れておらず雰囲気で書いている事も多いため、所々で未熟な部分があるかもしれません。あらかじめご了承ください。)
完成系
https://corporate-site-sample.vercel.app
トップページ
事業紹介ページ
会社情報ページ
採用ページ
一覧
詳細
ブログページ
一覧
詳細
お問い合わせページ
コーポレートサイトを制作するにあたってどんなページ構成にするかは好みによると思いますが、今回は
- トップページ
- 事業紹介ページ
- 会社情報ページ
- 採用ページ
- ブログページ
- お問い合わせページ
といったシンプルな構成にしてみました。これらはどのコーポレートサイトにおいても非常に良く見かける気がします。
実装
前置きはほどほどに、実装していきたいと思います。
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の中身も変わっているので確認してください。
{
"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
const Home: React.FC = () => {
return (
<h1>Hello World!!</h1>
)
}
export default Home
「.pages/index.tsx」を適当にTypeScriptっぽく書き換えて再度ビルド。http://localhost:3000/ にアクセスして「Hello World!!」と返ってくれば無事TypeScriptの導入に成功です。
各種ディレクトリ・ファイルを準備
これから大量のディレクトリ・ファイルを取り扱う事になるので、あらかじめ準備しておきましょう。一個一個ぽちぽち作っていくのはさすがに辛いので、シェルスクリプトを用意して一気にまとめて作ってしまいます。
$ 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
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
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()]
}
}
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>
)
}
import Button from "@material-ui/core/Button"
const Home: React.FC = () => {
return (
<>
<h1>Hello World!!</h1>
<Button variant="contained">Default</Button>
</>
)
}
export default Home
ちゃんとそれっぽいボタンが表示されていれば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シェアボタンが作れる
ルーティングを設定
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" }
]
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
共通レイアウトを作成
ヘッダーやフッターは全てのページで共通にしたいので、テンプレートとしてまとめておきます。
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
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}>
©{new Date().getFullYear()} Sample
</Typography>
</Grid>
</Container>
</div>
)
}
export default Footer
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
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
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
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
こんな感じでヘッダーとフッターが良い感じになっていればOK.
トップページを作成
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
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
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
こんな感じになっていればOKです。
Serviceページを作成
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
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
こんな感じになっていればOKです。
Company(企業情報)ページを作成。
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
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
こんな感じになってればOKです。
Recruit(採用)ページを作成
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
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
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
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
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
一覧ページ、詳細ページともにこんな感じになっていればOKです。
ブログページを作成
今回、ブログに関しては「micro CMS」を利用して配信していきます。
まず、以下の公式マニュアルに沿ってサービスの作成まで進めてください。
- API名: 任意(わかりやすければ何でもOK)
- エンドポイント: 任意(今回はblog)
- 型: リスト形式
- title(タイトル)
- テキストフィールド
- subTitle(サブタイトル)
- テキストエリア
- body(本文)
- リッチエディタ
- thumbnail(サムネイル画像)
- 画像
APIスキーマの作成が終わったら、「コンテンツの追加」から適当に記事を作成して公開します。
公開できたら、右上の「APIプレビュー」をクリック。
するとこんな感じでコンテンツを取得するためのcurlコマンドが表示されるので、ターミナルなどから確認してください。また、APIキーはメモなどに控えておきましょう。
NEXT_PUBLIC_MICRO_CMS_SERVICE_ID=<micro CMSのサービスID(ドメインみたいな部分)>
NEXT_PUBLIC_MICRO_CMS_API_KEY=<micro CMSのAPIキー>
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))
}
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
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
試しにあと何件か記事を追加してみましょう。
だいぶ良い感じになりました。ちゃんとページネーションも機能していますね。
次は個別記事ページを作成します。
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
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
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
こんな感じになっていればOKです。
あとはついでにトップページにも掲載しておきましょう。
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
トップページからも記事を取得できました。
Contact(お問い合わせ)ページを作成
ブログ同様、お問い合わせに関してもmicro CMSを利用させていただきます。
- API名: 任意(わかりやすければ何でもOK)
- エンドポイント: 任意(今回はcontacts)
- 型: リスト形式
- name(名前)
- テキストフィールド
- email(メールアドレス)
- テキストフィールド
- body(本文)
- テキストエリア
APIスキーマの作成が終わったら、「API設定」→「HTTPメソッド」からPOSTリクエストを有効化します。
また、micro CMSにPOSTリクエストを送る際はX-WRITE-API-KEYが必要になるので、画面の指示に従って作成し、「.env.local」ファイルに追記しておいてください。
NEXT_PUBLIC_MICRO_CMS_WRITE_API_KEY=<X-WRITE-API-KEY>
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
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
試しに送信してみましょう。
micro CMSの管理画面を確認し、先ほどの内容がちゃんと届いていれば成功です。
デプロイ
さて、ある程度の形が完成したので本番環境にデプロイしていきましょう。今回は「Vercel」を使います。
あらかじめ適当なGitHubリポジトリを作成し、先ほど作成したコードをプッシュしておいてください。
↑からGitHubアカウントでログイン。
右上の「New Project」をクリック。
該当のリポジトリをインポートします。
各種環境変数をセットし、右下の「Deploy」を押せばOKです。ビルドが始まるので終わるまで待ちましょう。
しばらくすると「Congratulations!」というメッセージが表示されるはずなので、「Visit」からちゃんと動いているか確認してください。
https://corporate-site-sample.vercel.app
Vercelとmicro CMSを連携
最後に、Vercelとmicro CMSの連携を行なっておきます。注意点として、今のままの状態だとmicroCMS側で記事を更新しても本番環境には適用されません。(静的なサイトであるため、再度ビルドしないと差分が反映されない。)
そこで、Webhookを利用して記事の新規作成や更新時に自動でビルドを走らせるようする必要があります。
Vercelのダッシュボードを開き、「settgings」→「Git」へ進むと「Deploy Hooks」という項目があるので
- Hooks名: 任意(わかりやすければ何でもOK)
- Branch: main(もしくはmaster)
それぞれ入力しWebhook URLを発行してください。
今度はmicro CMSの管理画面から「API設定」→「Webhook」へ進み、「カスタム通知」を選択。先ほどのWebhook URLを設定しましょう。
これにて無事連携完了です。心配な方は試しに新しい記事を作成してみてください。
備考
今回作成したコード: https://github.com/kazama1209/corporate-site-sample
もし正常に動かない場合、どこが間違っているのか確認するのに使ってください。
あとがき
以上、Next.js × TypeScript × microCMSで簡易的なコーポレートサイトを作ってみました。非常にシンプルな構成なので、あとは各々の好みにカスタマイズしていただければと思います。
今回は画像やアニメーションなどをほとんど入れてないので、その辺を工夫してみるとだいぶ変わるかもしれません。自分はデザイン的なセンスには自信が無いため、煮るなり焼くなり好きにしてください。
デザイン以外の部分で言えば、一応、ブログとお問い合わせの機能に関しては割と参考になるのではないかなと思います。