Skip to content

Migrate WordPress → Astro

Chuyển từ WordPress (PHP + MySQL) sang Astro (Static HTML) mang lại hiệu suất vượt trội, bảo mật cao hơn, và chi phí hosting thấp hơn. Đây là hướng dẫn chi tiết cho quá trình migration.

Cách 1: WordPress Export Tool

WordPress Admin → Tools → Export → All content
→ Tải về file XML

Cách 2: SQL Export (cho database lớn)

Terminal window
# Truy cập phpMyAdmin hoặc dùng WP-CLI
wp db export backup.sql
# Hoặc export chỉ posts
wp db export posts.sql --tables=wp_posts

Cách 3: REST API

Terminal window
# Lấy tất cả posts qua API
curl https://your-site.com/wp-json/wp/v2/posts?per_page=100 > posts.json
# Lấy pages
curl https://your-site.com/wp-json/wp/v2/pages > pages.json

Dùng tool wordpress-export-to-markdown:

Terminal window
# Install tool
npm install -g wordpress-export-to-markdown
# Convert
wordpress-export-to-markdown \
--input=export.xml \
--output=content \
--frontmatter

Hoặc dùng Python script:

import xml.etree.ElementTree as ET
import frontmatter
import os
def convert_wp_to_md(xml_file):
tree = ET.parse(xml_file)
root = tree.getroot()
# Namespace
ns = {'content': 'http://purl.org/rss/1.0/modules/content/'}
for item in root.findall('.//item'):
title = item.find('title').text
content = item.find('content:encoded', ns).text
# Create markdown with frontmatter
post = frontmatter.Post(content)
post['title'] = title
# Save
filename = f"{title.lower().replace(' ', '-')}.md"
with open(f'content/{filename}', 'w') as f:
f.write(frontmatter.dumps(post))
convert_wp_to_md('export.xml')

WordPress shortcodes cần chuyển thành Astro components:

WordPress:

[gallery ids="1,2,3"]
[button url="/contact" color="primary"]Contact Us[/button]

Astro Components:

src/components/Gallery.astro
---
const { ids } = Astro.props;
const imageIds = ids.split(',');
---
<div class="gallery">
{imageIds.map(id => (
<img src={`/images/${id}.jpg`} />
))}
</div>
src/components/Button.astro
---
const { url, variant = 'primary' } = Astro.props;
---
<a href={url} class={`btn btn-${variant}`}>
<slot />
</a>

Usage in Markdown:

import Gallery from '../components/Gallery.astro';
import Button from '../components/Button.astro';
<Gallery ids="1,2,3" />
<Button url="/contact" variant="primary">Contact Us</Button>

Khởi tạo với Starlight (cho content-heavy sites)

Section titled “Khởi tạo với Starlight (cho content-heavy sites)”
Terminal window
npm create astro@latest -- --template starlight
# Hoặc tự setup
npm create astro@latest wp-migration
cd wp-migration
npm install @astrojs/starlight
my-astro-site/
├── src/
│ ├── content/
│ │ ├── blog/ # Posts từ WordPress
│ │ │ ├── post-1.md
│ │ │ └── post-2.md
│ │ └── pages/ # Pages từ WordPress
│ │ └── about.md
│ ├── components/
│ │ ├── Gallery.astro
│ │ ├── Button.astro
│ │ └── WpContent.astro
│ └── layouts/
│ └── PostLayout.astro
├── public/
│ └── uploads/ # Media từ WordPress
└── astro.config.mjs

src/content/config.ts:

import { defineCollection, z } from 'astro:content';
const blogCollection = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
date: z.date(),
author: z.string().default('Admin'),
categories: z.array(z.string()).default([]),
tags: z.array(z.string()).default([]),
featuredImage: z.string().optional(),
excerpt: z.string().optional(),
}),
});
export const collections = {
'blog': blogCollection,
};
---
title: "Bài viết mẫu"
date: 2024-01-15T10:00:00Z
author: "Admin"
categories: ["Tutorial", "Web Dev"]
tags: ["astro", "wordpress"]
featuredImage: "/uploads/2024/01/image.jpg"
excerpt: "Tóm tắt bài viết"
---
Nội dung bài viết ở đây...

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

---
import { getCollection } from 'astro:content';
import PostLayout from '../../layouts/PostLayout.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();
---
<PostLayout frontmatter={post.data}>
<Content />
</PostLayout>

WordPress (header.php):

<!DOCTYPE html>
<html <?php language_attributes(); ?>>
<head>
<meta charset="<?php bloginfo('charset'); ?>">
<title><?php wp_title(); ?></title>
<?php wp_head(); ?>
</head>

Astro (Layout.astro):

---
const { title, description } = Astro.props;
---
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<title>{title}</title>
<meta name="description" content={description} />
<link rel="stylesheet" href="/styles/global.css" />
</head>
<body>
<Header />
<slot />
<Footer />
</body>
</html>

WordPress CSS thường có prefix .wp- hoặc dùng IDs. Clean up:

/* WordPress original */
#primary.content-area {
float: left;
width: 70%;
}
/* Astro version */
.content-area {
width: 70%;
}
@media (max-width: 768px) {
.content-area {
width: 100%;
}
}

WordPress dùng permalink structures khác nhau:

  • /?p=123 (default)
  • /2024/01/15/post-name/ (date-based)
  • /category/post-name/ (category-based)

astro.config.mjs:

export default defineConfig({
redirects: {
// Old WP URLs → New Astro URLs
'/?p=123': '/blog/bai-viet-moi',
'/2024/01/15/hello-world': '/blog/hello-world',
'/category/news/page/2': '/blog/page/2',
}
});

Hoặc dùng _redirects cho Netlify/Vercel:

public/_redirects
/?p=123 /blog/bai-viet-moi 301
/2024/* /blog/:splat 301

Thay Contact Form 7 bằng:

  1. Netlify Forms:
<form name="contact" netlify>
<input type="text" name="name" />
<input type="email" name="email" />
<textarea name="message"></textarea>
<button type="submit">Send</button>
</form>
  1. Formspree:
<form action="https://formspree.io/f/YOUR_ID" method="POST">
<!-- form fields -->
</form>
  1. Astro API Routes:
src/pages/api/contact.js
export async function POST({ request }) {
const data = await request.formData();
// Send email via SendGrid/Nodemailer
return new Response('OK');
}

Thay WordPress comments bằng:

  • Disqus: <script src="//site.disqus.com/embed.js"></script>
  • Giscus (GitHub Discussions): Free, open source
  • Utterances: GitHub issues-based
Terminal window
npm install @astrojs/sitemap
astro.config.mjs
import sitemap from '@astrojs/sitemap';
export default defineConfig({
site: 'https://yoursite.com',
integrations: [sitemap()],
});
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: 'Description',
site: context.site,
items: posts.map(post => ({
title: post.data.title,
pubDate: post.data.date,
description: post.data.excerpt,
link: `/blog/${post.slug}/`,
})),
});
}
Terminal window
npm run build
# Output trong thư mục dist/

1. Vercel (Recommended)

Terminal window
npm i -g vercel
vercel --prod

2. Netlify

Terminal window
npm i -g netlify-cli
netlify deploy --prod --dir=dist

3. Cloudflare Pages

  • Connect GitHub repo
  • Build command: npm run build
  • Output directory: dist

4. GitHub Pages

.github/workflows/deploy.yml
name: Deploy
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: withastro/action@v1
  • Kiểm tra tất cả internal links
  • Test forms hoạt động
  • Verify images load đúng
  • Check mobile responsive
  • Run Lighthouse audit (target: 90+)
  • Google Search Console: Submit new sitemap
  • Check for 404 errors
  • Monitor rankings
  • Tắt WordPress hosting (sau 1-2 tháng ổn định)
  • Backup WordPress files
  • Cancel WP plugins/services