Nextjsを共有サーバーにアップする書き出し方(SSG)

今日の志

  1. nextjsをフロントエンド、WordPressをヘッダレスCMSとして構築
  2. 一般的な共有サーバーへアップロード

できごと

  • 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;