I have a grand appreciation for anyone that steps up to the plate with an article.
Shifting gears to the subject at hand, this is an old method that was written about many years ago. The folks prior to me had implemented similar triggers and made copies of the Inserted and Deleted trigger tables into Temp Tables, just like you did, because MS didn't make the tables accessible in the scope of dynamic SQL.
For a 4 column update on just 10,000 rows of a (then) 132 column wide table, the trigger code took a whopping 4 minutes.
This is NOT the way to do "by column" auditing and I strongly recommend against such dynamic code methods.
The way I did it was to write code to examine the source table and write the trigger from that. Yes, that meant the trigger had 130 or so conditional (IF UPDATE() insert statements. After that, the time it took to execute the 4 column Update that I spoke of dropped from 4 MINUTES to 400 MILLI-Seconds.
You also jump through a fairly complex hoop with the following code to determine what the trigger action is...
-- selecting the action Insert, delete or update
SET @Type = (CASE WHEN EXISTS(SELECT * FROM INSERTED) AND EXISTS(SELECT * FROM DELETED)
THEN 'U' -- Set Action to Updated.
WHEN EXISTS(SELECT * FROM INSERTED)
THEN 'I' -- Set Action to Insert.
WHEN EXISTS(SELECT * FROM DELETED)
THEN 'D' -- Set Action to Deleted.
ELSE NULL -- Skip. It may have been a "failed delete".
Although it's not a real bad issue, you are hitting each trigger table twice. If you use a bit of negative logic, it can be simplified quite a bit.
SELECT @Type = CASE
WHEN NOT EXISTS (SELECT * FROM DELETED) THEN 'I'
WHEN NOT EXISTS (SELECT * FROM INSERTED) THEN 'D'
The method is also an extreme duplication of data and very difficult to assemble what a row looked like at a given point of time without a whole lot of work. Since the "NewValue" for the latest update lives in the source table, there is no really need for it and provides and extreme duplication of data. If you used that space, instead, for another datetime column so that you have two dates and times, one for when the OldValue started being valid and one for when it was updated to something else, PiT reconstruction of rows for reporting would be a lot easier.
I'll also state that if you do like this in your trigger code...
IF @PKCols IS NULL
RAISERROR('no PK on table %s', 16, -1, @TableName)
... the the source table has some really serious design issues.
So, while I really appreciate all the work you did to write this article, the premise of this article is seriously flawed. You must NOT use any trigger that dumps the INSERTED or DELETED trigger tables to Temp Tables. It's not just a casual "Code Smell". If either of those are present or you're doing any loops (and, yes, recursion is a form of bad looping) in your trigger code, your trigger code will be one of your biggest performance issues you'll ever have.