ProcessWire - Wordpress Migration: Unterschied zwischen den Versionen

Aus Wikizone
Wechseln zu: Navigation, Suche
(Die Seite wurde neu angelegt: „ https://processwire.com/talk/topic/6655-migration-from-wordpress-to-processwire/ Mit verschiedenen Tools kann man Content (Seiten, Felder, Bilder) in Process…“)
 
 
(Eine dazwischenliegende Version desselben Benutzers wird nicht angezeigt)
Zeile 2: Zeile 2:
  
 
Mit verschiedenen Tools kann man Content (Seiten, Felder, Bilder) in ProcessWire Importieren. Für Wordpress gibt es eine spezielle Erweiterung.
 
Mit verschiedenen Tools kann man Content (Seiten, Felder, Bilder) in ProcessWire Importieren. Für Wordpress gibt es eine spezielle Erweiterung.
  CSV Importer von Ryan Cramer für alle möglichen CSV Daten
+
  CSV Importer von Ryan Cramer für alle möglichen CSV Daten https://modules.processwire.com/modules/import-pages-csv/
 
  ProcessMigrator der über JSON Seiten und Inhalte erstellen kann. https://github.com/adrianbj/ProcessMigrator  
 
  ProcessMigrator der über JSON Seiten und Inhalte erstellen kann. https://github.com/adrianbj/ProcessMigrator  
 
  MigratorWordpress ist ein Submodul der auf ProcessMigrator aufsetzt  
 
  MigratorWordpress ist ein Submodul der auf ProcessMigrator aufsetzt  
Zeile 11: Zeile 11:
  
 
  https://github.com/adrianbj/ProcessWirePageLists
 
  https://github.com/adrianbj/ProcessWirePageLists
 +
 +
== Manual Migration ==
 +
Ryan hat eine Case Study hierzu:
 +
 +
https://processwire.com/talk/topic/3987-cmscritic-development-case-study/
 +
 +
Auszüge:
 +
 +
Data Conversion from WordPress to ProcessWire
 +
 +
One of the larger parts of the project was converting all of the data over from WordPress to ProcessWire. I wrote a conversion script so that we could re-import as many times as needed since new stories get added to cmscritic.com almost daily. In order to get the data out of WordPress, I queried the WordPress database directly (my local copy of it anyway) to extract what we needed from the tables wp_posts for the blog posts and pages, and then wp_terms, wp_term_relationships, and wp_term_taxonomy for the topics and tags.
 +
 +
WordPress stores its TinyMCE text in a state that is something in between text and HTML, with the most obvious thing being that there are no <p> tags present in the wp_posts database. Rather than trying to figure out the full methodology behind that, I just included WP's wp-formatting.php file and ran the wpautop() function on the body text before inserting into ProcessWire. I know a lot of people have bad things to say about WordPress's architecture, but I must admit that the fact that I can just include a single file from WordPress's core without worrying about any other dependencies was a nice situation, at least in this case.
 +
 +
In order to keep track of the WordPress pages imported into ProcessWire through repeat imports, I kept a "wpid" field in ProcessWire. That just held the WordPress post ID from the wp_posts table. That way, when importing, I could very easily tell if we needed to create a new page or modify an existing one.
 +
 +
Another factor that had to be considered during import was that the site used a lot of "Hana code", which looked like [hana-code-insert name="something" /]. I solved this by making our own version of the Hanna code module, which was posted earlier this week.
 +
 +
Here's an abbreviated look at how to import posts from WordPress to ProcessWire:
 +
 +
$wpdb = new PDO("mysql:dbname=wp_cmscritic;host=localhost", "root", "root",
 +
  array(PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES 'UTF8'"));
 +
 +
$posts = wire('pages')->get('/posts/');
 +
 +
$sql = "
 +
  SELECT * FROM wp_posts
 +
  WHERE post_type='post'
 +
  AND post_status='publish'
 +
  ORDER BY post_date
 +
  ";
 +
 +
$query = $wpdb->prepare($sql);
 +
$query->execute();
 +
 +
while($row = $query->fetch(PDO::FETCH_ASSOC)) {
 +
 
 +
  $post = $posts->child("wpid=$row[ID]"); // do we already have this post?
 +
 
 +
  if(!$post->id) {
 +
    // create a new post
 +
    $post = new Page();
 +
    $post->template = 'post';
 +
    $post->parent = $posts;
 +
    echo "Creating new post...\n";
 +
  }
 +
 
 +
  $post->of(false);
 +
  $post->name = wire('sanitizer')->pageName($row['post_name']);
 +
  $post->title = $row['post_title'];
 +
  $post->date = $row['post_date'];
 +
  $post->summary = $row['post_excerpt'];
 +
  $post->wpid = $row['ID'];
 +
 
 +
  // assign the bodycopy after adding <p> tags
 +
  // the wpautop() function is from WordPress /wp-includes/wp-formatting.php
 +
  $post->body = wpautop($row['post_content']);
 +
 
 +
  $post->save();
 +
  echo "Saved post: $post->path\n";
 +
}
 +
What I've left out here is the importing of images, topics, tags, and setting the correct authors for each post. If anyone is interested, I'll be happy to go more in depth on that, but didn't want to overwhelm this message with code.
 +
 +
Template File Structure
 +
 +
This site makes use of the $config->prependTemplateFile to automatically include the file _init.php before rendering a template file, and $config->appendTemplateFile to automatically include the file _main.php after. So the /site/config.php has this:
 +
 +
$config->prependTemplateFile = '_init.php';
 +
$config->appendTemplateFile = '_main.php';
 +
You may recognize this as being the same setup from the Skyscrapers profile. The _init.php includes files containing functions we want to be available to all of our templates, and set default values for the regions we populate:
 +
 +
/site/templates/_init.php
 +
 +
/**
 +
* Include function and hook definition files
 +
*
 +
*/
 +
require_once("./includes/render.php");
 +
require_once("./includes/hooks.php");
 +
 +
/**
 +
* Initialize variables populated by templates that get output in _main.php
 +
*
 +
*/
 +
$browserTitle = $page->get('browser_title|title');
 +
$body = "<h1>" . $page->get('headline|title') . "</h1>" . $page->body;
 +
$side = '';
 +
$renderMain = true; // whether to include the _main.php file
 +
The includes/render.php file that is included above includes several functions for generating markup of navigation and post summaries, or any other shared markup generation functions. Examples are renderPost(), renderNav(), renderTags(). This is similar to the blog.inc file from the Blog profile except that I'm letting these functions generate and return their own markup rather than splitting them into separate view files. I personally find this easier to maintain even if it's not as MVC.
 +
 +
The includes/hooks.php sets up any hooks I want to be present for all of my templates. I could have also done this with an autoload module, but found this to just be a little simpler since my hooks were only needed on the front-end. The main hook of interest is one that makes all posts look like they live off the root "/" level rather than "/posts/" (where they actually live). This was in order to keep consistency with the URLs as they were in WordPress, so that the new site would have all the same URL as the old site, without the need for 301 redirects.
 +
 +
/site/templates/includes/hooks.php
 +
/**
 +
* This hook modifies the default behavior of the Page::path function (and thereby Page::url)
 +
*
 +
* The primary purpose is to redefine blog posts to be accessed at a URL off the root level
 +
* rather than under /posts/ (where they actually live).
 +
*
 +
*/
 +
wire()->addHookBefore('Page::path', function($event) {
 +
  $page = $event->object;
 +
  if($page->template == 'post') {
 +
    // ensure that pages with template 'post' live off the root rather than '/posts/'
 +
    $event->replace = true;
 +
    $event->return = "/$page->name/";
 +
  }
 +
});
 +
Our /site/templates/_main.php contains the entire markup for the overall template used site wide, from <html> to </html>. It outputs those variables we defined in _init.php in the right places. For example, $body gets output in the <div id='bodycopy'>, $side gets output in the right <aside>, and $browserTitle gets output in the <title> tag.
 +
 +
/site/templates/_main.php
 +
 +
<?php if($renderMain): ?>
 +
<html>
 +
  <head>
 +
    <title><?=$browserTitle?></title>
 +
  </head>
 +
  <body>
 +
    <div id='masthead'>
 +
      // ...
 +
    </div>
 +
    <div id='content'>
 +
      <div id='bodycopy'><?=$body?></div>
 +
      <aside id='sidebar'><?=$side?></aside>
 +
    </div>
 +
    <footer>
 +
      // ...
 +
    </footer>
 +
  </body>
 +
</html>
 +
<?php endif; ?>
 +
We use the rest of the site's template files to simply populate those $body, $side and $browserTitle variables with the contents of the page. As an example, this is an abbreviated version of the /site/templates/post.php template:
 +
 +
/site/templates/post.php
 +
 +
// functions from /site/templates/includes/render.php
 +
$meta = renderMeta($page);
 +
$tags = renderTags($page);
 +
$authorBox = renderAuthor($page->createdUser);
 +
$comments = renderComments($page);
 +
 +
$body = "
 +
  <article class='post post-full'>
 +
    <header>
 +
      <h1>$page->title</h1>
 +
      $meta
 +
    </header>
 +
    $page->body
 +
    $tags
 +
    $authorBox
 +
    $comments
 +
  </article>
 +
  ";
 +
 +
if(count($page->related)) {
 +
  $side = "<h4>Related Stories</h4>" . renderNav($page->related);
 +
}
 +
What might also be of interest is the homepage template, as it handles the other part of routing of post URLs since they are living off the root rather than in /posts/. That means the homepage is what is triggering the render of each post:
 +
 +
/site/templates/home.php
 +
 +
if(strlen($input->urlSegment2)) {
 +
  // we only accept 1 URL segment here, so 404 if there are any more
 +
  throw new Wire404Exception();
 +
 +
} else if(strlen($input->urlSegment1)) {
 +
  // render the blog post named in urlSegment1
 +
  $name = $sanitizer->pageName($input->urlSegment1);
 +
  $post = $pages->get("/posts/")->child("name=$name");
 +
 
 +
  if($post->id) echo $post->render();
 +
    else throw new Wire404Exception();
 +
 
 +
  // tell _main.php not to include itself after this
 +
  $renderMain = false;
 +
 +
} else {
 +
  // regular homepage output
 +
  $limit = 7; // number of posts to render per page
 +
  $posts = $pages->find("parent=/posts/, limit=$limit, sort=-date");
 +
  $body = renderPosts($posts);
 +
}
 +
The rest of the site's template files were handled in the same way. Though most were a little simpler than this. Several were simply blank, since the default values populated in _init.php were all that some needed.
 +
 +
Front-end development using Foundation 4
 +
 +
The front-end was developed with the Foundation 4 CSS framework. I started with the Foundation blog template and then tweaked the markup and css till I had something that I thought was workable. Then Mike and I sent the _main.php template file back and forth a few times, tweaking and changing it further. There was no formal design process here. It was kind of a photoshop tennis (but in markup and CSS) where we collaborated on it equally, but all under Mike's direction. After a day or two of collaboration, I think we both felt like we had something that was very good for the reader, even if it didn't originate from a design in Photoshop or some other tool like that. I think it helps a lot that Foundation provides a great starting point and lends itself well to fine tuning it the way you want it. I also felt that the mobile-first methodology worked particularly well here.
 +
 +
Comments System using Disqus
 +
 +
 +
We converted the comments system over to Disqus while the site was still running WordPress. This was done for a few reasons: Disqus comments provide one of the best experiences for the user, in my opinion. They also are platform agnostic, in that we could convert the whole site from WP to PW and not have to change a thing about the comments… no data conversion or importing necessary. Lastly, ProcessWire's built-in comments system is not quite as powerful as WordPress's yet, so I wanted cmscritic.com to get an upgrade in that area rather than anything else, and Disqus is definitely an upgrade from WP's comments. In order to ensure that Disqus could recognize the relations of comment threads to posts, we again made use of that $page->wpid variable that keeps the original WordPress ID, and also relates to the ID used by the Disqus comments. This is only for posts that originated in WordPress, as new posts use a ProcessWire-specific ID.
 +
 +
=== Images ===
 +
 +
I'll cover these each separately. First I'll start with the images, and will come back to the others a little later when I've got more time. 
 +
 +
WordPress really only uses images for placement in body copy, so I extracted the links to them right out of there and imported them that way. I did this after the pages had already been imported. In order to keep track of which images had already been imported (so that I could feasibly run the importer multiple times without getting duplicate images), I turned on ProcessWire image "tags" option, and stored the original filename in there. Here's the function I used, which I've used many different variations of over the years with different sites. You basically just give it a $page you've already imported (but is still linking to the old site's images) and it converts the images linked in the body copy from the old site to the new.
 +
 +
function importImages(Page $page) {
 +
 
 +
  if(!$page->id) return 'You need to save this page first';
 +
 
 +
  $out = '';
 +
  $body = $page->body;
 +
 
 +
  // find all images reference in the 'body' field
 +
  $regex = '{ src="(http://www.cmscritic.com/wp-content/uploads/[^"]+)"}';
 +
  if(!preg_match_all($regex, $body, $matches)) return $out;
 +
 +
  foreach($matches[0] as $key => $fullMatch) {
 +
 
 +
    $url = $matches[1][$key]; // image URL
 +
    $tag = basename($url); // image filename
 +
    $tag = wire('sanitizer')->name($tag); // sanitized filename
 +
    $image = $page->images->getTag($tag); // do we already have it?
 +
 
 +
    if(!$image) {
 +
      // we don't already have this image, import it
 +
      try {
 +
        $page->images->add($url);
 +
      } catch(Exception $e) {
 +
        $out .= "<div>ERROR importing: $url</div>";
 +
        continue;
 +
      }
 +
      $numAdded++;
 +
      $image = $page->images->last(); // get image that was just added
 +
      $status = "NEW";
 +
    } else {
 +
      $status = "Existing";
 +
    }
 +
 
 +
    $image->tags = $tag;
 +
    // replace old image URL with new image URL
 +
    $body = str_replace($url, $image->url, $body);
 +
    // report what we did
 +
    $out .= "<div>$status: $image->basename</div>";
 +
  }
 +
 +
  // assign the updated $body back to the page
 +
  $page->body = $body;
 +
 +
  // return a printable report of what was done
 +
  return $out;
 +
}
 +
 +
=== Topics / Tags ===
 +
 +
Topics and tags: The first step was to create the parent pages and templates for these. For topics, there were only a few of them, so I created all the category pages ahead of time. On the other hand, with tags, there are 2000+ of those, so those are imported separately. Here are the manual steps that I performed in the PW admin before importing topics and tags:
 +
 +
Created template "topics" and page /topics/ that uses this template.
 +
Created template "topic" and 6 topic pages that use it, like /topics/cms-reviews/ for example.
 +
Created Page reference field "topics" with asmSelect input, set to use parent /topics/ and template "topic".
 +
Created template "tags" and page /tag/ that uses this template. Note that I used /tag/ as the URL rather than /tags/ for consistency with the old WordPress URLs. Otherwise I would prefer /tags/ as the URL for consistency with the template name.
 +
Created template "tag".
 +
Created Page reference field "tags" with PageAutocomplete input, set to use parent /tag/ and template "tag". I also set this one to allow creating of new pages from the field, so the admin can add new tags on the fly.
 +
Added the new "topics" and "tags" fields to the "post" template.
 +
With all the right templates, fields and pages setup, we're ready to import. WordPress stores the topics, tags and the relationships of them to posts in various tables, which you'll see referenced in the SQL query below. It took some experimenting with queries in PhpMyAdmin before I figured it out. But once I got the query down, I put it in a function called importTopicsAndTags(). This function needs a connection to the WordPress database, which is passed into the function as $wpdb. For more details on $wpdb, see the first post in this thread.
 +
 +
/**
 +
* Import WordPress topics and tags to ProcessWire
 +
*
 +
* This function assumes you will do your own $page->save(); later.
 +
*
 +
* @param PDO $wpdb Connection to WordPress database
 +
* @param Page $page The ProcessWire "post" page you want to add topics and tags to.
 +
*    This page must have a populated "wpid" field.
 +
* @return string Report of what was done.
 +
*
 +
*/
 +
function importTopicsAndTags(PDO $wpdb, Page $page) {
 +
  $out = '';
 +
  $sql = <<< _SQL
 +
 +
  SELECT wp_term_relationships.term_taxonomy_id, wp_term_taxonomy.taxonomy,
 +
  wp_term_taxonomy.description, wp_terms.name, wp_terms.slug
 +
  FROM wp_term_relationships
 +
  LEFT JOIN wp_term_taxonomy
 +
    ON wp_term_taxonomy.term_taxonomy_id=wp_term_relationships.term_taxonomy_id
 +
  LEFT JOIN wp_terms
 +
    ON wp_terms.term_id=wp_term_taxonomy.term_id
 +
  WHERE wp_term_relationships.object_id=$page->wpid
 +
  ORDER BY wp_term_relationships.term_order
 +
 +
_SQL;
 +
 +
  $query = $wpdb->prepare($sql);
 +
  $query->execute();
 +
 +
  while($row = $query->fetch(PDO::FETCH_ASSOC)) {
 +
 +
    if($row['taxonomy'] == 'category') {
 +
      // this is a topic: find the existing topic in PW
 +
      $topic = wire('pages')->get("/topics/$row[slug]/");
 +
      if($topic->id) {
 +
        // if $page doesn't already have this topic, add it
 +
        if(!$page->topics->has($topic)) $page->topics->add($topic);
 +
        // report what we did
 +
        $out .= "<div>Topic: $topic->title</div>";
 +
      }
 +
 +
    } else if($row['taxonomy'] == 'post_tag') {
 +
      // this is a tag: see if we already have it in PW
 +
      $tag = wire('pages')->get("/tag/$row[slug]/");
 +
      if(!$tag->id) {
 +
        // we don't already have this tag, so create it
 +
        $tag = new Page();
 +
        $tag->template = 'tag';
 +
        $tag->parent = '/tag/';
 +
        $tag->name = $row['slug'];
 +
        $tag->title = $row['name'];
 +
        $tag->save();
 +
      }
 +
      // if $page doesn't already have this tag, add it
 +
      if(!$page->tags->has($tag)) {
 +
        $page->tags->add($tag);
 +
        $out .= "<div>Tag: $tag->title</div>";
 +
      }
 +
    }
 +
  }
 +
 +
  return $out;
 +
}

Aktuelle Version vom 6. September 2018, 18:59 Uhr

https://processwire.com/talk/topic/6655-migration-from-wordpress-to-processwire/

Mit verschiedenen Tools kann man Content (Seiten, Felder, Bilder) in ProcessWire Importieren. Für Wordpress gibt es eine spezielle Erweiterung.

CSV Importer von Ryan Cramer für alle möglichen CSV Daten https://modules.processwire.com/modules/import-pages-csv/
ProcessMigrator der über JSON Seiten und Inhalte erstellen kann. https://github.com/adrianbj/ProcessMigrator 
MigratorWordpress ist ein Submodul der auf ProcessMigrator aufsetzt 
und XML Exporte aus Wordpress (Standard in Wordpress) in ProcessWire importiert
https://github.com/NicoKnoll/MigratorWordpress

Tipp für ProcessMigrator gibt es zusätzlich praktische Importe für Länder etc.

https://github.com/adrianbj/ProcessWirePageLists

Manual Migration[Bearbeiten]

Ryan hat eine Case Study hierzu:

https://processwire.com/talk/topic/3987-cmscritic-development-case-study/

Auszüge:

Data Conversion from WordPress to ProcessWire

One of the larger parts of the project was converting all of the data over from WordPress to ProcessWire. I wrote a conversion script so that we could re-import as many times as needed since new stories get added to cmscritic.com almost daily. In order to get the data out of WordPress, I queried the WordPress database directly (my local copy of it anyway) to extract what we needed from the tables wp_posts for the blog posts and pages, and then wp_terms, wp_term_relationships, and wp_term_taxonomy for the topics and tags.

WordPress stores its TinyMCE text in a state that is something in between text and HTML, with the most obvious thing being that there are no

tags present in the wp_posts database. Rather than trying to figure out the full methodology behind that, I just included WP's wp-formatting.php file and ran the wpautop() function on the body text before inserting into ProcessWire. I know a lot of people have bad things to say about WordPress's architecture, but I must admit that the fact that I can just include a single file from WordPress's core without worrying about any other dependencies was a nice situation, at least in this case. In order to keep track of the WordPress pages imported into ProcessWire through repeat imports, I kept a "wpid" field in ProcessWire. That just held the WordPress post ID from the wp_posts table. That way, when importing, I could very easily tell if we needed to create a new page or modify an existing one. Another factor that had to be considered during import was that the site used a lot of "Hana code", which looked like [hana-code-insert name="something" /]. I solved this by making our own version of the Hanna code module, which was posted earlier this week. Here's an abbreviated look at how to import posts from WordPress to ProcessWire: $wpdb = new PDO("mysql:dbname=wp_cmscritic;host=localhost", "root", "root", array(PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES 'UTF8'")); $posts = wire('pages')->get('/posts/'); $sql = " SELECT * FROM wp_posts WHERE post_type='post' AND post_status='publish' ORDER BY post_date "; $query = $wpdb->prepare($sql); $query->execute(); while($row = $query->fetch(PDO::FETCH_ASSOC)) { $post = $posts->child("wpid=$row[ID]"); // do we already have this post? if(!$post->id) { // create a new post $post = new Page(); $post->template = 'post'; $post->parent = $posts; echo "Creating new post...\n"; } $post->of(false); $post->name = wire('sanitizer')->pageName($row['post_name']); $post->title = $row['post_title']; $post->date = $row['post_date']; $post->summary = $row['post_excerpt']; $post->wpid = $row['ID']; // assign the bodycopy after adding

tags // the wpautop() function is from WordPress /wp-includes/wp-formatting.php $post->body = wpautop($row['post_content']); $post->save(); echo "Saved post: $post->path\n"; } What I've left out here is the importing of images, topics, tags, and setting the correct authors for each post. If anyone is interested, I'll be happy to go more in depth on that, but didn't want to overwhelm this message with code. Template File Structure This site makes use of the $config->prependTemplateFile to automatically include the file _init.php before rendering a template file, and $config->appendTemplateFile to automatically include the file _main.php after. So the /site/config.php has this: $config->prependTemplateFile = '_init.php'; $config->appendTemplateFile = '_main.php'; You may recognize this as being the same setup from the Skyscrapers profile. The _init.php includes files containing functions we want to be available to all of our templates, and set default values for the regions we populate: /site/templates/_init.php /** * Include function and hook definition files * */ require_once("./includes/render.php"); require_once("./includes/hooks.php"); /** * Initialize variables populated by templates that get output in _main.php * */ $browserTitle = $page->get('browser_title|title'); $body = "

" . $page->get('headline|title') . "

" . $page->body;

$side = ; $renderMain = true; // whether to include the _main.php file The includes/render.php file that is included above includes several functions for generating markup of navigation and post summaries, or any other shared markup generation functions. Examples are renderPost(), renderNav(), renderTags(). This is similar to the blog.inc file from the Blog profile except that I'm letting these functions generate and return their own markup rather than splitting them into separate view files. I personally find this easier to maintain even if it's not as MVC.

The includes/hooks.php sets up any hooks I want to be present for all of my templates. I could have also done this with an autoload module, but found this to just be a little simpler since my hooks were only needed on the front-end. The main hook of interest is one that makes all posts look like they live off the root "/" level rather than "/posts/" (where they actually live). This was in order to keep consistency with the URLs as they were in WordPress, so that the new site would have all the same URL as the old site, without the need for 301 redirects.

/site/templates/includes/hooks.php /**

* This hook modifies the default behavior of the Page::path function (and thereby Page::url)
*
* The primary purpose is to redefine blog posts to be accessed at a URL off the root level
* rather than under /posts/ (where they actually live). 
*
*/

wire()->addHookBefore('Page::path', function($event) {

 $page = $event->object;
 if($page->template == 'post') {
   // ensure that pages with template 'post' live off the root rather than '/posts/'
   $event->replace = true;
   $event->return = "/$page->name/";
 }

});

Our /site/templates/_main.php contains the entire markup for the overall template used site wide, from <html> to </html>. It outputs those variables we defined in _init.php in the right places. For example, $body gets output in the

, $side gets output in the right <aside>, and $browserTitle gets output in the <title> tag.

/site/templates/_main.php

<?php if($renderMain): ?> <html>

 <head>
   <title><?=$browserTitle?></title>
 </head>
 <body>
      // ...
<?=$body?>
     <aside id='sidebar'><?=$side?></aside>
   <footer>
     // ...
   </footer>
 </body>

</html> <?php endif; ?> We use the rest of the site's template files to simply populate those $body, $side and $browserTitle variables with the contents of the page. As an example, this is an abbreviated version of the /site/templates/post.php template:

/site/templates/post.php

// functions from /site/templates/includes/render.php $meta = renderMeta($page); $tags = renderTags($page); $authorBox = renderAuthor($page->createdUser); $comments = renderComments($page);

$body = "

 <article class='post post-full'>
   <header>

$page->title

     $meta
   </header>
   $page->body
   $tags
   $authorBox
   $comments
 </article>
 ";

if(count($page->related)) {

$side = "

Related Stories

" . renderNav($page->related);

} What might also be of interest is the homepage template, as it handles the other part of routing of post URLs since they are living off the root rather than in /posts/. That means the homepage is what is triggering the render of each post:

/site/templates/home.php

if(strlen($input->urlSegment2)) {

 // we only accept 1 URL segment here, so 404 if there are any more
 throw new Wire404Exception();

} else if(strlen($input->urlSegment1)) {

 // render the blog post named in urlSegment1 
 $name = $sanitizer->pageName($input->urlSegment1);
 $post = $pages->get("/posts/")->child("name=$name");
 
 if($post->id) echo $post->render();
   else throw new Wire404Exception();
 
 // tell _main.php not to include itself after this
 $renderMain = false;

} else {

 // regular homepage output
 $limit = 7; // number of posts to render per page
 $posts = $pages->find("parent=/posts/, limit=$limit, sort=-date");
 $body = renderPosts($posts); 

} The rest of the site's template files were handled in the same way. Though most were a little simpler than this. Several were simply blank, since the default values populated in _init.php were all that some needed.

Front-end development using Foundation 4

The front-end was developed with the Foundation 4 CSS framework. I started with the Foundation blog template and then tweaked the markup and css till I had something that I thought was workable. Then Mike and I sent the _main.php template file back and forth a few times, tweaking and changing it further. There was no formal design process here. It was kind of a photoshop tennis (but in markup and CSS) where we collaborated on it equally, but all under Mike's direction. After a day or two of collaboration, I think we both felt like we had something that was very good for the reader, even if it didn't originate from a design in Photoshop or some other tool like that. I think it helps a lot that Foundation provides a great starting point and lends itself well to fine tuning it the way you want it. I also felt that the mobile-first methodology worked particularly well here.

Comments System using Disqus


We converted the comments system over to Disqus while the site was still running WordPress. This was done for a few reasons: Disqus comments provide one of the best experiences for the user, in my opinion. They also are platform agnostic, in that we could convert the whole site from WP to PW and not have to change a thing about the comments… no data conversion or importing necessary. Lastly, ProcessWire's built-in comments system is not quite as powerful as WordPress's yet, so I wanted cmscritic.com to get an upgrade in that area rather than anything else, and Disqus is definitely an upgrade from WP's comments. In order to ensure that Disqus could recognize the relations of comment threads to posts, we again made use of that $page->wpid variable that keeps the original WordPress ID, and also relates to the ID used by the Disqus comments. This is only for posts that originated in WordPress, as new posts use a ProcessWire-specific ID.

Images[Bearbeiten]

I'll cover these each separately. First I'll start with the images, and will come back to the others a little later when I've got more time.

WordPress really only uses images for placement in body copy, so I extracted the links to them right out of there and imported them that way. I did this after the pages had already been imported. In order to keep track of which images had already been imported (so that I could feasibly run the importer multiple times without getting duplicate images), I turned on ProcessWire image "tags" option, and stored the original filename in there. Here's the function I used, which I've used many different variations of over the years with different sites. You basically just give it a $page you've already imported (but is still linking to the old site's images) and it converts the images linked in the body copy from the old site to the new.

function importImages(Page $page) {

 if(!$page->id) return 'You need to save this page first';
 
 $out = ;
 $body = $page->body;
 
 // find all images reference in the 'body' field
 $regex = '{ src="(http://www.cmscritic.com/wp-content/uploads/[^"]+)"}'; 
 if(!preg_match_all($regex, $body, $matches)) return $out;

 foreach($matches[0] as $key => $fullMatch) {
 
   $url = $matches[1][$key]; // image URL
   $tag = basename($url); // image filename
   $tag = wire('sanitizer')->name($tag); // sanitized filename
   $image = $page->images->getTag($tag); // do we already have it?
 
   if(!$image) {
     // we don't already have this image, import it
     try {
       $page->images->add($url);
     } catch(Exception $e) {
$out .= "
ERROR importing: $url
";
       continue;
     }
     $numAdded++;
     $image = $page->images->last(); // get image that was just added
     $status = "NEW";
   } else {
     $status = "Existing";
   }
 
   $image->tags = $tag; 
   // replace old image URL with new image URL
   $body = str_replace($url, $image->url, $body);
   // report what we did 
$out .= "
$status: $image->basename
";
 }

 // assign the updated $body back to the page
 $page->body = $body;

 // return a printable report of what was done
 return $out;

}

Topics / Tags[Bearbeiten]

Topics and tags: The first step was to create the parent pages and templates for these. For topics, there were only a few of them, so I created all the category pages ahead of time. On the other hand, with tags, there are 2000+ of those, so those are imported separately. Here are the manual steps that I performed in the PW admin before importing topics and tags:

Created template "topics" and page /topics/ that uses this template. Created template "topic" and 6 topic pages that use it, like /topics/cms-reviews/ for example. Created Page reference field "topics" with asmSelect input, set to use parent /topics/ and template "topic". Created template "tags" and page /tag/ that uses this template. Note that I used /tag/ as the URL rather than /tags/ for consistency with the old WordPress URLs. Otherwise I would prefer /tags/ as the URL for consistency with the template name. Created template "tag". Created Page reference field "tags" with PageAutocomplete input, set to use parent /tag/ and template "tag". I also set this one to allow creating of new pages from the field, so the admin can add new tags on the fly. Added the new "topics" and "tags" fields to the "post" template. With all the right templates, fields and pages setup, we're ready to import. WordPress stores the topics, tags and the relationships of them to posts in various tables, which you'll see referenced in the SQL query below. It took some experimenting with queries in PhpMyAdmin before I figured it out. But once I got the query down, I put it in a function called importTopicsAndTags(). This function needs a connection to the WordPress database, which is passed into the function as $wpdb. For more details on $wpdb, see the first post in this thread.

/**

* Import WordPress topics and tags to ProcessWire
*
* This function assumes you will do your own $page->save(); later. 
*
* @param PDO $wpdb Connection to WordPress database
* @param Page $page The ProcessWire "post" page you want to add topics and tags to. 
*     This page must have a populated "wpid" field. 
* @return string Report of what was done. 
*
*/

function importTopicsAndTags(PDO $wpdb, Page $page) {

 $out = ;
 $sql = <<< _SQL

 SELECT wp_term_relationships.term_taxonomy_id, wp_term_taxonomy.taxonomy, 
 wp_term_taxonomy.description, wp_terms.name, wp_terms.slug
 FROM wp_term_relationships
 LEFT JOIN wp_term_taxonomy 
   ON wp_term_taxonomy.term_taxonomy_id=wp_term_relationships.term_taxonomy_id
 LEFT JOIN wp_terms 
   ON wp_terms.term_id=wp_term_taxonomy.term_id
 WHERE wp_term_relationships.object_id=$page->wpid
 ORDER BY wp_term_relationships.term_order

_SQL;

 $query = $wpdb->prepare($sql);
 $query->execute();

 while($row = $query->fetch(PDO::FETCH_ASSOC)) {

   if($row['taxonomy'] == 'category') {
     // this is a topic: find the existing topic in PW
     $topic = wire('pages')->get("/topics/$row[slug]/");
     if($topic->id) {
       // if $page doesn't already have this topic, add it
       if(!$page->topics->has($topic)) $page->topics->add($topic);
       // report what we did
$out .= "
Topic: $topic->title
";
     }

   } else if($row['taxonomy'] == 'post_tag') {
     // this is a tag: see if we already have it in PW
     $tag = wire('pages')->get("/tag/$row[slug]/");
     if(!$tag->id) {
       // we don't already have this tag, so create it 
       $tag = new Page();
       $tag->template = 'tag';
       $tag->parent = '/tag/';
       $tag->name = $row['slug'];
       $tag->title = $row['name'];
       $tag->save();
     }
     // if $page doesn't already have this tag, add it
     if(!$page->tags->has($tag)) {
       $page->tags->add($tag);
$out .= "
Tag: $tag->title
";
     }
   }
 }

 return $out;

}