今日の志
- nextjsをフロントエンド、WordPressをヘッダレスCMSとして構築
- 一般的な共有サーバーへアップロード
できごと
- Nextjsの構成
- ブログの基本機能実装
- 個別ページの作成
- componentsの作成
- Graphqlの設定
- Graphqlの読み込み
- データを取得
- 取得データの中身
- 表示方法
- SSGでの書き出し方法
- ページネーションの構築
前提
Node.js v21.6.1
next v14.2.4
react v18.3.1
コード
next.config.js
const path = require('path');
module.exports = {
sassOptions: {
includePaths: [path.join(__dirname, 'styles')],
},
output: 'export', // 静的エクスポートを指定
trailingSlash: true, // 必要に応じてスラッシュを追加
distDir: 'out' // ここでoutディレクトリのパスを指定
};
pages/_app.js
import { useEffect } from 'react';
import { useRouter } from 'next/router';
import Script from 'next/script';
import * as gtag from '../lib/gtag';
// ここは独自で作ったファイル
import Layout from '../layouts/Head'; // Layoutコンポーネントのインポート
import Header from '../layouts/Header'; // Layoutコンポーネントのインポート
import Footer from '../components/Footer.js'; // Footerコンポーネントのインポート
import '../styles/global.scss'; // グローバルCSSのインポート
// ここは独自で作ったファイル
function MyApp({ Component, pageProps }) {
const router = useRouter();
useEffect(() => {
const handleRouteChange = (url) => {
gtag.pageview(url);
};
router.events.on('routeChangeComplete', handleRouteChange);
return () => {
router.events.off('routeChangeComplete', handleRouteChange);
};
}, [router.events]);
return (
<>
<Layout>
<Header />
<main>
<Component {...pageProps} />
</main>
<Footer />
</Layout>
</>
);
}
export default MyApp;
pages/index.js
// pages/index.js
import Link from 'next/link';
import { gql } from '@apollo/client';
import client from '../lib/apollo';
// import styles from '../styles/pages/Home.module.scss';
// import Head from '../layouts/Head';
const defaultImage = '/default-logo.jpg';
export default function Home({ posts, categories }) {
return (
<Head title="ポートフォリオサイト">
<div className="bg-gray-100 lg:py-24 py-12">
<div className="container p-4 mx-auto">
<h1 className={styles.title}>Tec Blog</h1>
<div className="grid items-start lg:grid-cols-6 lg:gap-10 relative">
<div className="lg:col-span-5">
<ul>
{posts.map((post) => (
<li key={post.slug} className={styles.post}>
<h2 className={styles.posttitle}>
<Link href={`/posts/${post.slug}`} legacyBehavior>
<a>{post.title}</a>
</Link>
</h2>
<div className={styles.postimg}>
<img
src={post.featuredImage && post.featuredImage.node ? post.featuredImage.node.mediaItemUrl : defaultImage}
alt={post.title}
style={{ width: '300px' }}
/>
</div>
<div className={styles.posttext} dangerouslySetInnerHTML={{ __html: post.excerpt }} />
</li>
))}
</ul>
</div>
<div className="lg:col-span-1 sticky top-0">
<h2 className={`${styles.subtitle}`}>Categories</h2>
<ul className="btns">
{categories.map((category) => (
<li key={category.slug} className="btn">
<Link href={`/${category.slug}`} legacyBehavior>
<a className={styles.categoryLink}>{category.name}</a>
</Link>
</li>
))}
</ul>
</div>
</div>
<div className="mt-12">
<div className="btns-center">
<div className="btn-min">
<Link href="/archive/1/">
ブログ一覧
</Link>
</div>
</div>
</div>
</div>
</div>
</Head>
);
}
export async function getStaticProps() {
const { data } = await client.query({
query: gql`
query HomePageData {
posts {
nodes {
title
excerpt
slug
featuredImage {
node {
mediaItemUrl
}
}
}
}
categories {
nodes {
name
slug
}
}
}
`
});
return {
props: {
posts: data.posts.nodes,
categories: data.categories.nodes,
},
};
}
pages/archive/[page].js
import { useEffect, useState } from 'react';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { gql } from '@apollo/client';
import client from '../../lib/apollo';
import PostItem from '../../components/PostItem';
import Pagination from '../../components/Pagination';
// import styles from '../../styles/layouts/Archive.module.scss';
// import Head from '../../layouts/Head';
const POSTS_PER_PAGE = 10; // 表示件数
export default function ArchivePage({ posts, pageInfo, currentPage, totalPosts, categories }) {
const router = useRouter();
const handlePageChange = (page) => {
router.push(`/archive/${page}`);
};
if (router.isFallback) {
return Loading...
;
}
return (
<Head title={`Blog - Page ${currentPage}`}>
<div className="px-8 py-12 lg:p-24 bg-gray-100 mx-auto">
<div className="container mx-auto">
<div className="grid items-start lg:grid-cols-6 lg:gap-10 relative">
<div className="lg:col-span-5">
<div className={styles.container}>
<h1> className={styles.title}>Archives - Page {currentPage}</h1>
<ul> className={styles.postList}>
{posts.map((post) => (
<PostItem key={post.slug} post={post} />
))}
</ul>
<Pagination
currentPage={currentPage}
totalPages={Math.ceil(totalPosts / POSTS_PER_PAGE)}
onPageChange={handlePageChange}
hasNextPage={pageInfo.hasNextPage}
/>
</div>
</div>
<div className="lg:col-span-1 sticky top-0">
<h2> className={`${styles.subtitle}`}>Categories</h2>
<ul> className="btns">
{categories.map((cat) => (
<li key={cat.slug} className="btn">
<Link href={`/${cat.slug}`} passHref>
<span> className={styles.categoryLink}>{cat.name}</span>
</Link>
</li>
))}
</ul>
</div>
</div>
</div>
</div>
</Head>
);
}
export async function getStaticPaths() {
const { data } = await client.query({
query: gql`
query GetTotalPosts($first: Int!) {
posts(first: $first) {
pageInfo {
endCursor
hasNextPage
}
}
}
`,
variables: {
first: POSTS_PER_PAGE
}
});
let hasNextPage = data.posts.pageInfo.hasNextPage;
let endCursor = data.posts.pageInfo.endCursor;
let page = 1;
const paths = [{ params: { page: '1' } }];
while (hasNextPage) {
page++;
const { data: nextData } = await client.query({
query: gql`
query GetNextPosts($first: Int!, $after: String) {
posts(first: $first, after: $after) {
pageInfo {
endCursor
hasNextPage
}
}
}
`,
variables: {
first: POSTS_PER_PAGE,
after: endCursor
}
});
paths.push({ params: { page: page.toString() } });
hasNextPage = nextData.posts.pageInfo.hasNextPage;
endCursor = nextData.posts.pageInfo.endCursor;
}
return { paths, fallback: false };
}
export async function getStaticProps({ params }) {
const page = parseInt(params.page, 10);
const { data } = await client.query({
query: gql`
query GetPosts($first: Int!, $after: String) {
posts(first: $first, after: $after) {
nodes {
title
excerpt
slug
featuredImage {
node {
mediaItemUrl
}
}
categories {
nodes {
name
slug
}
}
}
pageInfo {
endCursor
hasNextPage
hasPreviousPage
}
}
categories {
nodes {
name
slug
}
}
}
`,
variables: {
first: POSTS_PER_PAGE,
after: page > 1 ? btoa(`arrayconnection:${(page - 1) * POSTS_PER_PAGE - 1}`) : null,
},
});
return {
props: {
posts: data.posts.nodes,
pageInfo: data.posts.pageInfo,
currentPage: page,
categories: data.categories.nodes,
},
};
}
components/PostItem.js
import Link from 'next/link';
// import styles from '../styles/layouts/Category.module.scss';
const PostItem = ({ post }) => {
return (
<li className={styles.post}>
<h2 className={styles.posttitle}>
<Link href={`/posts/${post.slug}`} legacyBehavior>
<a>{post.title}</a>
</Link>
</h2>
<div className={styles.postimg}>
<img
src={post.featuredImage && post.featuredImage.node ? post.featuredImage.node.mediaItemUrl : '/default-logo.jpg'}
alt={post.title}
style={{ width: '300px' }}
/>
</div>
<div className={styles.posttext} dangerouslySetInnerHTML={{ __html: post.excerpt }} />
</li>
);
};
export default PostItem;
components/Pagination.js
// components/Pagination.js
import { useEffect } from 'react';
import Link from 'next/link';
import { useRouter } from 'next/router';
// import styles from '../styles/components/Pagination.module.scss';
const Pagination = ({ currentPage, totalPages, onPageChange, hasNextPage }) => {
const router = useRouter();
useEffect(() => {
const handleRouteChange = (url) => {
window.gtag('config', 'GA_TRACKING_ID', {
page_path: url,
});
};
router.events.on('routeChangeComplete', handleRouteChange);
return () => {
router.events.off('routeChangeComplete', handleRouteChange);
};
}, [router.events]);
const handlePageClick = (page) => {
onPageChange(page);
window.gtag('event', 'pagination_click', {
event_category: 'Pagination',
event_label: `Page ${page}`,
});
};
return (
<div className={styles.pagination}>
{currentPage > 1 && (
<Link href={`/archive/${currentPage - 1}`}>
<span className={styles.pageLink} onClick={() => handlePageClick(currentPage - 1)}>
Previous
</span>
</Link>
)}
{Array.from({ length: totalPages }, (_, i) => {
const pageNumber = i + 1;
return (
<Link key={pageNumber} href={`/archive/${pageNumber}`}>
<span
className={`${styles.pageLink} ${pageNumber === currentPage ? styles.active : ''}`}
onClick={() => handlePageClick(pageNumber)}
>
{pageNumber}
</span>
</Link>
);
})}
{hasNextPage && (
<Link href={`/archive/${currentPage + 1}`}>
<span className={styles.pageLink} onClick={() => handlePageClick(currentPage + 1)}>
Next
</span>
</Link>
)}
</div>
);
};
export default Pagination;