<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom"><title type="text">Blog posts by Matt Pallatt is not a developer</title><link href="http://world.optimizely.com" /><updated>2025-04-07T14:12:34.0000000Z</updated><id>https://world.optimizely.com/blogs/matt-pallatt-is-not-a-developer/</id> <generator uri="http://world.optimizely.com" version="2.0">Optimizely World</generator> <entry><title>Category Management - Going old school &amp; trying not to break anything.</title><link href="https://world.optimizely.com/blogs/matt-pallatt-is-not-a-developer/dates/2025/4/category-management---going-old-school--trying-not-to-break-anything/" /><id>&lt;p&gt;You wait a hour for a bus and then 3 come at once.&lt;/p&gt;
&lt;p&gt;The same thing happened to me recently where multiple clients with ageing websites (Opti 11) and a new content strategy wanted to see all of their content and categories mapped out so that they could update them outside of the singlular management experience that Optimizely provides.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;We looked around for options and needless to say that whilst there were some viable solutions for Opti 12, our clients options were limited to zero with Opti 11.&amp;nbsp;&lt;/p&gt;
&lt;p&gt;So, I set around creating a solution to allow them to better, one off, manage their categories using .... Excel!&lt;/p&gt;
&lt;p&gt;That&#39;s right, I fired up SQL Management Studio, and VBA for Excel, and I now share with you what has been so helpful for us.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Use this at your own risk.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;Get content and category data out of Opti&lt;/h2&gt;
&lt;p&gt;Temporary tables and the messages view to the rescue...&lt;/p&gt;
&lt;div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&lt;code&gt;DROP TABLE IF EXISTS #pages&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code&gt;DROP TABLE IF EXISTS #categories&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&lt;code&gt;DECLARE @website varchar(256) = &#39;www.erm.com&#39;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code&gt;DECLARE @i int = 0&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code&gt;DECLARE @j int = 0&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code&gt;DECLARE @n int = 0&amp;nbsp;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code&gt;DECLARE @m int = 0&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code&gt;DECLARE @catName varchar(100)&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code&gt;DECLARE @catNames varchar(max)&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code&gt;DECLARE @catIDs varchar(max)&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&lt;code&gt;CREATE TABLE #pages(pkID int identity(1,1), pageID int, linkurl varchar(500), PageName varchar(1000)) INSERT INTO #pages (pageID, linkurl, PageName) SELECT pkid, CONCAT(REPLACE(linkurl,&#39;~/&#39;,CONCAT(&#39;&quot;https://&#39;,@website,&#39;/&#39;)),&#39;&quot;&#39;), Name FROM tblContent INNER JOIN tblContentLanguage ON pkID = fkContentID WHERE linkurl is not null AND fkLanguageBranchID = 1 ORDER BY pkID&amp;nbsp;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code&gt;CREATE TABLE #categories(pkID int identity(1,1), catID int, catName varchar(100)); --INSERT INTO #categories(catID, catName) SELECT pkid, CONCAT(&#39;&quot;&#39;,CategoryName,&#39;&quot;&#39;) from tblCategory ORDER BY pkID&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&lt;code&gt;WITH RecursiveCatQ AS (&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code&gt;&amp;nbsp; &amp;nbsp; SELECT pkid, fkParentID, CategoryName, CAST(pkid AS VARCHAR(MAX)) AS recursivePath&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code&gt;&amp;nbsp; &amp;nbsp; FROM tblCategory&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code&gt;&amp;nbsp; &amp;nbsp; WHERE fkParentID IS NULL&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&lt;code&gt;&amp;nbsp; &amp;nbsp; UNION ALL&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&lt;code&gt;&amp;nbsp; &amp;nbsp; SELECT t.pkid, t.fkParentID, t.CategoryName, CAST(r.recursivePath + &#39;-&#39; + CAST(t.pkid AS VARCHAR(MAX)) AS VARCHAR(MAX)) AS HierarchyPath&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code&gt;&amp;nbsp; &amp;nbsp; FROM tblCategory t&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code&gt;&amp;nbsp; &amp;nbsp; INNER JOIN RecursiveCatQ r&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code&gt;&amp;nbsp; &amp;nbsp; ON t.fkParentID = r.pkid&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code&gt;)&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&lt;code&gt;INSERT INTO #categories (catID, catName)&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code&gt;SELECT pkid, CONCAT(&#39;&quot;&#39;,CategoryName,&#39;&quot;&#39;)&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code&gt;FROM RecursiveCatQ&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code&gt;ORDER BY recursivePath;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&lt;code&gt;SET @n = (SELECT COUNT(*) FROM #categories)&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code&gt;SET @catNames = &#39;,,&#39;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code&gt;SET @catIDs = &#39;,,&#39;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&lt;code&gt;WHILE @i &amp;lt;= @n&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code&gt;BEGIN&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code&gt;&amp;nbsp; &amp;nbsp; SET @i = @i + 1&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code&gt;SET @catNames = CONCAT(@catnames,&#39;,&#39;,(SELECT TOP 1 catName FROM #categories WHERE pkID = @i))&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code&gt;SET @catIDs = CONCAT(@catIDs,&#39;,&#39;,(SELECT TOP 1 catID FROM #categories WHERE pkID = @i))&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code&gt;END&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&lt;code&gt;PRINT @catIDs&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code&gt;PRINT @catnames&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&lt;code&gt;SET @i = 0&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code&gt;SET @m = (SELECT COUNT(*) FROM #pages)&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code&gt;SET @n = (SELECT COUNT(*) FROM #categories)&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&lt;code&gt;DECLARE @pageCatRow varchar(1000)&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code&gt;DECLARE @boolPageCat int&amp;nbsp;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code&gt;DECLARE @pID int&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code&gt;DECLARE @cID int&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&lt;code&gt;WHILE @i &amp;lt; @m -- pages / rows&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code&gt;BEGIN&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code&gt;SET @i = @i + 1;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code&gt;SET @j = 0&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code&gt;SET @pageCatRow = &#39;&#39;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&lt;code&gt;SET @pageCatRow = (SELECT top 1 CONCAT(pageID,&#39;,&#39;,linkurl,&#39;,&quot;&#39; ,PageName,&#39;&quot;&#39;) FROM #pages WHERE pkID = @i)&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&lt;code&gt;WHILE @j &amp;lt; @n -- categories / columns&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code&gt;BEGIN&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code&gt;SET @j = @j + 1&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code&gt;SET @pid = (SELECT pageID FROM #pages where pkID = @i)&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code&gt;SET @cid = (SELECT catID FROM #categories where pkid = @j)&amp;nbsp;&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code&gt;SET @boolPageCat = (SELECT COUNT(*) FROM tblContentCategory WHERE fkContentID= @pid AND fkCategoryID = @cid)&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code&gt;SET @pagecatrow = CONCAT(@pageCatRow,&#39;,&#39;,@boolPageCat)&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code&gt;END&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code&gt;PRINT @pageCatRow&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&lt;code&gt;END&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&amp;nbsp;&lt;/div&gt;
&lt;div&gt;&lt;code&gt;DROP TABLE #pages&lt;/code&gt;&lt;/div&gt;
&lt;div&gt;&lt;code&gt;DROP TABLE #categories&lt;/code&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;&lt;br /&gt;This produces something that looks like this...&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/859e4ac746a8492bbdab0d2ba5557389.aspx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Give it a copy and drop it into Excel - you might have to do your own formatting to allow your categorisers to better see what&#39;s happening - but from there, they can add 1s and 0s to the content and categories that they want to.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/8ac05cce0c4f436ea4e19d4893e8aa85.aspx&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;Push content and category data back into Opti&lt;/h2&gt;
&lt;p&gt;Once everything has been updated, take to Developer mode in Excel and you can use this...&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Sub MakeMeSomeSQL()&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;&amp;nbsp; &amp;nbsp; Dim ws As Worksheet&lt;/code&gt;&lt;br /&gt;&lt;code&gt;&amp;nbsp; &amp;nbsp; Dim lastRow As Long, lastCol As Long&lt;/code&gt;&lt;br /&gt;&lt;code&gt;&amp;nbsp; &amp;nbsp; Dim i As Long, j As Long&lt;/code&gt;&lt;br /&gt;&lt;code&gt;&amp;nbsp; &amp;nbsp; Set ws = ThisWorkbook.Worksheets(&quot;Sheet2&quot;)&lt;/code&gt;&lt;br /&gt;&lt;code&gt;&amp;nbsp; &amp;nbsp; lastRow = ws.Cells(ws.Rows.Count, 1).End(xlUp).Row&lt;/code&gt;&lt;br /&gt;&lt;code&gt;&amp;nbsp; &amp;nbsp; lastCol = ws.Cells(1, ws.Columns.Count).End(xlToLeft).Column&lt;/code&gt;&lt;br /&gt;&lt;code&gt;&amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/code&gt;&lt;br /&gt;&lt;code&gt;&amp;nbsp; &amp;nbsp; For i = 1 To lastRow&lt;/code&gt;&lt;br /&gt;&lt;code&gt;&amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/code&gt;&lt;br /&gt;&lt;code&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; For j = 4 To lastCol&lt;/code&gt;&lt;br /&gt;&lt;code&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/code&gt;&lt;br /&gt;&lt;code&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; If ws.Cells(i, j).Value = &quot;1&quot; Then&lt;/code&gt;&lt;br /&gt;&lt;code&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/code&gt;&lt;br /&gt;&lt;code&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; Debug.Print (&quot;INSERT INTO tblWorkContentCategory (fkWorkContentID,fkCategoryID,CategoryType,ScopeName) VALUES ((SELECT TOP 1 pkID FROM tblWorkContent WHERE fkContentID = &quot; &amp;amp; ws.Cells(i, 1) &amp;amp; &quot; ORDER BY pkID DESC),&quot; &amp;amp; Left(ws.Cells(1, j), InStr(ws.Cells(1, j), &quot;-&quot;) - 1) &amp;amp; &quot;,0,0)&quot;)&lt;/code&gt;&lt;br /&gt;&lt;code&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; Debug.Print (&quot;INSERT INTO tblContentCategory (fkContentID, fkCategoryID, CategoryType, fkLanguageBranchID, ScopeName) VALUES (&quot; &amp;amp; ws.Cells(i, 1) &amp;amp; &quot;,&quot; &amp;amp; Left(ws.Cells(1, j), InStr(ws.Cells(1, j), &quot;-&quot;) - 1) &amp;amp; &quot;,0,&lt;span style=&quot;background-color: rgb(241, 196, 15);&quot;&gt;2&lt;/span&gt;,0)&quot;)&lt;/code&gt;&lt;br /&gt;&lt;code&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/code&gt;&lt;br /&gt;&lt;code&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; End If&lt;/code&gt;&lt;br /&gt;&lt;code&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/code&gt;&lt;br /&gt;&lt;code&gt;&amp;nbsp; &amp;nbsp; &amp;nbsp; &amp;nbsp; Next j&lt;/code&gt;&lt;br /&gt;&lt;code&gt;&amp;nbsp; &amp;nbsp;&amp;nbsp;&lt;/code&gt;&lt;br /&gt;&lt;code&gt;&amp;nbsp; &amp;nbsp; Next i&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;End Sub&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;You&#39;ll want to ensure that your &lt;span style=&quot;background-color: rgb(241, 196, 15);&quot;&gt;language branch is set&lt;/span&gt;, but the above will create your own SQL to reinsert content and category data, which will look a bit like...&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/link/b0c5e707ea504b538c55c04987ecbdea.aspx&quot; /&gt;&lt;/p&gt;
&lt;p&gt;Just make sure to remove all the existing data first before you run the SQL that&#39;s created by the above...&lt;/p&gt;
&lt;p&gt;&lt;code&gt;DELETE FROM tblContentCategory WHERE fkContentID IN (SELECT pkID FROM tblContent &lt;span style=&quot;background-color: rgb(45, 194, 107);&quot;&gt;WHERE fkParentID = 329) -- only delete the data from pages you&#39;ll be reimporting&lt;/span&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt; DELETE FROM tblWorkContentCategory WHERE fkWorkContentID IN (SELECT pkID FROM tblWorkContent WHERE fkContentID IN (SELECT pkID FROM tblContent &lt;span style=&quot;background-color: rgb(45, 194, 107);&quot;&gt;WHERE fkParentID = 329)) -- only delete the data from pages you&#39;ll be reimporting&lt;/span&gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;As I say, use at your own risk, but if you have hundreds of pages, and tons of categories, the automation here has saved us and clients DAYS if not weeks.&lt;/p&gt;
&lt;p&gt;&amp;nbsp;&lt;/p&gt;</id><updated>2025-04-07T14:12:34.0000000Z</updated><summary type="html">Blog post</summary></entry> <entry><title>Integrating HubSpot CRM without the MA Connector</title><link href="https://world.optimizely.com/blogs/matt-pallatt-is-not-a-developer/dates/2024/6/integrating-crm-without-the-ma-connector/" /><id>&lt;p&gt;Have HubSpot CRM? Want to push user data into it from Optimizely? Don&amp;rsquo;t have any personalisation requirements with that data? Don&amp;rsquo;t want to pay $800 a month for the Marketing Automation Connector? &lt;a href=&quot;https://nuget.optimizely.com/package/?id=MP.OptiForm2HubSpotPush&quot;&gt;Then I have something that might interest you&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Recently we were working with a client who sat very much in the audience segment outlined above &amp;ndash; they have HubSpot, they collect data from their website, they aren&amp;rsquo;t quite at the point where they want to start using any data from HubSpot to deliver personalisation, and they definitely didn&amp;rsquo;t have a budget for Marketing Automation Connector monthly charges.&lt;/p&gt;
&lt;p&gt;Their requirement was for two specific forms &amp;ndash; a contact us form, and a newsletter subscription, and we reasoned we could give them something completely rigid, with set fields that would only answer their current need &amp;ndash; but we also knew, as I think every agency does, that what was a single requirement today could easily snowball tomorrow; so we got to looking at Optimizely Form actors and the HubSpot API.&lt;/p&gt;
&lt;p&gt;And it turns out that half the job of what the multi-thousand-dollar-a-year Marketing Automation Connector does is relatively straight forwards to replicate using the scaffolding that Optimizely have in place around their Forms implementation and the straightforward API implementation that HubSpot has.&lt;/p&gt;
&lt;p&gt;With &lt;a href=&quot;https://github.com/mattpallatt/MP.OptiForm2HubSpotPush?tab=readme-ov-file#mpoptiform2hubspotpush&quot;&gt;very little configuration&lt;/a&gt; (for my money, simpler than the MA connector), we built out an extension to Optimizely Forms that on data submission, makes a request to HubSpot for a form schema, then matches field names from HubSpot to Optimizely before submitting the data to HubSpot once the comparison is completed.&lt;/p&gt;
&lt;p&gt;It works alongside all the existing features of Optimizely Forms &amp;ndash; so you can continue to hold data in Optimizely if you want to use Visitor Groups/Audiences on the data you&amp;rsquo;re collecting, you can use the mail functionality to let users know you have their data (assuming you&amp;rsquo;re not sending that from HubSpot), as well as thank you pages and all the standard and bespoke fields that Optimizely offers. All you have to do is make sure that your Optimizely Form name is the same as your field in HubSpot &amp;ndash; simples.&lt;/p&gt;
&lt;p&gt;And there&amp;rsquo;s little reason that the same thing can&amp;rsquo;t be done with Salesforce, Dynamics and other CRM platforms.&lt;/p&gt;
&lt;p&gt;Anyway, it was so simple and easy to use I thought I&amp;rsquo;d carve it off and make it available for anyone who wanted to send their user data to HubSpot &amp;ndash; or extend it to other CRM platforms of their need. So by all means steal, doctor and reuse the code at &lt;a href=&quot;https://github.com/mattpallatt/MP.OptiForm2HubSpotPush&quot;&gt;https://github.com/mattpallatt/MP.OptiForm2HubSpotPush.&lt;/a&gt;&lt;/p&gt;</id><updated>2024-06-27T11:11:41.0000000Z</updated><summary type="html">Blog post</summary></entry></feed>