Skip to content

Instantly share code, notes, and snippets.

@Grawl
Created March 11, 2020 10:33

Revisions

  1. Grawl created this gist Mar 11, 2020.
    137 changes: 137 additions & 0 deletions uvue-server-plugin-amp.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,137 @@
    import posthtml from 'posthtml'
    export default {
    async rendered(response, context, app) {
    // TODO try to use route meta instead?
    if (context.url.endsWith('/amp')) {
    const html = response.body
    const transform = new TransformHTML(html)
    response.body = await transform.transform()
    }
    },
    }
    class TransformHTML {
    constructor(html) {
    this.html = html
    // TODO move this to separate files
    this.AMPScript = `<script async src='https://cdn.ampproject.org/v0.js'></script>`
    this.AMPStyle = `
    <style amp-boilerplate>
    body{-webkit-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-moz-animation:-amp-start 8s steps(1,end) 0s 1 normal both;-ms-animation:-amp-start 8s steps(1,end) 0s 1 normal both;animation:-amp-start 8s steps(1,end) 0s 1 normal both}@-webkit-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-moz-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-ms-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@-o-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}@keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}
    </style>
    <noscript>
    <style amp-boilerplate>
    body{-webkit-animation:none;-moz-animation:none;-ms-animation:none;animation:none}
    </style>
    </noscript>
    `
    this.plugin = new TransformHTMLPlugin()
    }
    async transform() {
    return posthtml([
    require('posthtml-minifier')({
    removeComments: true,
    minifyJS: true,
    minifyCSS: true,
    }),
    require('posthtml-collect-styles')(),
    this.plugin.transform(),
    require('posthtml-insert-at').default([
    {
    selector: 'title',
    append: this.AMPScript,
    behavior: 'outside',
    },
    {
    selector: 'title',
    append: this.AMPStyle,
    behavior: 'outside',
    },
    ]),
    ])
    .process(this.html)
    .then(result => result.html)
    .catch(error => {
    console.log({ error })
    })
    }
    }
    class TransformHTMLPlugin {
    constructor() {
    this.attrsToDelete = [
    'data-vue-meta',
    'data-server-rendered',
    ]
    }
    transform() {
    return tree => {
    this.rootAttrs(tree)
    this.removeAttrs(tree)
    this.removeLinks(tree)
    this.removeScripts(tree)
    this.AMP(tree)
    return tree
    }
    }
    rootAttrs(tree) {
    tree.match({ tag: 'html' }, html => {
    delete html.attrs['data-vue-meta-server-rendered']
    delete html.attrs['data-vue-meta']
    html.attrs['amp'] = ''
    return html
    })
    }
    removeAttrs(tree) {
    tree.walk(node => {
    if (node && node.attrs) {
    this.attrsToDelete.forEach(attr => {
    if (this.attrsToDelete.some(attrToDelete => attrToDelete === attr)) {
    delete node.attrs[attr]
    }
    })
    }
    return node
    })
    }
    removeLinks(tree) {
    tree.match({ tag: 'link' }, link => {
    if (
    link.attrs &&
    (
    link.attrs['rel'] === 'preload' ||
    link.attrs['rel'] === 'prefetch'
    )
    ) {
    return ''
    }
    return link
    })
    }
    removeScripts(tree) {
    tree.match({ tag: 'body' }, body => {
    body.content = body.content.map(child => {
    if (
    typeof child === 'object' &&
    child.tag === 'script'
    ) {
    return ''
    }
    return child
    })
    return body
    })
    }
    AMP(tree) {
    tree.match({ tag: 'style' }, style => {
    if (!style.attrs) {
    style.attrs = {}
    }
    style.attrs['amp-custom'] = ''
    return style
    })
    // TODO use amp-img instead of <img> and/or <picture>
    tree.match({ tag: 'img' }, img => {
    img.tag = 'amp-img'
    return img
    })
    }
    }