はじめに
ブログや成果物を載せていきたいなーと思った時に いろんなサービスを横断的に使うのもいいですけど、 集約するのがいいなと思いましたのでポートフォリオサイトを構築してみることにしました。
ポートフォリオサイトにあるといい機能を洗い出すと、現時点では下記となります。
- 自己紹介 / Profile
- ブログ / Blog
- 成果物情報 / Products
- お問い合わせ / Contact
- プライバシーポリシー等
ブログではメディア媒体を扱いますし、何よりMarkdownなどで書きやすくしておくう必要があります。
今回はAstro + Cloudflareを使い構築していくことにします。
- Blog / Products
- [Astro] png,jpegなど画像を
astro build実行時に webpに変換できる。
毎回画像変換なんてやってられません。 - [Astro] @astrojs/mdxで
コンポーネントを埋め込むことができる。
Markdownファイルで気軽に書くことができ、自分で関数を作り拡張できる。
- [Astro] png,jpegなど画像を
- Contact
- [Cloudflare] お問合せ受付メールとサクッとメール連携できると楽でいい!
Astro
Astro builds fast content sites, powerful web applications, dynamic server APIs, and everything in-between.
Connect, protect, and build everywhere
Make employees, applications and networks faster and more secure everywhere, while reducing complexity and cost.
このポートフォリオサイトを構築する過程で学んだTipsをまとめました。
全体構成
src配下はざっくりこのように配置しました。
基本的にAstroベースでシンプル構成です。
src
├── components
│ ├── Blog.astro
│ ├── Contact.astro
│ ├── ContactCTA.astro
│ ├── Footer.astro
│ ├── Nav.astro
│ ├── Products.astro
│ ├── Profile.astro
│ ├── ShareButtons.astro
│ ├── TableOfContents.astro
│ └── mdx
│ ├── AffiliateLink.astro
│ ├── ExternalLink.astro
│ ├── LinkCard.astro
│ └── Video.astro
├── content
│ ├── blog
│ │ ├── 2026-04-13_astro_cloudflare-portfolio
│ │ │ ├── assets1.jpg
│ │ │ ├── cover.png
│ │ │ └── index.mdx
│ │ └── 2026-04-xx_xxxxxxxxx
│ │ ├── assets1.jpg
│ │ ├── cover.png
│ │ └── index.mdx
│ ├── config.ts
│ └── products
│ ├── Product1.mdx
│ └── Product2.mdx
├── layouts
│ └── Layout.astro
└── pages
├── api
│ └── contact.ts
├── blog
│ ├── [slug].astro
│ └── index.astro
├── contact.astro
├── index.astro
├── privacy-policy.astro
├── products
│ ├── [slug].astro
│ └── index.astro
└── rss.xml.ts
Astroは静的コンテンツとの相性はいい
詳しくは公式ページを見ていただければ幸いですが、
SEO面やメディアファイルの最適化が優秀です。
このページにあるカバー画像とフッター画像は1MB超えですが、
astro buildした際には WebPに変換されます。
ブログ/成果物ページを手軽に更新する仕組みにする
フォルダ構成でアセットを管理する
フォルダごとに記事をまとめると、画像・動画を同じ場所に置けてスッキリします。
src/content/blog/
└── 2026-04-13_astro_cloudflare-portfolio/
├── index.mdx ← 記事本文
├── cover.png ← カバー画像
└── assets1.jpg ← カバー以外の画像
Content Collectionsを活用して、簡単に一覧ページに反映
Markdownファイルをコレクションとして管理することで、型安全なフロントマターが使えます。
import { defineCollection, z } from 'astro:content';
const blog = defineCollection({
type: 'content',
schema: ({ image }) =>
z.object({
title: z.string(),
date: z.string(),
tags: z.array(z.string()),
excerpt: z.string(),
emoji: z.string(),
cover: image().optional(),
draft: z.boolean().default(false),
}),
});
const products = defineCollection({
type: 'content',
schema: z.object({
name: z.string(),
description: z.string(),
emoji: z.string(),
tags: z.array(z.string()),
link: z.string().optional(),
repo: z.string().optional(),
status: z.enum(['Released', 'Beta', 'WIP']),
draft: z.boolean().default(false),
order: z.number().default(0),
}),
});
export const collections = { blog, products };
これを各mdxの最初に追加するだけで、反映完了です。
---
title: Astro+CloudflareでポートフォリオサイトをゼロからつくるTips
date: '2026-04-13'
tags: ['Astro', 'Cloudflare']
excerpt: コンテンツ駆動型フレームワーク「Astro」を「Cloudflare」で構築して簡易ポートフォリオサイトを公開する!
emoji: 🖌️
cover: './cover.png'
draft: false
---
Draftは非公開に、記事順を並べる
Blogはじっくり内容を練って作っていきたいことも出てくると踏んで、
draft: trueであれば、dev環境だけで表示されるので、ブログ作成を捗らせることもできます。
---
export async function getStaticPaths() {
const isDev = import.meta.env.DEV;
const posts = await getCollection('blog', ({ data }) => isDev || !data.draft);
// 日付降順(新しい順)に並べる
const sorted = posts.sort(
(a, b) => new Date(b.data.date).getTime() - new Date(a.data.date).getTime()
);
return sorted.map((post, index) => ({
params: { slug: post.slug },
props: {
post,
// より新しい記事(左 << )
newer: index > 0
? { slug: sorted[index - 1].slug, title: sorted[index - 1].data.title, emoji: sorted[index - 1].data.emoji }
: null,
// より古い記事(右 >> )
older: index < sorted.length - 1
? { slug: sorted[index + 1].slug, title: sorted[index + 1].data.title, emoji: sorted[index + 1].data.emoji }
: null,
},
}));
}
---
リンクカード表示できるようにする
mdxのいいところはMarkdownに関数を埋め込むこともできるところです。
URLのリンクカードをBlogではよく見かけますよね。
もちろんただのMarkdownではないものなのですが、 関連ブログからどんな概要なのか察知できることは大事ですね。
Astro+CloudflareでポートフォリオサイトをゼロからつくるTips — shocola88r
コンテンツ駆動型フレームワーク「Astro」を「Cloudflare」で構築して簡易ポートフォリオサイトを公開する!
サンプルコードも貼っておきます。
---
interface Props {
href: string;
}
const { href } = Astro.props;
type OgData = {
title: string;
description: string;
image: string;
favicon: string;
domain: string;
};
function extractMeta(html: string, patterns: RegExp[]): string {
for (const re of patterns) {
const m = html.match(re);
if (m?.[1]) return m[1].trim();
}
return '';
}
let og: OgData = { title: href, description: '', image: '', favicon: '', domain: '' };
try {
const url = new URL(href);
og.domain = url.hostname;
og.favicon = `https://www.google.com/s2/favicons?domain=${url.hostname}&sz=32`;
const res = await fetch(href, {
headers: { 'User-Agent': 'Mozilla/5.0 (compatible; Googlebot/2.1)' },
signal: AbortSignal.timeout(5000),
});
const html = await res.text();
og.title =
extractMeta(html, [
/<meta[^>]+property=["']og:title["'][^>]+content=["']([^"']+)["']/i,
/<meta[^>]+content=["']([^"']+)["'][^>]+property=["']og:title["']/i,
/<title[^>]*>([^<]+)<\/title>/i,
]) || href;
og.description = extractMeta(html, [
/<meta[^>]+property=["']og:description["'][^>]+content=["']([^"']+)["']/i,
/<meta[^>]+content=["']([^"']+)["'][^>]+property=["']og:description["']/i,
/<meta[^>]+name=["']description["'][^>]+content=["']([^"']+)["']/i,
/<meta[^>]+content=["']([^"']+)["'][^>]+name=["']description["']/i,
]);
og.image = extractMeta(html, [
/<meta[^>]+property=["']og:image["'][^>]+content=["']([^"']+)["']/i,
/<meta[^>]+content=["']([^"']+)["'][^>]+property=["']og:image["']/i,
]);
// 相対パスを絶対URLに変換
if (og.image && !og.image.startsWith('http')) {
og.image = new URL(og.image, href).toString();
}
} catch {
// フェッチ失敗時はURLのままフォールバック
}
---
<a href={href} class="link-card" target="_blank" rel="noopener noreferrer">
<div class="link-card-body">
<p class="link-card-title">{og.title}</p>
{og.description && (
<p class="link-card-desc">{og.description}</p>
)}
<div class="link-card-footer">
<img src={og.favicon} alt="" width="16" height="16" class="link-card-favicon" loading="lazy" />
<span class="link-card-domain">{og.domain}</span>
</div>
</div>
{og.image && (
<div class="link-card-thumb">
<img src={og.image} alt={og.title} loading="lazy" />
</div>
)}
</a>
<style>
.link-card {
display: flex;
align-items: stretch;
border: 1.5px solid var(--cream-dark);
border-radius: var(--radius);
overflow: hidden;
background: var(--cream);
transition: border-color 0.2s, box-shadow 0.2s, transform 0.2s;
text-decoration: none !important;
margin: 1.25rem 0;
min-height: 90px;
}
.link-card * {
text-decoration: none !important;
}
.link-card img {
max-width: none !important;
margin: 0 !important;
border-radius: 0 !important;
display: block;
}
.link-card:hover {
border-color: var(--choco-accent);
box-shadow: var(--shadow);
transform: translateY(-2px);
}
.link-card:visited .link-card-title {
color: var(--text-muted);
}
.link-card:visited .link-card-domain {
color: var(--choco-accent);
}
.link-card-body {
flex: 1;
min-width: 0;
padding: 0.85rem 1rem;
display: flex;
flex-direction: column;
gap: 0.3rem;
justify-content: center;
}
.link-card-title {
font-size: 0.925rem;
font-weight: 800;
color: var(--choco-dark);
line-height: 1.4;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
margin: 0;
}
.link-card-desc {
font-size: 0.8rem;
color: var(--text-muted);
line-height: 1.5;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
margin: 0;
}
.link-card-footer {
display: flex;
align-items: center;
gap: 0.35rem;
margin-top: 0.15rem;
}
.link-card-favicon {
width: 16px;
height: 16px;
border-radius: 3px;
object-fit: contain;
}
.link-card-domain {
font-size: 0.75rem;
color: var(--text-muted);
font-weight: 600;
}
.link-card-thumb {
flex-shrink: 0;
width: 160px;
background: var(--cream-mid);
overflow: hidden;
}
.link-card-thumb img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
@media (max-width: 500px) {
.link-card-thumb { width: 110px; }
}
</style>
まとめ
AstroはSSGペースで今回のようなブログなどにはよくマッチしていました。
Content Collectionsを使えばブログ管理も型安全になります。
定期的にブログ更新しながら、拡張を加えていくのも面白みがありますね。
ぜひ試してみてください。
