mirror of https://github.com/onweru/compose.git

Ray
11.10.2023 d67dd3fe3761936c02ae9c1ef1b30fe3e6e38ca2
add liteyoutube

Added:
- lite-yt-embed.css
- lite-yt-embed.css.
Modified:
- exampleSite/content/shortcodes.md
- layouts/partials/head.html
3 files added
2 files modified
312 ■■■■■ changed files
exampleSite/content/docs/compose/shortcodes.md 29 ●●●●● patch | view | raw | blame | history
layouts/partials/head.html 19 ●●●●● patch | view | raw | blame | history
layouts/shortcodes/liteyoutube.html 10 ●●●●● patch | view | raw | blame | history
static/css/lite-yt-embed.css 88 ●●●●● patch | view | raw | blame | history
static/js/lite-yt-embed.js 166 ●●●●● patch | view | raw | blame | history
exampleSite/content/docs/compose/shortcodes.md
@@ -68,6 +68,35 @@
{{< youtube "https://www.youtube.com/watch?v=MmG2ah5Df4g" >}}
#### Lite YouTube
The `liteyoutube` shortcode supports two parameters:
- videoid: the `videoid` parameter **is required**. However, you don't have to use it by name--you can just put in a YouTube video ID.
- params: the `params` parameter is **not** required. If you decide to use it, you must also use the `videoid` parameter.
##### Without `videoid`
```markdown
{{</* liteyoutube "MmG2ah5Df4g" */>}}
```
{{< liteyoutube "MmG2ah5Df4g" >}}
##### With `videoid` and `params`
The params string instructs YouTube to play only 20 seconds of the video starting at ten seconds and ending at 30 seconds. It also disables the player controls and enables the YouTube `JSAPI`.
```markdown
{{</* liteyoutube videoid="MmG2ah5Df4g" params="controls=0&start=10&end=30&modestbranding=2&rel=0&enablejsapi=1" */>}}
```
{{< liteyoutube videoid="MmG2ah5Df4g" params="controls=0&start=10&end=30&modestbranding=2&rel=0&enablejsapi=1" >}}
{{< tip >}}
You can browse the full list of YouTube parameters [here](https://developers.google.com/youtube/player_parameters#Parameters)
{{< /tip >}}
### Button
This adds a styled link (styled like a button). It takes two no-optional parameters:
layouts/partials/head.html
@@ -1,3 +1,17 @@
{{ $includeLiteYT := false }} <!-- begin search for liteyoutube shortcode. code borrowed from erichc Nov 2020 https://discourse.gohugo.io/t/automagically-loading-suitable-scripts-in-head/29429/8 -->
{{ if .IsNode }} <!-- list page -->
  {{ range .Pages }}    <!-- check child pages -->
    {{ range .Pages }}  <!-- another recursion because of the "/posts/xyz" structure; does work with category lists -->
      {{ $rawsummary := split .RawContent "<!--more-->" }}  <!-- prepare for checking only the part of the article that will show up on the list page -->
      {{ if (findRE "{{< liteyoutube" (index $rawsummary 0)) }} <!-- check if this part uses the shortcode -->
        {{ $includeLiteYT = true }}
      {{ end }}
    {{ end }} <!-- second range .Pages -->
  {{ end }} <!-- first range .Pages -->
{{ end }} <!-- IsNode -->
{{ if (.HasShortcode "liteyoutube") }} <!-- check if single page or list page itself use the shortcode -->
  {{ $includeLiteYT = true }}
{{ end }} <!-- end liteyoutube shortcode search -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
{{- hugo.Generator }}
@@ -9,6 +23,11 @@
<link rel="mask-icon" href='{{ absURL (printf "%ssafari-pinned-tab.svg" $iconsPath) }}'  color="#004750">
<link rel="shortcut icon" href='{{ absURL (printf "%sfavicon.ico" $iconsPath) }}'>
<meta name="msapplication-config" content='{{ absURL (printf "%sbrowserconfig.xml" $iconsPath) }}'>
{{ if $includeLiteYT }} <!-- if we find the liteyoutube shortcode in the page, insert the stylesheet and the javascript into the header -->
  <!-- the lite-yt-embed script and css retrieved from https://github.com/paulirish/lite-youtube-embed -->
  <link rel="stylesheet" href="{{.Site.BaseURL}}/css/lite-yt-embed.css">
  <script type="text/javascript" src="{{.Site.BaseURL}}/js/lite-yt-embed.js"></script>
{{ end }}
{{- $t := .Title }}
{{- $s := site.Title }}
{{- if in (lower $s) (lower $t) }}
layouts/shortcodes/liteyoutube.html
New file
@@ -0,0 +1,10 @@
{{ $videoid := .Get "videoid" }}
{{ if not $videoid }}
  {{ $videoid := .Get 0 }}
  {{ if not $videoid }}
    {{ errorf "'videoid' parameter required!" }}
  {{ end }}
{{ end }}
{{ $params := .Get "params" }} <!-- not required. List of params here: https://developers.google.com/youtube/player_parameters -->
<lite-youtube videoid="{{ $videoid }}"{{ if $params }} params="{{$params}}"{{ end }}></lite-youtube>
static/css/lite-yt-embed.css
New file
@@ -0,0 +1,88 @@
lite-youtube {
    background-color: #000;
    position: relative;
    display: block;
    contain: content;
    background-position: center center;
    background-size: cover;
    cursor: pointer;
    max-width: 720px;
    margin-left: auto;
    margin-right: auto;
}
/* gradient */
lite-youtube::before {
    content: '';
    display: block;
    position: absolute;
    top: 0;
    background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAADGCAYAAAAT+OqFAAAAdklEQVQoz42QQQ7AIAgEF/T/D+kbq/RWAlnQyyazA4aoAB4FsBSA/bFjuF1EOL7VbrIrBuusmrt4ZZORfb6ehbWdnRHEIiITaEUKa5EJqUakRSaEYBJSCY2dEstQY7AuxahwXFrvZmWl2rh4JZ07z9dLtesfNj5q0FU3A5ObbwAAAABJRU5ErkJggg==);
    background-position: top;
    background-repeat: repeat-x;
    height: 60px;
    padding-bottom: 50px;
    width: 100%;
    transition: all 0.2s cubic-bezier(0, 0, 0.2, 1);
}
/* responsive iframe with a 16:9 aspect ratio
    thanks https://css-tricks.com/responsive-iframes/
*/
lite-youtube::after {
    content: "";
    display: block;
    padding-bottom: calc(100% / (16 / 9));
}
lite-youtube > iframe {
    width: 100%;
    height: 100%;
    position: absolute;
    top: 0;
    left: 0;
    border: 0;
}
/* play button */
lite-youtube > .lty-playbtn {
    display: block;
    width: 68px;
    height: 48px;
    position: absolute;
    cursor: pointer;
    transform: translate3d(-50%, -50%, 0);
    top: 50%;
    left: 50%;
    z-index: 1;
    background-color: transparent;
    /* YT's actual play button svg */
    background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 68 48"><path d="M66.52 7.74c-.78-2.93-2.49-5.41-5.42-6.19C55.79.13 34 0 34 0S12.21.13 6.9 1.55c-2.93.78-4.63 3.26-5.42 6.19C.06 13.05 0 24 0 24s.06 10.95 1.48 16.26c.78 2.93 2.49 5.41 5.42 6.19C12.21 47.87 34 48 34 48s21.79-.13 27.1-1.55c2.93-.78 4.64-3.26 5.42-6.19C67.94 34.95 68 24 68 24s-.06-10.95-1.48-16.26z" fill="red"/><path d="M45 24 27 14v20" fill="white"/></svg>');
    filter: grayscale(100%);
    transition: filter .1s cubic-bezier(0, 0, 0.2, 1);
    border: none;
}
lite-youtube:hover > .lty-playbtn,
lite-youtube .lty-playbtn:focus {
    filter: none;
}
/* Post-click styles */
lite-youtube.lyt-activated {
    cursor: unset;
}
lite-youtube.lyt-activated::before,
lite-youtube.lyt-activated > .lty-playbtn {
    opacity: 0;
    pointer-events: none;
}
.lyt-visually-hidden {
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    height: 1px;
    overflow: hidden;
    position: absolute;
    white-space: nowrap;
    width: 1px;
  }
static/js/lite-yt-embed.js
New file
@@ -0,0 +1,166 @@
/**
 * A lightweight youtube embed. Still should feel the same to the user, just MUCH faster to initialize and paint.
 *
 * Thx to these as the inspiration
 *   https://storage.googleapis.com/amp-vs-non-amp/youtube-lazy.html
 *   https://autoplay-youtube-player.glitch.me/
 *
 * Once built it, I also found these:
 *   https://github.com/ampproject/amphtml/blob/master/extensions/amp-youtube (👍👍)
 *   https://github.com/Daugilas/lazyYT
 *   https://github.com/vb/lazyframe
 */
class LiteYTEmbed extends HTMLElement {
    connectedCallback() {
        this.videoId = this.getAttribute('videoid');
        let playBtnEl = this.querySelector('.lty-playbtn');
        // A label for the button takes priority over a [playlabel] attribute on the custom-element
        this.playLabel = (playBtnEl && playBtnEl.textContent.trim()) || this.getAttribute('playlabel') || 'Play';
        /**
         * Lo, the youtube placeholder image!  (aka the thumbnail, poster image, etc)
         *
         * See https://github.com/paulirish/lite-youtube-embed/blob/master/youtube-thumbnail-urls.md
         *
         * TODO: Do the sddefault->hqdefault fallback
         *       - When doing this, apply referrerpolicy (https://github.com/ampproject/amphtml/pull/3940)
         * TODO: Consider using webp if supported, falling back to jpg
         */
        if (!this.style.backgroundImage) {
          this.style.backgroundImage = `url("https://i.ytimg.com/vi/${this.videoId}/hqdefault.jpg")`;
        }
        // Set up play button, and its visually hidden label
        if (!playBtnEl) {
            playBtnEl = document.createElement('button');
            playBtnEl.type = 'button';
            playBtnEl.classList.add('lty-playbtn');
            this.append(playBtnEl);
        }
        if (!playBtnEl.textContent) {
            const playBtnLabelEl = document.createElement('span');
            playBtnLabelEl.className = 'lyt-visually-hidden';
            playBtnLabelEl.textContent = this.playLabel;
            playBtnEl.append(playBtnLabelEl);
        }
        playBtnEl.removeAttribute('href');
        // On hover (or tap), warm up the TCP connections we're (likely) about to use.
        this.addEventListener('pointerover', LiteYTEmbed.warmConnections, {once: true});
        // Once the user clicks, add the real iframe and drop our play button
        // TODO: In the future we could be like amp-youtube and silently swap in the iframe during idle time
        //   We'd want to only do this for in-viewport or near-viewport ones: https://github.com/ampproject/amphtml/pull/5003
        this.addEventListener('click', this.addIframe);
        // Chrome & Edge desktop have no problem with the basic YouTube Embed with ?autoplay=1
        // However Safari desktop and most/all mobile browsers do not successfully track the user gesture of clicking through the creation/loading of the iframe,
        // so they don't autoplay automatically. Instead we must load an additional 2 sequential JS files (1KB + 165KB) (un-br) for the YT Player API
        // TODO: Try loading the the YT API in parallel with our iframe and then attaching/playing it. #82
        this.needsYTApiForAutoplay = navigator.vendor.includes('Apple') || navigator.userAgent.includes('Mobi');
    }
    /**
     * Add a <link rel={preload | preconnect} ...> to the head
     */
    static addPrefetch(kind, url, as) {
        const linkEl = document.createElement('link');
        linkEl.rel = kind;
        linkEl.href = url;
        if (as) {
            linkEl.as = as;
        }
        document.head.append(linkEl);
    }
    /**
     * Begin pre-connecting to warm up the iframe load
     * Since the embed's network requests load within its iframe,
     *   preload/prefetch'ing them outside the iframe will only cause double-downloads.
     * So, the best we can do is warm up a few connections to origins that are in the critical path.
     *
     * Maybe `<link rel=preload as=document>` would work, but it's unsupported: http://crbug.com/593267
     * But TBH, I don't think it'll happen soon with Site Isolation and split caches adding serious complexity.
     */
    static warmConnections() {
        if (LiteYTEmbed.preconnected) return;
        // The iframe document and most of its subresources come right off youtube.com
        LiteYTEmbed.addPrefetch('preconnect', 'https://www.youtube-nocookie.com');
        // The botguard script is fetched off from google.com
        LiteYTEmbed.addPrefetch('preconnect', 'https://www.google.com');
        // Not certain if these ad related domains are in the critical path. Could verify with domain-specific throttling.
        LiteYTEmbed.addPrefetch('preconnect', 'https://googleads.g.doubleclick.net');
        LiteYTEmbed.addPrefetch('preconnect', 'https://static.doubleclick.net');
        LiteYTEmbed.preconnected = true;
    }
    fetchYTPlayerApi() {
        if (window.YT || (window.YT && window.YT.Player)) return;
        this.ytApiPromise = new Promise((res, rej) => {
            var el = document.createElement('script');
            el.src = 'https://www.youtube.com/iframe_api';
            el.async = true;
            el.onload = _ => {
                YT.ready(res);
            };
            el.onerror = rej;
            this.append(el);
        });
    }
    async addYTPlayerIframe(params) {
        this.fetchYTPlayerApi();
        await this.ytApiPromise;
        const videoPlaceholderEl = document.createElement('div')
        this.append(videoPlaceholderEl);
        const paramsObj = Object.fromEntries(params.entries());
        new YT.Player(videoPlaceholderEl, {
            width: '100%',
            videoId: this.videoId,
            playerVars: paramsObj,
            events: {
                'onReady': event => {
                    event.target.playVideo();
                }
            }
        });
    }
    async addIframe(){
        if (this.classList.contains('lyt-activated')) return;
        this.classList.add('lyt-activated');
        const params = new URLSearchParams(this.getAttribute('params') || []);
        params.append('autoplay', '1');
        params.append('playsinline', '1');
        if (this.needsYTApiForAutoplay) {
            return this.addYTPlayerIframe(params);
        }
        const iframeEl = document.createElement('iframe');
        iframeEl.width = 560;
        iframeEl.height = 315;
        // No encoding necessary as [title] is safe. https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html#:~:text=Safe%20HTML%20Attributes%20include
        iframeEl.title = this.playLabel;
        iframeEl.allow = 'accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture';
        iframeEl.allowFullscreen = true;
        // AFAIK, the encoding here isn't necessary for XSS, but we'll do it only because this is a URL
        // https://stackoverflow.com/q/64959723/89484
        iframeEl.src = `https://www.youtube-nocookie.com/embed/${encodeURIComponent(this.videoId)}?${params.toString()}`;
        this.append(iframeEl);
        // Set focus for a11y
        iframeEl.focus();
    }
}
// Register custom element
customElements.define('lite-youtube', LiteYTEmbed);