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
- Astro builds the site into plain static HTML.
- Decap CMS gives me an editor at
/admin. It stores every post as a Markdown file and commits it to GitHub. - GitHub holds the code and the content.
- Netlify is used only for one thing: handling the GitHub login for Decap. The site itself is not hosted there.
- Hostinger builds the site from GitHub and serves it (Or any other app hosting service like Netlify, Vercel).
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:
- Set your real domain as the site URL in the config (for AstroPaper:
websiteinsrc/config.ts). Sitemaps, RSS and canonical links are all built from it. - Delete the demo posts and images.
- Remove anything tied to another host, such as Vercel analytics.
- Keep a single lockfile. I use npm, so I deleted
pnpm-lock.yaml. - Check the Node version your Astro version needs (Astro 7 needs Node 22.12+), and pin it with
"engines"inpackage.jsonso the host uses the right one.
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:
- Callback URL:
https://api.netlify.com/auth/done - Uncheck Expire user authorization tokens. Decap can’t refresh tokens, so otherwise you get logged out every 8 hours.
- Repository permissions: Contents: Read and write (Metadata read-only is added automatically).
- Disable the webhook.
- Install the app on only your 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:
- Framework: Astro
- Build command:
npm run build - Output directory:
dist - Node version: 22 or later
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
- Favicon: an SVG favicon plus generated
favicon.ico,apple-touch-icon.png(180×180, solid background) and Android icons with a web manifest. A wide logo looks tiny in a browser tab, so a square variant (letters on a solid tile) reads much better. - Social preview images: make sure
og:imageis actually emitted. Many templates can generate one image per post at build time. - Structured data: mark posts as
BlogPostingwith real dates, and describe the site and yourself (WebSiteandPerson) on the homepage only. - Sitemap hygiene: keep search pages and disabled pages out of the sitemap, and mark them
noindex. - Google Search Console: add a Domain property and submit your sitemap.
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.