Merge branch 'develop'

This commit is contained in:
Sam Carlton 2021-05-15 21:21:43 -05:00
commit 8a50ad2111
16 changed files with 938 additions and 140 deletions

View file

@ -28,6 +28,8 @@ import fs from 'fs'
import replace_css_url from 'replace-css-url' import replace_css_url from 'replace-css-url'
import dotenv from 'dotenv' import dotenv from 'dotenv'
import { InlineCodeManager } from '@11ty/eleventy-assets' import { InlineCodeManager } from '@11ty/eleventy-assets'
import { transformSync } from 'esbuild'
import nuxtConfig from './nuxt.config' import nuxtConfig from './nuxt.config'
@ -39,6 +41,8 @@ function getAssetFilePath(componentName) {
return `./${componentName}` return `./${componentName}`
} }
const inlineAssetCache = new Map()
module.exports = function ( eleventyConfig ) { module.exports = function ( eleventyConfig ) {
// console.log('eleventyConfig', eleventyConfig) // console.log('eleventyConfig', eleventyConfig)
@ -51,17 +55,50 @@ module.exports = function ( eleventyConfig ) {
const cssManager = new InlineCodeManager() const cssManager = new InlineCodeManager()
const jsManager = new InlineCodeManager() const jsManager = new InlineCodeManager()
eleventyConfig.addJavaScriptFunction('usingComponent', function ( componentName ) { eleventyConfig.addJavaScriptFunction('usingComponent', async function ( componentName ) {
// console.log('Getting component', componentName) // console.log('Getting component', componentName)
const assetFileName = getAssetFilePath( componentName )
// console.log('Cache size', inlineAssetCache)
if ( componentName.includes('.js') ) { if ( componentName.includes('.js') ) {
// If a never before seen component, add the JS code // If a never before seen component, add the JS code
if(!jsManager.hasComponentCode(componentName)) { if( !inlineAssetCache.has( assetFileName ) ) {
const fileContents = fs.readFileSync(getAssetFilePath(componentName), { encoding: "UTF-8" })
let contents = ''
// if ( inlineAssetCache.has( assetFileName ) ) {
// console.log('Reading component from cache', componentName)
// console.log('Cache size', inlineAssetCache.size)
// contents = inlineAssetCache.get( assetFileName )
// } else {
// }
console.log('Reading component file', componentName)
const javascriptCode = fs.readFileSync( assetFileName, { encoding: "UTF-8" })
console.log('Minifying code', componentName)
const minified = await transformSync(javascriptCode, {
// loader: 'ts',
// bundle: true,
minify: true,
// format: 'iife',
})//minify( javascriptCode )
// console.log('minified', minified)
contents = minified.code
inlineAssetCache.set( assetFileName, contents )
// console.log('Got component', componentName, componentCss) // console.log('Got component', componentName, componentCss)
jsManager.addComponentCode(componentName, fileContents) jsManager.addComponentCode(componentName, contents)
} }
// Log usage for this url // Log usage for this url
@ -70,7 +107,7 @@ module.exports = function ( eleventyConfig ) {
} else if ( componentName.includes('.css') ) { } else if ( componentName.includes('.css') ) {
// If a never before seen component, add the CSS code // If a never before seen component, add the CSS code
if(!cssManager.hasComponentCode(componentName)) { if(!cssManager.hasComponentCode(componentName)) {
const fileContents = fs.readFileSync(getAssetFilePath(componentName), { encoding: "UTF-8" }) const fileContents = fs.readFileSync( assetFileName, { encoding: "UTF-8" })
// Replace urls in css with relative urls for dist folder // Replace urls in css with relative urls for dist folder
const parsedCss = replace_css_url( const parsedCss = replace_css_url(
@ -120,6 +157,10 @@ module.exports = function ( eleventyConfig ) {
return { return {
// https://www.11ty.dev/docs/config/#template-formats
// Default: html,liquid,ejs,md,hbs,mustache,haml,pug,njk,11ty.js
templateFormats: [ '11ty.js' ],
dir: { dir: {
input: 'pages-eleventy', input: 'pages-eleventy',
output: 'dist', output: 'dist',

View file

@ -236,12 +236,12 @@ class BuildLists {
const appType = getAppType( app ) const appType = getAppType( app )
if ( isVideo( app ) ) { if ( isVideo( app ) ) {
// this.endpointMaps.eleventy.add({ this.endpointMaps.eleventy.set(
// route: getVideoEndpoint(app), getVideoEndpoint(app),
// payload: buildVideoPayload( app, this.allVideoAppsList, this.lists.video ) buildVideoPayload( app, this.allVideoAppsList, this.lists.video )
// }) )
this.endpointMaps.nuxt.set( getVideoEndpoint(app), buildVideoPayload( app, this.allVideoAppsList, this.lists.video ) ) // this.endpointMaps.nuxt.set( getVideoEndpoint(app), buildVideoPayload( app, this.allVideoAppsList, this.lists.video ) )
return return
} }

View file

@ -1,3 +1,4 @@
import renderPoster from './poster.js'
function pill ( text ) { function pill ( text ) {
return /* html */` return /* html */`
@ -9,17 +10,19 @@ function pill ( text ) {
` `
} }
export default function ( video, options = {} ) { export default async function ( video, options = {} ) {
const { const {
width = '325px', width = '325px',
classes = 'w-full flex-shrink-0 flex-grow-0 border-2 border-transparent rounded-2xl overflow-hidden' classes = 'w-full flex-shrink-0 flex-grow-0 border-2 border-transparent rounded-2xl overflow-hidden'
} = options } = options
// Setup inline lazysizes // Setup inline lazysizes
this.usingComponent( 'node_modules/lazysizes/lazysizes.min.js' ) // await this.usingComponent( 'node_modules/lazysizes/lazysizes.min.js' )
// console.log('video', video) // console.log('video', video)
const posterHtml = renderPoster( video )
return /* html */` return /* html */`
<div class="video-card ${ classes }" style="max-width: ${ width }; flex-basis: ${ width }; scroll-snap-align: start;"> <div class="video-card ${ classes }" style="max-width: ${ width }; flex-basis: ${ width }; scroll-snap-align: start;">
<a <a
@ -29,18 +32,7 @@ export default function ( video, options = {} ) {
<div class="video-card-container relative overflow-hidden bg-black"> <div class="video-card-container relative overflow-hidden bg-black">
<div class="video-card-image ratio-wrapper"> <div class="video-card-image ratio-wrapper">
<div class="relative overflow-hidden w-full pb-16/9"> <div class="relative overflow-hidden w-full pb-16/9">
<picture> ${ posterHtml }
<source
sizes="${video.thumbnail.sizes}"
data-srcset="${video.thumbnail.srcset}"
type="image/jpg"
>
<img
data-src="${video.thumbnail.src}"
alt="${video.name}"
class="lazyload absolute h-full w-full object-cover"
>
</picture>
</div> </div>
</div> </div>
<div <div

View file

@ -0,0 +1,85 @@
import renderPoster from './poster.js'
function renderTimestamps ( video ) {
if ( video.timestamps.length > 0 ) return ''
const timestampsForRender = video.timestamps.map( timestamp => {
const [ minutes, seconds ] = timestamp.time.split(':')
return {
...timestamp,
inSeconds: (minutes * 60) + Number(seconds)
}
})
const timestampButtonsHtml = timestampsForRender.map( timestamp => (/* html */`
<button
time="${timestamp.time}"
class="inline-block text-xs rounded-lg py-1 px-2 border-2 border-white focus:outline-none border-opacity-0 neumorphic-shadow-inner">
${ timestamp.fullText }
</button>
`) ).join('')
return /* html */`
<div class="player-timestamps w-full max-w-4xl">
<div class="player-timestamps-wrapper overflow-x-auto overflow-y-visible whitespace-no-wrap py-2 space-x-2">
${ timestampButtonsHtml }
</div>
</div>
`
}
export default async function ( video, options = {} ) {
const {
coverBottomHtml = ''
// classes = 'w-full flex-shrink-0 flex-grow-0 border-2 border-transparent rounded-2xl overflow-hidden'
} = options
// Setup inline player script
await this.usingComponent( 'node_modules/can-autoplay/build/can-autoplay.min.js' )
await this.usingComponent( 'helpers/lite-youtube.js' )
// Setup inline lazysizes
await this.usingComponent( 'node_modules/lazysizes/lazysizes.min.js' )
// console.log('video', video)
const posterHtml = renderPoster( video )
const timestampsHtml = renderTimestamps( video )
return /* html */`
<lite-youtube
class="video-canvas w-screen flex flex-col justify-center items-center bg-black pt-16"
style="left:50%;right:50%;margin-left:-50vw;margin-right:-50vw;"
>
<script class="video-data" type="application/json">
${ JSON.stringify( video ) }
</script>
<div class="ratio-wrapper w-full max-w-4xl">
<div class="player-container relative overflow-hidden w-full pb-16/9">
<div class="player-poster cursor-pointer">
${ posterHtml }
<div class="video-card-overlay absolute inset-0 flex flex-col justify-center items-center bg-gradient-to-tr from-black to-transparent p-4" style="--gradient-from-color:rgba(0, 0, 0, 1); --gradient-to-color:rgba(0, 0, 0, 0.7);">
<div class="cover-top h-full"></div>
<div class="play-circle bg-white-2 bg-blur flex justify-center items-center outline-0 rounded-full ease p-4">
<svg viewBox="0 0 18 18" style="width:18px;height:18px;margin-left:3px">
<path fill="currentColor" d="M15.562 8.1L3.87.225c-.818-.562-1.87 0-1.87.9v15.75c0 .9 1.052 1.462 1.87.9L15.563 9.9c.584-.45.584-1.35 0-1.8z"></path>
</svg>
</div>
<div class="cover-bottom h-full">${ coverBottomHtml }</div>
</div>
</div>
</div>
</div>
${ timestampsHtml }
</lite-youtube>
`
}

View file

@ -0,0 +1,32 @@
export default function ( video ) {
const webpSource = {
...video.thumbnail,
srcset: video.thumbnail.srcset.replaceAll('ytimg.com/vi/', 'ytimg.com/vi_webp/').replace(/.png|.jpg|.jpeg/g, '.webp')
}
const mergedSources = {
webp: webpSource,
jpeg: video.thumbnail
}
return /* html */`
<picture>
${ Object.entries( mergedSources ).map( ([ key, source ]) => (/* html */`
<source
sizes="${ source.sizes }"
data-srcset="${ source.srcset }"
type="image/${ key }"
>
`) ).join('') }
<img
:data-src="${ video.thumbnail.src }"
alt="${ video.name }"
class="absolute inset-0 h-full w-full object-cover lazyload"
>
</picture>
`
}

View file

@ -10,7 +10,7 @@ function getCardType ( video ) {
return VideoCard return VideoCard
} }
export default function ( videos, options = {} ) { export default async function ( videos, options = {} ) {
const { const {
cardWidth = '325', cardWidth = '325',
@ -23,18 +23,23 @@ export default function ( videos, options = {} ) {
const uid = Math.random().toString(36).substr(2, 9) const uid = Math.random().toString(36).substr(2, 9)
const rowId = `row-${ uid }` const rowId = `row-${ uid }`
// Setup inline scroll script
await this.usingComponent( 'helpers/scroll.js' )
// Setup inline lazysizes // Setup inline lazysizes
this.usingComponent( 'helpers/scroll.js' ) await this.usingComponent( 'node_modules/lazysizes/lazysizes.min.js' )
// console.log('video', video) // console.log('video', video)
const cardsHtml = videos.map( video => { const renderedCards = await Promise.all(videos.map( async video => {
const Card = getCardType( video ) const Card = getCardType( video )
// console.log('Card', this.boundComponent(Card)( video ) ) // console.log('Card', this.boundComponent(Card)( video ) )
return this.boundComponent(Card)( video ) return await this.boundComponent(Card)( video )
} ).join('') } ))
const cardsHtml = renderedCards.join('')
// console.log( 'cardsHtml', cardsHtml ) // console.log( 'cardsHtml', cardsHtml )

View file

@ -56,4 +56,10 @@ export function getVideoEndpoint ( video ) {
return `/tv/${video.slug}` return `/tv/${video.slug}`
} }
export function getRouteType ( routeString ) {
// Remove first slash and split by remaining
// slashes to get first part of route
const [ routeType ] = routeString.substring(1).split('/')
return routeType
}

485
helpers/lite-youtube.js Normal file
View file

@ -0,0 +1,485 @@
// https://github.com/paulirish/lite-youtube-embed/blob/master/src/lite-yt-embed.js
// import canAutoPlay from 'can-autoplay'
/**
* 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
*/
function isObject( maybeObject ) {
return maybeObject === Object( maybeObject )
}
class LiteYTEmbed extends HTMLElement {
constructor() {
// Always call super first in constructor
super()
this._uid = Date.now()
this.$refs = {}
this.$refs['timestamps-scroll-container'] = this.querySelector('.player-timestamps-wrapper')
this.playerLoaded = false
this.player = null
this.playing = false
this.progressInterval = null
this.playerTime = 0
this.preconnected = false
this.highlightedTimestampElement = null
}
connectedCallback() {
this.videoId = this.getAttribute('videoid')
this.videoDataScript = this.querySelector('.video-data')
this.video = JSON.parse( this.videoDataScript.innerHTML )
this.playerContainer = this.querySelector('.player-container')
this.playerPoster = this.querySelector('.player-poster')
// console.log('canAutoplay from connectedCallback', canAutoplay)
console.log('video', this.video)
console.log('this.playerContainer', this.playerContainer)
// Start watchers here
// On hover (or tap), warm up the TCP connections we're (likely) about to use.
this.playerContainer.addEventListener('pointerover', this.warmConnections, {once: true})
if ( this.hasTimestamps() ) {
// Array.from( this.querySelectorAll(`.player-timestamps [time]`) )
this.timestamps().map( timestamp => {
const timestampButton = this.querySelector(`.player-timestamps [time="${timestamp.time}"]`)
timestampButton.addEventListener('click', e => {
this.seekTo(timestamp.inSeconds)
})
})
}
// 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.playerPoster.addEventListener('click', e => {
this.startPlayerLoad()
})
// Mounted
this.detectAutoplay()
.then( ({ willAutoplay }) => {
console.log('willAutoplay', willAutoplay)
// If we're allowed to autoplay
// then start loading the player
if ( willAutoplay === true ) {
this.startPlayerLoad()
}
})
}
// // TODO: Support the the user changing the [videoid] attribute
// attributeChangedCallback() {
// }
/**
* Add a <link rel={preload | preconnect} ...> to the head
*/
// static c(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
// }
addIframe() {
const classNames = 'absolute inset-0 h-full w-full object-cover'
// https://www.youtube-nocookie.com/embed/${video.id}?enablejsapi=1&autoplay=1&modestbranding=1&playsinline=1
// const params = new URLSearchParams(this.getAttribute('params') || [])
// params.append('autoplay', '1')
const iframeEl = document.createElement('iframe')
this.$refs['frame'] = iframeEl
iframeEl.width = '100%'
iframeEl.height = '100%'
// 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.id = this.frameId()
iframeEl.classList.add(...classNames.split(' '))
iframeEl.frameborder = '0'
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.video.id)}?enablejsapi=1&autoplay=1&modestbranding=1&playsinline=1`
// this.append(iframeEl)
this.playerContainer.innerHTML = ''
this.playerContainer.append( iframeEl )
this.classList.add('lyt-activated')
// Set focus for a11y
this.querySelector('iframe').focus()
}
// Computed methods
frameId = () => {
return `youtube-player-${this.video.id}-${this._uid}`
}
timestamps = () => {
return this.video.timestamps.map( timestamp => {
const [ minutes, seconds ] = timestamp.time.split(':')
return {
...timestamp,
inSeconds: (minutes * 60) + Number(seconds)
}
})
}
hasTimestamps = () => {
return this.timestamps().length > 0
}
hasPlayer = () => {
return this.player !== null
}
activeTimestamp = () => {
const currentTime = this.playerTime// / 100
const reversesTimestamps = [
...this.timestamps()
]
// reversesTimestamps.reverse()
let foundTimestamp = null
for (const timestamp of reversesTimestamps) {
const hasStarted = currentTime > 1
const currentTimeisAfterPreviousTimestamp = (foundTimestamp !== null) ? currentTime > foundTimestamp.inSeconds : true
// const isPastCurrentTime = currentTime > timestamp.inSeconds
// const isBeforeCurrentTime = currentTime > timestamp.inSeconds
const currentTimeIsBeforeThisTimestamp = currentTime < timestamp.inSeconds
if (currentTimeisAfterPreviousTimestamp && currentTimeIsBeforeThisTimestamp) {
return foundTimestamp
}
foundTimestamp = timestamp
}
// No active timestamp
return null
}
highlightActiveTimestamp = () => {
const activeClassList = 'border-opacity-100 bg-darkest'
const inactiveClassList = 'border-opacity-0 neumorphic-shadow-inner'
const activeTimestamp = this.activeTimestamp()
// If there's no active timestamp
// then stop
if ( activeTimestamp === null ) return
// console.log('activeTimestamp', activeTimestamp)
if ( isObject( this.highlightedTimestampElement ) && this.highlightedTimestampElement.time === activeTimestamp.time) return
// Change active timestamp
const newActiveTimestamp = this.querySelector(`[time="${activeTimestamp.time}"]`)
// If there's already a highlited time stamp
// then unhighlight it
if ( isObject( this.highlightedTimestampElement ) ) {
this.highlightedTimestampElement.classList.remove(...activeClassList.split(' '))
this.highlightedTimestampElement.classList.add(...inactiveClassList.split(' '))
}
newActiveTimestamp.classList.remove(...inactiveClassList.split(' '))
newActiveTimestamp.classList.add(...activeClassList.split(' '))
// Scroll to new timestamp
this.scrollToTimestampButton( newActiveTimestamp )
this.highlightedTimestampElement = newActiveTimestamp
// console.log('newActiveTimestamp', newActiveTimestamp)
// console.log('this.highlightedTimestampElement', this.highlightedTimestampElement)
}
scrollToTimestampButton ( timestampButton ) {
// If timestamp button doesn't exist
// then stop
if ( !isObject( timestampButton ) ) return
const timestampsScroller = this.$refs['timestamps-scroll-container']
// const [ timestampButton ] = this.$refs[`timestamp-${timestamp.time}`]
// https://stackoverflow.com/a/63773123/1397641
const newScrollPosition = timestampButton.offsetLeft - timestampsScroller.offsetLeft
// console.log('timestampsScroller', timestampsScroller)
// console.log('timestampButton', timestampButton)
// console.log('newScrollPosition', newScrollPosition)
timestampsScroller.scroll({ left: newScrollPosition, behavior: 'smooth' })
}
detectAutoplay = async () => {
// if ( !process.client ) return { willAutoplay: false }
// const { default: canAutoPlay } = await import('can-autoplay')
const willAutoplay = await canAutoplay.video()
// const willAutoplayMuted = await canAutoPlay.video({ muted: true, inline: true })
return {
willAutoplay: willAutoplay.result
}
}
seekTo = async (timestampInSeconds) => {
if (this.playerLoaded === false) {
await this.startPlayerLoad()
}
this.player.seekTo(timestampInSeconds)
}
// async playVideo() {
// if (this.playerLoaded === false) {
// await this.startPlayerLoad()
// }
// this.$nextTick(() => {
// // console.log('this.player', JSON.stringify(this.player))
// this.player.playVideo()
// })
// },
addPrefetch = (kind, url, as) => {
// console.log('prefetching', url)
const linkEl = document.createElement('link')
linkEl.rel = kind
linkEl.href = url
if (as) {
linkEl.as = as;
}
document.head.append(linkEl)
}
warmConnections = () => {
if (this.preconnected) return
console.log('Warming connections')
// The iframe document and most of its subresources come right off youtube.com
this.addPrefetch('preconnect', 'https://www.youtube-nocookie.com')
// The botguard script is fetched off from google.com
this.addPrefetch('preconnect', 'https://www.google.com')
// Not certain if these ad related domains are in the critical path. Could verify with domain-specific throttling.
this.addPrefetch('preconnect', 'https://googleads.g.doubleclick.net')
this.addPrefetch('preconnect', 'https://static.doubleclick.net')
this.preconnected = true
}
startPlayerLoad = async () => {
// console.log('Starting player load')
this.addIframe()
this.playerLoaded = true
await this.initializePlayer()
// this.$nextTick(() => {
// this.initializePlayer()
// })
}
initializePlayer = async () => {
console.log('Initializing player')
// Clear player
this.player = null
// Clear progession interval
clearInterval(this.progressInterval)
// If there are no timestamps
// then stop
if ( !this.hasTimestamps() ) {
console.log('No timestamps. Skipping Youtube API initialization')
this.playerLoaded = true
return
}
if (typeof YT === 'undefined') {
await this.initializeApi()
}
const stateHandlers = {
// unstarted
'-1': () => null,
// ended
'0': () => null,
// playing
'1': this.onPlayerPlaying,
// paused
'2': this.onPlayerPaused,
// buffering
'3': () => null,
// video cued
'4': () => null,
}
// console.log('frame', this.$refs['frame'])
// console.log('frame id', this.$refs['frame'].id)
const onReady = () => new Promise( resolve => {
// console.log('Started onReady')
this.player = new YT.Player(this.$refs['frame'].id, {
events: {
'onReady': readyEvent => {
// console.log('Resolving onReady')
this.onPlayerReady( readyEvent )
resolve( readyEvent )
},
'onStateChange': event => {
// console.log('state changed', event)
const stateHandler = stateHandlers[String(event.data)]
// console.log('stateHandler', stateHandler)
stateHandler(event)
}
}
})
})
// console.log('Waiting for ready')
const readyEvent = await onReady()
// console.log('Youtube Player API ready', readyEvent, JSON.stringify(this.player))
}
initializeApi = () => {
return new Promise( resolve => {
const tag = document.createElement('script')
tag.id = `youtube-api-script-${this._uid}`
tag.src = 'https://www.youtube.com/iframe_api'
const firstScriptTag = document.getElementsByTagName('script')[0]
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag)
window.onYouTubeIframeAPIReady = resolve
})
}
onPlayerPlaying = () => {
// console.log('Player playing')
this.playing = true
this.progressInterval = setInterval(() => {
// console.log('this.player.getCurrentTime()', this.player.getCurrentTime())
// If player is empty
// then stop
if (this.player === null) {
clearInterval(this.progressInterval)
return
}
// console.log('this.player', this.player.hasOwnProperty('getCurrentTime'))
this.playerTime = this.player.getCurrentTime()
this.highlightActiveTimestamp()
}, 500)
}
onPlayerPaused = () => {
console.log('Player paused')
this.playing = false
clearInterval(this.progressInterval)
}
onPlayerReady (event) {
// console.log('Player is ready', event, this.player )
}
}
// Register custom element
window.customElements.define('lite-youtube', LiteYTEmbed)

View file

@ -0,0 +1,45 @@
function makeFeaturedAppsString ( featuredApps ) {
return featuredApps.slice(0, 5).map(app => app.name).join(', ')
}
export function buildVideoStructuredData ( video, featuredApps, options ) {
// console.log('video', video)
const {
siteUrl
} = options
const thumbnailUrls = video.thumbnail.srcset.split(',').map( srcSetImage => {
const [ imageUrl ] = srcSetImage.trim().split(' ')
return imageUrl
})
const featuredAppsString = makeFeaturedAppsString( featuredApps )
const embedUrl = new URL( `${ siteUrl }/embed/rich-results-player` )
embedUrl.searchParams.append( 'youtube-id', video.id )
embedUrl.searchParams.append( 'name', video.name )
return {
"@context": "https://schema.org",
// https://developers.google.com/search/docs/data-types/video
// https://schema.org/VideoObject
"@type": "VideoObject",
"name": video.name,
"description": `Includes the following apps: ${featuredAppsString}`,
"thumbnailUrl": thumbnailUrls,
// https://en.wikipedia.org/wiki/ISO_8601
"uploadDate": video.lastUpdated.raw,
// "duration": "PT1M54S", // Need to updaet Youtube API Request for this
// "contentUrl": "https://www.example.com/video/123/file.mp4",
"embedUrl": embedUrl.href,
// "interactionStatistic": {
// "@type": "InteractionCounter",
// "interactionType": { "@type": "http://schema.org/WatchAction" },
// "userInteractionCount": 5647018
// },
// "regionsAllowed": "US,NL"
}
}

View file

@ -1,7 +1,7 @@
import fs from 'fs' import fs from 'fs'
import { JSDOM } from 'jsdom' import { JSDOM } from 'jsdom'
import config from '../nuxt.config' import config from '../nuxt.config.js'
console.log('Running Default Layout file') console.log('Running Default Layout file')
@ -94,6 +94,9 @@ const cleanNuxtLayout = ( layout ) => {
// Set link tags // Set link tags
document.querySelector('title').insertAdjacentHTML('afterend', templateVar('link-tags') ) document.querySelector('title').insertAdjacentHTML('afterend', templateVar('link-tags') )
// Add meta tags after title node
document.querySelector('title').insertAdjacentHTML('afterend', templateVar('structured-data') )
// Add meta tags after title node // Add meta tags after title node
document.querySelector('title').insertAdjacentHTML('afterend', templateVar('meta-tags') ) document.querySelector('title').insertAdjacentHTML('afterend', templateVar('meta-tags') )
@ -172,11 +175,30 @@ class DefaultLayout {
return Object.values(meta).join('') return Object.values(meta).join('')
} }
generateLinkTags = ( pageLinkTags = [] ) => { generateStructuredData = function ( renderData ) {
const {
structuredData = null
} = renderData
// console.log('renderData', Object.keys(renderData))
if ( structuredData === null ) return ''
const structuredDataJson = JSON.stringify( structuredData )
return `<script type="application/ld+json">${ structuredDataJson }</script>`
}
generateLinkTags = ( renderData ) => {
const {
headLinkTags = []
} = renderData
const linkTags = { const linkTags = {
...defaultLinkTags, ...defaultLinkTags,
...Object.fromEntries(pageLinkTags.map( mapLinkTag )) ...Object.fromEntries( headLinkTags.map( mapLinkTag ) )
} }
return Object.values( linkTags ).join('') return Object.values( linkTags ).join('')
@ -205,12 +227,15 @@ class DefaultLayout {
// Set link tags // Set link tags
// this.generateLinkTags() // this.generateLinkTags()
workingLayoutString = workingLayoutString.replace( templateVar('link-tags'), this.generateLinkTags() ) workingLayoutString = workingLayoutString.replace( templateVar('link-tags'), this.generateLinkTags( data ) )
// Add meta tags after title node // Add meta tags after title node
// this.generateMetaTags( data ) // this.generateMetaTags( data )
workingLayoutString = workingLayoutString.replace( templateVar('meta-tags'), this.generateMetaTags( data ) ) workingLayoutString = workingLayoutString.replace( templateVar('meta-tags'), this.generateMetaTags( data ) )
// Add structured data
workingLayoutString = workingLayoutString.replace( templateVar('structured-data'), this.generateStructuredData( data ) )
// Set page css // Set page css
// document.querySelector('head').insertAdjacentHTML('beforeend', this.getCss() ) // document.querySelector('head').insertAdjacentHTML('beforeend', this.getCss() )

40
package-lock.json generated
View file

@ -16,6 +16,7 @@
"can-autoplay": "^3.0.0", "can-autoplay": "^3.0.0",
"chance": "^1.1.7", "chance": "^1.1.7",
"cross-env": "^5.2.0", "cross-env": "^5.2.0",
"esbuild": "^0.11.20",
"jsdom": "^16.4.0", "jsdom": "^16.4.0",
"lazysizes": "^5.3.0-beta1", "lazysizes": "^5.3.0-beta1",
"markdown-it": "^11.0.1", "markdown-it": "^11.0.1",
@ -26,6 +27,7 @@
"pretty-bytes": "^5.5.0", "pretty-bytes": "^5.5.0",
"scroll-into-view-if-needed": "^2.2.26", "scroll-into-view-if-needed": "^2.2.26",
"slugify": "^1.4.6", "slugify": "^1.4.6",
"terser": "^4.8.0",
"vue-full-screen-file-drop": "^2.0.0" "vue-full-screen-file-drop": "^2.0.0"
}, },
"devDependencies": { "devDependencies": {
@ -5999,8 +6001,7 @@
"node_modules/buffer-from": { "node_modules/buffer-from": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
"integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A=="
"dev": true
}, },
"node_modules/buffer-json": { "node_modules/buffer-json": {
"version": "2.0.0", "version": "2.0.0",
@ -6795,8 +6796,7 @@
"node_modules/commander": { "node_modules/commander": {
"version": "2.20.3", "version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
"dev": true
}, },
"node_modules/commondir": { "node_modules/commondir": {
"version": "1.0.1", "version": "1.0.1",
@ -9128,6 +9128,15 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/esbuild": {
"version": "0.11.20",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.11.20.tgz",
"integrity": "sha512-QOZrVpN/Yz74xfat0H6euSgn3RnwLevY1mJTEXneukz1ln9qB+ieaerRMzSeETpz/UJWsBMzRVR/andBht5WKw==",
"hasInstallScript": true,
"bin": {
"esbuild": "bin/esbuild"
}
},
"node_modules/escalade": { "node_modules/escalade": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
@ -22997,7 +23006,6 @@
"version": "0.5.19", "version": "0.5.19",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz",
"integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==",
"dev": true,
"dependencies": { "dependencies": {
"buffer-from": "^1.0.0", "buffer-from": "^1.0.0",
"source-map": "^0.6.0" "source-map": "^0.6.0"
@ -23007,7 +23015,6 @@
"version": "0.6.1", "version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true,
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@ -23882,7 +23889,6 @@
"version": "4.8.0", "version": "4.8.0",
"resolved": "https://registry.npmjs.org/terser/-/terser-4.8.0.tgz", "resolved": "https://registry.npmjs.org/terser/-/terser-4.8.0.tgz",
"integrity": "sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw==", "integrity": "sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw==",
"dev": true,
"dependencies": { "dependencies": {
"commander": "^2.20.0", "commander": "^2.20.0",
"source-map": "~0.6.1", "source-map": "~0.6.1",
@ -23994,7 +24000,6 @@
"version": "0.6.1", "version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true,
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@ -31632,8 +31637,7 @@
"buffer-from": { "buffer-from": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
"integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A=="
"dev": true
}, },
"buffer-json": { "buffer-json": {
"version": "2.0.0", "version": "2.0.0",
@ -32280,8 +32284,7 @@
"commander": { "commander": {
"version": "2.20.3", "version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
"dev": true
}, },
"commondir": { "commondir": {
"version": "1.0.1", "version": "1.0.1",
@ -34229,6 +34232,11 @@
"is-symbol": "^1.0.2" "is-symbol": "^1.0.2"
} }
}, },
"esbuild": {
"version": "0.11.20",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.11.20.tgz",
"integrity": "sha512-QOZrVpN/Yz74xfat0H6euSgn3RnwLevY1mJTEXneukz1ln9qB+ieaerRMzSeETpz/UJWsBMzRVR/andBht5WKw=="
},
"escalade": { "escalade": {
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
@ -45481,7 +45489,6 @@
"version": "0.5.19", "version": "0.5.19",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz",
"integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==",
"dev": true,
"requires": { "requires": {
"buffer-from": "^1.0.0", "buffer-from": "^1.0.0",
"source-map": "^0.6.0" "source-map": "^0.6.0"
@ -45490,8 +45497,7 @@
"source-map": { "source-map": {
"version": "0.6.1", "version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
"dev": true
} }
} }
}, },
@ -46198,7 +46204,6 @@
"version": "4.8.0", "version": "4.8.0",
"resolved": "https://registry.npmjs.org/terser/-/terser-4.8.0.tgz", "resolved": "https://registry.npmjs.org/terser/-/terser-4.8.0.tgz",
"integrity": "sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw==", "integrity": "sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw==",
"dev": true,
"requires": { "requires": {
"commander": "^2.20.0", "commander": "^2.20.0",
"source-map": "~0.6.1", "source-map": "~0.6.1",
@ -46208,8 +46213,7 @@
"source-map": { "source-map": {
"version": "0.6.1", "version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
"dev": true
} }
} }
}, },

View file

@ -32,6 +32,7 @@
"can-autoplay": "^3.0.0", "can-autoplay": "^3.0.0",
"chance": "^1.1.7", "chance": "^1.1.7",
"cross-env": "^5.2.0", "cross-env": "^5.2.0",
"esbuild": "^0.11.20",
"jsdom": "^16.4.0", "jsdom": "^16.4.0",
"lazysizes": "^5.3.0-beta1", "lazysizes": "^5.3.0-beta1",
"markdown-it": "^11.0.1", "markdown-it": "^11.0.1",
@ -42,6 +43,7 @@
"pretty-bytes": "^5.5.0", "pretty-bytes": "^5.5.0",
"scroll-into-view-if-needed": "^2.2.26", "scroll-into-view-if-needed": "^2.2.26",
"slugify": "^1.4.6", "slugify": "^1.4.6",
"terser": "^4.8.0",
"vue-full-screen-file-drop": "^2.0.0" "vue-full-screen-file-drop": "^2.0.0"
}, },
"devDependencies": { "devDependencies": {

View file

@ -2,7 +2,7 @@ import dotenv from 'dotenv'
import config from '../nuxt.config.js' import config from '../nuxt.config.js'
import { getAppType } from '../helpers/app-derived.js' import { getAppType, getRouteType } from '../helpers/app-derived.js'
import { deviceSupportsApp } from '../helpers/devices.js' import { deviceSupportsApp } from '../helpers/devices.js'
import { makeLastUpdatedFriendly } from '../helpers/parse-date' import { makeLastUpdatedFriendly } from '../helpers/parse-date'
@ -70,9 +70,10 @@ export class AppTemplate {
before: function( data ) { before: function( data ) {
return data.filter( entry => { return data.filter( entry => {
const appType = getAppType( entry.payload.app ) // const [ _, routeType ] = entry.route.split('/')
const routeType = getRouteType( entry.route )
return appType === 'app' return routeType === 'app'
}) })
} }
}, },
@ -97,13 +98,15 @@ export class AppTemplate {
} }
} }
render( data ) { async render( data ) {
const { const {
app: { payload: { app, relatedVideos = [] } }, app: { payload: { app, relatedVideos = [] } },
'device-list': deviceList 'device-list': deviceList
} = data } = data
const hasRelatedVideos = relatedVideos.length > 0
// console.log('deviceList', deviceList) // console.log('deviceList', deviceList)
// console.log('video.payload', Object.keys(video.payload)) // console.log('video.payload', Object.keys(video.payload))
@ -123,8 +126,12 @@ export class AppTemplate {
const relatedLinksHtml = renderPageLinksHtml( app.relatedLinks ) const relatedLinksHtml = renderPageLinksHtml( app.relatedLinks )
const relatedVideosRowHtml = hasRelatedVideos ? await this.boundComponent(VideoRow)( relatedVideos ) : null
return /* html */` return /* html */`
<section class="container py-32"> <section class="container space-y-8 py-32">
<div class="intro-content flex flex-col items-center text-center min-h-3/4-screen md:min-h-0 space-y-8"> <div class="intro-content flex flex-col items-center text-center min-h-3/4-screen md:min-h-0 space-y-8">
<h1 class="title text-sm md:text-2xl font-bold"> <h1 class="title text-sm md:text-2xl font-bold">
${ data.mainHeading } ${ data.mainHeading }
@ -168,7 +175,7 @@ export class AppTemplate {
</div> </div>
</div> </div>
${ relatedVideos.length > 0 ? /* html */` ${ hasRelatedVideos ? /* html */`
<div <div
class="related-videos w-full" class="related-videos w-full"
> >
@ -176,7 +183,7 @@ export class AppTemplate {
Related Videos Related Videos
</h2> </h2>
${ this.boundComponent(VideoRow)( relatedVideos ) } ${ relatedVideosRowHtml }
</div> </div>
` : '' } ` : '' }

View file

@ -2,7 +2,7 @@ import dotenv from 'dotenv'
import config from '../nuxt.config.js' import config from '../nuxt.config.js'
import { getAppType } from '../helpers/app-derived.js' import { getAppType, getRouteType } from '../helpers/app-derived.js'
import { AppTemplate } from './app.11ty.js' import { AppTemplate } from './app.11ty.js'
@ -33,9 +33,9 @@ class FormulaTemplate extends AppTemplate {
alias: 'app', alias: 'app',
before: function( data ) { before: function( data ) {
return data.filter( entry => { return data.filter( entry => {
const appType = getAppType( entry.payload.app ) const routeType = getRouteType( entry.route )
return appType === 'formula' return routeType === 'formula'
}) })
} }
}, },

View file

@ -1,27 +1,56 @@
import dotenv from 'dotenv' import dotenv from 'dotenv'
import config from '../nuxt.config' import config from '../nuxt.config.js'
import VideoPlayer from '../components-eleventy/video/player.js'
import VideoRow from '../components-eleventy/video/row.js' import VideoRow from '../components-eleventy/video/row.js'
import { isVideo } from '../helpers/app-derived'
import { getRouteType } from '../helpers/app-derived.js'
import { buildVideoStructuredData } from '../helpers/structured-data.js'
// Setup dotenv // Setup dotenv
dotenv.config() dotenv.config()
export const makeTitle = function ( video ) { export const myChannelId = 'UCB3jOb5QVjX7lYecvyCoTqQ'
return `${ video.name } - ${ config.head.title }`
export const makeTitle = function ( name ) {
// console.log('tvEntry', tvEntry)
return `${ name } - ${ config.head.title }`
} }
export const makeDescription = function ( video ) { export const makeDescription = function ( tvEntry ) {
if ( video.payload.featuredApps.length === 0 ) return 'Apple Silicon performance and support videos' if ( tvEntry.payload.featuredApps.length === 0 ) return 'Apple Silicon performance and support videos'
const featuredAppsString = video.payload.featuredApps.slice(0, 5).map(app => app.name).join(', ') const featuredAppsString = tvEntry.payload.featuredApps.slice(0, 5).map(app => app.name).join(', ')
// console.log('video.payload.featuredApps', video.payload.featuredApps) // console.log('tvEntry.payload.featuredApps', tvEntry.payload.featuredApps)
return `Apple Silicon performance and support videos for ${ featuredAppsString }` return `Apple Silicon performance and support videos for ${ featuredAppsString }`
} }
function renderFeaturedApps ( featuredApps ) {
return /* html */`
<div
class="related-apps w-full"
>
<h2 class="subtitle text-xl md:text-2xl font-bold mb-3">
Related Apps
</h2>
<div class="featured-apps overflow-x-auto overflow-y-visible whitespace-no-wrap py-2 space-x-2">
${ featuredApps.map( app => ( /* html */`
<a
href="${ app.endpoint }"
role="button"
class="relative inline-flex items-center rounded-md px-4 py-2 leading-5 font-bold text-white border border-transparent focus:outline-none focus:border-indigo-600 neumorphic-shadow-inner bg-darker hover:bg-indigo-400 active:bg-indigo-600 transition duration-150 ease-in-out inline-block text-xs rounded-lg py-1 px-2"
>${ app.name }</a>
`) ).join('') }
</div>
</div>
`
}
class TV { class TV {
// or `async data() {` // or `async data() {`
// or `get data() {` // or `get data() {`
@ -32,62 +61,135 @@ class TV {
pagination: { pagination: {
data: 'eleventy-endpoints', data: 'eleventy-endpoints',
size: 1, size: 1,
alias: 'payload', alias: 'tvEntry',
before: function( data ) { before: function( endpoint ) {
return data.filter( entry => { // console.log('Before runs')
return entry.payload.hasOwnProperty('video') && isVideo( entry.payload.video )
return endpoint.filter( entry => {
const routeType = getRouteType( entry.route )
return routeType === 'tv'
}) })
}
}, },
},
// tags: [ 'tv' ],
eleventyComputed: { eleventyComputed: {
title: ({ payload: { video } }) => { title: data => {
// console.log('data', data) // Declare dependencies for Eleventy
return makeTitle( video ) // https://www.11ty.dev/docs/data-computed/#declaring-your-dependencies
}, data.tvEntry
description: ({ payload: { video } }) => {
return makeDescription( video ) return makeTitle( data.tvEntry.payload.video.name )
}, },
description: ( data ) => {
// Declare dependencies for Eleventy
// https://www.11ty.dev/docs/data-computed/#declaring-your-dependencies
data.tvEntry
return makeDescription( data.tvEntry )
}, },
permalink: ({ payload: { video } }) => { headLinkTags: data => {
// console.log('data', data) // Declare dependencies for Eleventy
return `tv/${ video.slug }/` // https://www.11ty.dev/docs/data-computed/#declaring-your-dependencies
data.tvEntry
return [
// Preload video thumbnail
// <link rel="preload" as="image" href="img.png" />
{
'rel': 'preload',
'as': 'image',
'href': `https://i.ytimg.com/vi_webp/${ data.tvEntry.payload.video.id }/sddefault.webp`
},
]
},
structuredData: data => {
// Declare dependencies for Eleventy
// https://www.11ty.dev/docs/data-computed/#declaring-your-dependencies
data.tvEntry
return buildVideoStructuredData( data.tvEntry.payload.video, data.tvEntry.payload.featuredApps, {
siteUrl: process.env.URL
} )
}
},
permalink: ( data ) => {
return data.tvEntry.route.substring(1) + '/'
} }
} }
} }
render({ payload: { video } }) { async render( data ) {
const {
tvEntry: {
// route,
payload: {
video,
relatedVideos = [],
featuredApps = []
}
},
// 'device-list': deviceList
} = data
// console.log('video.payload', Object.keys(video.payload)) // console.log('video.payload', Object.keys(video.payload))
const coverBottomHtml = /* html */`
<div class="page-heading h-full flex items-end md:p-4">
<h1 class="title text-xs text-left md:text-2xl font-bold">${ video.name }</h1>
</div>
`
const playerHtml = await this.boundComponent(VideoPlayer)( video, {
coverBottomHtml
} )
const hasFeaturedApps = featuredApps.length > 0
const featuredAppsHtml = hasFeaturedApps ? renderFeaturedApps( featuredApps ) : ''
const rowHtml = await this.boundComponent(VideoRow)( relatedVideos )
// const rowHtml = renderedRow.join('')
return /* html */` return /* html */`
<section class="container pb-16"> <section class="container pb-16">
<div class="flex flex-col items-center text-center space-y-6"> <div class="flex flex-col items-center text-center space-y-6">
<div class="video-canvas w-screen flex flex-col justify-center items-center bg-black pt-16" style="left:50%;right:50%;margin-left:-50vw;margin-right:-50vw;">
<div class="ratio-wrapper w-full max-w-4xl"> ${ playerHtml }
<div class="relative overflow-hidden w-full pb-16/9">
<iframe id="youtube-player-${ video.id }-17212" src="https://www.youtube-nocookie.com/embed/${ video.id }?enablejsapi=1&autoplay=1&modestbranding=1&playsinline=1" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen class="absolute h-full w-full"></iframe>
</div>
</div>
<!---->
</div>
<div class="md:flex w-full justify-between space-y-4 md:space-y-0 md:px-10"> <div class="md:flex w-full justify-between space-y-4 md:space-y-0 md:px-10">
<h1 class="title text-lg md:text-2xl font-bold">${ video.name }</h1>
<div class="channel-credit"><a href="https://www.youtube.com/channel/UCptwuAv0XQHo1OQUSaO6NHw" target="_blank" rel="noopener" role="button" class="relative inline-flex items-center rounded-md px-4 py-2 leading-5 font-bold text-white border border-transparent focus:outline-none focus:border-indigo-600 neumorphic-shadow focus:shadow-outline-indigo bg-darker hover:bg-indigo-400 active:bg-indigo-600 transition duration-150 ease-in-out">Subscribe to Max Tech</a></div> ${ video.channel.id !== myChannelId ? /* html */`
<div
class="channel-credit"
>
<a
href="https://www.youtube.com/channel/${ video.channel.id }?sub_confirmation=1"
target="_blank"
rel="noopener"
role="button"
class="relative inline-flex items-center rounded-md px-4 py-2 leading-5 font-bold text-white border border-transparent focus:outline-none focus:border-indigo-600 neumorphic-shadow focus:shadow-outline-indigo bg-darker hover:bg-indigo-400 active:bg-indigo-600 transition duration-150 ease-in-out"
>Subscribe to ${ video.channel.name }</a>
</div>
` : '' }
</div> </div>
<hr class="w-full"> <hr class="w-full">
<div class="related-apps w-full"> ${ featuredAppsHtml }
<h2 class="subtitle text-xl md:text-2xl font-bold mb-3">Related Apps</h2>
<div class="featured-apps overflow-x-auto overflow-y-visible whitespace-no-wrap py-2 space-x-2"><a href="/app/xcode" role="button" class="relative inline-flex items-center rounded-md px-4 py-2 leading-5 font-bold text-white border border-transparent focus:outline-none focus:border-indigo-600 neumorphic-shadow-inner bg-darker hover:bg-indigo-400 active:bg-indigo-600 transition duration-150 ease-in-out inline-block text-xs rounded-lg py-1 px-2">Xcode</a><a href="/app/logic-pro" role="button" class="relative inline-flex items-center rounded-md px-4 py-2 leading-5 font-bold text-white border border-transparent focus:outline-none focus:border-indigo-600 neumorphic-shadow-inner bg-darker hover:bg-indigo-400 active:bg-indigo-600 transition duration-150 ease-in-out inline-block text-xs rounded-lg py-1 px-2">Logic Pro</a><a href="/app/lightroom" role="button" class="relative inline-flex items-center rounded-md px-4 py-2 leading-5 font-bold text-white border border-transparent focus:outline-none focus:border-indigo-600 neumorphic-shadow-inner bg-darker hover:bg-indigo-400 active:bg-indigo-600 transition duration-150 ease-in-out inline-block text-xs rounded-lg py-1 px-2">Lightroom</a><a href="/app/lightroom-classic" role="button" class="relative inline-flex items-center rounded-md px-4 py-2 leading-5 font-bold text-white border border-transparent focus:outline-none focus:border-indigo-600 neumorphic-shadow-inner bg-darker hover:bg-indigo-400 active:bg-indigo-600 transition duration-150 ease-in-out inline-block text-xs rounded-lg py-1 px-2">Lightroom Classic</a><a href="/app/cinebench" role="button" class="relative inline-flex items-center rounded-md px-4 py-2 leading-5 font-bold text-white border border-transparent focus:outline-none focus:border-indigo-600 neumorphic-shadow-inner bg-darker hover:bg-indigo-400 active:bg-indigo-600 transition duration-150 ease-in-out inline-block text-xs rounded-lg py-1 px-2">Cinebench</a><a href="/app/final-cut-pro" role="button" class="relative inline-flex items-center rounded-md px-4 py-2 leading-5 font-bold text-white border border-transparent focus:outline-none focus:border-indigo-600 neumorphic-shadow-inner bg-darker hover:bg-indigo-400 active:bg-indigo-600 transition duration-150 ease-in-out inline-block text-xs rounded-lg py-1 px-2">Final Cut Pro</a><a href="/app/geekbench" role="button" class="relative inline-flex items-center rounded-md px-4 py-2 leading-5 font-bold text-white border border-transparent focus:outline-none focus:border-indigo-600 neumorphic-shadow-inner bg-darker hover:bg-indigo-400 active:bg-indigo-600 transition duration-150 ease-in-out inline-block text-xs rounded-lg py-1 px-2">Geekbench</a></div>
</div>
<div class="related-videos w-full"> <div class="related-videos w-full">
<h2 class="subtitle text-xl md:text-2xl font-bold mb-3">Related Videos</h2> <h2 class="subtitle text-xl md:text-2xl font-bold mb-3">Related Videos</h2>
${ this.boundComponent(VideoRow)( video.payload.relatedVideos ) } ${ rowHtml }
</div> </div>

View file

@ -81,6 +81,8 @@
import { getAppEndpoint } from '~/helpers/app-derived.js' import { getAppEndpoint } from '~/helpers/app-derived.js'
import { buildVideoStructuredData } from '~/helpers/structured-data.js'
import LinkButton from '~/components/link-button.vue' import LinkButton from '~/components/link-button.vue'
import EmailSubscribe from '~/components/email-subscribe.vue' import EmailSubscribe from '~/components/email-subscribe.vue'
import VideoRow from '~/components/video/row.vue' import VideoRow from '~/components/video/row.vue'
@ -93,43 +95,6 @@ function makeFeaturedAppsString ( featuredApps ) {
} }
function buildVideoStructuredData ( video, featuredApps ) {
// console.log('video', video)
const thumbnailUrls = video.thumbnail.srcset.split(',').map( srcSetImage => {
const [ imageUrl ] = srcSetImage.trim().split(' ')
return imageUrl
})
const featuredAppsString = makeFeaturedAppsString( featuredApps )
const embedUrl = new URL( `${ this.$config.siteUrl }/embed/rich-results-player` )
embedUrl.searchParams.append( 'youtube-id', video.id )
embedUrl.searchParams.append( 'name', video.name )
return {
"@context": "https://schema.org",
// https://developers.google.com/search/docs/data-types/video
// https://schema.org/VideoObject
"@type": "VideoObject",
"name": video.name,
"description": `Includes the following apps: ${featuredAppsString}`,
"thumbnailUrl": thumbnailUrls,
// https://en.wikipedia.org/wiki/ISO_8601
"uploadDate": video.lastUpdated.raw,
// "duration": "PT1M54S", // Need to updaet Youtube API Request for this
// "contentUrl": "https://www.example.com/video/123/file.mp4",
"embedUrl": embedUrl.href,
// "interactionStatistic": {
// "@type": "InteractionCounter",
// "interactionType": { "@type": "http://schema.org/WatchAction" },
// "userInteractionCount": 5647018
// },
// "regionsAllowed": "US,NL"
}
}
export default { export default {
components: { components: {
LinkButton, LinkButton,
@ -184,7 +149,9 @@ export default {
getAppEndpoint getAppEndpoint
}, },
head() { head() {
const structuredData = buildVideoStructuredData.bind(this)( this.video, this.featuredApps ) const structuredData = buildVideoStructuredData( this.video, this.featuredApps, {
siteUrl: this.$config.siteUrl
} )
return { return {