Build a Personal Blog with Astro
Build and deploy a static blog with a Git-based CMS and auto-deployments.
Introduction
30 Second Summary
Everyone has ideas worth sharing, but setting up a blog often means wrestling with databases, complex frameworks, and deployment headaches. What if your blog could be a collection of simple text files that automatically becomes a live website every time you hit save?
In this project, you will build a personal blog powered by Astro and deploy it to Vercel with a web-based content editor that lets you publish new posts without touching code.
What You'll Build
You will have a live blog where you type a new post into a browser-based editor, click save, and seconds later your readers see it on the public site.
By the end of this project, you'll have:
- A live personal blog with a hero section introducing you and a chronologically-sorted list of posts that readers can click through to read in full.
- A web-based content editor at Pages CMS where you create and edit blog posts in your browser, and each save commits a Markdown file directly to your GitHub repository.
- An automatic deployment pipeline where every push to your repository triggers Vercel to rebuild and publish your updated site within seconds.
- Secret Mission: Add an RSS feed so readers can subscribe to your blog and get notified of new posts.
Are there any prerequisites?
You should be comfortable using Git in the terminal — navigating to a folder, initializing a repository, and pushing to a GitHub remote.
You'll also need a GitHub account and a Vercel account (both free).
Before We Start
Before we dive in, let's lock in what you're about to build and why it matters to you.
Set Up the Astro Project
Every blog needs a foundation. Astro is a static site generator designed specifically for content-heavy sites like blogs. It renders everything to HTML at build time, so your blog loads fast with zero JavaScript sent to the browser.
In this step, you'll scaffold a fresh Astro project, confirm it runs locally, and push it to GitHub. This gives you a working base and a repository that Vercel can deploy from later.
In this step, get ready to:
- Scaffold a new Astro project using the minimal template.
- Run the dev server and confirm the starter page loads.
- Push the project to a new GitHub repository.
Check your Node.js version
Astro requires Node.js v22.12.0 or higher to run. Before scaffolding the project, confirm you have a compatible version installed.
- Check your installed Node.js version by running this command:
node --version
✔️ I see v22 or higher
Your Node.js version is compatible. You're ready to create the Astro project.
ⓧ I see an older version
You have Node.js installed, but Astro needs v22.12.0 or higher. You'll need to upgrade.
- On macOS or Linux, if you use nvm, upgrade by running:
- nvm install 22 followed by nvm use 22.
- On Windows, or if you don't use a version manager, download the latest LTS installer from nodejs.org and run it. The installer replaces your existing version.
After upgrading, run node --version again to confirm you see v22.12.0 or higher.
ⓧ Command not found
Node.js isn't installed on your machine yet. You'll need to install version 22 (the current LTS).
- Go to nodejs.org and download the LTS installer for your operating system.
- Run the installer and accept the default settings.
- Close and reopen your terminal so it picks up the new installation.
Run node --version again. You should now see v22.12.0 or higher.
Why does Astro need Node 22?
Astro relies on modern JavaScript features only available in Node.js v22.12.0 and above. Odd-numbered Node versions (like v23 or v25) are experimental and not supported by Astro.
Scaffold a new Astro project
The Astro CLI includes an interactive wizard that creates your project folder, downloads a template, installs packages, and initializes Git. You'll answer a few questions to configure everything.
- Open your terminal by pressing Cmd+Space (macOS) or the Windows key (Windows), then typing Terminal and pressing Enter.
- Move to your Desktop by running this command:
cd ~/Desktop
- Start the Astro setup wizard by running this command:
npm create astro@latest
The wizard will ask you several questions. Answer each one as follows:
- If asked to install create-astro, enter y.
- When asked where to create your project, type ./my-blog.
- When shown a list of starter templates, use the arrow keys to select Empty (the minimal template) and press Enter. You'll notice a blog template is available too, but we're starting with minimal so you can learn how Astro works by building each piece yourself.
- When asked to install dependencies, enter y.
- When asked to initialize a new git repository, enter y.
- If asked about any additional options (such as AI agent files), you can decline by entering n.
What does this command do?
The npm create astro@latest command downloads and runs the official Astro scaffolding tool. It creates a project folder with all the starter files, installs your npm dependencies, and sets up a local Git repository in one step.
The minimal (Empty) template gives you a bare starting point with no sample content. Astro offers a blog template too, but starting from scratch means you'll understand every file you create instead of inheriting code you haven't written yet.
- Once the wizard finishes, move into your new project folder by running:
cd my-blog
- Confirm the project files exist by running:
ls
You should see files like astro.config.mjs, package.json, and a src folder listed in the output.
Wizard failed or template not downloading?
If you see an error mentioning the template download failing, try clearing your npm cache and running the command again:
npx clear-npx-cache
npm create astro@latest
Help me fix a failed Astro scaffold
Open the project and start the dev server
- Switch back to VS Code.
- Click File in the top menu bar.
- Click Open Folder.
- Navigate to your Desktop and select the my-blog folder, then click Open.
Trust the authors
If VS Code shows a prompt asking whether you trust the authors of the files in this folder, click Yes, I trust the authors.
- Open the integrated terminal in VS Code by pressing Ctrl+` (macOS and Windows).
- Start the Astro dev server by running this command:
npm run dev
Before you check the browser, what do you think you'll see at the URL? The project uses the Empty template, so predict what an empty Astro site looks like.
- Open your browser and navigate to http://localhost:4321.
You should see the Astro starter page. It displays a simple welcome message confirming Astro is running.
Don't see the page at localhost:4321?
Check your terminal output. If it shows a different port number, use that URL instead. Make sure you ran npm run dev from inside the my-blog folder (your terminal prompt should show my-blog in the path).
Help me troubleshoot the Astro dev server
Push your project to GitHub
Your project runs locally, but it needs to live on GitHub so Vercel can access and deploy it later. The Astro wizard already initialized a Git repository and made an initial commit, so you just need to create the remote and push.
- Stop the dev server by pressing Ctrl+C in your VS Code terminal.
- Open github.com in your browser and sign in.
- Click the + icon in the top-right corner and select New repository.
- Enter my-blog as the repository name.
- Leave all other settings as default (Public is fine) and click Create repository.
Why not initialize with a README?
GitHub offers to add a README, .gitignore, or license when creating a repository. Leave these unchecked. Your local project already has its own files and Git history. Adding files on GitHub would create a conflict when you try to push.
- On the new repository page, GitHub shows a section titled "push an existing repository from the command line." Copy the repository URL shown there.
- Back in your VS Code terminal, connect your local project to GitHub by running this command:
git remote add origin [[YOUR_REPO_URL="https://github.com/your-username/my-blog.git"]]
- Push your code to GitHub by running:
git push -u origin main
What do you expect to see after the push completes? Think about whether there should be output confirming success.
Your terminal should show output confirming the branch was pushed successfully and is now tracking the remote.
- Refresh your GitHub repository page in the browser. You should see your Astro project files (astro.config.mjs, package.json, src/ folder) listed there.
Push rejected or permission denied?
If you see a permission error, make sure you're signed in to GitHub in your terminal. You may need to authenticate with a personal access token or SSH key depending on your setup.
If the push is rejected because the remote contains work you don't have locally, you likely initialized the GitHub repo with a README. Delete the repository on GitHub, recreate it without any initialization files, and try again.
Help me fix my GitHub push error
Your Astro project is scaffolded, running locally, and backed up on GitHub. Next up, you'll build the blog homepage with a hero section and a list of posts.
Build the Blog Homepage
Your Astro project is running, but the default page is blank. A blog needs two things up front: content to display, and a homepage that displays it.
In this step, you will define a content collection that tells Astro where your blog posts live and what shape they take. Then you will build a homepage that queries those posts and renders them alongside a hero introduction.
In this step, get ready to:
- Define a type-safe blog content collection with a Zod schema.
- Create a shared layout component for consistent page structure.
- Build a homepage that displays a hero section and a list of blog posts.
Define the blog content collection
Astro uses content collections to manage groups of related content like blog posts. You define a schema once, and Astro validates every Markdown file against it at build time.
- Create a new file called src/content.config.ts in your project. This is where Astro looks for collection definitions.
- Paste the following code into the file:
import { defineCollection } from "astro:content";
import { glob } from "astro/loaders";
import { z } from "astro/zod";
const blog = defineCollection({
loader: glob({ base: "./src/content/blog", pattern: "**/*.md" }),
schema: z.object({
title: z.string(),
description: z.string(),
pubDate: z.coerce.date(),
}),
});
export const collections = { blog };
What does this code do?
This file tells Astro that a collection called blog exists. The glob() loader points at the ./src/content/blog folder and picks up every .md file it finds there.
The Zod schema requires three frontmatter fields: title, description, and pubDate. If any Markdown file is missing one, Astro throws an error at build time instead of silently breaking.
- Save src/content.config.ts.
Now the collection needs at least one post to load. You will create the folder structure and a sample Markdown file.
- Create the folder src/content/blog/ inside your project by running this command in the terminal:
mkdir -p src/content/blog
- Create a new file at src/content/blog/first-post.md and paste the following content:
---
title: "Building My Blog with Astro"
description: "How I set up this blog using Astro, a modern static site generator that ships zero JavaScript by default."
pubDate: 2026-07-29
---
## Why Astro?
Astro is a static site generator that takes a content-first approach. Unlike frameworks like React or Next.js that ship JavaScript to the browser by default, Astro renders everything to static HTML at build time.
This means my blog loads incredibly fast because visitors download only HTML and CSS. No JavaScript runtime, no hydration delays.
## What I learned
Setting up content collections in Astro was surprisingly straightforward. You define a schema for your content using Zod, point Astro at a folder of Markdown files, and query them with a simple API.
The best part is the type safety. If I forget to add a required field to a blog post's frontmatter, Astro tells me at build time instead of breaking in production.
What does the frontmatter do?
The section between the --- markers at the top is called frontmatter. It provides the structured metadata (title, description, date) that your schema validates. Everything below the second --- is the post body rendered as HTML.
- Save first-post.md.
- Check your terminal where the dev server is running. You should see a message containing Syncing content followed by Synced content. This confirms Astro found your collection and validated the post against your schema.
Don't see the syncing message?
- Make sure content.config.ts is inside src/ (not at the project root).
- Confirm first-post.md is inside src/content/blog/ and its frontmatter includes all three required fields (title, description, pubDate).
- Try stopping the dev server with Ctrl+C and restarting it with npm run dev.
Still stuck? Why isn't Astro detecting my content collection?
Create the layout and homepage
Every page on your blog will share the same HTML shell: a doctype, head tags, and a centered container. A layout component lets you write this once and reuse it everywhere.
- Create a new folder src/layouts/ and a file called BaseLayout.astro inside it.
- Paste the following template code into BaseLayout.astro:
---
interface Props {
title: string;
}
const { title } = Astro.props;
---
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{title}</title>
</head>
<body>
<div class="container">
<slot />
</div>
</body>
</html>
What does this template do?
The frontmatter (between --- markers) accepts a title prop from whatever page uses this layout. The <slot /> tag is where each page's unique content gets injected.
- Now add the global styles below the closing </html> tag in the same file:
<style is:global>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: system-ui, -apple-system, sans-serif;
line-height: 1.6;
color: #1a1a1a;
background-color: #fafafa;
}
.container {
max-width: 720px;
margin: 0 auto;
padding: 2rem 1rem;
}
a {
color: #2563eb;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
</style>
Why is:global?
Astro scopes styles to the component by default. The is:global attribute tells Astro these styles apply site-wide (resets, typography, link colors). Every page using this layout inherits them.
- Save src/layouts/BaseLayout.astro.
With the layout ready, it is time to build the homepage that uses it. You will replace the default index.astro page with one that shows a hero section and lists your blog posts.
- Open src/pages/index.astro (the file from the starter template). Delete all existing content and paste the following:
---
import BaseLayout from "../layouts/BaseLayout.astro";
import { getCollection } from "astro:content";
const posts = (await getCollection("blog")).sort(
(a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf()
);
---
<BaseLayout title="My Blog">
<header class="hero">
<h1>Hi, I'm Alex</h1>
<p>Welcome to my blog. I write about web development, design, and things I'm learning along the way.</p>
</header>
What does the frontmatter do here?
The getCollection("blog") call fetches every post from your blog collection. The .sort() arranges them newest-first by comparing pubDate values. The template then wraps everything in your BaseLayout and starts with a hero header introducing the blog author.
- Directly below the closing </header> tag, add the posts section that maps over your blog entries:
<section class="posts">
<h2>Recent Posts</h2>
{posts.map((post) => (
<article class="post-card">
<a href={`/blog/${post.id}`}>
<h3>{post.data.title}</h3>
</a>
<time datetime={post.data.pubDate.toISOString()}>
{post.data.pubDate.toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
})}
</time>
<p>{post.data.description}</p>
</article>
))}
</section>
</BaseLayout>
What does the posts section do?
The posts.map() loops through every blog entry and renders a card for each one. Each card shows the post title as a link (pointing to /blog/${post.id}), a formatted date, and the description from frontmatter.
- Save src/pages/index.astro and refresh your browser at http://localhost:4321.
- You should see the hero heading "Hi, I'm Alex" and a "Recent Posts" section with your first blog post listed (unstyled for now).
Seeing an error instead of your homepage?
- If you see a "collection not found" error, stop the dev server with Ctrl+C and restart with npm run dev. Astro sometimes needs a restart after creating content.config.ts.
- Check that BaseLayout.astro is inside src/layouts/ (not src/pages/).
Still stuck? Help me debug why my Astro homepage is showing an error.
The content is showing but looks plain. Time to add scoped styles that make the hero and post cards look polished.
- At the bottom of src/pages/index.astro, below the closing </BaseLayout> tag, add the hero and heading styles:
<style>
.hero {
margin-bottom: 3rem;
padding-bottom: 2rem;
border-bottom: 1px solid #e5e7eb;
}
.hero h1 {
font-size: 2rem;
margin-bottom: 0.5rem;
}
.hero p {
color: #6b7280;
font-size: 1.1rem;
}
.posts h2 {
font-size: 1.4rem;
margin-bottom: 1.5rem;
}
- Directly below the .posts h2 block (still inside the <style> tag), add the post-card styles:
.post-card {
margin-bottom: 2rem;
padding-bottom: 1.5rem;
border-bottom: 1px solid #f3f4f6;
}
.post-card h3 {
font-size: 1.2rem;
margin-bottom: 0.25rem;
}
.post-card time {
font-size: 0.85rem;
color: #9ca3af;
}
.post-card p {
margin-top: 0.5rem;
color: #4b5563;
}
</style>
Why are these styles scoped?
Unlike the is:global styles in BaseLayout, these styles are scoped to index.astro only. Astro automatically adds unique class attributes so they never leak into other pages.
- Save src/pages/index.astro.
Before you refresh, what do you expect the page to look like now that styles are applied?
- Refresh your browser at http://localhost:4321.
- You should see a clean homepage with "Hi, I'm Alex" as a large heading, a gray tagline below it separated by a subtle border, and your "Building My Blog with Astro" post card showing its title, date (July 29, 2026), and description.
Styles not showing up?
- Make sure the <style> block is outside the <BaseLayout> component tags (below </BaseLayout>), not inside them.
- Check that you have the closing </style> tag at the very end of the file.
- Try a hard refresh with Cmd+Shift+R (macOS) or Ctrl+Shift+R (Windows) to clear cached styles.
Still stuck? Why aren't my Astro scoped styles rendering?
✔️ Awesome, I've got everything!
Great. Double check you have saved both src/layouts/BaseLayout.astro and src/pages/index.astro.
ⓧ I'd like to double check the full code
Your src/pages/index.astro should contain these sections in order (scroll up to confirm each is present):
- Frontmatter with BaseLayout import, getCollection import, and posts sorted by date.
- Template with <BaseLayout> wrapping a .hero header and a .posts section using posts.map().
- A <style> block with rules for .hero, .hero h1, .hero p, .posts h2, .post-card, .post-card h3, .post-card time, and .post-card p.
Your src/layouts/BaseLayout.astro should contain:
- Frontmatter with a Props interface accepting title.
- A full HTML document with <slot /> inside a .container div.
- A <style is:global> block with resets, body typography, container centering, and link colors.
Your src/content.config.ts should export a collections object with a blog key using glob() and a Zod schema requiring title, description, and pubDate.
Your homepage is live with a hero introduction and a blog post listed below it. Next up, you will make those post links actually work by creating a dynamic route that renders each post's full content.
Add the Blog Detail Page
Your homepage now shows blog post cards with clickable titles. But clicking one leads to a dead end because no page exists at that URL yet.
Astro uses file-based routing. A file inside src/pages/ automatically becomes a URL. For dynamic routes where one template generates many pages, you use bracket syntax in the filename.
In this step, get ready to:
- Create a dynamic route file with getStaticPaths() to generate one page per blog post.
- Render each post's full Markdown content with its title and publication date.
Create the dynamic route file
When you name a file with brackets like [id].astro, Astro treats it as a dynamic route. You export a getStaticPaths() function that tells Astro which pages to generate at build time.
- In the VS Code Explorer sidebar, right-click the src/pages folder and select New Folder.
- Name the folder blog.
- Right-click the new blog folder, select New File, and name it [id].astro.
- Add the frontmatter script section by pasting the following code into [id].astro:
---
import { getCollection, render } from "astro:content";
import BaseLayout from "../../layouts/BaseLayout.astro";
export async function getStaticPaths() {
const posts = await getCollection("blog");
return posts.map((post) => ({
params: { id: post.id },
props: { post },
}));
}
const { post } = Astro.props;
const { Content } = await render(post);
---
What does this code do?
- getStaticPaths() queries the blog collection and maps each post to a URL parameter. For a post with ID first-post, Astro generates the page at /blog/first-post.
- render(post) converts the Markdown body into a Content component you can drop into the template like any other Astro component.
- props: { post } passes the entire post object (including frontmatter data) to the page template via Astro.props.
- Now add the HTML template directly below the closing --- by pasting this code:
<BaseLayout title={post.data.title}>
<article>
<a href="/" class="back-link">Back to all posts</a>
<h1>{post.data.title}</h1>
<time datetime={post.data.pubDate.toISOString()}>
{post.data.pubDate.toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
})}
</time>
<div class="content">
<Content />
</div>
</article>
</BaseLayout>
What's happening here?
- The template wraps everything in your BaseLayout component, passing the post title as the page <title>.
- <Content /> renders the full Markdown body as HTML. This is where your blog post's headings, paragraphs, and code blocks appear.
- The "Back to all posts" link gives readers a way to return to the homepage.
- Save [id].astro.
- Switch to your browser and click the "Building My Blog with Astro" link on the homepage.
You should see the full blog post content at localhost:4321/blog/first-post. The text is unstyled for now, but you can read the title, date, and Markdown body.
Seeing a 404 page?
- Confirm the file is at src/pages/blog/[id].astro. The brackets in the filename are literal characters, not placeholders.
- Check that the getStaticPaths() function is exported (it must have the export keyword).
- Make sure your dev server is still running. If it crashed, run npm run dev again in the terminal.
Help me debug my Astro dynamic route at /blog/first-post returning a 404.
Style the blog post page
The content renders but looks cramped and unformatted. Scoped styles will give the page proper spacing and typography.
- In [id].astro, add this style block below the closing </BaseLayout> tag:
<style>
.back-link {
display: inline-block;
margin-bottom: 1.5rem;
font-size: 0.9rem;
}
h1 {
font-size: 2rem;
margin-bottom: 0.5rem;
}
time {
font-size: 0.85rem;
color: #9ca3af;
}
.content {
margin-top: 2rem;
}
.content :global(h2) {
font-size: 1.4rem;
margin-top: 2rem;
margin-bottom: 0.75rem;
}
.content :global(p) {
margin-bottom: 1rem;
}
.content :global(code) {
background-color: #f3f4f6;
padding: 0.15rem 0.4rem;
border-radius: 0.25rem;
font-size: 0.9em;
}
</style>
What does :global() do?
Astro scopes styles to the component by default. The Markdown content rendered by <Content /> generates HTML that lives outside your component's scope.
The :global() selector tells Astro to apply the style to any matching element inside .content, even if it was generated by a child component like the Markdown renderer.
- Save [id].astro and refresh the blog post page in your browser.
The page now has clean typography with a subtle back link, a large title, a muted date, and well-spaced Markdown content below.
Markdown headings and paragraphs still unstyled?
- Make sure your selectors use .content :global(h2) (with a space between .content and :global). Without the space, the selector targets a different element.
- Confirm the <Content /> component sits inside a <div class="content"> wrapper in your template. The styles target children of that wrapper.
Help me fix my :global() styles not applying to rendered Markdown content in Astro.
Verify the full blog post flow
Before you click, predict: what URL do you expect to see in the browser when you click the post link from the homepage?
- Navigate back to localhost:4321 (the homepage).
- Click the "Building My Blog with Astro" post title.
You land on localhost:4321/blog/first-post and see the full blog post with its title, the formatted publication date, the "Why Astro?" and "What I learned" headings, and all the Markdown body content.
- Click the "Back to all posts" link at the top of the blog post to confirm it takes you back to the homepage.
✔️ Awesome, I've got everything!
Great. Double check you've saved [id].astro and the navigation between homepage and blog post works in both directions.
ⓧ I'd like to double check the full code
Your src/pages/blog/[id].astro file is built from the three chunks above. Scroll up and confirm each section is present in this order:
- The frontmatter section (between --- fences) with imports, getStaticPaths(), and the render() call.
- The HTML template with <BaseLayout>, the back link, title, date, and <Content />.
- The <style> block with selectors for .back-link, h1, time, and the .content :global() rules.
Your blog now has working navigation between the homepage and individual posts. Next up, you will connect a web-based CMS so you can create and edit posts without touching code.
Connect Pages CMS
Right now, adding a new blog post means writing a Markdown file by hand, committing it, and pushing to GitHub.
Anyone without code access cannot contribute. Pages CMS is a free, open-source web editor that commits Markdown files directly to your repository. You configure it with a single YAML file, and from that point on you can create and edit posts from a browser without ever touching Git.
In this step, get ready to:
- Create a .pages.yml configuration file that tells Pages CMS about your blog collection.
- Push the config to GitHub and connect Pages CMS to your repository.
- Create a new blog post through the CMS editor and verify it lands in your repo.
Create the Pages CMS config file
Pages CMS reads a single .pages.yml file at the root of your repository to learn what content it should manage and where to store media uploads.
- In VS Code, create a new file called .pages.yml in the root of your my-blog project (the same level as astro.config.mjs).
- Paste the following configuration into the file:
media:
input: public/media
output: /media
content:
- name: blog
label: Blog Posts
type: collection
path: src/content/blog
filename: "{fields.title}.md"
fields:
- name: title
label: Title
type: string
- name: description
label: Description
type: string
- name: pubDate
label: Publication Date
type: date
- name: body
label: Body
type: rich-text
What does this config do?
- media tells Pages CMS to store uploaded images in public/media and serve them from the /media path.
- content defines a collection called blog that points at src/content/blog. This matches the folder your Astro content collection already reads from.
- filename: "{fields.title}.md" means new posts get their filename from the title field you enter in the editor, with a .md extension.
- The fields list mirrors the Zod schema in src/content.config.ts so the CMS editor shows the right inputs for each frontmatter property.
- Save .pages.yml.
- Confirm the file exists by running:
ls .pages.yml
You should see .pages.yml printed in the output.
Don't see .pages.yml?
- Make sure you created the file in the project root (same folder as package.json), not inside src/.
- Files starting with a dot are hidden by default on macOS. Run ls -a to show hidden files.
- help me find my .pages.yml file
✔️ Awesome, I've got everything!
Great. Make sure you have saved the file before moving on.
ⓧ I'd like to double check the full code
media:
input: public/media
output: /media
content:
- name: blog
label: Blog Posts
type: collection
path: src/content/blog
filename: "{fields.title}.md"
fields:
- name: title
label: Title
type: string
- name: description
label: Description
type: string
- name: pubDate
label: Publication Date
type: date
- name: body
label: Body
type: rich-text
Push to GitHub and connect Pages CMS
Pages CMS reads the .pages.yml directly from your GitHub repository. You need to push it before the CMS can detect your blog collection.
- Commit and push the config file by running these commands in your terminal:
git add .pages.yml
git commit -m "Add Pages CMS config"
git push
You should see Git confirm the push to your main branch.
Push rejected or authentication error?
- If Git asks for credentials, follow the GitHub authentication prompt that appears. On macOS you may see a browser pop-up. On Windows, a credential manager window may open.
- If you see "rejected" in the output, run git pull --rebase first, then try git push again.
- help me push my code to GitHub
Now you will connect the hosted Pages CMS app to your repository.
- Open your browser and go to app.pagescms.org.
- Click the button to sign in with GitHub.
- GitHub will ask you to authorize Pages CMS. Click **Authorize** to continue.
What is Pages CMS asking for?
Pages CMS installs a GitHub App on your account that gives it read and write access to repository contents. It edits files in your repository directly. There is no separate CMS database for content.
- When prompted to install the GitHub App, click **Install**.
- Select your my-blog repository (or choose "All repositories" if you prefer).
- Once installed, Pages CMS shows your repository. Click it to open the editor.
- You should see **Blog Posts** listed in the sidebar. This confirms Pages CMS read your .pages.yml config successfully.
Time to test the full workflow by creating a new post through the CMS.
- Click **Blog Posts** in the sidebar.
- Click the button to create a new entry.
- Fill in the fields:
- Enter My Second Post in the **Title** field.
- Enter A post created entirely from the CMS. in the **Description** field.
- Set today's date in the **Publication Date** field.
- Type a few sentences of body text in the **Body** editor. Anything you like.
- Click **Save** to commit the new file to your repository.
Before you check, what do you think happened in your GitHub repository when you clicked Save?
- Open your GitHub repository in a browser and navigate to the src/content/blog/ directory.
You should see a new Markdown file (named after your post title) alongside first-post.md. Pages CMS committed it directly to your main branch.
Don't see the new file in GitHub?
- Make sure you clicked **Save** in the Pages CMS editor (not just filled in fields).
- Refresh the GitHub page. It does not auto-update.
- Confirm you installed the GitHub App on the correct account and selected the my-blog repository during installation.
- help me troubleshoot Pages CMS not committing to my repo
💡 Why Pages CMS over editing files manually?
You just created a blog post without writing any frontmatter, without opening a terminal, and without running a single Git command. Anyone with access to the CMS can do the same.
Because Pages CMS stores content as flat files in your Git repo, there is no external database to maintain and no vendor lock-in. If you ever stop using the CMS, your Markdown files remain exactly where Astro expects them.
Your blog now has a proper content management workflow. Next up, you will deploy the whole site to the internet so anyone can read it.
Deploy to Vercel
Your blog works locally and your content management workflow is ready. But right now, only you can see it.
Deploying to Vercel gives your blog a public URL that anyone can visit. It also sets up automatic redeployments, so every time Pages CMS commits a new post to GitHub, Vercel rebuilds and publishes the updated site within seconds.
In this step, get ready to:
- Import your GitHub repository into Vercel and deploy your blog to a public URL.
- Test the full content pipeline from Pages CMS to live site.
Import your repository and deploy
Vercel auto-detects Astro projects and configures the correct build settings for you. No adapter or extra configuration needed for a static site.
- Open your browser and go to vercel.com.
- Sign in with your GitHub account.
- On the Vercel dashboard, click the Add New… button and select Project.
- You will see a list of your GitHub repositories. Find your blog repository and click Import.
- On the configure project screen, Vercel detects Astro and pre-fills the build settings. Leave everything as-is.
- Click Deploy.
What is Vercel doing?
Vercel clones your repository, runs the Astro build command, and uploads the resulting static HTML and CSS files to its global CDN. Because Astro ships zero JavaScript by default, your blog loads almost instantly for visitors anywhere in the world.
Vercel also installs a webhook on your GitHub repository. Every future push to the main branch triggers a fresh build and deploy automatically.
The deployment takes about 30-60 seconds. Once it finishes, Vercel shows a success screen with a preview of your live site.
- Click the deployed URL (it ends in .vercel.app) to open your live blog in a new tab.
You should see your homepage with the hero section, your blog post titles, and the dates. Click a post title to confirm the detail page works too.
Don't see your repository in the list?
If your repository does not appear, Vercel may not have permission to access it. Click Configure GitHub App on the import screen and make sure the repository is included in the list of accessible repos.
If you still cannot find it, confirm you pushed all your code to the main branch on GitHub.
Help me troubleshoot why Vercel cannot find my GitHub repository.
Test the full content pipeline
The real power of this setup is the loop: write in Pages CMS, the CMS commits to GitHub, and Vercel picks up the change and redeploys. Let's prove the whole chain works end-to-end.
- Switch back to app.pagescms.org in your browser.
- Select your blog repository.
- Click to create a new blog post.
- Fill in a title, description, and a short body. Use any content you like.
- Click Save in the CMS editor.
Pages CMS commits a new Markdown file directly to your GitHub repository's main branch. That push triggers Vercel to rebuild.
- Wait about 30 seconds for the redeployment to complete.
Before you check the live site, what do you expect to see on the homepage now?
- Open your .vercel.app URL and refresh the page.
Your new post appears in the list alongside the first post. The entire pipeline worked: CMS to GitHub to Vercel to live site, with no manual steps in between.
Why is this powerful?
You now have a continuous deployment pipeline. Anyone with access to your Pages CMS can publish content without knowing Git, running terminal commands, or touching code. The site rebuilds itself automatically.
🙋♀️ New post not appearing after 30 seconds?
Open your GitHub repository in a browser and check if the new Markdown file exists in src/content/blog/. If it is missing, the CMS save did not commit successfully. Try saving again.
If the file is in GitHub but the site has not updated, go to your Vercel dashboard and check the Deployments tab. Look for a recent deployment triggered by the new commit. If it failed, click into the deployment to see the build logs.
Help me figure out why my Vercel site is not redeploying after a Pages CMS commit.
Your blog is live on the internet with a fully automated publishing workflow. Next up, the Secret Mission challenges you to add an RSS feed so readers can subscribe to your posts.
Secret mission
Add an RSS Feed
Your blog looks great, but how do readers know when you publish something new? In this secret mission, you will add an RSS feed so anyone can subscribe and get notified of new posts automatically.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely on free services, so there are no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- Vercel project (free tier, no cost for static sites).
- GitHub repository (free).
- Pages CMS GitHub App installation (free, no data stored outside your repo).
- Local my-blog project folder on your Desktop.
Keep everything running
No action needed. Choose this if you are still actively building or want to keep your blog live.
- Your Vercel site stays live indefinitely on the free Hobby plan with no charges.
- Pages CMS has no running costs because it uses your GitHub repo as the database.
- Your GitHub repository remains free regardless of activity.
Pause - I'll come back to this later
Shut down any running local processes to free up memory. Your live site and repository stay intact for when you return.
- If the Astro dev server is still running in your terminal, press Ctrl+C to stop it.
- Close the my-blog project in VS Code.
- Your Vercel deployment and GitHub repository remain active at no cost. You can pick up where you left off anytime.
Delete - I don't want to use this again
Remove all project resources and start fresh. Follow each step below to fully clean up.
- Delete the Vercel project: open the Vercel dashboard, select your blog project, click Settings in the sidebar, scroll to the bottom of the General page, and click Delete in the Delete Project section. Confirm by entering the project name and clicking Continue.
- Delete the GitHub repository: go to your repository page on GitHub, click Settings, scroll down to the Danger Zone section, and click Delete this repository. Confirm by typing the repository name and clicking Delete this repository.
- Uninstall the Pages CMS GitHub App: on GitHub, click your profile picture, then Settings. Under Integrations, click Applications. Click Installed GitHub Apps, find Pages CMS, click Configure, then click Uninstall.
- Delete the local project folder by running this command in your terminal:
rm -rf ~/Desktop/my-blog
On Windows, run this instead:
Remove-Item -Recurse -Force ~\Desktop\my-blog
That's a Wrap!
That's a Wrap!
Nice work! You've built a fully functional personal blog powered by Astro that ships zero JavaScript to the browser, managed through a web-based CMS, and deployed live on Vercel.
You've learned how to:
- Built a multi-page static site with Astro that renders Markdown content into fast, JavaScript-free HTML pages using content collections and dynamic routes.
- Connected Pages CMS as a web-based content editor that commits Markdown files directly to your GitHub repository, letting anyone publish without touching code.
- Deployed to Vercel with a continuous deployment pipeline that automatically rebuilds your live site every time new content is pushed to the main branch.
- Secret Mission: Added an RSS feed using the official @astrojs/rss integration so readers can subscribe to your blog.
Ready to quiz yourself?