We had an interesting challenge recently which involved generating email markup via the page editor. Most html emails are build up from tables – in this situation we needed to generate several rows of content such that we were adding multiple sublayouts each representing a row:
- Sublayout in placeholder ‘body’: <tr><td>New row content</td></tr>
- Sublayout in placeholder ‘body’: <tr><td>Next new row content</td></tr>
- Sublayout in placeholder ‘body’: <tr><td>etc</td></tr>
In normal usage of the Page Editor you would be adding div’s to build up your html – unfortunately emails rely on a parent or several nested parent <table> tags.
When adding new sublayouts to the page the page editor didnt recognize where the table cells and rows would live (note the position of Add to here):
If this was done via divs you would expect to see something like:
One of the strict requirements of the work was the output markup matched existing templates exactly – the original templates had been rigorously tested in email clients. Because of this we really wanted to write the markup into our layouts and sublayouts so it matched the original html (albeit broken into placeholders and components).
After testing at cell and row level we found rewriting tags when in page editor mode helped the page editor respect the page layout much better.
The solution we arrived at was to tap in at page level and rewrite the whole markup in page editor. One of the assemblies that ships with Sitecore is the HtmlAgilityPack – its great for parsing and manipulating html.
There are 2 steps required:
- Tap into the page render
- Rewrite the content
First, lets rewrite the content:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 |
using System; using System.Globalization; using System.IO; using System.Text; using HtmlAgilityPack; namespace ###.Web.UI { public static class MarkupRewriter { /// <summary> /// Rewrite table, tr and td tags to divs /// </summary> public static string RewriteTables(string markup) { HtmlDocument document = new HtmlDocument(); document.LoadHtml(markup); bool validTable = ModifyContent(document, "table", "table"); bool validRow = ModifyContent(document, "tr", "table-row"); bool validCell = ModifyContent(document, "td", "table-cell"); return RenderHtmlDocument(document); } private static bool ModifyContent(HtmlDocument document, string tag, string displayStyle) { HtmlNodeCollection cells = document.DocumentNode.SelectNodes(String.Concat("//", tag)); if (cells != null) { foreach (HtmlNode cell in cells) { cell.Name = "div"; AddStyle(cell, "display", displayStyle); if (cell.Attributes["height"] != null) { AddStyle(cell, "height", String.Concat(cell.Attributes["height"].Value, "px;")); } if (cell.Attributes["width"] != null) { AddStyle(cell, "width", String.Concat(cell.Attributes["width"].Value, "px;")); } if (cell.Attributes["bgcolor"] != null) { AddStyle(cell, "background-color", String.Concat(cell.Attributes["bgcolor"].Value, ";")); } //add any new conversions here } } return cells != null; } private static void AddStyle(HtmlNode node, string styleTag, string styleValue) { if (node.Attributes["style"] == null) { node.Attributes.Append("style", String.Concat(styleTag, ":", styleValue, ";")); } else { node.Attributes["style"].Value = CleanTag(String.Concat(node.Attributes["style"].Value, ";", String.Concat(styleTag, ":", styleValue, ";"))); } } private static string CleanTag(string value) { while (value.IndexOf(";;") > 0) { value = value.Replace(";;", ";"); } return value; } private static string RenderHtmlDocument(HtmlDocument document) { StringBuilder sb = new StringBuilder(); StringWriter sw = new StringWriter(sb, CultureInfo.CurrentCulture); document.Save(sw); sw.Close(); return sb.ToString(); } } } |
Then tie into the layout’s render:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Web; using System.Web.UI; using System.Web.UI.WebControls; using ###.Web.UI; using SitecoreContext = Sitecore.Context; namespace ###.Web.Application.Email.Layouts { public partial class EmailLayout : System.Web.UI.Page { protected override void Render(HtmlTextWriter writer) { if (SitecoreContext.PageMode.IsPageEditorEditing) { // setup a TextWriter to capture the markup TextWriter tw = new StringWriter(); HtmlTextWriter htw = new HtmlTextWriter(tw); // render the markup into our surrogate TextWriter base.Render(htw); // get the captured markup as a string string pageSource = tw.ToString(); // render the markup into the output stream verbatim writer.Write(MarkupRewriter.RewriteTables(pageSource)); } else { base.Render(writer); } } } } |
What I like about this approach:
- You only need to make the change once rather than per tag
- The markup you write into the layouts and sublayouts is the original
What I don’t like about this approach:
- Adding the style tags doesn’t check for existence – you could end up with duplicate tags if the original style attribute contains the new value. This would be easy to resolve by parsing the existing value in the AddStyle method