[SKIP TO CONTENT]
LAST_UPDATE: 30 September 2026
Go back
BY Junrong Li

> Building a Personal Blog with Astro and Decap CMS

I wanted a personal blog that is fast, cheap to host, and still lets me write in a proper editor instead of pushing Markdown files by hand. This is the stack I ended up with, and the steps that actually worked.

The stack

Writer → yoursite.com/admin (Decap CMS)
          ├─ login: GitHub, brokered by Netlify
          └─ "Publish" commits a Markdown file to GitHub
GitHub push → Hostinger runs npm run build → serves dist/

Every time I publish a post, it becomes a Git commit, and every commit triggers a rebuild. No database, no server to patch.

Step 1: Start from an Astro blog template

Pick an Astro blog template, copy it into a new folder, and get it running locally:

npm install
npm run dev

Before going further:

Run npm run build once to make sure everything compiles, then push the project to a GitHub repository. A private repository works fine.

Step 2: Add Decap CMS

Decap is just two static files in public/admin/.

public/admin/index.html loads the editor:

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta name="robots" content="noindex" />
    <title>Content Manager</title>
  </head>
  <body>
    <script src="https://unpkg.com/decap-cms@^3.0.0/dist/decap-cms.js"></script>
  </body>
</html>

public/admin/config.yml tells Decap where the content lives. The fields must match your template’s frontmatter:

backend:
  name: github
  repo: your-github-user/your-repo
  branch: main
  base_url: https://api.netlify.com
  site_domain: your-auth-site.netlify.app

media_folder: public/images/blog
public_folder: /images/blog

collections:
  - name: blog
    label: Blog
    folder: src/data/blog
    create: true
    slug: "{{slug}}"
    fields:
      - { name: title, label: Title, widget: string }
      - { name: description, label: Description, widget: text }
      - { name: pubDatetime, label: Published, widget: datetime }
      - { name: tags, label: Tags, widget: list, default: ["others"] }
      - { name: draft, label: Draft, widget: boolean, required: false }
      - { name: body, label: Body, widget: markdown }

One gotcha: Decap may save dates as strings, while Astro content schemas often expect a real date. In src/content.config.ts, use z.coerce.date() instead of z.date() for date fields, or the first post saved from the CMS can break the build.

Step 3: Let Decap log in with GitHub (via Netlify)

Decap needs a GitHub token to commit on your behalf, and something has to do the OAuth exchange. Netlify offers this as a free service, even for sites hosted elsewhere.

Create a placeholder Netlify site. On Netlify, use “Upload your project files” and drop in a folder containing any index.html. Nothing is really served from it. Put its xxx.netlify.app name in site_domain above. Don’t use Netlify Identity or git-gateway: Identity is deprecated, and git-gateway only makes sense if the site is hosted on Netlify.

Create a GitHub App, not an OAuth App. A classic OAuth App asks for access to all your repositories. A GitHub App can be installed on just the blog repository:

Connect them. In the Netlify site, go to Site configuration → Access & security → OAuth → Install provider → GitHub, and paste the GitHub App’s Client ID and secret.

Add your real domain to the Netlify site. This is the step that cost me the most time. After you authorize on GitHub, the login popup says “Authorized” and then just sits there. The reason: Netlify only sends the token back to the primary domain of the Netlify site. Add your blog’s domain in the Netlify site’s domain settings and make sure the exact host you open /admin on is the primary domain. You don’t need to move your DNS to Netlify.

If Netlify refuses to make your www host primary because DNS isn’t verified, a workaround is to temporarily point the www CNAME at your-auth-site.netlify.app, click “Set as primary domain” once it verifies, then point DNS back to your real host. Netlify keeps the setting.

Step 4: Deploy on Hostinger

In hPanel, add a Node.js Web App, import the GitHub repository, and use:

Turn on automatic deployment on push. From now on, every commit (yours or Decap’s) rebuilds the site.

Then connect your domain and choose one canonical host. I use www. Hostinger serves the site with LiteSpeed, which reads .htaccess, so the redirect can live in the repository as public/.htaccess:

RewriteEngine On
RewriteCond %{HTTP_HOST} ^yoursite\.com$ [NC]
RewriteRule ^(.*)$ https://www.yoursite.com/$1 [R=301,L]

Whatever host you choose, it must match the Netlify primary domain from Step 3, or the CMS login breaks again.

Step 5: Write, and keep Git in sync

Open https://www.yoursite.com/admin/, log in with GitHub, and publish. The post appears on the site a minute or two later.

Because Decap commits straight to main, your local copy falls behind whenever you publish. If you later change the template locally, git push will be rejected until you pull. I set Git to rebase on pull:

git config pull.rebase true

My rule is simple: posts and uploaded images are only edited in Decap, and code is only edited locally. The two never touch the same files, so git pull then git push never conflicts.

Finishing touches worth doing

Wrapping up

The result is a blog with no database and no server to maintain, a proper editor for writing, and every post versioned in Git. The only moving part that surprised me was the login handshake, and that comes down to one rule: the domain you open /admin on must be the primary domain of the Netlify site that brokers the login.

TAGGED_NODES: