microCMSプロジェクトの立ち上げ・API設定
microCMSプロジェクトの立ち上げ
microCMSプロジェクトを立ち上げます。
API設定
今回設定するAPIは以下の3つ。
- 設定API
- カテゴリAPI
- 記事API
これだけですべてを完結させます(タイトル回収)
設定APIは配列を持たないのでオブジェクト形式にします。
あとはここに洗い出した要素をすべて入れていくだけです。
Next.jsプロジェクト立ち上げ ~ 初期セットアップ・クライアント接続テストまで
Next.jsプロジェクト作成
今回は 3apis-microcms
というプロジェクト名で開発します。
$ npx create-next-app@latest 3apis-microcms
色々聞かれますがすべてYes。
$ npm run dev
上記コマンドを叩いて下の画面が出てきたらOK。
gitセットアップ
諸々成功したら、gitのセットアップ、初回コミットもしておきます。
$ git init
$ git remote add origin [リポジトリURL]
$ git fetch //fetchテスト
$ git add -A
$ git commit -m "first commit"
$ git push origin main
microCMSセットアップ
セットアップは公式チュートリアルの通りやってみましょう。
npm経由でsdkをインストールします。
$ npm i microcms-js-sdk
.envファイルを作成します。
$ touch .env
MICROCMS_API_KEY=xxxxxxxxxx
MICROCMS_SERVICE_DOMAIN=xxxxxxxxxx
microCMSの管理画面からAPIキーとサービスドメインを持ってきて保存しましょう。
(必ず$ git status
や.gitignore
で追跡から除外されていることを確認してください)
次にsrc下にlibsディレクトリを作成し、その中にclient.tsファイルを作成します。
microCMSとの接続に必要なコードを記述するファイルです。
$ mkdir src/libs
$ touch src/libs/client.ts
// libs/microcms.ts
import { createClient } from 'microcms-js-sdk';
// 環境変数にMICROCMS_SERVICE_DOMAINが設定されていない場合はエラーを投げる
if (!process.env.MICROCMS_SERVICE_DOMAIN) {
throw new Error('MICROCMS_SERVICE_DOMAIN is required');
}
// 環境変数にMICROCMS_API_KEYが設定されていない場合はエラーを投げる
if (!process.env.MICROCMS_API_KEY) {
throw new Error('MICROCMS_API_KEY is required');
}
// Client SDKの初期化を行う
export const client = createClient({
serviceDomain: process.env.MICROCMS_SERVICE_DOMAIN,
apiKey: process.env.MICROCMS_API_KEY,
});
クライアント接続テスト
試しに、設定項目を出力してみます。
microCMS上でダミーデータを設定します。
これをTOP画面に表示してみます。
import Link from 'next/link';
import { client } from '../libs/client';
import { MicroCMSImage } from 'microcms-js-sdk';
// 型定義
export type Config = {
id: string;
logoImg: MicroCMSImage;
title: string;
subtitle: string;
companyName: string;
companyAddress: string;
contactImg: MicroCMSImage;
contactText: string;
}
//getリクエストでconfigを取得
export const getConfig = async () => {
const topPageData = await client.get<Config>({
endpoint: "config",
})
return topPageData;
}
export default async function Home() {
const config = await getConfig();
return (
<main>
<h1>設定一覧</h1>
<ul>
<li>
<p>ロゴ画像</p>
<img src={`${config.logoImg.url}`} alt="ロゴ画像" width="auto" height="150px" />
</li>
<li>
<p>サイトタイトル</p>
<p>{config.title}</p>
</li>
<li>
<p>サブタイトル</p>
<p>{config.subtitle}</p>
</li>
<li>
<p>会社名</p>
<p>{config.companyName}</p>
</li>
<li>
<p>会社住所</p>
<p>{config.companyAddress}</p>
</li>
<li>
<p>お問い合わせ画像</p>
<img src={`${config.contactImg.url}`} alt="ロゴ画像" width="auto" height="150px" />
</li>
<li>
<p>お問い合わせテキスト</p>
<p>{config.contactText}</p>
</li>
</ul>
</main>
);
}
ローカルサーバーを起動して見てみます。
$ npm run dev
うまく表示されました。
Sassセットアップ・スタイル初期化
Sassセットアップ
スタイリングのためにsassをセットアップします。
sassのインストール
$ npm i sass
必要なディレクトリ・scssファイルを作成します。
$ mkdir src/share
$ mkdir src/share/styles
$ touch _index.scss _mixin.scss _variables.scss
@mixin sp {
@media screen and (max-width: 767px) {
@content;
}
}
@mixin ov-sp {
@media screen and (min-width: 768px) {
@content;
}
}
@mixin ov-pc {
@media screen and (min-width: 1367px) {
@content;
}
}
@forward "./mixin";
@forward "./variables";
//ここには変数を入れていきます
$main-text-color: #4A4A4A
mixinやvariablesなどは全てのファイルで一度読み込ませたいのでsassOptionsを利用します。
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
sassOptions: {
additionalData: `@use "src/share/styles/index" as *;`,
implementation: 'sass',
},
};
export default nextConfig;
初期化・global.scss編集
初期スタイルを初期化するためdesytle.cssを適用させます。
↓からダウンロード
また、global.scssも編集します。
//フォントのインポート
@import url("https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@100..900&display=swap");
html {
//リキッドの適用
font-size: calc(10 / 1440 * 100vw);
@include sp {
font-size: calc(10 / 375 * 100vw);
}
body {
font-size: 1.6rem;
line-height: 2;
overflow-x: hidden;
color: $main-text-color;
font-family: "Noto Sans JP", serif;
}
}
.sp-none {
@include sp {
display: none !important;
}
}
.pc-none {
@include ov-sp {
display: none !important;
}
}
これをTOPレイアウト(app/layout.tsx)にインポートします。
import type { Metadata } from "next";
import "@/share/styles/destyle.css"; //追加
import "@/share/styles/global.scss"; //追加
export const metadata: Metadata = {
title: "3APIs-microCMS",
description: "API3つでコーポレートサイトのすべてを完結させる(迫真)",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="ja">
<body>
{children}
</body>
</html>
);
}
サイトを確認してみます。
いい感じですね。
コンポーネント・レイアウト構築
各ページのレイアウトを構築していきます。
レイアウト構築時は、microCMSとの接続は一旦無視します。
最初からjsを組み込んでいくとコードが煩雑になる可能性があるからです。
(私が未熟者故ですが...。)
Headerコンポーネント
import React from 'react'
import styles from './Header.module.scss'
import Link from 'next/link'
const Header = () => {
return (
<header className={styles['c-header']}>
<Link href={`/`}>
<h1>Logo</h1>
</Link>
<nav className={styles['c-header__nav']}>
<ul className={`${styles['c-header__nav__list']}`}>
<li className={styles['c-header__nav__list__item']}>
<Link href={`/articles`}>お知らせ</Link>
</li>
<li className={styles['c-header__nav__list__item']}>
<Link href={`/company`}>企業情報</Link>
</li>
<li className={styles['c-header__nav__list__item']}>
<Link href={`/categories`}>事業紹介</Link>
</li>
<li className={styles['c-header__nav__list__contact']}>
<Link href={`/`}>お問い合わせ</Link>
</li>
</ul>
</nav>
</header>
)
}
export default Header
.c-header {
z-index: 999;
position: fixed;
top: 1.5rem;
left: 50%;
transform: translateX(-50%);
height: 8rem;
padding-left: 1.5rem;
width: calc(100% - 8rem);
box-shadow: 0 0 1rem rgba(0, 0, 0, 0.1);
display: flex;
align-items: center;
justify-content: space-between;
background-color: white;
&__nav {
height: 100%;
&__list {
display: flex;
align-items: center;
gap: 5rem;
height: 100%;
&__item {
font-size: 1.4rem;
}
&__contact {
width: 18rem;
height: 100%;
background-color: #f0f0f0;
line-height: 8rem;
text-align: center;
font-weight: bold;
}
}
}
}
Footerコンポーネント
import React from 'react'
import styles from './Footer.module.scss'
import Link from 'next/link'
import classNames from 'classnames'
const Footer = () => {
return (
<footer className={styles['c-footer']}>
<div className={styles['c-footer__wrapper']}>
<div className={styles['c-footer__company']}>
<h2>株式会社UNOTAME</h2>
<p>テスト県テスト市テスト町00-0</p>
</div>
<div className={styles['c-footer__menu']}>
<ul className={styles['c-footer__list']}>
<li className={classNames(
styles['c-footer__item'],
styles['--parent']
)}>
<Link href={`/articles`}>企業情報</Link>
</li>
<li className={styles['c-footer__item']}>
<Link href={`/articles`}>代表挨拶</Link>
</li>
<li className={styles['c-footer__item']}>
<Link href={`/articles`}>沿革</Link>
</li>
<li className={styles['c-footer__item']}>
<Link href={`/articles`}>経営メンバー</Link>
</li>
<li className={styles['c-footer__item']}>
<Link href={`/articles`}>お問い合わせ</Link>
</li>
</ul>
<ul className={styles['c-footer__list']}>
<li className={classNames(
styles['c-footer__item'],
styles['--parent']
)}>
<Link href={`/categories`}>事業紹介</Link>
</li>
<li className={styles['c-footer__item']}>
<Link href={`/articles`}>カテゴリ1</Link>
</li>
<li className={styles['c-footer__item']}>
<Link href={`/articles`}>カテゴリ2</Link>
</li>
<li className={styles['c-footer__item']}>
<Link href={`/articles`}>カテゴリ3</Link>
</li>
<li className={styles['c-footer__item']}>
<Link href={`/articles`}>カテゴリ4</Link>
</li>
</ul>
</div>
</div>
</footer>
)
}
export default Footer
.c-footer {
background-color: #4a4a4a;
color: white;
&__wrapper {
width: calc(100% - 44rem);
margin-inline: auto;
display: flex;
align-items: top;
justify-content: space-between;
padding-block: 7rem;
}
&__company {
h2 {
font-size: 2rem;
font-weight: bold;
margin-bottom: 1.5rem;
}
}
&__menu {
display: flex;
gap: 10rem;
}
&__list {
display: flex;
flex-direction: column;
}
&__item {
margin-bottom: 1.5rem;
&.--parent {
margin-bottom: 2rem;
font-size: 2rem;
font-weight: bold;
}
}
}
お問い合わせコンポーネント
import React from 'react'
import styles from './Contact.module.scss'
import Button from '../Button/Button'
const Contact = () => {
return (
<div className="l-container">
<div className={styles['c-contact']}>
<div>
<h3>お問い合わせ</h3>
<p>お問い合わせのテキストが入ります。お問い合わせのテキストが入ります。お問い合わせのテキストが入ります。お問い合わせのテキストが入ります。</p>
<Button color="gray" href="/contact">詳細</Button>
</div>
<img src="./img-thumbnail.png" alt="" />
</div>
</div>
)
}
export default Contact
.c-contact {
padding-block: 18rem 12rem;
display: flex;
justify-content: space-between;
gap: 3rem;
& > * {
width: 50%;
}
h3 {
font-size: $fs--item-title;
font-weight: bold;
}
p {
padding-block: 1.5rem 3rem;
}
}
ボタンコンポーネント
import classNames from "classnames";
import { FC, ButtonHTMLAttributes } from "react";
import styles from "./Button.module.scss"
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
color?: "gray" | "white";
href?: string;
}
const Button: FC<ButtonProps> = ({
color = "gray",
children,
className,
href,
...props
}) => {
return (
<button className={classNames(
styles['c-button'],
styles[color],
className
)} {...props}>
<a href={href}>
{children}
</a>
</button>
)
}
export default Button;
.c-button {
width: 27rem;
text-align: center;
height: 6.4rem;
&.gray {
background-color: $color-main;
color: white;
}
a {
display: block;
width: 100%;
height: 100%;
line-height: 6.4rem;
}
}
TOPページ
import Link from 'next/link';
import { client } from '../libs/client';
import { MicroCMSImage } from 'microcms-js-sdk';
import styles from './Home.module.scss'
import Button from '@/share/components/Button/Button';
export default async function Home() {
return (
<div className={styles['p-home']}>
{/* mv */}
<div className={styles['p-home__mv']}>
<div className={styles['p-home__title']}>
<h1>サイトタイトルが入ります。サイトタイトルが入ります。</h1>
<span>サブタイトルが入ります。</span>
</div>
</div>
{/* 事業紹介 */}
<div className="l-container">
<section className={styles['p-home__business']}>
<h2 className={styles['p-home__business__header']}>事業紹介</h2>
<ul className={styles['p-home__business__list']}>
<li className={styles['p-home__business__item']}>
<div>
<h3>カテゴリタイトル</h3>
<p>カテゴリの説明が入ります。カテゴリの説明が入ります。カテゴリの説明が入ります。カテゴリの説明が入ります。カテゴリの説明が入ります。カテゴリの説明が入ります。カテゴリの説明が入ります。カテゴリの説明が入ります。</p>
<Button color="gray" href="/categories">詳細</Button>
</div>
<img src="./img-thumbnail.png" alt="" />
</li>
<li className={styles['p-home__business__item']}>
<div>
<h3>カテゴリタイトル</h3>
<p>カテゴリの説明が入ります。カテゴリの説明が入ります。カテゴリの説明が入ります。カテゴリの説明が入ります。カテゴリの説明が入ります。カテゴリの説明が入ります。カテゴリの説明が入ります。カテゴリの説明が入ります。</p>
<Button color="gray" href="/categories">詳細</Button>
</div>
<img src="./img-thumbnail.png" alt="" />
</li>
<li className={styles['p-home__business__item']}>
<div>
<h3>カテゴリタイトル</h3>
<p>カテゴリの説明が入ります。カテゴリの説明が入ります。カテゴリの説明が入ります。カテゴリの説明が入ります。カテゴリの説明が入ります。カテゴリの説明が入ります。カテゴリの説明が入ります。カテゴリの説明が入ります。</p>
<Button color="gray" href="/categories">詳細</Button>
</div>
<img src="./img-thumbnail.png" alt="" />
</li>
<li className={styles['p-home__business__item']}>
<div>
<h3>カテゴリタイトル</h3>
<p>カテゴリの説明が入ります。カテゴリの説明が入ります。カテゴリの説明が入ります。カテゴリの説明が入ります。カテゴリの説明が入ります。カテゴリの説明が入ります。カテゴリの説明が入ります。カテゴリの説明が入ります。</p>
<Button color="gray" href="/categories">詳細</Button>
</div>
<img src="./img-thumbnail.png" alt="" />
</li>
</ul>
<div className={styles['p-home__business__button']}>
<Button color='gray' href='/categories'>事業一覧</Button>
</div>
</section>
<section className={styles['p-home__company']}>
<ul className={styles['p-home__company__list']}>
<li className={styles['p-home__company__item']}>
<a href="/">
<h3>Company</h3>
<span>会社概要</span>
</a>
</li>
<li className={styles['p-home__company__item']}>
<a href="/">
<h3>代表挨拶</h3>
<span>CEO Message</span>
</a>
</li>
<li className={styles['p-home__company__item']}>
<a href="/">
<h3>History</h3>
<span>沿革</span>
</a>
</li>
<li className={styles['p-home__company__item']}>
<a href="/">
<h3>Member</h3>
<span>経営メンバー</span>
</a>
</li>
</ul>
</section>
<section className={styles['p-home__articles']}>
<ul className={styles['p-home__articles__list']}>
{/* max3つ */}
<li className={styles['p-home__articles__item']}>
<a href="">
<img src="./img-thumbnail.png" alt="" />
<div>
<time>2024/12/10</time>
<span>カテゴリ</span>
</div>
<p>説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。</p>
</a>
</li>
<li className={styles['p-home__articles__item']}>
<a href="">
<img src="./img-thumbnail.png" alt="" />
<div>
<time>2024/12/10</time>
<span>カテゴリ</span>
</div>
<p>説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。</p>
</a>
</li>
<li className={styles['p-home__articles__item']}>
<a href="">
<img src="./img-thumbnail.png" alt="" />
<div>
<time>2024/12/10</time>
<span>カテゴリ</span>
</div>
<p>説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。</p>
</a>
</li>
</ul>
<div className={styles['p-home__articles__button']}>
<Button color='gray' href='/articles'>記事一覧</Button>
</div>
</section>
</div>
</div>
);
}
.p-home {
&__mv {
height: 50rem;
width: 100%;
margin-bottom: 20rem;
text-align: center;
position: relative;
}
&__title {
width: $width-container--top;
max-width: calc(1366px - 440px);
position: absolute;
top: 30%;
left: 50%;
transform: translateX(-50%);
h1 {
font-size: $fs--title;
font-weight: bold;
margin-bottom: 1.5rem;
}
span {
font-size: $fs--subtitle;
font-weight: bold;
}
}
&__business {
&__header {
padding-bottom: 1.5rem;
border-bottom: 1px solid $color-border;
font-weight: bold;
}
&__list {
margin-block: 5rem;
}
&__item {
display: flex;
justify-content: space-between;
gap: 3rem;
&:not(:last-child) {
margin-bottom: 8rem;
}
& > * {
width: 50%;
}
&:nth-child(even) {
flex-direction: row-reverse;
}
h3 {
font-size: $fs--item-title;
font-weight: bold;
}
p {
padding-block: 1.5rem 3rem;
}
}
&__button {
text-align: center;
}
}
&__company {
margin-block: 18rem;
background-color: $color-main;
padding: 4.5rem;
&__list {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
gap: 3rem;
}
&__item {
width: calc(50% - 1.5rem);
height: 22rem;
padding: 5rem;
background-color: white;
a {
width: 100%;
}
h3 {
font-size: $fs--page-title;
font-weight: bold;
margin-bottom: 2rem;
}
span {
font-weight: bold;
}
}
}
&__articles {
&__list {
display: flex;
gap: 1.6rem;
}
&__item {
width: 33.3%;
img {
width: 100%;
}
div {
padding-block: 0.5rem;
font-size: $fs--small;
time {
margin-right: 1.5rem;
}
span {
}
}
}
&__button {
padding-top: 5rem;
text-align: center;
}
}
}
カテゴリ一覧ページ
import React from 'react'
import styles from "./Categories.module.scss"
import Button from '@/share/components/Button/Button'
function categories() {
return (
<div className={styles['p-categories']}>
<div className="l-page__title">
<h2>事業詳細</h2>
</div>
<div className="l-container">
<ul className={styles['p-categories__list']}>
<li className={styles['p-categories__item']}>
<h3>サービス名</h3>
<img src="./img-thumbnail.png" alt="" />
<Button color="gray" href="/">詳細</Button>
<p>説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。</p>
</li>
<li className={styles['p-categories__item']}>
<h3>サービス名</h3>
<img src="./img-thumbnail.png" alt="" />
<Button color="gray" href="/">詳細</Button>
<p>説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。</p>
</li>
<li className={styles['p-categories__item']}>
<h3>サービス名</h3>
<img src="./img-thumbnail.png" alt="" />
<Button color="gray" href="/">詳細</Button>
<p>説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。</p>
</li>
<li className={styles['p-categories__item']}>
<h3>サービス名</h3>
<img src="./img-thumbnail.png" alt="" />
<Button color="gray" href="/">詳細</Button>
<p>説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。</p>
</li>
</ul>
</div>
</div>
)
}
export default categories
.p-categories {
&__list {
}
&__item {
text-align: center;
&:not(:last-child) {
margin-bottom: 12rem;
}
h3 {
font-size: $fs--item-title;
font-weight: bold;
margin-bottom: 3rem;
text-align: center;
}
img {
width: 100%;
}
button {
margin-block: 3rem;
}
p {
}
}
}
カテゴリ詳細ページ
import React from 'react'
import styles from "./Category.module.scss"
import Button from '@/share/components/Button/Button'
function category() {
return (
<div className={styles['p-category']}>
<div className="l-page__title">
<h2>事業詳細</h2>
</div>
<div className="l-container">
<div className="p-single">
<h2 className='p-single__title'></h2>
<img src="./img-thumbnail.png" alt="" className='p-single__thumbnail' />
<p>説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。<br />
<br />
説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。<br />
<br />
説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。</p>
</div>
</div>
</div>
)
}
export default category
記事一覧
import React from 'react'
import styles from './Articles.module.scss'
function articles() {
return (
<div className={styles['p-articles']}>
<div className="l-page__title">
<h2>記事一覧</h2>
</div>
<div className="l-container">
<ul className={styles['p-articles__list']}>
<li className={styles['p-articles__item']}>
<a href="">
<img src="./img-thumbnail.png" alt="" />
<div className={styles['p-articles__desc']}>
<div>
<time>2024/12/10</time>
<span>カテゴリ</span>
</div>
<p>説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。</p>
</div>
</a>
</li>
<li className={styles['p-articles__item']}>
<a href="">
<img src="./img-thumbnail.png" alt="" />
<div className={styles['p-articles__desc']}>
<div>
<time>2024/12/10</time>
<span>カテゴリ</span>
</div>
<p>説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。</p>
</div>
</a>
</li>
<li className={styles['p-articles__item']}>
<a href="">
<img src="./img-thumbnail.png" alt="" />
<div className={styles['p-articles__desc']}>
<div>
<time>2024/12/10</time>
<span>カテゴリ</span>
</div>
<p>説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。</p>
</div>
</a>
</li>
<li className={styles['p-articles__item']}>
<a href="">
<img src="./img-thumbnail.png" alt="" />
<div className={styles['p-articles__desc']}>
<div>
<time>2024/12/10</time>
<span>カテゴリ</span>
</div>
<p>説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。</p>
</div>
</a>
</li>
</ul>
</div>
</div>
)
}
export default articles
.p-articles {
&__list {
}
&__item {
padding-bottom: 4rem;
border-bottom: 1px solid $color-border;
a {
display: flex;
gap: 3rem;
}
&:not(:last-child) {
margin-bottom: 2rem;
}
img {
width: 25rem;
}
}
&__desc {
width: 100%;
div {
margin-bottom: 1rem;
time {
margin-right: 1.5rem;
font-size: $fs--small;
}
span {
font-size: $fs--small;
}
}
}
}
記事詳細ページ
import React from 'react'
import styles from './Article.module.scss'
function article() {
return (
<div className={styles['p-categories']}>
<div className="l-page__title">
<h2>記事詳細</h2>
</div>
<div className="l-container">
<div className="p-single">
<h2 className='p-single__title'>タイトルが入りますタイトルが入りますタイトルが入りますタイトルが入りますタイトルが入ります</h2>
<img src="./img-thumbnail.png" alt="" className='p-single__thumbnail' />
<p>説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。<br />
<br />
説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。<br />
<br />
説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。説明が入ります。</p>
</div>
</div>
</div>
)
}
export default article
グローバルCSS・変数
@import url("https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@100..900&display=swap");
html {
font-size: calc(10 / 1366 * 100vw);
@include sp {
font-size: calc(10 / 375 * 100vw);
}
@include ov-pc {
font-size: 10px;
}
body {
font-size: $fs--base;
line-height: 2;
overflow-x: hidden;
color: $color-main;
font-family: "Noto Sans JP", serif;
}
main {
padding-top: 11rem;
}
}
.sp-none {
@include sp {
display: none !important;
}
}
.pc-none {
@include ov-sp {
display: none !important;
}
}
.l-container {
width: $width-container--top;
max-width: calc(1366px - 440px);
margin-inline: auto;
}
.l-page__title {
height: 31rem;
margin-bottom: 18rem;
position: relative;
h2 {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: $fs--page-title;
font-weight: bold;
}
}
.p-single {
&__title {
font-size: $fs--page-title;
}
&__thumbnail {
width: 100%;
}
&__desc {
time {
font-size: $fs--small;
margin-right: 1.5rem;
}
span {
font-size: $fs--small;
}
}
img {
width: 100%;
}
}
//color
$color-main: #4a4a4a;
$color-main--bg: #ffffff;
$color-border: rgba(0, 0, 0, 0.16);
//width
$width-container--top: calc(100% - 44rem);
$width-container--content: calc(100% - 64rem);
//font-size
$fs--base: 1.6rem;
$fs--title: 6.4rem;
$fs--subtitle: 3.2rem;
$fs--page-title: 3.2rem;
$fs--item-title: 2.4rem;
$fs--section-title: 2rem;
$fs--small: 1.2rem;
microCMS統合
各データfetch
// libs/microcms.ts
import { createClient, MicroCMSDate, MicroCMSImage, MicroCMSListResponse, MicroCMSQueries } from 'microcms-js-sdk';
// 環境変数にMICROCMS_SERVICE_DOMAINが設定されていない場合はエラーを投げる
if (!process.env.MICROCMS_SERVICE_DOMAIN) {
throw new Error('MICROCMS_SERVICE_DOMAIN is required');
}
// 環境変数にMICROCMS_API_KEYが設定されていない場合はエラーを投げる
if (!process.env.MICROCMS_API_KEY) {
throw new Error('MICROCMS_API_KEY is required');
}
// Client SDKの初期化を行う
export const client = createClient({
serviceDomain: process.env.MICROCMS_SERVICE_DOMAIN,
apiKey: process.env.MICROCMS_API_KEY,
});
// configの型定義
export type Config = {
id: string;
logoImg: MicroCMSImage;
title: string;
subtitle: string;
companyName: string;
companyAddress: string;
contactImg: MicroCMSImage;
contactText: string;
}
//getリクエストでconfigを取得
export const getConfig = async () => {
const configData = await client.get<Config>({
endpoint: "config",
})
return configData;
}
export type Category = {
id: string;
title: string;
thumbnail: MicroCMSImage;
overview: string;
body: string;
} & MicroCMSDate;
export const getCategories = async (queries?: MicroCMSQueries): Promise<MicroCMSListResponse<Category>> => {
const listData = await client.getList<Category>({
endpoint: "categories",
queries,
});
return listData
};
export const getCategory = async (id: string) => {
const data = await client.get({
endpoint: "categories",
contentId: `${id}`,
});
return data;
}
export type Article = {
id: string;
title: string;
thumbnail: MicroCMSImage;
body: string;
category: Category;
} & MicroCMSDate;
export const getArticles = async (queries?: MicroCMSQueries): Promise<MicroCMSListResponse<Article>> => {
const listData = await client.getList<Article>({
endpoint: "articles",
queries,
});
return listData
};
export const getArticle = async (id: string) => {
const data = await client.get({
endpoint: "articles",
contentId: `${id}`,
});
return data;
}
TOPページ ↔︎ config
import { getArticles, getCategories, getConfig } from '../libs/client';
import styles from './Home.module.scss'
import Button from '@/share/components/Button/Button';
import { toDate } from '@/libs/utils';
import Image from 'next/image';
export default async function Home() {
const config = await getConfig();
const { contents: categories } = await getCategories();
const { contents: articles } = await getArticles();
return (
<div className={styles['p-home']}>
{/* mv */}
<div className={styles['p-home__mv']}>
<div className={styles['p-home__title']}>
<h1>{config.title}</h1>
<span>{config.subtitle}</span>
</div>
</div>
{/* 事業紹介 */}
<div className="l-container">
<section className={styles['p-home__business']}>
<h2 className={styles['p-home__business__header']}>事業紹介</h2>
<ul className={styles['p-home__business__list']}>
{categories.map((category) => {
return (
<li className={styles['p-home__business__item']} key={category.id}>
<div>
<h3>{category.title}</h3>
<p>{category.overview}</p>
<Button color="gray" href={`/categories/${category.id}`}>詳細</Button>
</div>
<Image src={category.thumbnail.url} alt="" />
</li>
)
})
}
</ul>
<div className={styles['p-home__business__button']}>
<Button color='gray' href='/categories'>事業一覧</Button>
</div>
</section>
<section className={styles['p-home__company']}>
<ul className={styles['p-home__company__list']}>
<li className={styles['p-home__company__item']}>
<a href="/company">
<h3>Company</h3>
<span>会社概要</span>
</a>
</li>
<li className={styles['p-home__company__item']}>
<a href="/message">
<h3>代表挨拶</h3>
<span>CEO Message</span>
</a>
</li>
<li className={styles['p-home__company__item']}>
<a href="/history">
<h3>History</h3>
<span>沿革</span>
</a>
</li>
<li className={styles['p-home__company__item']}>
<a href="/member">
<h3>Member</h3>
<span>経営メンバー</span>
</a>
</li>
</ul>
</section>
<section className={styles['p-home__articles']}>
<ul className={styles['p-home__articles__list']}>
{/* max3つ */}
{articles.slice(0, 3).map((article) => {
return (
<li className={styles['p-home__articles__item']} key={article.id}>
<a href={`/articles/${article.id}`}>
<Image src={article.thumbnail.url} alt="" />
<div>
<time>{toDate(new Date(article.updatedAt))}</time>
<span>{article.category && article.category.title}</span>
</div>
<p>{article.title}</p>
</a>
</li>
)
})
}
</ul>
<div className={styles['p-home__articles__button']}>
<Button color='gray' href='/articles'>記事一覧</Button>
</div>
</section>
</div>
</div>
);
}
カテゴリ一覧 ↔︎ categories
import React from 'react'
import styles from "./Categories.module.scss"
import Button from '@/share/components/Button/Button'
import { getCategories } from '@/libs/client';
import Image from 'next/image';
async function categories() {
const { contents: categories } = await getCategories();
return (
<div className={styles['p-categories']}>
<div className="l-page__title">
<h2>事業詳細</h2>
</div>
<div className="l-container">
<ul className={styles['p-categories__list']}>
{categories.map((category) => {
return (
<li className={styles['p-categories__item']} key={category.id}>
<h3>{category.title}</h3>
<Image src={category.thumbnail.url} alt="" />
<Button color="gray" href={`/categories/${category.id}`}>詳細</Button>
<p>{category.overview}</p>
</li>
)
})}
</ul>
</div>
</div>
)
}
export default categories
カテゴリ詳細 ↔︎ categories
import React from 'react'
import styles from "./Category.module.scss"
import { getCategory } from '@/libs/client'
import parse from 'html-react-parser'
import Image from 'next/image'
async function category({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const category = await getCategory(id);
return (
<div className={styles['p-category']}>
<div className="l-page__title">
<h2>事業詳細</h2>
</div>
<div className="l-container">
<div className="p-single">
<h2 className='p-single__title'>{category.title}</h2>
<Image src={category.thumbnail.url} alt="" className='p-single__thumbnail' />
<p>{category.overview}</p>
<div className="p-single__body">
{parse(category.body)}
</div>
</div>
</div>
</div>
)
}
export default category
記事一覧 ↔︎ articles
import React from 'react'
import styles from './Articles.module.scss'
import { getArticles } from '@/libs/client';
import { toDate } from '@/libs/utils';
import Image from 'next/image';
async function articles() {
const { contents: articles } = await getArticles();
return (
<div className={styles['p-articles']}>
<div className="l-page__title">
<h2>記事一覧</h2>
</div>
<div className="l-container">
<ul className={styles['p-articles__list']}>
{articles.map((article) => {
return (
<li className={styles['p-articles__item']} key={article.id}>
<a href={`/articles/${article.id}`}>
<Image src={article.thumbnail.url} alt="" />
<div className={styles['p-articles__desc']}>
<div>
<time>{toDate(new Date(article.updatedAt))}</time>
<span>{article.category && article.category.title}</span>
</div>
<p>{article.title}</p>
</div>
</a>
</li>
)
})
}
</ul>
</div>
</div>
)
}
export default articles
記事詳細 ↔︎ articles
import React from 'react'
import styles from './Article.module.scss'
import { getArticle } from '@/libs/client';
import parse from 'html-react-parser'
import { toDate } from '@/libs/utils';
import Image from 'next/image';
async function article({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const article = await getArticle(id);
return (
<div className={styles['p-categories']}>
<div className="l-page__title">
<h2>記事詳細</h2>
</div>
<div className="l-container">
<div className="p-single">
<h2 className='p-single__title'>{article.title}</h2>
<div className="p-single__desc">
<time>{toDate(new Date(article.updatedAt))}</time>
<span>{article.category && article.category.title}</span>
</div>
<Image src={article.thumbnail.url} alt="" className='p-single__thumbnail' />
<div className="p-single__body">
{parse(article.body)}
</div>
</div>
</div>
</div>
)
}
export default article
Vercelで公開
ビルドする
Vercelで公開するには、ビルドを通さなければいけません。
$ npx next build
使用していない関数があったり、タグがタグになっていたりすると怒られるので、エラー内容をみつつ修正していきましょう。
githubにプッシュ
最後にもう一度最新状態のソースをgithubへプッシュします。
$ git add -A
$ git commit -m "comment"
$ git push origin [branch]
Vercelでは、開発環境と本番環境のブランチをgit上で分けておくと、それぞれの画面を確認できたりして便利です。
(今回はmainブランチをそのまま本番環境として使用します。)
Vercel
Vercelにアクセスしたら、Nextプロジェクトを選択します。
私の環境だけかわかりませんが、2つ目以降の要素が設定されないケースを確認しました。
もし環境情報周りでエラーを吐く場合は、プロジェクト管理画面から直接環境情報を記載し、再度デプロイすることをおすすめします。
完成
※まだエラーが多少あるので近々修正します。
補足
今回は、コーポレートサイトに必要な最低限の要素を3つのAPIで構築できるようにしました。
お気づきの方もいるかと思いますが、企業情報に関するページを作成していません。
企業情報のカテゴリを作成し、それに紐づく記事を作成 → 絞り込みで実装できますが、今回はあえて独立したページにしました。
独自のデザインやレイアウトが多くなるためです。
基本的な構築・スタイリングができれば独立ページの構築もそこまで難しくありません。
再利用性のあるものはAPIにし、それ以外はハードコーディングといったハイブリッド式のサイトもあっていいのではないでしょうか。
また、記事とカテゴリを関連づけているので、事業詳細ページの中で関連記事を一覧表示することも可能です。
機能山盛りでもいいかもしれないですが、今回はできるだけシンプルに構築してみました。
終わりに
今回、初めてのAdvent Calendarということでかなり力を入れて書きました。
恥ずかしながらエンジニアとしては2年弱ほどしか経験していないひよっこではありますが、アナタの参考になれば幸いです。
microCMSは真のヘッドレスCMSとしてこれからも広く利用されるのではないでしょうか。
少なくとも私は 管理画面UIのシンプルさ・カスタマイズのしやすさ・モダン開発環境への対応などの観点からとても好きなサービスです。
皆さんもぜひ利用してみてください。
それでは.
宣伝
現在、私はUNOTAMEという屋号で個人開発やお客様への業務支援を行っています。
エンジニアリングによるAツール開発だけでなく、I活用コンサルティングやSNS運用代行・広告運用代行なども承っております。
興味のある方はぜひ下記よりご連絡ください。
==========
UNOTAME 新山