Add ability to override client assets : logo - favicon - PWA icons - PWA manifest name and description (#2897)
* Add client-overrides storage to config * Add static-serve for client overrides * Move backgroun-image logo from bundle to css tag for runtime content hash * Add dynamic JSON manifest * Add content hash for manifest, favicon and logo Co-authored-by: kimsible <kimsible@users.noreply.github.com>
This commit is contained in:
		
							parent
							
								
									27647da17f
								
							
						
					
					
						commit
						caf2aaf4f9
					
				
					 15 changed files with 114 additions and 9 deletions
				
			
		| 
						 | 
				
			
			@ -62,7 +62,7 @@
 | 
			
		|||
 | 
			
		||||
      .icon.icon-logo {
 | 
			
		||||
        display: inline-block;
 | 
			
		||||
        background: url('../assets/images/logo.svg') no-repeat;
 | 
			
		||||
        background-repeat: no-repeat;
 | 
			
		||||
        width: 23px;
 | 
			
		||||
        height: 24px;
 | 
			
		||||
        margin-right: .5rem;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,9 +7,16 @@
 | 
			
		|||
    <meta name="theme-color" content="#fff" />
 | 
			
		||||
    <meta property="og:platform" content="PeerTube" />
 | 
			
		||||
    <!-- Web Manifest file -->
 | 
			
		||||
    <link rel="manifest" href="/manifest.webmanifest">
 | 
			
		||||
    <link rel="manifest" href="/manifest.webmanifest?[manifestContentHash]">
 | 
			
		||||
 | 
			
		||||
    <link rel="icon" type="image/png" href="/client/assets/images/favicon.png" />
 | 
			
		||||
    <link rel="icon" type="image/png" href="/client/assets/images/favicon.png?[faviconContentHash]" />
 | 
			
		||||
 | 
			
		||||
    <!-- logo background-image -->
 | 
			
		||||
    <style type="text/css">
 | 
			
		||||
      .icon-logo {
 | 
			
		||||
        background-image: url(/client/assets/images/logo.svg?[logoContentHash]);
 | 
			
		||||
      }
 | 
			
		||||
    </style>
 | 
			
		||||
 | 
			
		||||
    <!-- base url -->
 | 
			
		||||
    <base href="/">
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -85,6 +85,11 @@ storage:
 | 
			
		|||
  captions: 'storage/captions/'
 | 
			
		||||
  cache: 'storage/cache/'
 | 
			
		||||
  plugins: 'storage/plugins/'
 | 
			
		||||
  # Overridable client files : logo.svg, favicon.png and icons/*.png (PWA) in client/dist/assets/images
 | 
			
		||||
  # Could contain for example assets/images/favicon.png
 | 
			
		||||
  # If the file exists, peertube will serve it
 | 
			
		||||
  # If not, peertube will fallback to the default fil
 | 
			
		||||
  client_overrides: 'storage/client-overrides/'
 | 
			
		||||
 | 
			
		||||
log:
 | 
			
		||||
  level: 'info' # debug/info/warning/error
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -86,6 +86,11 @@ storage:
 | 
			
		|||
  captions: '/var/www/peertube/storage/captions/'
 | 
			
		||||
  cache: '/var/www/peertube/storage/cache/'
 | 
			
		||||
  plugins: '/var/www/peertube/storage/plugins/'
 | 
			
		||||
  # Overridable client files : logo.svg, favicon.png and icons/*.png (PWA) in client/dist/assets/images
 | 
			
		||||
  # Could contain for example assets/images/favicon.png
 | 
			
		||||
  # If the file exists, peertube will serve it
 | 
			
		||||
  # If not, peertube will fallback to the default fil
 | 
			
		||||
  client_overrides: '/var/www/peertube/storage/client-overrides/'
 | 
			
		||||
 | 
			
		||||
log:
 | 
			
		||||
  level: 'info' # debug/info/warning/error
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -22,6 +22,7 @@ storage:
 | 
			
		|||
  captions: 'test1/captions/'
 | 
			
		||||
  cache: 'test1/cache/'
 | 
			
		||||
  plugins: 'test1/plugins/'
 | 
			
		||||
  client_overrides: 'test1/client-overrides/'
 | 
			
		||||
 | 
			
		||||
admin:
 | 
			
		||||
  email: 'admin1@example.com'
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -22,6 +22,7 @@ storage:
 | 
			
		|||
  captions: 'test2/captions/'
 | 
			
		||||
  cache: 'test2/cache/'
 | 
			
		||||
  plugins: 'test2/plugins/'
 | 
			
		||||
  client_overrides: 'test2/client-overrides/'
 | 
			
		||||
 | 
			
		||||
admin:
 | 
			
		||||
  email: 'admin2@example.com'
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -22,6 +22,7 @@ storage:
 | 
			
		|||
  captions: 'test3/captions/'
 | 
			
		||||
  cache: 'test3/cache/'
 | 
			
		||||
  plugins: 'test3/plugins/'
 | 
			
		||||
  client_overrides: 'test3/client-overrides/'
 | 
			
		||||
 | 
			
		||||
admin:
 | 
			
		||||
  email: 'admin3@example.com'
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -22,6 +22,7 @@ storage:
 | 
			
		|||
  captions: 'test4/captions/'
 | 
			
		||||
  cache: 'test4/cache/'
 | 
			
		||||
  plugins: 'test4/plugins/'
 | 
			
		||||
  client_overrides: 'test4/client-overrides/'
 | 
			
		||||
 | 
			
		||||
admin:
 | 
			
		||||
  email: 'admin4@example.com'
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -22,6 +22,7 @@ storage:
 | 
			
		|||
  captions: 'test5/captions/'
 | 
			
		||||
  cache: 'test5/cache/'
 | 
			
		||||
  plugins: 'test5/plugins/'
 | 
			
		||||
  client_overrides: 'test5/client-overrides/'
 | 
			
		||||
 | 
			
		||||
admin:
 | 
			
		||||
  email: 'admin5@example.com'
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -22,6 +22,7 @@ storage:
 | 
			
		|||
  captions: 'test6/captions/'
 | 
			
		||||
  cache: 'test6/cache/'
 | 
			
		||||
  plugins: 'test6/plugins/'
 | 
			
		||||
  client_overrides: 'test6/client-overrides/'
 | 
			
		||||
 | 
			
		||||
admin:
 | 
			
		||||
  email: 'admin6@example.com'
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,3 +1,4 @@
 | 
			
		|||
import { constants, promises as fs } from 'fs'
 | 
			
		||||
import * as express from 'express'
 | 
			
		||||
import { join } from 'path'
 | 
			
		||||
import { root } from '../helpers/core-utils'
 | 
			
		||||
| 
						 | 
				
			
			@ -39,20 +40,40 @@ clientsRouter.use(
 | 
			
		|||
)
 | 
			
		||||
 | 
			
		||||
// Static HTML/CSS/JS client files
 | 
			
		||||
 | 
			
		||||
const staticClientFiles = [
 | 
			
		||||
  'manifest.webmanifest',
 | 
			
		||||
  'ngsw-worker.js',
 | 
			
		||||
  'ngsw.json'
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
for (const staticClientFile of staticClientFiles) {
 | 
			
		||||
  const path = join(root(), 'client', 'dist', staticClientFile)
 | 
			
		||||
 | 
			
		||||
  clientsRouter.get('/' + staticClientFile, (req: express.Request, res: express.Response) => {
 | 
			
		||||
  clientsRouter.get(`/${staticClientFile}`, (req: express.Request, res: express.Response) => {
 | 
			
		||||
    res.sendFile(path, { maxAge: STATIC_MAX_AGE.SERVER })
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Dynamic PWA manifest
 | 
			
		||||
clientsRouter.get('/manifest.webmanifest', asyncMiddleware(generateManifest))
 | 
			
		||||
 | 
			
		||||
// Static client overrides
 | 
			
		||||
const staticClientOverrides = [
 | 
			
		||||
  'assets/images/logo.svg',
 | 
			
		||||
  'assets/images/favicon.png',
 | 
			
		||||
  'assets/images/icons/icon-36x36.png',
 | 
			
		||||
  'assets/images/icons/icon-48x48.png',
 | 
			
		||||
  'assets/images/icons/icon-72x72.png',
 | 
			
		||||
  'assets/images/icons/icon-96x96.png',
 | 
			
		||||
  'assets/images/icons/icon-144x144.png',
 | 
			
		||||
  'assets/images/icons/icon-192x192.png',
 | 
			
		||||
  'assets/images/icons/icon-512x512.png'
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
for (const staticClientOverride of staticClientOverrides) {
 | 
			
		||||
  const overridePhysicalPath = join(CONFIG.STORAGE.CLIENT_OVERRIDES_DIR, staticClientOverride)
 | 
			
		||||
  clientsRouter.use(`/client/${staticClientOverride}`, asyncMiddleware(serveClientOverride(overridePhysicalPath)))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
clientsRouter.use('/client/locales/:locale/:file.json', serveServerTranslations)
 | 
			
		||||
clientsRouter.use('/client', express.static(distPath, { maxAge: STATIC_MAX_AGE.CLIENT }))
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -130,3 +151,28 @@ function sendHTML (html: string, res: express.Response) {
 | 
			
		|||
 | 
			
		||||
  return res.send(html)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function generateManifest (req: express.Request, res: express.Response) {
 | 
			
		||||
  const manifestPhysicalPath = join(root(), 'client', 'dist', 'manifest.webmanifest')
 | 
			
		||||
  const manifestJson = await fs.readFile(manifestPhysicalPath, 'utf8')
 | 
			
		||||
  const manifest = JSON.parse(manifestJson)
 | 
			
		||||
 | 
			
		||||
  manifest.name = CONFIG.INSTANCE.NAME
 | 
			
		||||
  manifest.short_name = CONFIG.INSTANCE.NAME
 | 
			
		||||
  manifest.description = CONFIG.INSTANCE.SHORT_DESCRIPTION
 | 
			
		||||
 | 
			
		||||
  res.json(manifest)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function serveClientOverride (path: string) {
 | 
			
		||||
  return async (req: express.Request, res: express.Response, next: express.NextFunction) => {
 | 
			
		||||
    try {
 | 
			
		||||
      await fs.access(path, constants.F_OK)
 | 
			
		||||
      // Serve override client
 | 
			
		||||
      res.sendFile(path, { maxAge: STATIC_MAX_AGE.SERVER })
 | 
			
		||||
    } catch {
 | 
			
		||||
      // Serve dist client
 | 
			
		||||
      next()
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -68,7 +68,8 @@ const CONFIG = {
 | 
			
		|||
    CAPTIONS_DIR: buildPath(config.get<string>('storage.captions')),
 | 
			
		||||
    TORRENTS_DIR: buildPath(config.get<string>('storage.torrents')),
 | 
			
		||||
    CACHE_DIR: buildPath(config.get<string>('storage.cache')),
 | 
			
		||||
    PLUGINS_DIR: buildPath(config.get<string>('storage.plugins'))
 | 
			
		||||
    PLUGINS_DIR: buildPath(config.get<string>('storage.plugins')),
 | 
			
		||||
    CLIENT_OVERRIDES_DIR: buildPath(config.get<string>('storage.client_overrides'))
 | 
			
		||||
  },
 | 
			
		||||
  WEBSERVER: {
 | 
			
		||||
    SCHEME: config.get<boolean>('webserver.https') === true ? 'https' : 'http',
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,5 @@
 | 
			
		|||
import { join } from 'path'
 | 
			
		||||
import { randomBytes } from 'crypto'
 | 
			
		||||
import { JobType, VideoRateType, VideoResolution, VideoState } from '../../shared/models'
 | 
			
		||||
import { ActivityPubActorType } from '../../shared/models/activitypub'
 | 
			
		||||
import { FollowState } from '../../shared/models/actors'
 | 
			
		||||
| 
						 | 
				
			
			@ -710,6 +711,14 @@ registerConfigChangedHandler(() => {
 | 
			
		|||
 | 
			
		||||
// ---------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
const FILES_CONTENT_HASH = {
 | 
			
		||||
  MANIFEST: generateContentHash(),
 | 
			
		||||
  FAVICON: generateContentHash(),
 | 
			
		||||
  LOGO: generateContentHash()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ---------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
export {
 | 
			
		||||
  WEBSERVER,
 | 
			
		||||
  API_VERSION,
 | 
			
		||||
| 
						 | 
				
			
			@ -792,8 +801,10 @@ export {
 | 
			
		|||
  VIDEO_PLAYLIST_PRIVACIES,
 | 
			
		||||
  PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME,
 | 
			
		||||
  ASSETS_PATH,
 | 
			
		||||
  FILES_CONTENT_HASH,
 | 
			
		||||
  loadLanguages,
 | 
			
		||||
  buildLanguages
 | 
			
		||||
  buildLanguages,
 | 
			
		||||
  generateContentHash
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ---------------------------------------------------------------------------
 | 
			
		||||
| 
						 | 
				
			
			@ -895,3 +906,7 @@ function buildLanguages () {
 | 
			
		|||
 | 
			
		||||
  return languages
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function generateContentHash () {
 | 
			
		||||
  return randomBytes(20).toString('hex')
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,6 @@
 | 
			
		|||
import * as express from 'express'
 | 
			
		||||
import { buildFileLocale, getDefaultLocale, is18nLocale, POSSIBLE_LOCALES } from '../../shared/models/i18n/i18n'
 | 
			
		||||
import { CUSTOM_HTML_TAG_COMMENTS, EMBED_SIZE, PLUGIN_GLOBAL_CSS_PATH, WEBSERVER } from '../initializers/constants'
 | 
			
		||||
import { CUSTOM_HTML_TAG_COMMENTS, EMBED_SIZE, PLUGIN_GLOBAL_CSS_PATH, WEBSERVER, FILES_CONTENT_HASH } from '../initializers/constants'
 | 
			
		||||
import { join } from 'path'
 | 
			
		||||
import { escapeHTML, sha256 } from '../helpers/core-utils'
 | 
			
		||||
import { VideoModel } from '../models/video/video'
 | 
			
		||||
| 
						 | 
				
			
			@ -101,6 +101,9 @@ export class ClientHtml {
 | 
			
		|||
    let html = buffer.toString()
 | 
			
		||||
 | 
			
		||||
    if (paramLang) html = ClientHtml.addHtmlLang(html, paramLang)
 | 
			
		||||
    html = ClientHtml.addManifestContentHash(html)
 | 
			
		||||
    html = ClientHtml.addFaviconContentHash(html)
 | 
			
		||||
    html = ClientHtml.addLogoContentHash(html)
 | 
			
		||||
    html = ClientHtml.addCustomCSS(html)
 | 
			
		||||
    html = await ClientHtml.addAsyncPluginCSS(html)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -136,6 +139,18 @@ export class ClientHtml {
 | 
			
		|||
    return htmlStringPage.replace('<html>', `<html lang="${paramLang}">`)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private static addManifestContentHash (htmlStringPage: string) {
 | 
			
		||||
    return htmlStringPage.replace('[manifestContentHash]', FILES_CONTENT_HASH.MANIFEST)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private static addFaviconContentHash(htmlStringPage: string) {
 | 
			
		||||
    return htmlStringPage.replace('[faviconContentHash]', FILES_CONTENT_HASH.FAVICON)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private static addLogoContentHash(htmlStringPage: string) {
 | 
			
		||||
    return htmlStringPage.replace('[logoContentHash]', FILES_CONTENT_HASH.LOGO)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private static addTitleTag (htmlStringPage: string, title?: string) {
 | 
			
		||||
    let text = title || CONFIG.INSTANCE.NAME
 | 
			
		||||
    if (title) text += ` - ${CONFIG.INSTANCE.NAME}`
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -54,6 +54,11 @@ storage:
 | 
			
		|||
  captions: '../data/captions/'
 | 
			
		||||
  cache: '../data/cache/'
 | 
			
		||||
  plugins: '../data/plugins/'
 | 
			
		||||
  # Overridable client files : logo.svg, favicon.png and icons/*.png (PWA) in client/dist/assets/images
 | 
			
		||||
  # Could contain for example assets/images/favicon.png
 | 
			
		||||
  # If the file exists, peertube will serve it
 | 
			
		||||
  # If not, peertube will fallback to the default fil
 | 
			
		||||
  client_overrides: '../data/client-overrides/'
 | 
			
		||||
 | 
			
		||||
log:
 | 
			
		||||
  level: 'info' # debug/info/warning/error
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue