After much research I found the solution and it turns out to be well known for importing pages to and from publishing sites. So why did it take me so long to find it. :(
The root cause is the lack of a means to correct the layout on a publishing page when moving it, with the result being the page's layout still points to the layout at the old location. That layout is no longer available because it's on a different web.
To fix the problem I wrote a quick one-off that loops thru the available layouts and re-assigns the layout with the same title to each page. The code is below.
One oddity is the the page layouts returned from GetAvailablePageLayouts() have web specific URLs, but they didn't work. Looking at the URLs of page in a site that worked revealed the layout URLs were site specific. Not sure why that is, but building the URL in the new layout value to be site specific worked.
Change the site and web urls to what you need them to be.
using System.Collections.Generic;
using System.Linq;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Publishing;
namespace FixPublishingPages
{
class Program
{
static void Main()
{
using (var site = new SPSite("http://site"))
using (var web = site.AllWebs["web"])
{
var pubWeb = PublishingWeb.GetPublishingWeb(web);
var layoutValues = GetWebLayouts(pubWeb);
var pages = pubWeb.PagesList;
foreach (var page in pages.Items.Cast<SPListItem>())
{
if (!PublishingPage.IsPublishingPage(page)) continue;
var layoutTitle = GetLayoutTitle(page);
CorrectPageLayout(page, layoutValues[layoutTitle]);
}
}
}
private static Dictionary<string, SPFieldUrlValue> GetWebLayouts(PublishingWeb pubWeb)
{
var layouts = new Dictionary<string, SPFieldUrlValue>();
var webLayouts = pubWeb.GetAvailablePageLayouts();
foreach (var availableLayout in webLayouts)
{
layouts[availableLayout.Title] = new SPFieldUrlValue
{
Description = availableLayout.Title,
Url = string.Format("{0}{1}", pubWeb.Web.Site.Url, availableLayout.ServerRelativeUrl)
};
}
return layouts;
}
private static string GetLayoutTitle(SPListItem page)
{
var pageLayout = (string)page[FieldId.PageLayout];
return new SPFieldUrlValue(pageLayout).Description;
}
private static void CorrectPageLayout(SPListItem page, SPFieldUrlValue layoutValue)
{
var pubPage = PublishingPage.GetPublishingPage(page);
if (pubPage.ListItem.File.CheckOutType != SPFile.SPCheckOutType.None)
pubPage.ListItem.File.UndoCheckOut();
pubPage.CheckOut();
pubPage.ListItem[FieldId.PageLayout] = layoutValue;
pubPage.ListItem.UpdateOverwriteVersion();
pubPage.ListItem.File.CheckIn("Corrected page layout after import.", SPCheckinType.MajorCheckIn);
}
}
}