Skip to main content
Back to Blog

Build a Personal Homepage from Scratch in 3 Days: Astro + Cloudflare Pages, Zero-Cost Deployment

1/25/2026

Preface: the best way to record your technical growth

As a developer, I always wanted my own space to record technical notes, share project experience, and showcase learning outcomes. I wrote many articles, but they were scattered across platforms. I built interesting projects, but there was no unified place to show them.

Then I discovered Astro, a modern static site generator that let me build a professional, beautiful, full-featured personal site in less than a day. It supports blogs, projects, and resumes, and most importantly, deployment and maintenance are simple. Today I am sharing the full practical guide.


Why Astro + Cloudflare Pages?

Before we start, here are the benefits of this stack:

Astro core advantages:

  • Zero JavaScript by default, excellent performance
  • Supports React, Vue, Svelte, and more
  • Built-in Content Collections with type safety
  • MDX support to use components inside Markdown
  • SEO-friendly, auto sitemap

Cloudflare Pages strengths:

  • Free static hosting with unlimited bandwidth
  • Global CDN (290+ PoPs)
  • Automatic HTTPS
  • GitHub/GitLab integration with auto deploy
  • Edge functions support for extensibility
  • Zero-config cold starts, deployment in seconds

This combo gives you modern dev ergonomics with zero-cost global deployment and strong performance.


Step 1: Project initialization

Create a new Astro project:

# npm
npm create astro@latest

# pnpm
pnpm create astro

# yarn
yarn create astro

In the interactive installer:

  1. Choose “Include sample files” and pick “Blog”
  2. Choose “Install dependencies”
  3. Choose “How do you plan to deploy?” and select “Cloudflare Pages” or “Other”

Enter the project directory:

cd your-project-name

Step 2: Configure project metadata

Open astro.config.mjs:

import { defineConfig } from 'astro/config';

export default defineConfig({
  site: 'https://your-domain.com',  // replace with your domain or Cloudflare Pages default domain
  base: '/',  // usually root for Cloudflare Pages
  integrations: [
    // add integrations later
  ],
});

Notes:

  • Cloudflare default domain format: your-project.pages.dev
  • For custom domains, add the domain in Cloudflare and configure DNS
  • base is usually '/' unless you have a special path

Step 3: Personalize your homepage

1. Edit the homepage content

Edit src/pages/index.astro:

---
import Layout from '../layouts/Layout.astro';
---

<Layout title="gfish Homepage">
  <main>
    <h1>Hello, I am Zhang San!</h1>
    <p>Full-stack engineer | Open-source enthusiast | Tech blogger</p>

    <section class="projects">
      <h2>My Projects</h2>
      <ul>
        <li>
          <h3>Project 1: XXX</h3>
          <p>Project description...</p>
          <a href="project-link">View details</a>
        </li>
        <!-- more projects -->
      </ul>
    </section>

    <section class="skills">
      <h2>Tech Stack</h2>
      <p>JavaScript, TypeScript, React, Vue, Node.js...</p>
    </section>
  </main>
</Layout>

<style>
  main {
    max-width: 800px;
    margin: 0 auto;
    padding: 2rem;
  }
  h1 {
    font-size: 3rem;
    margin-bottom: 1rem;
  }
  section {
    margin: 3rem 0;
  }
</style>

2. Add blog content

Create .md or .mdx files in src/content/blog:

---
title: "My First Blog Post"
description: "This is the summary"
pubDate: 2025-01-25
tags: ["Astro", "Frontend"]
---

# Body

Your blog content goes here with full Markdown support.

3. Create a blog list page

src/pages/blog/index.astro:

---
import { getCollection } from 'astro:content';
import Layout from '../../layouts/Layout.astro';

const allPosts = await getCollection('blog');
const sortedPosts = allPosts.sort((a, b) =>
  b.data.pubDate.valueOf() - a.data.pubDate.valueOf()
);
---

<Layout title="Blog List">
  <main>
    <h1>My Blog</h1>
    <ul>
      {sortedPosts.map(post => (
        <li>
          <a href={`/blog/${post.slug}/`}>
            <h2>{post.data.title}</h2>
            <p>{post.data.description}</p>
            <time>{post.data.pubDate.toDateString()}</time>
          </a>
        </li>
      ))}
    </ul>
  </main>
</Layout>

4. Create a blog detail page

src/pages/blog/[slug].astro:

---
import { getCollection } from 'astro:content';
import Layout from '../../layouts/Layout.astro';

export async function getStaticPaths() {
  const posts = await getCollection('blog');
  return posts.map(post => ({
    params: { slug: post.slug },
    props: { post },
  }));
}

const { post } = Astro.props;
const { Content } = await post.render();
---

<Layout title={post.data.title}>
  <main>
    <h1>{post.data.title}</h1>
    <p>{post.data.description}</p>
    <time>{post.data.pubDate.toDateString()}</time>
    <hr />
    <Content />
  </main>
</Layout>


Step 4: Make it look good

Use Tailwind CSS

Install Tailwind CSS:

npx astro add tailwind

Use Tailwind classes in components:

<div class="container mx-auto px-4">
  <h1 class="text-4xl font-bold text-center my-8">
    Welcome to my homepage
  </h1>
  <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
    <!-- project cards -->
  </div>
</div>

Add dark mode

Create src/layouts/Layout.astro:

---
interface Props {
  title: string;
}

const { title } = Astro.props;
---

<!doctype html>
<html lang="zh-CN">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width" />
    <title>{title}</title>
  </head>
  <body>
    <slot />
  </body>
</html>

<style is:global>
  :root {
    --bg-color: #ffffff;
    --text-color: #1a1a1a;
  }

  html.dark {
    --bg-color: #1a1a1a;
    --text-color: #ffffff;
  }

  body {
    background-color: var(--bg-color);
    color: var(--text-color);
    transition: background-color 0.3s, color 0.3s;
  }
</style>

Add a toggle button:

<script>
  function toggleDark() {
    document.documentElement.classList.toggle('dark');
  }
</script>

<button onClick="toggleDark">
  Toggle theme
</button>

Step 5: Deploy to Cloudflare Pages

1. Create a GitHub repo

Create a new repo on GitHub:

  • Repo name: your-project-name (e.g. personal-homepage)
  • Public
  • Optional README

2. Configure build scripts

In package.json (if not already):

{
  "scripts": {
    "dev": "astro dev",
    "start": "astro dev",
    "build": "astro build",
    "preview": "astro preview"
  }
}

If you use Pagefind:

{
  "scripts": {
    "dev": "astro dev",
    "build": "astro build && pagefind --site dist --output-path dist/pagefind",
    "preview": "astro preview"
  }
}

3. Create wrangler.toml

At the project root:

# Cloudflare Pages config
# Docs: https://developers.cloudflare.com/pages/platform/build-configuration/

name = "your-project-name"
compatibility_date = "2024-01-01"

[site]
bucket = "./dist"

[build]
command = "npm run build"
cwd = ""

[build.upload]
format = "service-worker"

[env.production]
PUBLIC_ALLOW_INDEX = "1"

[env.preview]
PUBLIC_ALLOW_INDEX = "0"

4. Connect Cloudflare Pages

Option A: Cloudflare Dashboard (recommended first time)

  1. Log in: https://dash.cloudflare.com
  2. “Workers & Pages” -> “Create application” -> “Pages” -> “Connect to Git”
  3. Select your GitHub repo and authorize
  4. Build settings:
    • Build command: npm run build
    • Output directory: dist
    • Node version: latest LTS (e.g. 20)
  5. Click “Save and Deploy”

Option B: Wrangler CLI (for advanced users)

# install Wrangler
npm install -g wrangler

# login
wrangler login

# deploy
wrangler pages deploy dist --project-name=your-project-name

5. Custom domain (optional)

If you have your own domain:

  1. Go to your Pages project
  2. “Custom domains” -> “Set up a custom domain”
  3. Enter your domain (e.g. blog.yourdomain.com)
  4. Configure DNS records as instructed
  5. Wait for SSL to be issued

Within minutes your site is live:

  • default domain: https://your-project.pages.dev
  • custom domain: https://your-custom-domain.com

6. Auto deploy

Cloudflare Pages watches your Git pushes:

  • push to main triggers production deploy
  • pull requests get preview URLs
  • deployment history is visible in the dashboard

Step 6: Add advanced features

Use Pagefind:

npm install -D pagefind

Add a build hook:

{
  "scripts": {
    "build": "astro build && pagefind dist"
  }
}

Add a search box:

<script is:inline src="/pagefind/pagefind.js"></script>

<input id="search" type="search" placeholder="Search..." />

<script>
  const searchBox = document.getElementById('search');
  searchBox.addEventListener('input', (e) => {
    pagefind.search(e.target.value);
  });
</script>

2. Reading progress bar

src/components/ProgressBar.astro:

<div id="progress-bar"></div>

<style>
  #progress-bar {
    position: fixed;
    top: 0;
    left: 0;
    width: 0%;
    height: 4px;
    background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
    transition: width 0.1s;
    z-index: 1000;
  }
</style>

<script>
  window.addEventListener('scroll', () => {
    const winScroll = document.body.scrollTop || document.documentElement.scrollTop;
    const height = document.documentElement.scrollHeight - document.documentElement.clientHeight;
    const scrolled = (winScroll / height) * 100;
    document.getElementById('progress-bar').style.width = scrolled + '%';
  });
</script>

3. RSS feed

Install RSS:

npx astro add rss

Create src/pages/rss.xml.js:

import rss from '@astrojs/rss';
import { getCollection } from 'astro:content';

export async function GET(context) {
  const posts = await getCollection('blog');
  return rss({
    title: 'My Blog',
    description: 'My tech blog RSS Feed',
    site: context.site,
    items: posts.map(post => ({
      title: post.data.title,
      pubDate: post.data.pubDate,
      description: post.data.description,
      link: `/blog/${post.slug}/`,
    })),
  });
}

gfish Homepage: what I built

Using the stack above, I built my own site: gfish-home-public. It includes:

Core features:

  • Bilingual (zh/en)
  • Full-text search (Pagefind)
  • Blog system (MDX + Content Collections)
  • Project cards
  • Reading progress bar
  • Dark mode
  • Breadcrumbs
  • Back-to-top
  • RSS
  • SEO (JSON-LD, Open Graph)

Tech highlights:

  • Astro 4.5.0 + Vite
  • Tailwind CSS + custom theme
  • TypeScript type safety
  • Zod validation
  • Responsive design
  • Accessibility (WCAG 2.1 AA)
  • Cloudflare Pages CDN
  • Wrangler automation

Project: https://github.com/gxj1134506645/gfish-home-public

If this helps, feel free to star it.

Live preview: https://gfish.pages.dev (example URL, replace with your actual)


Summary and outlook

From this guide, you now know:

  1. How to bootstrap an Astro project
  2. How to deploy free on Cloudflare Pages
  3. How to customize a personal homepage
  4. How to add advanced features

A professional personal site is your digital home. It helps you:

  • record and share your growth
  • showcase projects and skills
  • build your personal brand
  • connect with like-minded developers

Cloudflare Pages vs other options: Compared to GitHub Pages and Vercel, Cloudflare Pages offers:

  • true global CDN (290+ PoPs)
  • unlimited free bandwidth
  • faster deployments
  • edge function support

Next steps:

  • Add comments (Giscus, Utterances)
  • Add analytics (Cloudflare Web Analytics, Umami)
  • Support i18n
  • Update content regularly
  • Optimize SEO and performance
  • Try Pages Functions for dynamic features

References

Astro docs: https://docs.astro.build

Cloudflare Pages docs: https://developers.cloudflare.com/pages

Wrangler CLI docs: https://developers.cloudflare.com/workers/wrangler

Tailwind CSS docs: https://tailwindcss.com/docs

Pagefind: https://pagefind.app

My GitHub repo: https://github.com/gxj1134506645/gfish-home-public


欢迎关注公众号FishTech Notes,一块交流使用心得