diff --git a/client/00-startup.js b/client/00-startup.js index b5d0c4d64..11ecec83f 100644 --- a/client/00-startup.js +++ b/client/00-startup.js @@ -15,6 +15,7 @@ import '/client/components/settings/migrationProgress'; // Import cron settings import '/client/components/settings/cronSettings'; +// Custom head tags // Mirror Meteor login token into a cookie for server-side file route auth // This enables cookie-based auth for /cdn/storage/* without leaking ROOT_URL diff --git a/client/components/settings/settingBody.css b/client/components/settings/settingBody.css index c7d3a3fda..387bffa85 100644 --- a/client/components/settings/settingBody.css +++ b/client/components/settings/settingBody.css @@ -230,3 +230,22 @@ li.has-error .form-group .wekan-form-control { border-color: #a94442; box-shadow: inset 0 1px 1px rgba(0,0,0,0.075); } + +/* Constrain textarea elements with balanced left/right spacing */ +.setting-content .content-body .main-body textarea.wekan-form-control { + width: calc(100% - 20px); + margin: 0 10px; + max-width: calc(100vw - 320px); + box-sizing: border-box; +} + +/* For nested custom head/manifest/assetlinks sections */ +.setting-content .content-body .main-body .custom-head-settings textarea.wekan-form-control, +.setting-content .content-body .main-body .custom-manifest-settings textarea.wekan-form-control, +.setting-content .content-body .main-body .custom-assetlinks-settings textarea.wekan-form-control { + width: calc(100% - 20px); + margin-left: 0; + margin-right: 10px; + max-width: calc(100vw - 240px); + box-sizing: border-box; +} diff --git a/client/components/settings/settingBody.jade b/client/components/settings/settingBody.jade index 88ae22eb2..ba3696ef9 100644 --- a/client/components/settings/settingBody.jade +++ b/client/components/settings/settingBody.jade @@ -460,6 +460,41 @@ template(name='layoutSettings') textarea#support-page-text.wekan-form-control= currentSetting.supportPageText li button.js-support-save.primary {{_ 'save'}} + li + a.flex.js-toggle-custom-head + .materialCheckBox(class="{{#if currentSetting.customHeadEnabled}}is-checked{{/if}}") + span {{_ 'custom-head-tags-enabled'}} + li + .custom-head-settings(class="{{#if currentSetting.customHeadEnabled}}{{else}}hide{{/if}}") + ul + li + .title {{_ 'custom-head-meta-tags'}} + textarea#custom-head-meta.wekan-form-control= customHeadMetaTagsValue + li + .title {{_ 'custom-head-link-tags'}} + textarea#custom-head-links.wekan-form-control= customHeadLinkTagsValue + li + a.flex.js-toggle-custom-manifest + .materialCheckBox(class="{{#if currentSetting.customManifestEnabled}}is-checked{{/if}}") + span {{_ 'custom-manifest-enabled'}} + li + .custom-manifest-settings(class="{{#if currentSetting.customManifestEnabled}}{{else}}hide{{/if}}") + .title {{_ 'custom-head-manifest-content'}} + textarea#custom-manifest-content.wekan-form-control= customManifestContentValue + li + button.js-custom-head-save.primary {{_ 'save'}} + li + a.flex.js-toggle-custom-assetlinks + .materialCheckBox(class="{{#if currentSetting.customAssetLinksEnabled}}is-checked{{/if}}") + span {{_ 'custom-assetlinks-enabled'}} + li + .custom-assetlinks-settings(class="{{#if currentSetting.customAssetLinksEnabled}}{{else}}hide{{/if}}") + ul + li + .title {{_ 'custom-assetlinks-content'}} + textarea#custom-assetlinks-content.wekan-form-control= customAssetLinksContentValue + li + button.js-custom-assetlinks-save.primary {{_ 'save'}} li.layout-form .title {{_ 'oidc-button-text'}} .form-group diff --git a/client/components/settings/settingBody.js b/client/components/settings/settingBody.js index abc5fcb89..29c6976e1 100644 --- a/client/components/settings/settingBody.js +++ b/client/components/settings/settingBody.js @@ -759,6 +759,182 @@ BlazeComponent.extendComponent({ this.setLoading(false); }, + toggleCustomHead() { + this.setLoading(true); + const customHeadEnabled = !$('.js-toggle-custom-head .materialCheckBox').hasClass('is-checked'); + $('.js-toggle-custom-head .materialCheckBox').toggleClass('is-checked'); + $('.custom-head-settings').toggleClass('hide'); + Settings.update(ReactiveCache.getCurrentSetting()._id, { + $set: { customHeadEnabled }, + }); + this.setLoading(false); + }, + + toggleCustomManifest() { + this.setLoading(true); + const customManifestEnabled = !$('.js-toggle-custom-manifest .materialCheckBox').hasClass('is-checked'); + $('.js-toggle-custom-manifest .materialCheckBox').toggleClass('is-checked'); + $('.custom-manifest-settings').toggleClass('hide'); + Settings.update(ReactiveCache.getCurrentSetting()._id, { + $set: { customManifestEnabled }, + }); + this.setLoading(false); + }, + + saveCustomHeadSettings() { + this.setLoading(true); + const customHeadMetaTags = $('#custom-head-meta').val() || ''; + let customManifestContent = $('#custom-manifest-content').val() || ''; + + // Validate and clean JSON if present + if (customManifestContent.trim()) { + const cleanResult = this.cleanAndValidateJSON(customManifestContent); + if (cleanResult.error) { + this.setLoading(false); + alert(`Invalid manifest JSON: ${cleanResult.error}`); + return; + } + customManifestContent = cleanResult.json; + // Update the textarea with cleaned version + $('#custom-manifest-content').val(customManifestContent); + } + + const customHeadLinkTags = $('#custom-head-links').val() || ''; + + try { + Settings.update(ReactiveCache.getCurrentSetting()._id, { + $set: { + customHeadMetaTags, + customHeadLinkTags, + customManifestContent, + }, + }); + } catch (e) { + return; + } finally { + this.setLoading(false); + } + }, + + cleanAndValidateJSON(content) { + if (!content || !content.trim()) { + return { json: content }; + } + + try { + // Try to parse as-is + const parsed = JSON.parse(content); + return { json: JSON.stringify(parsed, null, 2) }; + } catch (e) { + const errorMsg = e.message; + + // If error is "unexpected non-whitespace character after JSON data" + if (errorMsg.includes('unexpected non-whitespace character after JSON data')) { + try { + // Try to find and extract valid JSON by finding matching braces/brackets + const trimmed = content.trim(); + let depth = 0; + let endPos = -1; + let inString = false; + let escapeNext = false; + + for (let i = 0; i < trimmed.length; i++) { + const char = trimmed[i]; + + if (escapeNext) { + escapeNext = false; + continue; + } + + if (char === '\\') { + escapeNext = true; + continue; + } + + if (char === '"' && !escapeNext) { + inString = !inString; + continue; + } + + if (inString) continue; + + if (char === '{' || char === '[') { + depth++; + } else if (char === '}' || char === ']') { + depth--; + if (depth === 0) { + endPos = i + 1; + break; + } + } + } + + if (endPos > 0) { + const cleanedContent = trimmed.substring(0, endPos); + const parsed = JSON.parse(cleanedContent); + return { json: JSON.stringify(parsed, null, 2) }; + } + } catch (fixError) { + // If fix attempt fails, return original error + } + } + + // Remove trailing commas (common error) + if (errorMsg.includes('Unexpected token')) { + try { + const fixed = content.replace(/,(\s*[}\]])/g, '$1'); + const parsed = JSON.parse(fixed); + return { json: JSON.stringify(parsed, null, 2) }; + } catch (fixError) { + // Continue to error return + } + } + + return { error: errorMsg }; + } + }, + + toggleCustomAssetLinks() { + this.setLoading(true); + const customAssetLinksEnabled = !$('.js-toggle-custom-assetlinks .materialCheckBox').hasClass('is-checked'); + $('.js-toggle-custom-assetlinks .materialCheckBox').toggleClass('is-checked'); + $('.custom-assetlinks-settings').toggleClass('hide'); + Settings.update(ReactiveCache.getCurrentSetting()._id, { + $set: { customAssetLinksEnabled }, + }); + this.setLoading(false); + }, + + saveCustomAssetLinksSettings() { + this.setLoading(true); + let customAssetLinksContent = $('#custom-assetlinks-content').val() || ''; + + // Validate and clean JSON if present + if (customAssetLinksContent.trim()) { + const cleanResult = this.cleanAndValidateJSON(customAssetLinksContent); + if (cleanResult.error) { + this.setLoading(false); + alert(`Invalid assetlinks JSON: ${cleanResult.error}`); + return; + } + customAssetLinksContent = cleanResult.json; + // Update the textarea with cleaned version + $('#custom-assetlinks-content').val(customAssetLinksContent); + } + + try { + Settings.update(ReactiveCache.getCurrentSetting()._id, { + $set: { + customAssetLinksContent, + }, + }); + } catch (e) { + return; + } finally { + this.setLoading(false); + } + }, + saveSupportSettings() { this.setLoading(true); const supportTitle = ($('#support-title').val() || '').trim(); @@ -808,6 +984,11 @@ BlazeComponent.extendComponent({ 'click a.js-toggle-support': this.toggleSupportPage, 'click a.js-toggle-support-public': this.toggleSupportPublic, 'click button.js-support-save': this.saveSupportSettings, + 'click a.js-toggle-custom-head': this.toggleCustomHead, + 'click a.js-toggle-custom-manifest': this.toggleCustomManifest, + 'click button.js-custom-head-save': this.saveCustomHeadSettings, + 'click a.js-toggle-custom-assetlinks': this.toggleCustomAssetLinks, + 'click button.js-custom-assetlinks-save': this.saveCustomAssetLinksSettings, 'click a.js-toggle-display-authentication-method': this .toggleDisplayAuthenticationMethod, }, diff --git a/client/config/blazeHelpers.js b/client/config/blazeHelpers.js index 059802e39..beef694fa 100644 --- a/client/config/blazeHelpers.js +++ b/client/config/blazeHelpers.js @@ -1,4 +1,10 @@ import { ReactiveCache } from '/imports/reactiveCache'; +import { + DEFAULT_ASSETLINKS, + DEFAULT_HEAD_LINKS, + DEFAULT_HEAD_META, + DEFAULT_SITE_MANIFEST, +} from '/imports/lib/customHeadDefaults'; import { Blaze } from 'meteor/blaze'; import { Session } from 'meteor/session'; import { @@ -42,6 +48,38 @@ Blaze.registerHelper('currentSetting', () => { return ret; }); +Blaze.registerHelper('customHeadMetaTagsValue', () => { + const setting = ReactiveCache.getCurrentSetting(); + if (setting && typeof setting.customHeadMetaTags === 'string') { + return setting.customHeadMetaTags; + } + return DEFAULT_HEAD_META; +}); + +Blaze.registerHelper('customHeadLinkTagsValue', () => { + const setting = ReactiveCache.getCurrentSetting(); + if (setting && typeof setting.customHeadLinkTags === 'string') { + return setting.customHeadLinkTags; + } + return DEFAULT_HEAD_LINKS; +}); + +Blaze.registerHelper('customManifestContentValue', () => { + const setting = ReactiveCache.getCurrentSetting(); + if (setting && typeof setting.customManifestContent === 'string') { + return setting.customManifestContent; + } + return DEFAULT_SITE_MANIFEST; +}); + +Blaze.registerHelper('customAssetLinksContentValue', () => { + const setting = ReactiveCache.getCurrentSetting(); + if (setting && typeof setting.customAssetLinksContent === 'string') { + return setting.customAssetLinksContent; + } + return DEFAULT_ASSETLINKS; +}); + Blaze.registerHelper('currentUser', () => { const ret = ReactiveCache.getCurrentUser(); return ret; diff --git a/client/lib/utils.js b/client/lib/utils.js index ed2692977..9a89684a4 100644 --- a/client/lib/utils.js +++ b/client/lib/utils.js @@ -758,11 +758,12 @@ Utils = { }, setCustomUI(data) { + const productName = (data && data.productName) ? data.productName : 'Wekan'; const currentBoard = Utils.getCurrentBoard(); if (currentBoard) { - document.title = `${currentBoard.title} - ${data.productName}`; + document.title = `${currentBoard.title} - ${productName}`; } else { - document.title = `${data.productName}`; + document.title = productName; } }, diff --git a/config/router.js b/config/router.js index 6b1ab3ea9..1f03580fe 100644 --- a/config/router.js +++ b/config/router.js @@ -298,7 +298,11 @@ FlowRouter.route('/global-search', { Utils.manageCustomUI(); Utils.manageMatomo(); - document.title = TAPi18n.__('globalSearch-title'); + + // Set title with product name + const settings = Settings.findOne({}); + const productName = (settings && settings.productName) ? settings.productName : 'Wekan'; + document.title = `${TAPi18n.__('globalSearch-title')} - ${productName}`; if (FlowRouter.getQueryParam('q')) { Session.set( diff --git a/imports/i18n/data/en.i18n.json b/imports/i18n/data/en.i18n.json index 187d065da..be4b09b82 100644 --- a/imports/i18n/data/en.i18n.json +++ b/imports/i18n/data/en.i18n.json @@ -942,6 +942,13 @@ "authentication-method": "Authentication method", "authentication-type": "Authentication type", "custom-product-name": "Custom Product Name", + "custom-head-tags-enabled": "Enable custom head tags", + "custom-head-meta-tags": "Custom meta tags (HTML)", + "custom-head-link-tags": "Custom link tags (HTML)", + "custom-manifest-enabled": "Enable custom web manifest", + "custom-head-manifest-content": "Custom web manifest content (JSON)", + "custom-assetlinks-enabled": "Enable custom assetlinks.json", + "custom-assetlinks-content": "Custom assetlinks.json content (JSON)", "layout": "Layout", "hide-logo": "Hide Logo", "hide-card-counter-list": "Hide card counter list on All Boards", diff --git a/imports/lib/customHeadDefaults.js b/imports/lib/customHeadDefaults.js new file mode 100644 index 000000000..702600a7d --- /dev/null +++ b/imports/lib/customHeadDefaults.js @@ -0,0 +1,553 @@ +export const DEFAULT_HEAD_META = ` + + + + + + +`; + +export const DEFAULT_HEAD_LINKS = ` + + + + +`; + +export const DEFAULT_SITE_MANIFEST = `{ + "name": "Wekan", + "short_name": "Wekan", + "icons": [ + { + "src": "svg-etc/wekan-logo-512.svg", + "sizes": "any", + "type": "image/svg" + }, + { + "src": "android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "Square150x150Logo.scale-100.png", + "sizes": "150x150", + "type": "image/png" + }, + { + "src": "Square44x44Logo.scale-100.png", + "sizes": "44x44", + "type": "image/png" + }, + { + "src": "StoreLogo.scale-100.png", + "sizes": "50x50", + "type": "image/png" + }, + { + "src": "maskable_icon.png", + "sizes": "474x474", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "monochrome-icon-512x512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "monochrome" + }, + { + "src": "windows11/SmallTile.scale-100.png", + "sizes": "71x71" + }, + { + "src": "windows11/SmallTile.scale-125.png", + "sizes": "89x89" + }, + { + "src": "windows11/SmallTile.scale-150.png", + "sizes": "107x107" + }, + { + "src": "windows11/SmallTile.scale-200.png", + "sizes": "142x142" + }, + { + "src": "windows11/SmallTile.scale-400.png", + "sizes": "284x284" + }, + { + "src": "windows11/Square150x150Logo.scale-100.png", + "sizes": "150x150" + }, + { + "src": "windows11/Square150x150Logo.scale-125.png", + "sizes": "188x188" + }, + { + "src": "windows11/Square150x150Logo.scale-150.png", + "sizes": "225x225" + }, + { + "src": "windows11/Square150x150Logo.scale-200.png", + "sizes": "300x300" + }, + { + "src": "windows11/Square150x150Logo.scale-400.png", + "sizes": "600x600" + }, + { + "src": "windows11/Wide310x150Logo.scale-100.png", + "sizes": "310x150" + }, + { + "src": "windows11/Wide310x150Logo.scale-125.png", + "sizes": "388x188" + }, + { + "src": "windows11/Wide310x150Logo.scale-150.png", + "sizes": "465x225" + }, + { + "src": "windows11/Wide310x150Logo.scale-200.png", + "sizes": "620x300" + }, + { + "src": "windows11/Wide310x150Logo.scale-400.png", + "sizes": "1240x600" + }, + { + "src": "windows11/LargeTile.scale-100.png", + "sizes": "310x310" + }, + { + "src": "windows11/LargeTile.scale-125.png", + "sizes": "388x388" + }, + { + "src": "windows11/LargeTile.scale-150.png", + "sizes": "465x465" + }, + { + "src": "windows11/LargeTile.scale-200.png", + "sizes": "620x620" + }, + { + "src": "windows11/LargeTile.scale-400.png", + "sizes": "1240x1240" + }, + { + "src": "windows11/Square44x44Logo.scale-100.png", + "sizes": "44x44" + }, + { + "src": "windows11/Square44x44Logo.scale-125.png", + "sizes": "55x55" + }, + { + "src": "windows11/Square44x44Logo.scale-150.png", + "sizes": "66x66" + }, + { + "src": "windows11/Square44x44Logo.scale-200.png", + "sizes": "88x88" + }, + { + "src": "windows11/Square44x44Logo.scale-400.png", + "sizes": "176x176" + }, + { + "src": "windows11/StoreLogo.scale-100.png", + "sizes": "50x50" + }, + { + "src": "windows11/StoreLogo.scale-125.png", + "sizes": "63x63" + }, + { + "src": "windows11/StoreLogo.scale-150.png", + "sizes": "75x75" + }, + { + "src": "windows11/StoreLogo.scale-200.png", + "sizes": "100x100" + }, + { + "src": "windows11/StoreLogo.scale-400.png", + "sizes": "200x200" + }, + { + "src": "windows11/SplashScreen.scale-100.png", + "sizes": "620x300" + }, + { + "src": "windows11/SplashScreen.scale-125.png", + "sizes": "775x375" + }, + { + "src": "windows11/SplashScreen.scale-150.png", + "sizes": "930x450" + }, + { + "src": "windows11/SplashScreen.scale-200.png", + "sizes": "1240x600" + }, + { + "src": "windows11/SplashScreen.scale-400.png", + "sizes": "2480x1200" + }, + { + "src": "windows11/Square44x44Logo.targetsize-16.png", + "sizes": "16x16" + }, + { + "src": "windows11/Square44x44Logo.targetsize-20.png", + "sizes": "20x20" + }, + { + "src": "windows11/Square44x44Logo.targetsize-24.png", + "sizes": "24x24" + }, + { + "src": "windows11/Square44x44Logo.targetsize-30.png", + "sizes": "30x30" + }, + { + "src": "windows11/Square44x44Logo.targetsize-32.png", + "sizes": "32x32" + }, + { + "src": "windows11/Square44x44Logo.targetsize-36.png", + "sizes": "36x36" + }, + { + "src": "windows11/Square44x44Logo.targetsize-40.png", + "sizes": "40x40" + }, + { + "src": "windows11/Square44x44Logo.targetsize-44.png", + "sizes": "44x44" + }, + { + "src": "windows11/Square44x44Logo.targetsize-48.png", + "sizes": "48x48" + }, + { + "src": "windows11/Square44x44Logo.targetsize-60.png", + "sizes": "60x60" + }, + { + "src": "windows11/Square44x44Logo.targetsize-64.png", + "sizes": "64x64" + }, + { + "src": "windows11/Square44x44Logo.targetsize-72.png", + "sizes": "72x72" + }, + { + "src": "windows11/Square44x44Logo.targetsize-80.png", + "sizes": "80x80" + }, + { + "src": "windows11/Square44x44Logo.targetsize-96.png", + "sizes": "96x96" + }, + { + "src": "windows11/Square44x44Logo.targetsize-256.png", + "sizes": "256x256" + }, + { + "src": "windows11/Square44x44Logo.altform-unplated_targetsize-16.png", + "sizes": "16x16" + }, + { + "src": "windows11/Square44x44Logo.altform-unplated_targetsize-20.png", + "sizes": "20x20" + }, + { + "src": "windows11/Square44x44Logo.altform-unplated_targetsize-24.png", + "sizes": "24x24" + }, + { + "src": "windows11/Square44x44Logo.altform-unplated_targetsize-30.png", + "sizes": "30x30" + }, + { + "src": "windows11/Square44x44Logo.altform-unplated_targetsize-32.png", + "sizes": "32x32" + }, + { + "src": "windows11/Square44x44Logo.altform-unplated_targetsize-36.png", + "sizes": "36x36" + }, + { + "src": "windows11/Square44x44Logo.altform-unplated_targetsize-40.png", + "sizes": "40x40" + }, + { + "src": "windows11/Square44x44Logo.altform-unplated_targetsize-44.png", + "sizes": "44x44" + }, + { + "src": "windows11/Square44x44Logo.altform-unplated_targetsize-48.png", + "sizes": "48x48" + }, + { + "src": "windows11/Square44x44Logo.altform-unplated_targetsize-60.png", + "sizes": "60x60" + }, + { + "src": "windows11/Square44x44Logo.altform-unplated_targetsize-64.png", + "sizes": "64x64" + }, + { + "src": "windows11/Square44x44Logo.altform-unplated_targetsize-72.png", + "sizes": "72x72" + }, + { + "src": "windows11/Square44x44Logo.altform-unplated_targetsize-80.png", + "sizes": "80x80" + }, + { + "src": "windows11/Square44x44Logo.altform-unplated_targetsize-96.png", + "sizes": "96x96" + }, + { + "src": "windows11/Square44x44Logo.altform-unplated_targetsize-256.png", + "sizes": "256x256" + }, + { + "src": "windows11/Square44x44Logo.altform-lightunplated_targetsize-16.png", + "sizes": "16x16" + }, + { + "src": "windows11/Square44x44Logo.altform-lightunplated_targetsize-20.png", + "sizes": "20x20" + }, + { + "src": "windows11/Square44x44Logo.altform-lightunplated_targetsize-24.png", + "sizes": "24x24" + }, + { + "src": "windows11/Square44x44Logo.altform-lightunplated_targetsize-30.png", + "sizes": "30x30" + }, + { + "src": "windows11/Square44x44Logo.altform-lightunplated_targetsize-32.png", + "sizes": "32x32" + }, + { + "src": "windows11/Square44x44Logo.altform-lightunplated_targetsize-36.png", + "sizes": "36x36" + }, + { + "src": "windows11/Square44x44Logo.altform-lightunplated_targetsize-40.png", + "sizes": "40x40" + }, + { + "src": "windows11/Square44x44Logo.altform-lightunplated_targetsize-44.png", + "sizes": "44x44" + }, + { + "src": "windows11/Square44x44Logo.altform-lightunplated_targetsize-48.png", + "sizes": "48x48" + }, + { + "src": "windows11/Square44x44Logo.altform-lightunplated_targetsize-60.png", + "sizes": "60x60" + }, + { + "src": "windows11/Square44x44Logo.altform-lightunplated_targetsize-64.png", + "sizes": "64x64" + }, + { + "src": "windows11/Square44x44Logo.altform-lightunplated_targetsize-72.png", + "sizes": "72x72" + }, + { + "src": "windows11/Square44x44Logo.altform-lightunplated_targetsize-80.png", + "sizes": "80x80" + }, + { + "src": "windows11/Square44x44Logo.altform-lightunplated_targetsize-96.png", + "sizes": "96x96" + }, + { + "src": "windows11/Square44x44Logo.altform-lightunplated_targetsize-256.png", + "sizes": "256x256" + }, + { + "src": "android/android-launchericon-512-512.png", + "sizes": "512x512" + }, + { + "src": "android/android-launchericon-192-192.png", + "sizes": "192x192" + }, + { + "src": "android/android-launchericon-144-144.png", + "sizes": "144x144" + }, + { + "src": "android/android-launchericon-96-96.png", + "sizes": "96x96" + }, + { + "src": "android/android-launchericon-72-72.png", + "sizes": "72x72" + }, + { + "src": "android/android-launchericon-48-48.png", + "sizes": "48x48" + }, + { + "src": "ios/16.png", + "sizes": "16x16" + }, + { + "src": "ios/20.png", + "sizes": "20x20" + }, + { + "src": "ios/29.png", + "sizes": "29x29" + }, + { + "src": "ios/32.png", + "sizes": "32x32" + }, + { + "src": "ios/40.png", + "sizes": "40x40" + }, + { + "src": "ios/50.png", + "sizes": "50x50" + }, + { + "src": "ios/57.png", + "sizes": "57x57" + }, + { + "src": "ios/58.png", + "sizes": "58x58" + }, + { + "src": "ios/60.png", + "sizes": "60x60" + }, + { + "src": "ios/64.png", + "sizes": "64x64" + }, + { + "src": "ios/72.png", + "sizes": "72x72" + }, + { + "src": "ios/76.png", + "sizes": "76x76" + }, + { + "src": "ios/80.png", + "sizes": "80x80" + }, + { + "src": "ios/87.png", + "sizes": "87x87" + }, + { + "src": "ios/100.png", + "sizes": "100x100" + }, + { + "src": "ios/114.png", + "sizes": "114x114" + }, + { + "src": "ios/120.png", + "sizes": "120x120" + }, + { + "src": "ios/128.png", + "sizes": "128x128" + }, + { + "src": "ios/144.png", + "sizes": "144x144" + }, + { + "src": "ios/152.png", + "sizes": "152x152" + }, + { + "src": "ios/167.png", + "sizes": "167x167" + }, + { + "src": "ios/180.png", + "sizes": "180x180" + }, + { + "src": "ios/192.png", + "sizes": "192x192" + }, + { + "src": "ios/256.png", + "sizes": "256x256" + }, + { + "src": "ios/512.png", + "sizes": "512x512" + }, + { + "src": "ios/1024.png", + "sizes": "1024x1024" + } + ], + "screenshots": [ + { + "src": "screenshot1.webp", + "sizes": "1280x720", + "type": "image/webp" + }, + { + "src": "screenshot2.webp", + "sizes": "1280x720", + "type": "image/webp" + } + ], + "theme_color": "#000000", + "background_color": "#000000", + "start_url": "sign-in", + "display": "standalone", + "orientation": "any", + "categories": [ + "productivity" + ], + "description": "Open Source kanban with MIT license", + "dir": "auto", + "prefer_related_applications": false, + "display_override": [ + "standalone" + ] +} +`; + +export const DEFAULT_ASSETLINKS = `[ { + "relation": ["delegate_permission/common.handle_all_urls"], + "target": { + "namespace": "android_app", + "package_name": "team.wekan.boards.twa", + "sha256_cert_fingerprints": [ + "AA:AA:ED:7D:4C:9C:5A:A3:B5:DA:10:66:14:34:07:5D:EB:BE:96:CD:82:7B:09:46:47:13:65:29:5B:EA:96:30", + "61:41:86:5B:05:13:9B:64:5F:39:75:5A:16:C3:F2:22:25:6C:DA:74:B9:B0:8C:5F:93:B0:D2:26:65:16:1B:E6" + ] + } + } + ] +`; diff --git a/models/settings.js b/models/settings.js index 00107b8e4..4f45b0c8f 100644 --- a/models/settings.js +++ b/models/settings.js @@ -118,6 +118,34 @@ Settings.attachSchema( type: String, optional: true, }, + customHeadEnabled: { + type: Boolean, + optional: true, + }, + customHeadMetaTags: { + type: String, + optional: true, + }, + customHeadLinkTags: { + type: String, + optional: true, + }, + customManifestEnabled: { + type: Boolean, + optional: true, + }, + customManifestContent: { + type: String, + optional: true, + }, + customAssetLinksEnabled: { + type: Boolean, + optional: true, + }, + customAssetLinksContent: { + type: String, + optional: true, + }, accessibilityPageEnabled: { type: Boolean, optional: true, diff --git a/public/.well-known/assetlinks.json b/public/.well-known/assetlinks.json.default similarity index 100% rename from public/.well-known/assetlinks.json rename to public/.well-known/assetlinks.json.default diff --git a/public/site.webmanifest b/public/site.webmanifest.default similarity index 100% rename from public/site.webmanifest rename to public/site.webmanifest.default diff --git a/server/00checkStartup.js b/server/00checkStartup.js index 8cf8f69e5..60f3f3929 100644 --- a/server/00checkStartup.js +++ b/server/00checkStartup.js @@ -48,6 +48,10 @@ import './migrations/comprehensiveBoardMigration'; // Import file serving routes import './routes/universalFileServer'; +import './routes/customHeadAssets'; + +// Import server-side custom head rendering +import './lib/customHeadRender'; // Note: Automatic migrations are disabled - migrations only run when opening boards // import './boardMigrationDetector'; diff --git a/server/lib/customHeadRender.js b/server/lib/customHeadRender.js new file mode 100644 index 000000000..e6d84cacb --- /dev/null +++ b/server/lib/customHeadRender.js @@ -0,0 +1,54 @@ +import { WebApp } from 'meteor/webapp'; +import { WebAppInternals } from 'meteor/webapp'; +import Settings from '/models/settings'; + +Meteor.startup(() => { + // Use Meteor's official API to modify the HTML boilerplate + WebAppInternals.registerBoilerplateDataCallback('wekan-custom-head', (request, data) => { + try { + const setting = Settings.findOne(); + + // Initialize head array if it doesn't exist + if (!data.head) { + data.head = ''; + } + + // Always set title tag based on productName + const productName = (setting && setting.productName) ? setting.productName : 'Wekan'; + data.head += `\n