Google AMP Aftermath: Replacing Images Using ActionFilters

Today, I show you the results of my attempt to make my site work with Google Amp along with a way to change tags in your resulting HTML.

Written by Jonathan "JD" Danylko • Last Updated: • MVC •

Tablet and Cell Phone

When I mentioned using ASP.NET MVC to create Google AMP pages about two weeks ago, I thought the method of using DisplayModes would be easy.

I was mistaken.

I thought the process would be cut-and-dry, but with every web project, that doesn't always happen, does it?

As an alternate approach to DisplayModes, I created a new Amp controller that did the exact same function as the general blog post only with a different View folder (Amp) for my Blog detail pages.

Everything seemed to work fine with the new Amp Controller.

Now, sit back and enjoy the fruits of my labors. I logged into the Google Search Console to see my progress.

DanylkoWeb's Google Amp Pages Results

Well, so much for feeling I accomplished something. :-)

What Happened?

I'm taking the errors on my page one step at a time. For now, I want to draw attention to two particular errors.

  1. Stylesheet CDATA attribute missing or invalid - Include valid CDATA in the page <style> tag. A <style> tag requires specific CDATA. AMP pages without this information in that tag may appear in Google Search results without AMP-specific display features.

    The funny thing about this is I've added CDATA to all of my styles in my AMP pages every which way to Sunday and used their Structured Data Testing Tool which says everything is ok, but still tells me there are errors on the page.

    On top of that, their example they have doesn't even have CDATA included.

    Needless to say, I'm holding off on following up on this error until Google can explain things a little better to me.

  2. Unknown Syntax Error - Review your page for errors. AMP pages that do not follow the AMP specifications may appear in Google search results without AMP-specific display features.

    This error couldn't be more vague. As I clicked on each AMP page, I noticed a pattern. It seems that for every image on an AMP page, you need to rename the tag to use <amp-img> instead of <img>.

    To top that off, Google requires a closing tag (<amp-img></amp-img>) as opposed to a self-closing tag (<app-img />).

    This presents a problem especially when you've built a content management system. I have to change all of my img tags to a different tag and make them use a closing tag. I have all of my content with HTML image tags inside a database.

For this post, I will focus on changing the image tags in the database using ASP.NET MVC and look over the remaining issues in upcoming future posts.

Where to Start?

Since we don't want to change our data in the database, we need to do it at the controller level.

That means we need an ActionFilter.

As you know, I'm quite crazy about ActionFilters so this gives me another reason to build up a library by adding one more.

With ActionFilters, we can manipulate the returning HTML to match our needs.

Of course, we will need the entire HTML response by building it as it comes through in the OnActionExecuting and finally update the HTML in the OnResultExecuted before it's sent back to the browser.

ActionFilters\UseAmpImageAttribute.cs

public class UseAmpImageAttribute : ActionFilterAttribute
{
    private HtmlTextWriter _htmlTextWriter;
    private StringWriter _stringWriter;
    private StringBuilder _stringBuilder;
    private HttpWriter _output;
    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        _stringBuilder = new StringBuilder();
        _stringWriter = new StringWriter(_stringBuilder);
        _htmlTextWriter = new HtmlTextWriter(_stringWriter);
        _output = (HttpWriter)filterContext.RequestContext.HttpContext.Response.Output;
        filterContext.RequestContext.HttpContext.Response.Output = _htmlTextWriter;
    }
    public override void OnResultExecuted(ResultExecutedContext filterContext)
    {
        var response = _stringBuilder.ToString();
        response = UpdateAmpImages(response);
        _output.Write(response);
    }
    private string UpdateAmpImages(string response)
    {
        // Use HtmlAgilityPack (install-package HtmlAgilityPack)
        var doc = GetHtmlDocument(response);
        var imageList = doc.DocumentNode.Descendants("img");
        const string ampImage = "amp-img";
        HtmlNode.ElementsFlags.Add("amp-img"HtmlElementFlag.Closed);
        foreach (var imgTag in imageList)
        {
            var original = imgTag.OuterHtml;
            var replacement = imgTag.Clone();
            replacement.Name = ampImage;
            response = response.Replace(original, replacement.OuterHtml);
        }
        return response;
    }
    private HtmlDocument GetHtmlDocument(string htmlContent)
    {
        var doc = new HtmlDocument
        {
            OptionOutputAsXml = true,
            OptionDefaultStreamEncoding = Encoding.UTF8
        };
        doc.LoadHtml(htmlContent);
        return doc;
    }
}

The meat of the UseAmpImage ActionFilter is in the UpdateAmpImages and uses the HtmlAgilityPack. You can grab that from NuGet as you can see from the comment in the method.

While I could've used a RegEx, I've mentioned before that it's easier to parse the HTML than come up with a weird expression.

After we ask the Html Agility Pack to grab all of the images from the HTML, we add the custom tag called amp-img to the HtmlNode.ElementsFlags to close itself when done instead of self-closing.

        HtmlNode.ElementsFlags.Add("amp-img"HtmlElementFlag.Closed);

In each image tag, we clone the original and change the name from img to amp-img. This allows us to transfer all of the attributes over to the replacement.

Once we have our amp-img built, we can replace it in our resulting HTML.

UPDATE: I've updated this method in a more recent blog post. Please refer to Google AMP Aftermath: Replacing Image Results.

Apply Gently To Your ActionResult

Now that the hard part is over, we add our UseAmpImage to our Controller.

[UseAmpImage]
public class AmpController

If we access an Amp page, you'll notice we succeeded in replacing the image in our HTML.

img Successfully replaced with amp-img

Conclusion

Today, I showed you how to use an ActionFilter to examine the contents of HTML and filter it to manipulate and apply certain characteristics to an individual element.  

Now that we have that one issue out of the way, I'll be updating my site soon so I can examine the next batch of issues.

Stay Tuned!

Did you find this post helpful in converting your regular pages over to Google Amp pages? Post your comments below. 

The Complete Guide To Google AMP with ASP.NET MVC

ASP.NET 8 Best Practices on Amazon

ASP.NET 8 Best Practices by Jonathan Danylko


Reviewed as a "comprehensive guide" and a "roadmap to excellence" with over 120 Best Practices for ASP.NET Core 8, Jonathan's first book by Packt Publishing explores proven techniques for every phase of the SDLC.

Learn industry-standard concepts to improve your coding, debugging, and deployment of ASP.NET Core websites.

Order now on Amazon.com button

Picture of Jonathan "JD" Danylko

Jonathan "JD" Danylko is an author, web architect, and entrepreneur who's been programming for over 30 years. He's developed websites for small, medium, and Fortune 500 companies since 1996.

He currently works at Insight Enterprises as an Architect.

When asked what he likes to do in his spare time, he replies, "I like to write and I like to code. I also like to write about code."

comments powered by Disqus