How to do an effective website refactoring
So you’ve built a website. It’s running and your customer - whether a business client or your internal customer - is happy. Success. Time to celebrate.
But then in a week, sometimes in a month or maybe even a year later, the customer comes back and asks for changes.
The website is up. The requirements change
The content structure was apparently not thought through as well as anticipated. Or perhaps, it is because business requirements or the way how customers are using the web changed. But you are stuck with the structure of content that you have. It’s not about templates only, those you can rewrite rather easily, but what about your content structure? Say your business users already wrote a hundred or more of pieces of articles or pages or simple content with old structure and they aren’t about to rewrite it from scratch. What do you do about that? Manipulate live customer data on production server? Ouch, that’s some hot, dangerous action. Most of us would instinctively shy away from the task. We will try to inflate estimates for such work to be done to make the customer not insist much on such change.
But perhaps there's an easy way to get out of this conundrum. Perhaps, Magnolia can aid you in restructuring your live content without too much of pain or overhead. Without too much of the danger of messing up the content.
On the following lines, I’ll describe one way of doing such refactoring. I’m of course not sure if the example I’m describing matches exactly your scenario, but if not, it can at least give you some ideas and inspiration to the task at hand and increase your confidence in doing the task.
A case of website refactoring
The refactoring that I had to face was as follows. The customer originally required having a number of items of type A or type B added to the piece of content as related items. To make it more concrete say they wanted a piece of content, e.g. Tour associated with the guides who could have been either internal employees or external consultants.
Originally they just listed the guides who could be hired to perform the tour for those who were interested. That’s how it was until today. Today the customer comes and says they’d like to do a little change. They want me to not just list the tour guides (internal first, then external), but they want now to have those ordered. And not just based on some calculated score. They want to have full control of the order so they can push the guides they prefer on top. Now how do I do that?
Originally, the task seemed rather simple. Have one multi-select field for external tour guides and have one multi-select field for internal tour guides. Piece of cake. But now I can’t really combine those since they come up from different content apps and there’s not a single ChooseDialog that works over both.
So what is the way out of this?
How to implement the code refactoring
Let’s change our 2 types of tour guides from just simple references into subcomponents. Also let’s change our 2 multi-select fields into the subarea with list of components, one being of employee-kind-of-tour-guide and other one being contractor. This way our content editor can control the order. In template it’s also rather simple change to look up subcomponents instead of just values of two multi-select fields.
Let’s get to the more complicated part - data migration. What we have is 2 fields (externalGuide, internalGuide) with multi-values:
tourComponent
|-tourComponent
|--title=“fu bar”
|--internalGuide={1234-1234-1234, 1234-abcd-1234}
|--externalGuide={2345-2345-2345, 2345-abcd-2345}
`--mgnl:template=foo-web:components/guides
What we want is something like:
tourComponent
|-tourComponent
|--title=“fu bar”
`--guides
|-0
| |-link=1234-1234-1234
| `-mgnl:template=foo-web:components/guides_employeeGuides
|-1
| |-link=2345-2345-2345
| `-mgnl:template=foo-web:components/guides_consultantGuides
|-2
| |-link=2345-abcd-2345
| `-mgnl:template=foo-web:components/guides_consultantGuides
`-3
|-link=1234-abcd-1234
`-mgnl:template=foo-web:components/guides_employeeGuides
What we have to do is move those id’s into components to allow editors to fix the order, but not to lose any id.
Let’s start by finding components that we need to migrate:
website = ctx.getJCRSession("website")
qm = website.getWorkspace().getQueryManager();
q = qm.createQuery("select * from [mgnl:component] where [mgnl:template]=‘foo-web:components/guides'", "JCR-SQL2");
rs = q.execute();
Now let’s try to find if there’s anything to modify in given component:
results = rs.nodes
while (results.hasNext()) {
result = results.next()
if (!result.hasNode(“guides")) {
guides = result.addNode(“guides", "mgnl:area");
intP = processProfiles("internalGuide", guides, “foo-web:components/guides_employeeGuides")
extP = processProfiles("externalGuide", guides, “foo-web:components/guides_consultantGuides")
if ( intP || extP ) {
website.save()
} else {
guides();
}
}
}
Now let’s look at how to implement processProfiles() method so that we can actually upgrade the profiles:
def processProfiles(name, guides, template) {
foundProfiles = false
if(result.hasProperty(name)){
intProfiles = result.getProperty(name).getValues();
intProfiles.each {
foundProfiles = true
profileId = it.string
comp = guides.addNode(helper.getUniqueName(guides, "0"), "mgnl:component")
comp.setProperty("link", profileId)
comp.setProperty("mgnl:template", template)
}
}
return foundProfiles
}
One extra thing I’d need to make this work, would be access to the helper class (preferably at the beginning of the script):
helper = Components.getComponent(NodeNameHelper.class)
You might also want to add possibility to activate the nodes on top of the changes so that you can propagate all you did to public instances as part of the migration. How do we do that? Let’s see …
To be able to activate only those tours that have been already published, we first need to check the status of publication prior to any node modification:
while (results.hasNext()) {
result = results.next()
status = NodeTypes.Activatable.getActivationStatus(result);
}
and once we know the status, we can act upon after migrating our tour node:
...
if ( intP || extP ) {
website.save()
println("Saved")
if (status == NodeTypes.Activatable.ACTIVATION_STATUS_MODIFIED){
println "was modified, not publishing";
}
else if (status == NodeTypes.Activatable.ACTIVATION_STATUS_ACTIVATED){
println "publish";
commands.executeCommand("publish",["path”:guides.path,"userName”:"migration-script","recursive":true,"repository":"website"])
}
}
...
one last thing we need is to obtain commands manager before even starting the loop:
commands = Components.getComponent(CommandsManager.class)
Done. Safe. Future-proof
And that's really it. Not so difficult after all, right? We managed to migrate all our customers data and not cause any damage while doing so. We also did so in an automated, replicable way so we can easily run the process on test instance before doing so in production.
Of course your use case will likely be slightly different, but can still be done with little modifications.
Good luck.