AIコーディングツール「Cline」で技術ブログをリメイク!開発効率と可能性を探る
Table of Contents
はじめに
2021年9月、Next.jsの学習を兼ねて技術ブログを作成しました。
当時はコードブロックの実装やGoogle Analyticsの導入など、基本的な機能の実装に注力し、「今後どんどん改善していきたい」という意気込みを持って開発を進めていました。
しかし、その後は忙しさにかまけてブログ自体の開発は停滞し、やがて記事の更新も途絶えてしまいました。
そんな中、VSCode拡張の「Cline」が注目を集めています。
これはLLMを活用してコーディングを支援するツールです。
一週間の休暇を取得する機会があったため、この期間を利用してClineを使った技術ブログのリメイクに挑戦することにしました。
Clineの魅力
Clineについては、既に多くの先人の方々が詳しく解説する記事や動画を公開しています。
そのため、ここでは私がClineを使ってみて感じた魅力について述べたいと思います。
Clineは、LLMを活用した強力なAIコーディングツールです。
その最大の特徴は、自然言語での対話を通じてコーディングを進められる点です。
まるで熟練エンジニアとペアプログラミングをしているような感覚で、スムーズに開発を進めることができます。
具体的には、Clineでは以下のようなことが可能です。
- プロジェクト内のファイル構造の把握: プロジェクト全体の構成を理解し、必要なファイルに素早くアクセスできます。
- ファイルの閲覧: ファイルの内容を確認し、修正箇所を特定できます。
- コード・ドキュメントの生成: 指示に基づき、コードやドキュメントを自動生成します。
- ターミナルでのコマンド実行と結果の閲覧: コマンドの実行とその結果確認をCline内で行えます。
- ヘッドレスブラウザでのWebページの閲覧: Webページを閲覧し、情報を取得できます (作成したページの確認も可)。
- 多彩なLLMモデルの利用: 多様なLLMモデルを切り替えて利用できます。
これらの機能を自然言語で指示できるため、コーディングの効率が格段に向上します。
なお、Clineにはいくつかの派生ツールが存在していますが、私が今回利用したのは「CoolCline」です。
私は他のツールは使っていないので違いはあまり理解していませんが、「Roo Code」や「Bao-Cline」などの機能をまとめたもののようです。
Clineでの実際のやり取りの例
Clineを使ったことのない方にもイメージしやすいよう、実際のやり取りの例を挙げてみます。
赤枠が私の入力、青枠がClineの返答です。
サイトマップを自動生成する機能を実装しようと思っています。
Next.jsを使っているので、以下の機能が使えそうだと思うのですが、こちらを使って実装する方法を検討して下さい。
可能な限り運用時のコストが低い、メンテナンス性の高い方法を実装したいです。
(Next.jsのgenerateSitemaps関数の公式ドキュメントを貼る)
このタスクでは、Next.jsのサイトマップ自動生成機能を実装する必要があります。以下の点を考慮して実装方法を検討します:
- プロジェクト構造の分析:
- ブログ記事(posts/)
- 固定ページ(about, works, tags等)
- 動的ルート(posts/[id], tags/[tag])
- 必要なファイル:
- app/sitemap.ts(メインのサイトマップ)
- app/lib/constants.ts(BASE_URLなどの定数)
- 実装方針:
- 固定ページと動的ページ(ブログ記事、タグページ)を分けて生成
- メンテナンス性を考慮し、定数やヘルパー関数を活用
- ブログ記事の更新日時はファイルのメタデータを利用
まず、constants.tsファイルを作成して基本設定を定義します。
(その後、constants.tsファイルのコードを生成して提案)
サイトの情報はconfig/data.jsonにまとめてあるので、そちらをimportして使うことはできますか?
config/data.jsonの内容を確認して、サイト情報を取得する必要があります。
(data.jsonの内容を確認)
config/data.jsonからサイト情報を取得できることが確認できました。これを使用して、constants.tsとsitemap.tsを実装し直します。
(constants.tsファイルのコードを生成して提案)
constants.tsの作成が完了したので、次にblog.tsの内容を確認してブログ記事の取得方法を把握します。
(blog.tsの内容を確認)
blog.tsの内容を確認できました。getAllPosts関数を使用して全ての公開済み記事を取得し、サイトマップを生成できます。また、getAllTags関数を使用してタグページのURLも生成できます。これらの情報を使用してsitemap.tsを実装します。
(sitemap.tsファイルのコードを生成して提案)
サイトマップの自動生成機能を実装しました。
実装内容:
- src/app/lib/constants.ts
- config/data.jsonからサイトURLを取得
- サイトマップの優先度と更新頻度の設定を定義
- src/app/sitemap.ts
- 固定ページ(ホーム、about、works等)のURL生成
- getAllPosts関数を使用して公開済みブログ記事の動的URL生成(記事の日付をlastModifiedとして使用)
- getAllTags関数を使用して公開済み記事のタグページのURL生成
特徴: - 既存の設定(config/data.json)と機能(blog.ts)を活用し、メンテナンス性を確保
- 公開済みコンテンツのみを含め、適切なSEO設定を実現
- 新しい記事やタグを追加するだけで自動的にサイトマップが更新
サイトマップは /sitemap.xml で自動的に生成され、アクセス可能になります。
どうですか?
すごくないですか?
実際に生成されたコードは一発で問題なく動作し、サイトマップの自動生成機能を実装することができました。
今回取り組んだリメイク作業
今回、私が取り組んだリメイク作業は以下の通りです。
- デザインの刷新
- Next.jsのApp Routerへの移行
- Mantineの導入
- KaTeXを使ったMDXでの数式表示の実装
- PWA対応
- SEO対策とOGP画像生成
特にこだわったのはデザインの刷新です。
その中でも、トップページに表示されているターミナル風のWelcomeメッセージ表示は、私が特にこだわって作成した部分です。
実はあのコンポーネント自体は2年以上前に実装していたもので、今回のリメイクはそのデザインをベースに統一感を持たせたものとしました。
そして、デザインの刷新とともに今回実現したかったことが、Next.jsのApp Routerへの移行です。
App RouterはNext.jsのバージョン13.4で導入された新しいルーティングシステムで、従来のPages Routerよりもパフォーマンスが向上していると言われています。
App RouterとPages Routerはプロジェクトのディレクトリ構造が異なるため、新規でプロジェクトを作成し、既存のコンポーネントを移行する形でリメイクを進めました。
様々なコンポーネントが実装されているMantineを使いたかったので、Mantineの公式ドキュメントでも紹介されているテンプレートをベースに開発することとしました。
今回実装した目玉機能
MDXでの数式表示
rehype-katexを使ってMDXで数式を表示する機能を実装しました。
以下のように、MDXファイル内で数式を記述することができます。
e^{i\theta} = \cos\theta + i\sin\theta
これにより、以下のように表示されます。
SEO対策とOGP画像生成
こちらのドキュメントを参考に、各種SEO対策を実装しました。
サイトマップ
Next.jsのMetadata APIを使用して、動的なサイトマップを生成しています (src/app/sitemap.ts
として実装)。
ページの種類ごとに更新頻度と優先度を設定することで、検索エンジンのクローラーに適切な情報を提供しています。
- ホームページ: 毎日更新(優先度1.0)
- 固定ページ: 週次更新(優先度0.8)
- ブログ記事: 月次更新(優先度0.6)
- タグページ: 週次更新(優先度0.4)
import { MetadataRoute } from 'next';
import { getAllPosts, getAllTags } from '@/lib/blog';
import { BASE_URL, SITEMAP_CHANGE_FREQ, SITEMAP_PRIORITY } from '@/lib/constants';
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
// 固定ページのURL
const staticPages = [
{
url: BASE_URL,
lastModified: new Date(),
changeFrequency: SITEMAP_CHANGE_FREQ.HOME,
priority: SITEMAP_PRIORITY.HOME,
},
{
url: `${BASE_URL}/about`,
lastModified: new Date(),
changeFrequency: SITEMAP_CHANGE_FREQ.STATIC_PAGE,
priority: SITEMAP_PRIORITY.STATIC_PAGE,
},
{
url: `${BASE_URL}/works`,
lastModified: new Date(),
changeFrequency: SITEMAP_CHANGE_FREQ.STATIC_PAGE,
priority: SITEMAP_PRIORITY.STATIC_PAGE,
},
{
url: `${BASE_URL}/posts`,
lastModified: new Date(),
changeFrequency: SITEMAP_CHANGE_FREQ.STATIC_PAGE,
priority: SITEMAP_PRIORITY.STATIC_PAGE,
},
{
url: `${BASE_URL}/tags`,
lastModified: new Date(),
changeFrequency: SITEMAP_CHANGE_FREQ.STATIC_PAGE,
priority: SITEMAP_PRIORITY.STATIC_PAGE,
},
];
// ブログ記事のURL(公開済みの記事のみ)
const posts = getAllPosts(false); // includeUnpublished = false
const postPages = posts.map((post) => ({
url: `${BASE_URL}/posts/${post.id}`,
lastModified: new Date(post.date),
changeFrequency: SITEMAP_CHANGE_FREQ.BLOG_POST,
priority: SITEMAP_PRIORITY.BLOG_POST,
}));
// タグページのURL
const tags = getAllTags(false); // includeUnpublished = false
const tagPages = tags.map((tag) => ({
url: `${BASE_URL}/tags/${tag}`,
lastModified: new Date(),
changeFrequency: SITEMAP_CHANGE_FREQ.TAG_PAGE,
priority: SITEMAP_PRIORITY.TAG_PAGE,
}));
return [...staticPages, ...postPages, ...tagPages];
}
クローラー設定
検索エンジンのクローラーに対して、適切なアクセス制御を設定しています (src/app/robots.ts
として実装)。
APIエンドポイントへのアクセスを制限しつつ、コンテンツページへの適切なクロールを許可する設定としました。
また、サイトマップのURLも明示的に指定しています。
import type { MetadataRoute } from 'next';
export default function robots(): MetadataRoute.Robots {
return {
rules: {
userAgent: '*',
allow: '/',
disallow: '/api/',
},
sitemap: 'https://hackfront.dev/sitemap.xml',
};
}
RSSフィード
feed
パッケージを使用して、ブログ記事のRSSフィードを生成しています (scripts/generate-feed.mjs
として実装)。
ビルド時にgenerate-feed.mjs
スクリプトが実行され、以下の情報を含むフィードが生成されます。
- 記事のタイトルと説明
- 投稿日時
- 著者情報
- 記事のURL
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import matter from 'gray-matter';
import { Feed } from 'feed';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const postsDirectory = path.join(__dirname, '../posts');
const siteData = JSON.parse(fs.readFileSync(path.join(__dirname, '../src/app/config/data.json'), 'utf8'));
function getAllPosts() {
const fileNames = fs.readdirSync(postsDirectory);
const allPostsData = fileNames
.filter((fileName) => fileName.endsWith('.mdx'))
.map((fileName) => {
const id = fileName.replace(/\.mdx$/, '');
const fullPath = path.join(postsDirectory, fileName);
const fileContents = fs.readFileSync(fullPath, 'utf8');
const { data } = matter(fileContents);
return {
id,
title: data.title,
date: data.date,
description: data.description,
tags: data.tags || [],
isPublished: data.isPublished ?? true,
};
})
.filter((post) => post.isPublished);
return allPostsData.sort((a, b) => (a.date < b.date ? 1 : -1));
}
async function generateFeed() {
const posts = getAllPosts();
const siteUrl = siteData.siteUrl;
const date = new Date();
const feed = new Feed({
title: siteData.title,
description: siteData.description,
id: siteUrl,
link: siteUrl,
language: 'ja',
image: `${siteUrl}/logo.svg`,
favicon: `${siteUrl}/favicon.ico`,
copyright: `All rights reserved ${date.getFullYear()}, ${siteData.author}`,
updated: date,
feedLinks: {
rss2: `${siteUrl}/feed.xml`,
},
author: {
name: siteData.author,
email: siteData.mail,
link: siteUrl,
},
});
posts.forEach((post) => {
const url = `${siteUrl}/posts/${post.id}`;
feed.addItem({
title: post.title,
id: url,
link: url,
description: post.description,
date: new Date(post.date),
author: [
{
name: siteData.author,
email: siteData.mail,
link: siteUrl,
},
],
});
});
fs.writeFileSync('./public/feed.xml', feed.rss2());
}
generateFeed().catch(console.error);
ダイナミックOGP画像
記事URLをソーシャルメディアで共有した際に表示されるOGP画像ですが、ブログを運営していく上で結構労力がかかる作業です。
QiitaやZennなどのプラットフォームでは、記事のタイトルや投稿日時を元にOGP画像を自動生成してくれる機能がありますが、自前のブログではどうしても手動で作成する必要があります。
そこで、今回はEdge Runtimeを活用した高速なOGP (Open Graph Protocol)画像生成を実装しています (src/app/api/og/route.tsx
として実装)。
ブログのブランディングを意識し、以下の特徴を持つ画像を動的に生成します。
- ターミナル風のデザイン採用
- グラデーションを用いた視認性の高い表現
- 記事タイトルと投稿日の動的な組み込み
- 1200x630ピクセルのSNS最適化サイズ
画像生成には、next/og
パッケージを使用しています。
このパッケージのImageResponse
クラスを利用することで、HTML要素を画像に変換することができます。
ただし、このパッケージではsatoriというパッケージを内部で利用しているため、利用できるCSSプロパティに制限があることに注意が必要です。
そのため、人手で実装しようと結構苦労しそうですが、この点はClineを使うことでTailwindCSSによりスタイリングしたコンポーネントと同じデザインを再現することができました。
import { ImageResponse } from 'next/og';
import { NextRequest } from 'next/server';
import siteData from '../../config/data.json';
export const runtime = 'edge';
const WindowControls = () => (
<div style={{ display: 'flex', gap: '8px' }}>
<div
style={{
width: '12px',
height: '12px',
borderRadius: '9999px',
backgroundColor: '#ff5f56',
}}
/>
<div
style={{
width: '12px',
height: '12px',
borderRadius: '9999px',
backgroundColor: '#ffbd2e',
}}
/>
<div
style={{
width: '12px',
height: '12px',
borderRadius: '9999px',
backgroundColor: '#27c93f',
}}
/>
</div>
);
export async function GET(req: NextRequest) {
try {
const { searchParams } = new URL(req.url);
const title = searchParams.get('title') ?? siteData.title;
const date = searchParams.get('date');
const formatDate = (dateStr: string) => {
const date = new Date(dateStr);
const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
const dayOfWeek = days[date.getDay()];
return `${dateStr} (${dayOfWeek})`;
};
return new ImageResponse(
(
<div
style={{
background: 'rgb(26, 27, 38)',
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '40px',
}}
>
<div
style={{
background: 'linear-gradient(to bottom right, #8b5cf6, #ec4899)',
borderRadius: '8px',
width: '100%',
height: '100%',
padding: '1.5px',
display: 'flex',
}}
>
<div
style={{
borderRadius: '7px',
background: 'rgb(26, 27, 38)',
display: 'flex',
flexDirection: 'column',
width: '100%',
height: '100%',
}}
>
<div
style={{
borderTopLeftRadius: '8px',
borderTopRightRadius: '8px',
borderBottom: '1px solid rgb(36, 40, 59)',
background: 'rgb(26, 27, 38, 0.5)',
padding: '8px 16px',
fontSize: '14px',
color: 'rgb(169, 177, 214)',
display: 'flex',
flexDirection: 'column',
}}
>
<div
style={{
height: '20px',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
}}
>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<WindowControls />
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<svg
width="20"
height="20"
viewBox="0 0 10.583334 10.583334"
style={{ marginLeft: '12px', marginRight: '4px' }}
>
<path d="M 0.28397387,10.283974 1.5617015,2.3455284 3.3116258,1.1953993 Z" fill="#00ff00" />
<path
d="M 3.6672441,1.2365066 C 6.7110938,-1.0843383 8.2273756,1.7443967 10.283974,0.37314007 L 9.0965356,3.9864574 C 6.6443289,5.9107596 5.4034251,2.8745578 2.3481789,5.0166724 Z"
fill="#00ff00"
/>
</svg>
<span style={{ color: 'rgb(169, 177, 214)' }}>{siteData.title}</span>
</div>
</div>
</div>
</div>
<div
style={{
borderBottomLeftRadius: '8px',
borderBottomRightRadius: '8px',
flex: 1,
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
gap: '20px',
padding: '60px',
}}
>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '12px' }}>
<div
style={{
fontSize: '48px',
fontWeight: 'bold',
background: 'linear-gradient(to right, #8b5cf6, #ec4899)',
backgroundClip: 'text',
color: 'transparent',
maxWidth: '800px',
textAlign: 'center',
}}
>
{title}
</div>
{date && (
<div
style={{
fontSize: '24px',
color: 'rgb(158, 206, 106)',
marginBottom: '8px',
}}
>
{formatDate(date)}
</div>
)}
<div
style={{
fontSize: '24px',
color: 'rgb(169, 177, 214)',
}}
>
{siteData.description}
</div>
</div>
</div>
</div>
</div>
</div>
),
{
width: 1200,
height: 630,
}
);
} catch (e: any) {
console.log(`${e.message}`);
return new Response(`Failed to generate the image`, {
status: 500,
});
}
}
各記事のOGP画像では、記事タイトルと投稿日時をクエリパラメータとして渡すことで、動的にOGP画像を生成しています。
例えば、https://hackfront.dev/api/og?title=こんな感じでタイトルを入力&date=2025-01-23
というURLにアクセスすることで、以下のようなOGP画像を生成することができます。
Vercelのリソースを考慮し、過度なリクエストは控えていただきたいのですが、数回程度の試用であれば問題ありません。
ぜひお試しください。
今回の取り組みからわかったClineの強みと注意点
今回のリメイク作業を通じて、Clineの強みと注意点がいくつか見えてきました。
Clineの強み
自然言語を通したコーディング
Clineの自然言語によるコーディング機能は、実際の使用感が想像以上に優れています。
例えば「このファイルと同じようなデザインで実装して」という指示に対し、Clineは約10秒で具体的な提案を返してきます。
人間のエンジニアであれば「同じようなデザイン」の解釈に時間がかかるところを、Clineは即座に理解して実装を提案します。
また、提案が意図と異なる場合も、「ここをこう修正して」という形で細かな調整が可能です。
特筆すべきは、公式ドキュメントに基づいた実装能力の高さです。
明確な指示と最新のドキュメントを提供することで、Clineは質の高い実装を提案します。
これは特にフロントエンド開発において重要で、ベストプラクティスが頻繁に更新される中、最新の実装手法を反映できます。
ただし、「Clineによりエンジニアは不要になるのか」という議論については、現実的には異なる見方が必要です。
情報の取捨選択や適切な指示を行う人間の役割は依然として重要で、最終的なプロダクトの品質は使用者の技術理解度に大きく依存します。
多彩なLLMモデルの利用
Clineでは、多くのLLMモデルを利用することができます。
その上で私が特に便利だと感じたのが、様々なLLMモデルへの仲介を行うサービス「OpenRouter」です。
1週間単位に新しいモデルが公開される現代において、様々なモデルの性能を試してみたいというニーズは非常に高いと思いますが、そのたびに提供する会社にアカウント登録し、入金するのは面倒です。
OpenRouterであれば、様々なモデルを1つのAPIキーで利用することができるので、非常に便利です。
その上で、私が試した限りでは、現状Clineを使うのであれば「anthropic/claude-3.5-sonnet:beta」の一強かな、という印象を受けました。
最近話題となっている「deepseek/deepseek-chat (DeepSeek V3)」を私も使ってみたのですが、生成速度がかなり遅く、出力も安定しないことが多かったです。
とはいえ、価格が1/10以下というのは魅力的であることは間違いないので、今後の改善に期待したいところです。
Clineの注意点
Clineの注意点ですが、基本的にはただ一つ、「現状コストがかなりかかる」ということです。
今回、私は3日間かけてClineを使って作業しましたが、その間にかかったコストは約$55 (156円/ドルで約8,500円)でした。
もちろん、うまい使い方を理解できていなかったうちに無駄にコストをかけてしまった部分もありますが、個人利用として考えると少し高いかなと感じました。
(私自身がコーディングした場合、とても3日間で実装できる作業量ではなかったので、もちろんかなり助かったのは事実ですが…)
このように考えると、やはりこの1/10以下のコストで実現できるDeepSeek V3が魅力的であることも理解できます。
Clineのコストが増大する主な要因は、対話の長期化によるコンテキストの増加です。
Clineは過去の全てのやり取りをコンテキストとしてLLMに提供することで、一貫性のある提案を可能にしています。
しかし、この仕組みにより対話が長くなるほど1ターンあたりのコストが上昇していきます。
それを踏まえて、Clineを使う際に意識すべきことをまとめてみました。
一度のやり取りで実装する機能は一つに絞る
これは要するに一度のやり取りの長さをなるべく短くすることが目的です。
複数の機能を一度に実装しようとすると、与える指示も、確認すべき既存のソースコードも増えてしまいます。
また、一度に複数の機能を実装しようとすると、生成されたコードが正しく動作するか確認するのも大変です。
普段のコーディングと同じだとは思いますが、Issueを明確にし、それのみを解決するように指示を出すことが重要です。
既存のコードを確認する際は、コードの場所を明示する
Clineはコンテキストを持っていないため、既存のコードを確認する際には、そのコードがどこにあるのかを明示する必要があります。
特に何も指示を与えなくても、Clineはファイル名からなんとなくのあたりをつけて内容を確認してくれますが、それだと無駄に多くのコードを確認し、その分コンテキストが増大してしまいます。
また、既に実装したコンポーネントを使って欲しいのに、それに気づかずに新規でコンポーネントを作成してしまうこともあります。
そういう意味でも、やはりコーディングを完全にClineに任せるのではなく、ユーザもソフトウェアの構成を把握しておくことはマストだと思います。
こまめにgit commitし、やり取りがうまくいかなかった場合は生成物を破棄する
Clineはやり取りが長くなるとコストがかかるため、やり取りがうまくいかない場合は、思い切って生成物を破棄してやり直すことも重要です。
そのような場合に、こまめにgit commitしておけば、破棄したとしても元に戻すことができます。
Clineはターミナルでのコード実行もできるため、エラー文を読んで勝手に修正することもありますが、うまくいかずに泥沼にハマって謎の修正を繰り返す挙動が見られることもあります。
何より恐ろしいのはそうしている間にもどんどんコストがかかっていくことです。
ユーザから見て、どのような方針で何を直そうとしているのかがわからなくなった場合、おそらくLLMも状況が整理できずに手当たり次第に修正を試みているのだと思います。
そのような場合、やり取りを一度リセットして、もう一度最初からやり直すことも重要です。
リセットする場合、次は何を試みてだめだったのかを最初から明示すると、簡単に問題が解決されることも多かったです。
また、ユーザ自身がWeb検索し、その問題を解決する手段をコピペすることもかなり有効でした。
Clineの有効活用法 (?)
その上で、今後私が試そうと思っていることは、ユーザとClineが共通認識を持つための作業メモを作成することです。
具体的には、どのファイルにどのような処理が実装されているのかを記録するためのメモを用意し、やり取りの開始時に必ずそれを確認してもらい、コーディングが完了したらそのメモを更新するようにすることです。
Clineには「Custom Instructions」という機能があり、ここに設定した指示はシステムプロンプトとして機能します。
そのため、ここにそのような指示を記載しておけば、ユーザの指示を実現するためにはどのファイルを確認すべきかをClineが理解できるようになり、やり取りがスムーズに進むのではないかと考えています。
今度実際に試してみたいと思っているので、その結果もまた報告させていただきます。
まとめ
本記事では、AIコーディングツール「Cline」を用いた技術ブログのリメイク体験を共有しました。
Clineの活用により、開発効率を大幅に向上させることができましたが、コスト面での注意も必要です。
Clineは強力なツールですが、その能力を最大限に引き出すためにはユーザ側の工夫も重要です。
本記事が、Clineの活用を検討している方々にとって参考になれば幸いです。