In May 2026, I migrated this site, which I had run on WordPress for many years, to a setup built with Sanity and Astro. It is now hosted on Cloudflare Pages.
It was not that I had any fatal complaints about WordPress. If all you want to do is write and publish articles, it is perfectly convenient, and even a free theme such as Cocoon comes with most of the features you need.
Over time, however, plugin updates, backups, security measures, and notifications about suspicious access being blocked gradually became a burden. The site also felt a little slow. Whenever I wanted to fine-tune the design or page structure, I had to dig into how the theme worked.
After various twists and turns, I decided to try turning my personal site into something closer to an old-fashioned homepage.[1] As I worked on it, I realized that shaping WordPress into exactly what I had in mind was surprisingly difficult. If I was going to build elaborate static pages, perhaps it would be easier to write the source code directly. My images were in such a mess that I could no longer tell what was going on, and I certainly could not remember where I had put all my custom PHP. I wanted to return the entire site to a simple state that I could understand for myself. Like the old days, when I made HTML files and uploaded them by FTP, I wanted a setup where I could see both the contents of the site and the path they took to publication.
That was how the migration began.
This article covers what I actually did, from choosing the technology and migrating the WordPress articles to designing the URLs, switching the production site, and moving the mail server. It is a record of the decisions I made—and the things I had to watch out for—while migrating a personal site.
The main goal: reduce the maintenance and security burden
My primary goal in rebuilding the site was to reduce the maintenance work and security concerns that came with WordPress. At the same time, I wanted to bring information about all my activities together in one place and make it easier to understand the state of my own content.
In WordPress, both posts and static pages are inevitably shaped by the conventions of the theme and plugins. This site contains not only ordinary articles, but also artworks and experimental projects, series, and logs that connect content published on external services. Rather than treating all of these as merely different things inside a blog, I wanted to give each of them a form that would be easier to manage.
So I decided to try a trendy, up-to-date idea: a headless CMS setup that separates content management from the way the content is displayed as web pages.
The final setup
The migrated site has roughly the following structure:
Sanity (manages articles, images, and taxonomies)
↓ fetched with GROQ at build time
Astro (static pages are written directly;
article pages are generated from Sanity)
↓ outputs static HTML
Cloudflare Pages (hosting, custom domains, and redirects)The code is managed on GitHub, and updates to the master branch are deployed to Cloudflare Pages. I separated out the contact form and email as well: the form uses Formspree, while email is hosted with Sakura Mailbox.
The public site consists primarily of static HTML. WordPress and a database do not have to run every time someone opens a page. This is close to what I wanted: a site that is simple under the hood, quick to display, and has a smaller attack surface.
Comparing microCMS and Sanity—and choosing Sanity
When considering headless CMS options, I initially compared microCMS and Sanity.
microCMS has a Japanese interface and plenty of information aimed at users in Japan, so it looked approachable for a first-time user. Sanity, on the other hand, offers highly flexible schemas and lets you structure article bodies with Portable Text. You can also customize the management interface itself in code.
The biggest reason I ultimately chose Sanity was that its free tier appeared more than sufficient for a personal site. The development work looked somewhat more difficult, but I started with a casual “I’ll probably figure it out somehow” attitude.
I did not see the English-language management interface as much of a problem either. AI could help me along, and I thought it might even be a chance to improve my English.
Once I started using Sanity, I found it useful that I could define separate data types for articles (log), static pages (page), categories, tags, series, projects, and publishing platforms. Instead of adapting my content to a ready-made blog structure, I could decide relatively freely what structure the site itself needed.
I chose Astro because I wanted a static site
My knowledge of web development had more or less stopped around the era of Movable Type and early WordPress, so part of me was simply curious to see how modern websites were built. In practical terms, however, I chose Astro because I wanted a static site.
For a site centered on articles, there is no need for a server to assemble every page on every visit. The pages can be generated as static HTML in advance and then delivered as they are. Static sites are fast, their public-facing setup is simple, and compared with WordPress, there is much less to worry about from a security perspective.
Sanity, Astro, and the migration scripts could all be handled within the JavaScript/TypeScript and Node.js ecosystem. The WordPress XML conversion, Sanity schemas, and Astro components could therefore be connected using broadly the same set of tools.
I was also able to keep the production build entirely static while using Sanity’s preview features locally only when I needed them.
Bringing over the WordPress posts—and reorganizing them
For this migration, I did more than simply copy the WordPress articles into another CMS. I reorganized the data structure as well.
At first, I separated ordinary articles into a log type and static pages into a page type in Sanity. I later moved the static pages to Astro itself; see “A note on static pages: moving them to Astro midway through the project” below.
I also reorganized not only the standard WordPress categories and tags, but the custom taxonomies I had registered, turning them into independent data types.
log
├── category
├── tags[]
├── series[]
├── projects[]
└── platforms[]This means an article can hold references indicating that it belongs to a particular project, forms part of a series, and relates to a particular platform.
You can build a similar structure with custom taxonomies in WordPress. In Sanity, however, I found the relationships simpler and the consequences of deleting or editing an item easier to contain.
A note on static pages: moving them to Astro midway through the project
Early in the migration, my design was to manage both ordinary articles (log) and static pages (page) in Sanity. As the work progressed, however, I found that pages such as About and Projects were easier to handle directly in Astro. For these pages, I wanted to edit not just a body of text in an article-like format, but the structure and presentation of the entire page.
I therefore converted the static-page content stored in Sanity back into HTML and wrote it out as Astro files such as about-this-site.astro and projects.astro. At the same time, I removed the process that read page documents from Sanity and dynamically generated static pages from them. The pages generated from Sanity are now mainly log articles and taxonomy pages.
Today, pages such as About and Projects have their content and layouts written in an HTML-like form directly inside Astro files. Updating them requires a build and deployment through GitHub, but because they do not change as often as articles, this is not a major inconvenience. If anything, being able to survey the entire page as source code has brought the site one step closer to the “old-fashioned homepage” style of management I had originally imagined.
Converting WordPress XML into data for Sanity
I exported the data from WordPress as XML and used Node.js scripts to convert it into NDJSON that could be imported into Sanity.
The overall process was as follows:
- Export the articles and taxonomy data from WordPress.
- Extract posts, static pages, categories, and other data from the XML.
- Convert the body HTML into Portable Text, images, HTML embeds, and other appropriate forms.
- Connect articles and taxonomies using Sanity references.
- Generate the NDJSON.
- Back up Sanity’s production dataset, then import the data.
- Check the item counts and inspect a selection of representative articles.
The migration included 167 published posts. There were 166 main images, 499 body images, and 625 image URLs; by the end, there were no unresolved images. The source material also included 115 Moshimo affiliate embeds and 20 tables.
I did not inspect every visual detail. Instead, I established conversion rules, processed the material in bulk, and checked the parts most likely to cause problems. Reviewing every old article would have taken a very long time, so I decided to deal with smaller issues gradually as I encountered them.
Not every WordPress-specific element needed a perfect cleanup
Fortunately, the old site did not make heavy use of blocks or shortcodes tied to a particular WordPress theme. My priority was therefore to migrate everything into a simple form that remained readable as information.
Rather than scrutinizing every article one by one, I focused on major articles and those that used unusual blocks. I left the rest alone for the time being, even if their presentation became somewhat simpler, as long as the text remained readable and the images and links worked.
Ordinary body content was converted to Portable Text, while images were handled as Sanity assets. Affiliate elements, tables, and other material that was difficult to structure were separated into htmlEmbed blocks. This allowed article-specific HTML to remain where necessary, while removing script elements and event-handler attributes.
Trying to convert everything into immaculate Portable Text could have erased expressions or layouts unique to particular articles. Keeping all the WordPress HTML without question, on the other hand, would have carried over theme dependencies and unnecessary code.
The important thing was to distinguish between what should be structured, what should remain as limited HTML, and what should be removed.
Keeping article URLs unchanged wherever possible
For individual articles, I retained the same /{slug}/ URLs used by the old WordPress site. I reorganized the article archive and taxonomy pages under /nonfiction/, but set up redirects from their old URLs.
/category/tools/ → /nonfiction/category/tools/
/blog/ → /nonfiction/
/feed/ → /rss.xmlAlong with changing the routes, I updated the redirects, canonical URLs, Open Graph URLs, internal links, and XML sitemap as a single set of related tasks. Japanese slugs and percent-encoded URLs were also included in the checks.
My priority was to avoid breaking existing search results, external links, and bookmarks as much as possible.
Hosting the site on Cloudflare Pages
I chose Cloudflare Pages as the hosting platform. Once again, a major reason was that the free tier seemed sufficient for a personal site. Even if I eventually needed a paid plan, there were still options suitable for a small-scale site.
Cloudflare Pages can publish the dist output generated by Astro as-is. It also provides GitHub integration, preview deployments, custom domains, and edge-level redirects.
Rather than switching the production domain immediately, I first deployed the site to a pages.dev preview URL. I checked the homepage, articles, static pages, RSS, robots.txt, the sitemap, redirects, and the contact form before connecting oneoffobject.com and www.oneoffobject.com.
Moving the mail server required more caution than moving the website
The email migration was more complicated than the website migration.
Before the move, ColorfulBox managed both the web server and the mail server around the same domain. The domain used by my print-on-demand shop also relied on ColorfulBox for email. I could not simply switch the website first and risk breaking the DNS records used by email.
I therefore treated the production website switch and the email migration as separate tasks, moving the mailboxes to Sakura Mailbox first. The reason for that choice was also straightforward: it was inexpensive, at roughly ¥1,000 per year.
I organized the MX, SPF, DKIM, DMARC, and Search Console verification TXT records. Only after confirming that I could both send and receive email did I switch the website’s DNS to Cloudflare.
The approximate timeline was as follows:
May 4 Began considering the migration
May 12 Moved email to Sakura and confirmed sending and receiving
May 13 Switched the production domain to Cloudflare Pages
May 17 Marked the migration project complete and took WordPress offline
Through around May 20 Continued checking and adjusting detailsWhat I checked after the build succeeded
A successful Astro npm run build was not enough to put my mind at ease.
I inspected the generated HTML in dist and opened representative articles, static pages, taxonomy pages, and project pages. I also checked RSS, robots.txt, the XML sitemap, canonical URLs, and redirects from the old URLs.
For the contact form, I submitted an actual message and confirmed that it reached the completion page, that the notification email arrived, and that its Reply-To address was set to the email entered by the sender. After the DNS switch, I checked both the website and email once again.
Publishing an article in Sanity does not directly rewrite the static HTML already online. On this site, however, a Sanity webhook calls a Cloudflare Pages Deploy Hook. Publishing therefore triggers Cloudflare Pages to rebuild and deploy the site automatically. The update normally reaches production after a few minutes, so there is no need to run a build manually. In day-to-day use, updating the site is as simple as publishing from my local Sanity Studio.
The actual cost and timeline
I started considering the migration on May 4, made the main production switch on May 13, and marked the project complete on May 17. I continued adjusting smaller details afterward, and things appear to have mostly settled down by around May 20.
The only new fixed cost was roughly ¥1,000 per year for Sakura Mailbox. Sanity and Cloudflare Pages are currently operating within their free tiers.
As of August 2026, Sanity’s free tier includes 10,000 documents, 100 GB of asset storage, 250,000 API requests per month, and 100 GB of data transfer per month. On the Cloudflare Pages free plan, requests and bandwidth for static assets are unlimited, with up to 500 builds per month. This site currently uses around 200 Sanity documents and approximately 126 MB of assets, while its API usage remains below ten percent of the monthly limit. For a personal site updated at a rate of roughly one article per day, both free tiers provide plenty of room. If paid plans eventually become necessary, Sanity starts at $15 per month for a single-person operation, while the Cloudflare Pro plan, which increases the build allowance, starts at the equivalent of $20 per month.
Realistically, this site is very unlikely to require either paid plan. Unless the services change their terms significantly, I should be able to keep it running for little more than the domain fee. As a very small independent business, I am extremely happy about that.
Unfortunately, the timing was not ideal: I had only just paid the annual ColorfulBox renewal fee around March, so I did not cancel it immediately. There would be no prorated refund if I cancelled now anyway. I have no plans to use it, but I will leave it in place for the time being and cancel before the next renewal. After that, I will no longer have to pay for web hosting. Hooray.
Special thanks: Codex
Codex did a great deal of work during this migration. It was especially helpful with labor-intensive tasks such as converting the XML, modifying the code, and organizing the checklist.
That said, it was not a matter of handing everything to AI and watching the project finish itself. Without at least some knowledge on the human side, it is difficult to understand what is wrong or give useful instructions. A site might appear to display correctly while its URLs or email are still broken.
If you are not concerned about the details, delegating most of the work might produce something functional. The further I went, however, the more I found myself caring about canonical URLs, redirects, embeds, the usability of the management interface, tiny differences in font sizes, margins, and so on. The final decisions and checks still needed to remain with the human operator.
Updating articles after the migration
Writing articles in Sanity Studio was a little unfamiliar at first, but I soon got used to it.
Even after the migration, I have had to fix bugs in the article editor and customize its input interface. Today, however, I can paste Markdown written in Obsidian or another editor directly into Sanity and convert headings, lists, footnotes, callouts, and other elements into Portable Text.
Conclusion
I had naively assumed that this migration would be little more than replacing WordPress with Sanity, but it was not that simple. It took a great deal of work to inspect and separate, one by one, the information I had casually placed in WordPress and the responsibilities that my hosting provider had previously handled as a bundle.
Still, I am glad I did it.
After experimenting with a headless CMS, I found that this degree of freedom suits someone like me who tends to care about unnecessarily small details. The drawback of having so much freedom is that it makes me want to implement even more features.
Making the public site static has made it lighter and simpler, while the backend is easier to improve around my own workflow. Compared with the WordPress site, the barrier to publishing updates is now much lower.
That is the story of how I rebuilt my personal site, moving from WordPress to Sanity and Astro.
- Back to text
A note from when I decided to reorganize the site and experiment with GEO: GEO時代のほめぱげ — GEO対策にブログをホームページ化してみる|OneOffObject (in Japanese).
DID YOU ENJOY THIS ARTICLE?
制作活動を支援する / Support my work
文章、アート、デザイン、個人開発を続けるための支援を、コーヒー一杯分から受け付けています。 / Help me keep making essays, art, design, and independent software.

Information
