Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save KennFatt/b94329520a15e8c909773813f429971f to your computer and use it in GitHub Desktop.

Select an option

Save KennFatt/b94329520a15e8c909773813f429971f to your computer and use it in GitHub Desktop.
enhanced-pwa-everything.user.js
// ==UserScript==
// @name Enhanced PWA Everything
// @description Preserve the site's manifest when possible, patch it into a more installable form, otherwise inject a fallback manifest.
// @author KennFatt
// @version 0.0.5
// @match *://*/*
// @grant none
// @run-at document-idle
// @noframes
// ==/UserScript==
(function () {
'use strict';
const INJECTED_ATTR = 'data-pwa-everything';
const DEFAULT_THEME_COLOR = '#000000';
const DEFAULT_BG_COLOR = '#000000';
function getAppTitle() {
return (document.title || location.hostname || 'App').trim();
}
function absoluteUrl(url) {
try {
return new URL(url, document.baseURI).href;
} catch {
return null;
}
}
function toBase64Utf8(str) {
return btoa(unescape(encodeURIComponent(str)));
}
function toManifestDataUrl(manifest) {
return 'data:application/manifest+json;base64,' + toBase64Utf8(JSON.stringify(manifest));
}
function guessScopeFromPath(pathname) {
if (!pathname || pathname === '/') return '/';
const idx = pathname.lastIndexOf('/');
return idx <= 0 ? '/' : pathname.slice(0, idx + 1);
}
function makeSvgIcon(size, label) {
const safeLabel = String(label || 'A').slice(0, 2).toUpperCase();
const svg =
`<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 ${size} ${size}">` +
`<rect width="100%" height="100%" rx="${Math.round(size * 0.18)}" ry="${Math.round(size * 0.18)}" fill="#000000"/>` +
`<text x="50%" y="54%" text-anchor="middle" dominant-baseline="middle" ` +
`font-family="Arial, sans-serif" font-size="${Math.round(size * 0.42)}" fill="#ffffff">${safeLabel}</text>` +
`</svg>`;
return 'data:image/svg+xml;base64,' + toBase64Utf8(svg);
}
function buildFallbackIcons() {
const label = getAppTitle().charAt(0) || 'A';
return [
{
src: makeSvgIcon(192, label),
sizes: '192x192',
type: 'image/svg+xml',
purpose: 'any'
},
{
src: makeSvgIcon(512, label),
sizes: '512x512',
type: 'image/svg+xml',
purpose: 'any'
}
];
}
function getExistingManifestLink() {
return document.querySelector('link[rel~="manifest"][href]');
}
function removeOldInjectedManifest() {
document.querySelectorAll(`link[rel~="manifest"][${INJECTED_ATTR}="true"]`).forEach(el => el.remove());
}
function injectManifest(manifest) {
if (!document.head) return;
removeOldInjectedManifest();
const link = document.createElement('link');
link.rel = 'manifest';
link.href = toManifestDataUrl(manifest);
link.setAttribute(INJECTED_ATTR, 'true');
document.head.prepend(link);
}
function buildFallbackManifest() {
const title = getAppTitle();
return {
name: title,
short_name: title.slice(0, 12) || title,
start_url: location.href,
scope: guessScopeFromPath(location.pathname),
display: 'standalone',
theme_color: DEFAULT_THEME_COLOR,
background_color: DEFAULT_BG_COLOR,
icons: buildFallbackIcons()
};
}
function normalizeIcons(icons, manifestUrl) {
if (!Array.isArray(icons) || icons.length === 0) {
return buildFallbackIcons();
}
const normalized = icons
.filter(icon => icon && typeof icon === 'object' && typeof icon.src === 'string' && icon.src.trim())
.map(icon => {
const out = { ...icon };
const src = manifestUrl ? absoluteUrl(new URL(icon.src, manifestUrl).href) : absoluteUrl(icon.src);
if (src) out.src = src;
if (!out.type) {
const lower = String(out.src).toLowerCase();
if (lower.endsWith('.png')) out.type = 'image/png';
else if (lower.endsWith('.jpg') || lower.endsWith('.jpeg')) out.type = 'image/jpeg';
else if (lower.endsWith('.webp')) out.type = 'image/webp';
else if (lower.endsWith('.svg')) out.type = 'image/svg+xml';
}
return out;
});
return normalized.length ? normalized : buildFallbackIcons();
}
function normalizeShortName(name) {
return String(name || '').trim().slice(0, 12);
}
function patchManifest(originalManifest, manifestUrl) {
const fallback = buildFallbackManifest();
const patched = (originalManifest && typeof originalManifest === 'object')
? JSON.parse(JSON.stringify(originalManifest))
: {};
const title = getAppTitle();
if (!patched.name && !patched.short_name) {
patched.name = title;
patched.short_name = normalizeShortName(title);
} else {
if (!patched.name && patched.short_name) patched.name = String(patched.short_name);
if (!patched.short_name && patched.name) patched.short_name = normalizeShortName(patched.name);
}
if (!patched.start_url || typeof patched.start_url !== 'string' || !patched.start_url.trim()) {
patched.start_url = fallback.start_url;
}
if (!patched.scope || typeof patched.scope !== 'string' || !patched.scope.trim()) {
patched.scope = guessScopeFromPath(location.pathname);
}
patched.display = 'standalone';
if (!patched.theme_color) {
patched.theme_color = DEFAULT_THEME_COLOR;
}
if (!patched.background_color) {
patched.background_color = DEFAULT_BG_COLOR;
}
patched.icons = normalizeIcons(patched.icons, manifestUrl);
return patched;
}
async function fetchManifestJson(manifestUrl) {
const response = await fetch(manifestUrl, {
credentials: 'include',
cache: 'no-store'
});
if (!response.ok) {
throw new Error(`Failed to fetch manifest: ${response.status} ${response.statusText}`);
}
return await response.json();
}
async function patchOrFallback() {
const existingLink = getExistingManifestLink();
if (!existingLink) {
injectManifest(buildFallbackManifest());
console.debug('[PWA Everything] No existing manifest found. Injected fallback.');
return;
}
const href = existingLink.getAttribute('href');
const manifestUrl = absoluteUrl(href);
if (!manifestUrl) {
injectManifest(buildFallbackManifest());
console.warn('[PWA Everything] Existing manifest href was invalid. Injected fallback.');
return;
}
try {
const manifestJson = await fetchManifestJson(manifestUrl);
const patched = patchManifest(manifestJson, manifestUrl);
injectManifest(patched);
console.debug('[PWA Everything] Existing manifest preserved and patched.');
} catch (error) {
console.warn('[PWA Everything] Could not read existing manifest. Injecting fallback.', error);
injectManifest(buildFallbackManifest());
}
}
patchOrFallback();
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment