ウェブサイト検索

GhostとNext.jsでブログを構築する方法


著者は、Write for Donationsプログラムの一環として、寄付を受けるためにMozilla Foundationを選択しました。

紹介

ブログは自分の知識、経験、またはニュースを他の人と共有するための媒体です。Ghostは、現代的なブログや出版物を構築し運営するためのオープンソースプラットフォームです。

Ghostはフロントエンド用のテンプレートを提供していますが、デザインやカスタム機能を追加する柔軟性は限られています。一方、Next.jsはプロダクション最適化、パフォーマンス、SEOなどの問題に対する解決策を提供するReactフレームワークです。Next.jsはReactを使用してアプリケーションを構築する際の開発者体験において優位性をもたらします。Next.jsをGhostと組み合わせることで、より優れたパフォーマンスとSEOを持つ静的生成されたブログを構築できます。また、デザインをカスタマイズしたり、望む機能を追加したりすることも可能です。

このチュートリアルでは、Ghostを使って記事を管理し、Next.jsを用いてブログのフロントエンドを構築します。このアプローチにより、ブログのコンテンツ、デザイン、機能を自分で制御できます。UbuntuサーバーにGhostを自己ホスティングし、DigitalOcean MarketplaceのGhost One-Click Dropletを使用してデプロイできます。

前提条件

このチュートリアルを完了するためには、次のものが必要です:

  • DigitalOceanのアカウント。お持ちでない場合は、新しいアカウントにサインアップしてください。
  • メモリが最低1GBのUbuntuサーバーにGhostがインストールされています。DigitalOceanでGhost One-Click Dropletをデプロイするか、手動セットアップのためにGhostのドキュメントに従うことができます。
  • あなたのGhostブログにポイントするための登録されたドメイン名。一般的なドメインレジストラからDigitalOceanのネームサーバーにポイントする方法に関するガイドで、ドメインの設定についての指示を見つけることができます。
  • Node.jsのローカル開発環境。Node.jsのインストール方法とローカル開発環境の作成方法に従ってください。
  • あなたのローカルマシンでのNext.jsプロジェクト。Getting Startedガイドのライブラリのセットアップステップを通じてNext.jsプロジェクトを作成できます。create-next-appを使用して、設定のデフォルトを受け入れます。

注意: Next.jsプロジェクトはTypeScriptまたはJavaScriptのどちらでも作成できます。このチュートリアルではJavaScriptを使用していますが、どちらも機能します。

  • Tailwindがローカルマシンにインストールされ、Next.js用に設定されました。Next.jsをインストールした後は、Next.jsと一緒にTailwind CSSをインストールするガイドの指示に従ってください。
  • Visual Studio Codeのようなテキストエディタ。このチュートリアルではnanoを使用します。
  • Reactに対する親しみは、How To Code in React.jsシリーズを通じて得ることができます。

ステップ1 — Ghostでの投稿の公開

このステップでは、Ghostでアカウントを作成し、今後のステップで表示するコンテンツがあるように、サンプル記事をいくつか公開します。

Ghostがインストールされ、設定されると、ブログを管理するための管理者アカウントが必要になります。YOUR_DOMAIN/ghostに移動してください。ここで、YOUR_DOMAINはGhostインストールプロセス中に入力したURLです。

あなたはGhostへようこそのページを見ることになります。そこでアカウントを作成するよう招待されます。あなたのサイトのタイトル、名前、メールアドレス、そしてパスワードを入力してください。それからアカウントを作成して出版を開始するを押します。

左のサイドバーの最初に何をしたいですか?の下で、最初の投稿を書くオプションを選択します。ブログ投稿のタイトルと本文を入力し、右上の公開をクリックします。続ける、最終確認を押し、その後今すぐ投稿を公開ボタンをクリックします。

YOUR_DOMAIN/ghostにナビゲートして、ダッシュボードを表示してください。左側のサイドバーで、サイトを表示を選択して、サイトのライブプレビューを確認します:

このチュートリアルの後半では、あなたのウェブサイトにいくつかのブログ投稿を表示します。ダッシュボードから、左側のサイドバーで投稿を選択し、さらに2つのブログ投稿を作成します。それらにユニークな名前を付けて、後で識別できるようにします。

このステップでは、Ghostで管理者ページを設定し、ブログ投稿を公開します。あなたのブログは現在、Ghostによって提供されているデフォルトのテンプレートを使用しています。次のステップでは、よりデザインの柔軟性を持たせるNext.jsプロジェクトを作成します。Ghostをコンテンツ管理システム(CMS)として使用し、Next.jsを使用してフロントエンドを構築します。

ステップ2 — Next.jsプロジェクトの作成

あなたは今、ブログのためにGhostを用意しました。このステップでは、Next.jsを使用してブログのフロントエンドを作成し、Tailwind CSSを使用してスタイルを追加します。

好みのコードエディタで作成したNext.jsプロジェクトを開いてください。pages/index.jsファイルを開きます:

  1. nano pages/index.js

ファイル内で既存のコードを削除し、next/head から Head コンポーネントをインポートし、React コンポーネントを作成するために以下を追加してください。

import Head from 'next/head';

export default function Home() {
	return (
	);
}

Head コンポーネントを使用すると、HTML内で使用する <title> や <meta> といったタグを追加できます。

Headコンポーネントを拡張するために、returnステートメントに<div>タグを含めて追加します。<div>タグ内に、<title>と<meta>タグを取る<Head>タグを追加してください。

...
return (
	<div>
	     <Head>
	            <title>My First Blog</title>
	            <meta name="description" content="My personal blog created with Next.js and Ghost" />
            </Head>
	</div>
);
...

<title> タグはあなたのウェブページのタイトルを My First Blog として設定します。 <meta> タグは name 属性が "description" に設定され、content 属性が "My personal blog created with Next.js and Ghost" に設定されています。このタイトルと説明はあなたのウェブサイトのプレビューに表示されます。

タイトルと説明を設定したので、記事のリストを表示します。このコードスニペットは、当面の間モックデータを使用します。次のセクションでは、Ghostからブログ投稿を取得します。<Head>タグの下に、<li>タグを使用して記事のリストを追加してください。

...
<div>
	<Head>
	...
	</Head>
	<main className="container mx-auto py-10">
		<h1 className="text-center text-3xl">My Personal Blog</h1>
		<div className="flex justify-center mt-10 ">
			<ul className="text-xl">
				<li>How to build and deploy your blog on DigitalOcean</li>
				<li>How to style a Next.js website</li>
				<li>How to cross-post your articles automatically</li>
			</ul>
		</div>
	</main>
</div>
...

あなたは今、ヘッダーと <div> タグが含まれた <main> タグを追加しました。 <div> タグには、いくつかの模擬記事タイトルの無秩序なリストが含まれています。 <main>、<h1>、<div>、および <ul> タグには、それぞれ className 属性が含まれています。これらは Tailwind スタイルクラスを含んでいます。

index.jsファイルは次のようになっているはずです:

import Head from 'next/head';

export default function Home() {
	return (
		<div>
			<Head>
				<title>My First Blogp</title>
				<meta
					name="description"
					content="My personal blog created with Next.js and Ghost"
				/>
			</Head>
			<main className="container mx-auto py-10">
				<h1 className="text-center text-3xl">My Personal Blog</h1>
				<div className="flex justify-center mt-10 ">
					<ul className="text-xl">
						<li>How to build and deploy your blog on DigitalOcean</li>
						<li>How to style a Next.js website</li>
						<li>How to cross-post your articles automatically</li>
					</ul>
				</div>
			</main>
		</div>
	);
}

ファイルを保存して閉じてください。

ローカルでウェブサーバーを起動します。Yarnを使用している場合は、次のコマンドを実行してください:

  1. yarn dev

もし npm を使用している場合は、次のコマンドを実行してください:

  1. npm run dev

ブラウザで https://localhost:3000 に移動すると、モックデータからのブログ記事のリストが表示されるはずです。あなたのホームページは次のように表示されるでしょう:

このステップでは、ブログのホームページを作成し、モック記事のリストを追加しました。次のステップでは、Ghostからブログ投稿を取得します。

ステップ3 — Ghostからすべてのブログ投稿を取得する

このステップでは、Ghostで作成したブログ投稿を取得し、ブラウザに表示します。

Ghostから記事を取得するには、まずGhost Content API用のJavaScriptライブラリをインストールする必要があります。キーボードショートカットCTRL+Cを使ってサーバーを停止します。次のコマンドをターミナルで実行してライブラリをインストールしてください。

Yarnを使用している場合は、次のコマンドを実行してください:

  1. yarn add @tryghost/content-api

もし npm を使用している場合は、次のコマンドを実行してください:

  1. npm i @tryghost/content-api

ライブラリが正常にインストールされたので、今度はブログ投稿を取得するロジックを保存するファイルを作成します。pagesディレクトリ内に、utilsという名前の新しいフォルダーを作成してください。

  1. mkdir utils

そのフォルダー内に ghost.js という名前の新しいファイルを作成します:

  1. nano pages/utils/ghost.js

ghost.js ファイルで、Ghost Content API ライブラリから GhostContentAPI モジュールをインポートします。新しい GhostContentAPI オブジェクトを初期化し、その値を定数変数 api に保存します。ホスト、API キー、およびバージョンの値を渡す必要があります。コードは以下のようになります:

import GhostContentAPI from "@tryghost/content-api";

const api = new GhostContentAPI({
	url: `YOUR_URL`,
	key: `YOUR_API_KEY`,
	version: 'v5.0'
});

YOUR_URLの値は、Ghostをインストールしたときに設定したドメイン名で、プロトコルを含み、最も可能性が高いのはhttps://です。

Ghost APIキーを見つけるには、以下の手順に従ってください:

  1. YOUR_DOMAIN/ghostにナビゲートします(YOUR_DOMAINは設定したURLです)そして、管理者の資格情報でログインします。
  2. 左側のサイドバーの下にあるギアアイコンを押して、設定ページにアクセスしてください。
  3. 高度なカテゴリーで、左のサイドバーにある統合をクリックします。統合ページが表示され、可能な統合のリストと、下部にカスタム統合が表示されます。
  4. + カスタム統合を追加を押します。ポップアップが表示され、統合の名前を入力するよう求められます。
  5. 名前フィールドに統合の名前を入力し、作成をクリックしてください。これにより、カスタム統合を設定するページに移動します。
  6. コンテンツ API キー(管理者 API キーではなく)をコピーしてください。
  7. 保存を押してカスタム統合を保存します。

ghost.jsファイルで、YOUR_API_KEYをコピーしたAPIキーに置き換えてください。

GhostContentAPIを初期化したので、Ghostインストールからすべての記事を取得する非同期関数を書くことになります。この関数は、タグに関係なくブログ投稿を取得します。以下のコードをあなたのghost.jsファイルにコピー&ペーストしてください:

...
export async function getPosts() {
  return await api.posts
    .browse({
        include:"tags",
        limit: "all"
    })
    .catch(err => {
      console.error(err);
    });
}

約束が解決された後、getPosts() 非同期関数はブログ投稿を返します。それは GhostContentAPI の posts.browse() メソッドを使用し、include と limit パラメータを取ります。あなたは include の値を tags に設定し、コンテンツと共にタグを取得します。limit の値は all に設定されており、すべてのブログ投稿を取得します。エラーが発生した場合は、それがブラウザコンソールに記録されます。

この時点で、api/ghost.jsファイルには次のコードが含まれています:

import GhostContentAPI from '@tryghost/content-api';

const api = new GhostContentAPI({
	url: `YOUR_URL`,
	key: `YOUR_API_KEY`,
	version: 'v5.0',
});

export async function getPosts() {
	return await api.posts
		.browse({
			include: 'tags',
			limit: 'all',
		})
		.catch((err) => {
			console.error(err);
		});
}

ファイルを保存して閉じてください。

投稿のリストを表示するには、index.jsファイルを開いてください。Headインポートの上にgetPosts関数をインポートするために、以下の強調表示された行を追加してください:

import { getPosts } from './utils/ghost';
import Head from 'next/head';

…

これから、Next.jsがビルド時にページをプリレンダリングできるようにする非同期関数(getStaticProps())を作成します。この関数は、静的生成を利用したい場合に便利です。

Homeコンポーネントの後に、非同期関数getStaticProps()を作成し、関数本体内でgetPosts()メソッドを呼び出します。getPosts()メソッドから値propsを返します。コードは以下のようになります:

...
export async function getStaticProps() {
	const posts = await getPosts();
	return { props: { posts } };
}

ファイルを保存してください。

あなたの getStaticProps() メソッドが定義されたので、npm run dev または Yarn を使用している場合は yarn dev を使ってサーバーを再起動してください。

ブラウザのページには静的データがまだ表示されています。ページはGhostから取得したデータを表示していません。これは、値を取得しているが、それをレンダリングしていないためです。

あなたは、index.jsにいくつかの変更を加えて、HomeコンポーネントがGhostから取得した値を使用できるようにします。

サーバーを停止するには CTRL-C を押し、その後 index.js を編集するために開いてください:

  1. nano pages/index.js

index.jsに以下のハイライトされた変更を加えてください:

export default function Home({posts}) {
	return (
		<div>
			<Head>
				<title>My First Blog</title>
				<meta
					name="description"
					content="My personal blog created with Next.js and Ghost"
				/>
			</Head>
			<main className="container mx-auto py-10">
				<h1 className="text-center text-3xl">My Personal Blog</h1>
				<div className="flex justify-center mt-10 ">
					<ul className="text-xl">
						{posts.map((post) => (
							<li key={post.title}>{post.title}</li>
						))}
					</ul>
				</div>
			</main>
		</div>
	);
}

このコードでは、まずpostsを分解し、Homeコンポーネントにパラメータとして渡します。それから、モックに使用した静的コンテンツを含む<li>タグを、ブログ投稿のタイトルを取得する関数に置き換えます。

あなたは、propsから取得したpostsコレクションを反復処理するためにarray.map()メソッドを使用して、各投稿のtitleを取得します。最後に、投稿のタイトルを含む<li>タグを返します。

ファイルを保存して閉じてください。Yarnを使用している場合は、npm run devまたはyarn devを使用してサーバーを再起動します。

localhost:3000 に戻ります。これで、あなたのブログは Ghost から記事のリストを表示します。

あなたの index.js ファイルのコードは次のようになります。

import { getPosts } from './utils/ghost';
import Head from 'next/head';

export default function Home({ posts }) {
	return (
		<div>
			<Head>
				<title>My First Blog</title>
				<meta
					name="description"
					content="My personal blog created with Next.js and Ghost"
				/>
			</Head>
			<main className="container mx-auto py-10">
				<h1 className="text-center text-3xl">My Personal Blog</h1>
				<div className="flex justify-center mt-10 ">
					<ul className="text-xl">
						{posts.map((post) => (
							<li key={post.title}>{post.title}</li>
						))}
					</ul>
				</div>
			</main>
		</div>
	);
}

export async function getStaticProps() {
	const posts = await getPosts();
	return { props: { posts } };
}

あなたのホームページは以下のように表示されます:

あなたのブログは現在、CMSから投稿タイトルを取得して表示しました。しかし、個々の投稿はまだ表示されていません。次のセクションでは、動的ルートを作成し、投稿の内容をレンダリングします。

ステップ4 — 各投稿のレンダリング

このステップでは、Ghostから各ブログ投稿のコンテンツを取得するコードを書き、動的ルートを作成し、投稿タイトルをホームページのリンクとして追加します。

Next.jsでは、同じレイアウトでページをレンダリングするための動的ルートを作成できます。動的ルートは、コンポーネントを再利用することでコードの冗長性を減らすのに役立ちます。動的ルートを作成すると、すべての投稿がレンダリングのために同じファイルを使用します。各投稿ごとにページを作成する必要はありません。

動的ルートを作成し、個々の投稿をレンダリングするには、次のことを行う必要があります:

  1. ブログ投稿の内容を取得する関数を書いてください。
  2. 動的ルートを作成する。
  3. アイテムのリストにブログリンクを追加してください。

ghost.js ファイルでは、すべてのブログ投稿のリストを取得するために getPosts() 関数を書きました。次に、スラッグに基づいて投稿のコンテンツを取得する getSinglePost() 関数を書きます。

Ghostは、タイトルを使用して記事のスラッグを自動的に生成します。たとえば、記事のタイトルが「My First Post」の場合、Ghostはmy-first-postをスラッグとして生成します。このスラッグは記事を識別するのに役立ち、あなたのドメインURLに追加してコンテンツを表示することができます。

getSinglePost() 関数は postSlug をパラメータとして受け取り、対応するブログ投稿の内容を返します。

サーバーがまだ動いている場合は停止し、その後pages/utils/ghost.jsを編集のために開いてください。

ghost.jsファイルのgetPosts()関数の下に、async関数getSinglePost()を追加してエクスポートしてください。

...
export async function getSinglePost(postSlug) {
    return await api.posts
      .read({
        slug: postSlug
      })
      .catch(err => {
        console.error(err);
      });
  }

getSinglePost() 関数は GhostContentAPI の posts.read() メソッドを使用し、posts.read() メソッドに postSlug パラメーターを渡します。

最終的な /utils/ghost.js ファイルには以下のコードが含まれています:

import GhostContentAPI from '@tryghost/content-api';

const api = new GhostContentAPI({
	url: `YOUR_URL`,
	key: `YOUR_API_KEY`,
	version: 'v5.0',
});

export async function getPosts() {
	return await api.posts
		.browse({
			include: 'tags',
			limit: 'all',
		})
		.catch((err) => {
			console.error(err);
		});
}

export async function getSinglePost(postSlug) {
    return await api.posts
      .read({
        slug: postSlug
      })
      .catch(err => {
        console.error(err);
      });
  }

ファイルを保存して閉じてください。

Next.jsでは、ファイル名に角括弧を追加することで([param])、動的ルートを作成できます。例えば、/post/[slug].jsは動的ルートを作成します。

pagesディレクトリ内に、/post/[slug].jsという新しいファイルを作成します:

nano pages/post/\[slug\].js

注: nano コマンドは、名前にブラケットを含むファイルを作成するために必要な、バックスラッシュ (\) でエスケープされたブラケットを表示します。

ファイル名 [slug].js は、パス /post/ の後に続く任意の文字列と一致します。Next.js はビルド時にあなたのすべての投稿のページを作成します。

あなたの/post/[slug].jsファイルで、../utils/ghost.jsファイルからgetPosts()とgetSinglePost()の両方の関数をインポートしてください。

/post/[slug].jsファイルには、投稿のテンプレートも含まれています。次のコードを/post/[slug].jsファイルに追加してください:

import { getPosts, getSinglePost } from '../utils/ghost';

export default function PostTemplate(props) {
	const { post } = props;
	const { title, html, feature_image } = post;
	return (
		<main className="container mx-auto py-10">
			<h1 className="text-center text-3xl">{title}</h1>
			<article
				className="mt-10 leading-7 text-justify"
				dangerouslySetInnerHTML={{ __html: html }}
			/>
		</main>
	);
}

export const getStaticProps = async ({ params }) => {
	const post = await getSinglePost(params.slug);
	return {
		props: { post },
	};
};

PostTemplate()関数は、関数型コンポーネントを作成します。この関数では、propsからpostオブジェクトを抽出し、postオブジェクトからtitle、html、およびfeature_imageを引き出します。このコンポーネントは、ページを作成するために使用されるHTMLを返します。

Ghost APIはブログコンテンツをHTMLで返すため、<article>タグにはpostオブジェクトから抽出したHTML値を持つdangerouslySetInnerHTML属性が含まれています。dangerouslySetInnerHTML属性は、ReactがブラウザのDOMでinnerHTMLを使用する代替手段です。HTMLが信頼できないソースから来ている場合は、dangerouslySetInnerHTML属性に渡す前にHTMLをサニタイズする必要があります。

getStaticProps() 非同期関数は、スラグに対応するブログ投稿のコンテンツを取得します。

コンテンツをURLに正しくマッピングするには、Next.jsがパスの値(あなたの場合はslug)を知っている必要があります。これを行うには、getStaticPaths()関数を使用できます。getStaticProps()関数と似て、getStaticPaths()と呼ばれる非同期関数を作成し、スラッグのリストを返します。次の関数を[slug].jsファイルの最後に追加してください:

...
export const getStaticPaths = async () => {
	const allPosts = await getPosts();
	return {
		paths: allPosts.map(({ slug }) => {
			return {
				params: { slug },
			};
		}),
		fallback: false,
	};
};

注意: fallbackの値をfalseに設定することで、getStaticPaths()によって返されないパスは404ページになります。

/post/[slug].jsファイルは次のようになります:

import { getPosts, getSinglePost } from '../utils/ghost';

export default function PostTemplate(props) {
	const { post } = props;
	const { title, html, feature_image } = post;
	return (
		<main className="container mx-auto py-10">
			<h1 className="text-center text-3xl">{title}</h1>
			<article
				className="mt-10 leading-7 text-justify"
				dangerouslySetInnerHTML={{ __html: html }}
			/>
		</main>
	);
}

export const getStaticProps = async ({ params }) => {
	const post = await getSinglePost(params.slug);
	return {
		props: { post },
	};
};

export const getStaticPaths = async () => {
	const allPosts = await getPosts();
	return {
		paths: allPosts.map(({ slug }) => {
			return {
				params: { slug },
			};
		}),
		fallback: false,
	};
};

ファイルを保存して閉じてください。Yarnを使用している場合は、npm run devまたはyarn devを使用してサーバーを再起動します。

localhost:3000/post/スラグに移動し、スラグをあなたのブログ投稿のいずれかに対応するスラグで置き換えます。これで、ブラウザにレンダリングされた<^<スラグ<^>に対応する投稿のブログコンテンツにアクセスできます。

今、投稿にアクセスするにはURLを手動で入力する必要があり、不便です。これを修正するには、ホームページから投稿に移動できるようにアイテムのリストにハイパーリンクを追加することができます。

サーバーがまだ実行中であれば停止し、pages/index.js を編集のために再度開いてください。

index.jsのハイライトされた部分を次のように編集し、インポートとLinkコンポーネントを追加してください:

import { getPosts } from './utils/ghost';
import Head from 'next/head';
import Link from 'next/link';

export default function Home(props) {
	return (
		<div>
			<Head>
				<title>My First Blog</title>
				<meta
					name="description"
					content="My personal blog created with Next.js and Ghost"
				/>
			</Head>
			<main className="container mx-auto py-10">
				<h1 className="text-center text-3xl">My Personal Blog</h1>
				<div className="flex justify-center mt-10 ">
				<ul className="text-xl">
					{props.posts.map((post) => (
						<li key={post.title}>
							<Link href={`post/${post.slug}`}>{post.title}</Link>
						</li>
					))}
				</ul>
				</div>
			</main>
		</div>
	);
}

export async function getStaticProps() {
	const posts = await getPosts();
	return { props: { posts } };
}

Next.jsは、クライアントサイドのルーティングを可能にするLinkコンポーネントを提供します。あなたのindex.jsファイルでは、next/linkからLinkコンポーネントをインポートします。<li>タグ内で、投稿タイトルをLinkコンポーネントにネストします。HTMLのアンカータグのように、Linkタグもhref属性を取ります。スラッグをhref属性の値として渡します。

ファイルを保存し、npm run dev または yarn dev コマンドを実行して開発サーバーを再起動してください。

ブラウザでlocalhost:3000にナビゲートします。次に、任意の投稿タイトルをクリックすると、対応するブログ投稿に移動します。あなたのブログページは以下のように表示されます:

あなたの投稿ページには、ステップ1で提供したタイトルと本文の内容が表示されます。ここに示されているサンプルは、「DigitalOceanでブログを作成してデプロイする方法」というタイトルで、このチュートリアルの導入部分の内容です。

このステップでは、Ghost CMSから個々の投稿内容を取得する機能を追加し、それらを個別のページとしてレンダリングし、インデックスページから各投稿へのリンクを作成しました。これで、Ghostから記事を取得する完全なブログが作成されました。

結論

この記事では、GhostをCMSとしてDigitalOceanのDroplet上にデプロイしました。Next.jsを使用して独自の静的生成サイトを作成し、Ghost CMSを接続してNext.jsのウェブサイトにコンテンツを提供しました。静的生成されたウェブサイトであるため、クライアントサイドでのJavaScriptの量が減り、パフォーマンスとSEOが向上します。また、TailwindCSSを使用してブログにカスタムデザインを追加しました。

現在、あなたのブログはあなたのパソコン上でローカルに実行されています。次のステップとして、他の人が閲覧できるようにデジタルオーシャンのアプリプラットフォームにデプロイすることができます。始めるには、アプリプラットフォームにNext.jsアプリをデプロイするチュートリアルに従ってください。

ブログにさらに多くの機能を追加することもできます。たとえば、Algoliaを使用して検索機能を実装したり、n8nを使って投稿の公開プロセスを自動化したり、タグに基づいて記事をリストする機能を追加したりできます。

関連記事: