WordPress-to-Craft CMS

Posted 3 days ago

In November, I began reacquainting myself with Craft CMS, a platform I first used a decade ago and always admired. Although I focused on WordPress for the past eight years, I wanted to refresh my Craft skills while exploring new job opportunities. I started with Ryan Irelan’s free Craft CMS 5 Quick-Start Guide on CraftQuest, which updated me on the changes from version 2 to 5. Eager to go deeper, I subscribed to CraftQuest for a month to take the Real World Craft CMS 5 course and apply my knowledge to a real-world project.

The best way to develop new web development skills is to simply “just build websites.” To deepen my Craft skills from CraftQuest, I chose to recreate this site by migrating it from WordPress to Craft on my local computer.

The Craft team published a Craft 101 page for WordPress developers interested in migrating their sites from WordPress into Craft. The first step for me was to install DDEV, which is the recommended local development environment for working with Craft.

DDEV requires a Docker provider on your system and I chose Lima because it was free and open source. After installing Lima, I installed DDEV using the easy to follow instructions provided in their documentation.

The Craft team created a WordPress Starter Project and a WP Import plugin to make it easy to migrate from WordPress to Craft. (The plugin is included in the starter project.) I followed the instructions of the Craft 101 page to install the plugin and to run the import. In under an hour, I had the content of my site and basic Craft templates to start building upon to recreate my site using Craft.

Note: Once you have walked through the process of installing the plugin and importing your content, the Craft 101 page gives you a basic overview of Craft (a tour). And then there is a very helpful chart (vocabulary) of WordPress terms and their Craft equivilent to help you get started.

Creating and modifying templates in Craft

Now that I had all of my content and assests imported, it was time to start building templates in Craft to display my content.

I decided to start templates for the posts and pages. The Starter Project already had some files set up for pages and posts so I just modified those templates. I copied code from my WordPress template, pasted it into the Twig template and refactored the PHP into Twig. It took me less than 30 minutes.

WordPress code:

<article class="post">
  <h1><?php the_title();?></h1>
  <?php the_content(); ?>
</article>

Craft code:

<article class="post">
  <h1>{{ entry.title }}</h1>
  {{ entry.postContent }}
</article>

The syntax for the Twig template variables for Craft is not much different than WordPress so it makes it pretty easy to make the connections to what I already know and am used to. Twig is very easy to learn. It did not take me long to pick it back up and then be able to make the conversion from my original template code in WordPress into the new Craft templates.

One thing that had changed in Craft since the last time I used it was that to display the content, the template variable had changed from {{ entry.body }} to {{ entry.postContent }}. I learned that a couple of weeks ago when I was trying to build another template based on an example that was from several years ago.

It was thrilling to be able quickly have most of the site content working after just thirty minutes of work.

Homepage template

My homepage template has two different queries: one to display “posts” and one to display “notes,” which is a custom post type I had created in WordPress for shorter thoughts and content that I would normally post on social media. 1

The query code for Craft is not much different from WordPress. The syntax is a bit more concise.

The query code for Craft:

{% set posts = craft.entries
  .section('posts')
  .orderBy('postDate DESC')
  .limit(9)
%}

Compared to a WordPress query:

<?php
  $args = array(
    'post_type' => 'posts',
    'posts_per_page' => 9,
    'orderby' => 'date',
    'order' => 'DESC',
  );
  $posts_query = new WP_Query($args);
?>

The code above is an example. In WordPress, we can query posts through the WordPress loop or in my case, I used a query_posts to set the number of posts to show to 9.

I have a lot of different conditionals in my post listing code. I have one to check if there is a cover image available. If a cover image is not provided, I have a default image to display. The other conditional in my post listing is looking for “weeknote” posts. If a post is a weeknote, I display a “Week of” with a date range that is entered when creating the post.

In WordPress, I used Advanced Custom Fields to create a field and only conditionally show it in the edit screen if the post was assigned to weeknotes category. I have another conditional to check to make sure that field has content. The WordPress Import did not carry over this field so I will have to go back and add a new field to Craft.

Here is a comparison of my the code in WordPress and the code in my Craft template.

WordPress:

<div class="latest-posts">
  <h2><span>Latest from My Blog</span><a href="https://jeffbridgforth.com/feed" class="feed-icon" aria-label="Blog Post RSS Feed"><svg></svg></a></h2>

<?php query_posts('showposts=9'); ?>
  <div class="post-listing grid">
  <?php while (have_posts()) : the_post(); ?>
    <article>
    <?php if (has_post_thumbnail()) :?>
      <figure><a href="<?php the_permalink() ?>" ><img src="<?php the_post_thumbnail_url() ;?>" alt="<?php the_title();?>" /></a></figure>
    <?php else: ?>
      <figure><a href="<?php the_permalink();?>"><img src="http://jeffbridgforth.com/wp-content/uploads/philip-swinburn-vS7LVkPyXJU-unsplash.jpg" /></a></figure>
    <?php endif; ?>
      <div class="date"><?php echo get_the_date(); ?></div>
      <h3><a href="<?php the_permalink(); ?>" rel="bookmark"><?php the_title(); ?></a></h3>
    <?php if(in_category('weeknotes')):?>
    <?php $week = get_field('week_of');?>
    <?php if($week):?>
      <div class="week-of"><?php the_field('week_of');?></div>
    <?php endif;?>
   <?php endif;?>
    </article>
  <?php endwhile; ?>
  </div>
  <p class="all-posts"><a href="/archive/">View All Posts</a></p>
</div>

Craft:

{% set posts = craft.entries
  .section('posts')
  .orderBy('postDate DESC')
  .limit(limit)
%}

<div class="post-listing grid">
{% for post in posts %}
  <article>
  {% set coverImage = post.coverPhoto %}
  {% if coverImage | length %}
    <figure><a href="{{ post.url}}"><img src="{{ coverImage.one.url() }}" alt="{{post.title}}" /></a></figure>
  {% else %}
    <figure><a href="{{ post.url}}"><img src="/uploads/philip-swinburn-vS7LVkPyXJU-unsplash.jpg" alt="{{post.title}}" /></a></figure>
  {% endif %}
    <div class="date"><time datetime="{{ entry.postDate|atom }}">{{ post.postDate|date }}</time></div>
    <h3><a href="{{ post.url() }}" rel="bookmark">{{ post.title }}</a></h3>
  </article>
{% else %}
  <p>No posts, yet!</p>
{% endfor %}
</div>

You might notice in my query for Craft that I am using a variable for the limit in the query. The post listing is a separate template (component). When I call the template in my homepage template, I define the limit in that template. This allows me to reuse the post listing template and set the limit of posts to show through the template call.

{% include "_partials/_post-listing" with {
  limit: 9,
}%}

I used a similar strategy with the notes listing, creating a separate template for the listing. That template uses a third template for the note card display.

Issue with featured image import

One issue that I did run into was that the featured images from my WordPress site did not make it into the new Craft site through the import. I use the featured image as my cover image in my post listing. I reached out to the Craft team through the Craft Discord channel. It is a bug and they appreciated me making them aware of it. They created a GitHub issue and it was fixed as of two weeks ago (at the time of writing this post). I appreciate the responsiveness of the Craft team and that there is a forum to interact with them.

Blog Archive template

The next big piece that I decided to tackle with the blog archive listing page. This page includes several different features that I needed to address:

  • The blog listing. I list all the blog entries grouped by year.
  • The heading for the page included the total number of posts
  • Search
  • Jump links to navigate to a specific year (reuses the query set up to group the posts by year)
  • A layout selector. I added this function last year that allows the user to switch from the default grid listing that includes an image to a simplified list that just includes the title and date of the posts.
The default grid layout
The simplified layout which is a linear list without cover images

Blog listing

I started with the default grid listing view which needed to group the posts by the year it was written. I have worked through Ryan Irland’s Building a Content Archive course on CraftQuest and learned several things that helped me build my template. He built a similar layout where posts were grouped by years and I was able to take his example and create my own template to meet the needs of my site. It was pretty easy to knock that task out.

Here is the simplified Craft code for the listing:

{% set allEntries = craft.entries.section('posts').all() %}
{% for year, entriesInYear in allEntries | group("postDate | date('Y')") %}
{% set yearCount = entriesInYear|length%}

<h2 id="year-{{ year }}">{{ year }} ({{ yearCount }} posts)</h2>

<div class="post-listing">
{% for entry in entriesInYear %}
  <article>...Post content goes here </article>
{% endfor %}
</div>
<ul class="archive-jump-links">
{% for year, entriesInYear in allEntries | group("postDate | date('Y')") %}
  <li><a href="#year-{{ year }}">{{ year }}</a></li>
{% endfor %}
</ul>

Heading with post count

This was pretty easy to pull off. I just needed to set a variable that equaled the length of the entries and then added the variable to my template.

{% set entries = craft.entries.section('posts') %}
{% set entryCount = entries|length %}
<h1>Blog Archive ({{ entryCount }} posts)</h1>

Layout selector

I built the layout selector functionality in WordPress by assigning a query string to the link for the inactive layout. I used $_GET to collect the value of the query string and then run some conditionals on the page to change the layout through both code structure and by adding classes to style those layouts differently. The challenge in recreating this in Craft was to convert my PHP into Twig.

It was actually much easier to accomplish in Craft with Twig 2. I was able to set a variable that captured the “layout” query string. And then I used that variable throughout the rest of my Twig template to write conditional code to change the layouts.

{% set layout = craft.app.request.getQueryParam('layout') %}
{% if layout %}
{% set format = 'block-listing' %}
{% else %}
{% set format = 'grid' %}
{% endif %}
<ul class="layout-selector">
  <li><a href="/archive" {% if format == 'grid'%}class="active"{% endif %}>Image Grid</a></li>
  <li><a href="/archive?layout=no-image" {% if format == 'block-listing' %}class="active"{% endif %}>Simplified View</a></li>
</ul>
<article>
{% if format == 'grid' %}
{% set coverImage = entry.coverPhoto %}
  {% if coverImage | length %}
  <figure><a href="{{ entry.url}}"><img src="{{ coverImage.one.url() }}" alt="{{entry.title}}" /></a></figure>
  {% else %}
  <figure><a href="{{ entry.url}}"><img src="/uploads/philip-swinburn-vS7LVkPyXJU-unsplash.jpg" alt="{{entry.title}}" /></a></figure>
  {% endif %}
{% endif %}

{% if format == 'grid' %}
  <div class="date"><time datetime="{{ entry.postDate|atom }}">{{ entry.postDate|date }}</time></div>
{% endif %}
  <h3><a href="{{ entry.url() }}" rel="bookmark">{{ entry.title }}</a></h3>
{% if format == 'block-listing' %}
  <div class="date"><time datetime="{{ entry.postDate|atom }}">{{ entry.postDate|date }}</time></div>
{% endif %}
</article>

The final thing I needed to address on the archive page with the search functionality. I had watched another short course that Ryan Irland did on search in Craft. The code for the search form is very similar to WordPress

WordPress code:

<form method="get" id="searchform" action="<?php bloginfo('home'); ?>/">
  <div>
    <input class="searchbox" type="text" value="<?php echo wp_specialchars($s, 1); ?>" name="s" id="s" size="15"/>
    <input type="hidden" name="post_type" value="post" />
    <input type="submit" id="searchsubmit" value="Search" />
  </div>
</form>

Craft code:

<div class="search">
  <form action="{{ url('search/results')}}">
    <input type="search" name="q" placeholder="Search">
    <input type="submit" value="Search Now">
  </form>
</div>

One difference from WordPress to Craft was that I limited the search to posts with a hidden field in the search form. In Craft, I set up a query in the search results page.

I did have to relearn how to do pagination in Craft. My first attempts were unsuccessful but I was able to get help on the Craft Discord channel that helped me to understand that I was adding a parameter to the query that needed to be removed in order for the pagination to work.

Search result excerpt

One thing that I have not yet been able to replicate is have an excerpt in my search listing that includes the search query highlighted, much like you would find in Google search results. I am using the Relevanssi plugin in WordPress. I have yet to find any plugins in Craft that will accomplish the same thing. And my coding skills are not deep enough on my own to figure out a solution.

Note: I did find a Craft plugin called SearchExcerpt that was compatible with Craft 3.

After some discussion on the Craft Discord and some research, I believe that I would need to create a custom module (which I believe is the Craft equivalent of the functions.php in WordPress) or a plugin to add this functionality. Twig does not let you write PHP functions in your Twig templates. I would need to spend more time in the classroom to learn how to do either of those.

RSS feeds

One thing that is different about Craft is that RSS functionality is not an out of the box feature like it is in WordPress. I am guessing that is because WordPress started as a blogging software in which RSS is an important feature. But Craft is a true CMS and would not necessarily need an RSS feed depending on what kind of a site you are building.

But it is very easy to set RSS feeds up in Craft. I found an article in the Craft knowledge base titled RSS and Atom Feeds. When I first tried to use the code from the example in the article, my code was not working. What I had failed to think about is that the field for my content was named postContent, which came from the WordPress Starter Kit (and I would be free to change the name if I wanted to–the flexibility of Craft).

Once I updated the Twig variable with the correct field name, it worked. Other than that, I just tweaked the code for the two sections (posts and notes) I wanted to create RSS feeds for. Easy peasy.

Where to go from here

I still have some tasks to do to completely convert my site from WordPress to Craft.

  • I need to redo my portfolio page. I am thinking I would create a separate section of content or create the portfolio listing as a matrix field.
  • I want to redo the comments. Currently I am using the default template that came with the WordPress Starter Kit from Craft.
  • I would need to reimport the site into Craft in order for my featured images to be imported into the posts and to grab the content that I have written since I started working on the project.
  • I would like to find a solution to have highlighted search excerpts on the search results page. As I have researched it, it looks like I need to create a custom module or plugin to create that functionality. I would need to go back the classroom to learn about both of those approached. CraftQuest has courses on both so I may invest in another month.
  • I also would like to create a solution that displays more recent posts by “hours ago” or days ago” instead of the date like I currently have on my WordPress site. That would also involved a module or plugin.

At this point in time, I am not sure if I will convert my live site from WordPress to Craft. This project has been a great way for me to reacquaint myself with Craft but I am not sure if it will go any further than that. I would have to look into new hosting and there are some other considerations that I would need to think through and decide if it is worth the cost, both financially and time.

I hope by documenting my experience that other WordPress developers could learn and have a pathway to converting WordPress sites into Craft. Given the current climate with WordPress, I think it is a smart business decision and one to offer clients.


Footnotes

1. I created notes so that I could own my own content. I was also beginning to move away from Twitter but wanted a forum to share shorter pieces of content that I would have normally posted on social media.

2. My PHP solution might have been able to be simpler than the way that I did it. I had learned this way of changing page content by updating WordPress queries through query strings. I used this approach on a Video Catalog that I built for Principal Media. I had first learned the approach through another developer’s code on an earlier project.

3 Comments

  1. Nicolas

    March 7th, 2025

    Thank you for this post. Was setting up and organizing templates (naming, folders, how they interact with each other) easier or harder than on WordPress?

    Outside of the templating changes, how was the backend authoring experience (UI/UX)? Did you migrate from a Gutenberg setup? Do you feel Craft does more “out of the box” as compared to WordPress, which relies heavily on plugins?

  2. Jeff Bridgforth: WordPress-to-Craft CMS | ResearchBuzz: Firehose

    March 8th, 2025

    […] Bridgforth: WordPress-to-Craft CMS . “The Craft team created a WordPress Starter Project and a WP Import plugin to make it easy […]

  3. Reddit, DuckDuckGo, Link Rot, More: Sunday Afternoon ResearchBuzz, March 9, 2025 – ResearchBuzz

    March 9th, 2025

    […] Bridgforth: WordPress-to-Craft CMS . “The Craft team created a WordPress Starter Project and a WP Import plugin to make it easy […]

Comment on this post