2
0
Эх сурвалжийг харах

Merge pull request #7119 from weseek/feat/custom-headers

feat: Custom headers
Yuki Takei 3 жил өмнө
parent
commit
aa1e16582c

+ 3 - 3
packages/app/public/static/locales/en_US/admin.json

@@ -498,8 +498,8 @@
     "custom_title_detail_placeholder1": "<code>&#123;&#123;sitename&#125;&#125;</code> - The site name of this wiki.",
     "custom_title_detail_placeholder2": "<code>&#123;&#123;pagename&#125;&#125;</code> - The page name of the current page.",
     "custom_title_detail_placeholder3": "<code>&#123;&#123;pagepath&#125;&#125;</code> - The page path of the current page.",
-    "custom_header": "Custom HTML header",
-    "custom_header_detail": "You can customize HTML header that applies all pages. Your custom script will be inserted in <code>&lt;header&gt;</code> but above other <code>&lt;script&gt;</code> tags.<br>Relaod page to see changes.",
+    "custom_noscript": "Custom Noscript",
+    "custom_noscript_detail": "You can customize Noscript code that applies all pages. Your custom Noscript will be inserted into the <code>&lt;noscript&gt;</code> tag that is located as the first element of body.<br>Relaod page to see changes.",
     "custom_css": "Custom CSS",
     "write_css": "You can write CSS that is applied to whole system.",
     "ctrl_space": "Ctrl+Space to autocomplete",
@@ -980,7 +980,7 @@
     "ADMIN_FUNCTION_UPDATE": "Update Function",
     "ADMIN_CODE_HIGHLIGHT_UPDATE": "Update Code Highlight",
     "ADMIN_CUSTOM_TITLE_UPDATE": "Update Custom Title",
-    "ADMIN_CUSTOM_HTML_HEADER_UPDATE": "Update Custom HTML header",
+    "ADMIN_CUSTOM_NOSCRIPT_UPDATE": "Update Custom noscript",
     "ADMIN_CUSTOM_CSS_UPDATE": "Update Custom CSS",
     "ADMIN_CUSTOM_SCRIPT_UPDATE": "Update Custom script",
     "ADMIN_ARCHIVE_DATA_UPLOAD": "Upload Archived Data",

+ 3 - 3
packages/app/public/static/locales/ja_JP/admin.json

@@ -506,8 +506,8 @@
     "custom_title_detail_placeholder1": "<code>&#123;&#123;sitename&#125;&#125;</code> - この Wiki のサイト名",
     "custom_title_detail_placeholder2": "<code>&#123;&#123;pagename&#125;&#125;</code> - 現在表示中のページ名",
     "custom_title_detail_placeholder3": "<code>&#123;&#123;pagepath&#125;&#125;</code> - 現在表示中のページパス",
-    "custom_header": "カスタム HTML Header",
-    "custom_header_detail": "システム全体に適用される HTML を記述できます。<code>&lt;header&gt;</code> タグ内の他の <code>&lt;script&gt;</code> タグ読み込み前に展開されます。<br>変更の反映はページの更新が必要です。",
+    "custom_noscript": "カスタム Noscript",
+    "custom_noscript_detail": "システム全体に適用される HTML を記述できます。<code>&lt;body&gt;</code> タグ内の最初の <code>&lt;noscript&gt;</code> タグ内に展開されます。<br>変更の反映はページの更新が必要です。",
     "custom_css": "カスタム CSS",
     "write_css": " システム全体に適用されるCSSを記述できます。",
     "ctrl_space": "Ctrl+Space でコード補完",
@@ -988,7 +988,7 @@
     "ADMIN_FUNCTION_UPDATE": "機能設定の更新",
     "ADMIN_CODE_HIGHLIGHT_UPDATE": "コードハイライト設定の更新",
     "ADMIN_CUSTOM_TITLE_UPDATE": "カスタムタイトル設定の更新",
-    "ADMIN_CUSTOM_HTML_HEADER_UPDATE": "カスタム HTML Header 設定の更新",
+    "ADMIN_CUSTOM_NOSCRIPT_UPDATE": "カスタム noscript 設定の更新",
     "ADMIN_CUSTOM_CSS_UPDATE": "カスタム CSS 設定の更新",
     "ADMIN_CUSTOM_SCRIPT_UPDATE": "カスタムスクリプト設定の更新",
     "ADMIN_ARCHIVE_DATA_UPLOAD": "アーカイブデータのアップロード",

+ 3 - 3
packages/app/public/static/locales/zh_CN/admin.json

@@ -506,8 +506,8 @@
     "custom_title_detail_placeholder1": "<code>&#123;&#123;站点名称&#125;&#125;</code>-此wiki的站点名称。",
     "custom_title_detail_placeholder2": "<code>&#123;&#123;页名&#125;&#125;</code>-当前页的页名。",
     "custom_title_detail_placeholder3": "<code>&#123;&#123;页面路径&#125;&#125;</code>-当前页面的页面路径。",
-    "custom_header": "自定义HTML标题",
-    "custom_header_detail": "您可以自定义应用所有页面的HTML标题。您的自定义脚本将插入<code>&lt;header&gt;</code>中,但位于其他<code>&lt;script&gt;</code>标记之上。<br>重新链接页面以查看更改。",
+    "custom_noscript": "自定义 Noscript 标题",
+    "custom_noscript_detail": "您可以自定义应用所有页面的 Noscript 代码。 您的自定义 Noscript 将被插入到作为 body 的第一个元素的 <code>&lt;noscript&gt;</code> 标签中。<br>重新链接页面以查看更改。",
     "custom_css": "自定义CSS",
     "write_css": "您可以编写应用于整个系统的CSS。",
     "ctrl_space": "Ctrl+Space 自动完成",
@@ -988,7 +988,7 @@
     "ADMIN_FUNCTION_UPDATE": "更新函数",
     "ADMIN_CODE_HIGHLIGHT_UPDATE": "更新代码高亮",
     "ADMIN_CUSTOM_TITLE_UPDATE": "更新自定义标题",
-    "ADMIN_CUSTOM_HTML_HEADER_UPDATE": "更新自定义 HTML 标头",
+    "ADMIN_CUSTOM_NOSCRIPT_UPDATE": "更新自定义 noscript 标头",
     "ADMIN_CUSTOM_CSS_UPDATE": "更新自定义 CSS",
     "ADMIN_CUSTOM_SCRIPT_UPDATE": "更新自定义脚本",
     "ADMIN_ARCHIVE_DATA_UPLOAD": "上传存档数据",

+ 8 - 12
packages/app/src/client/services/AdminCustomizeContainer.js

@@ -36,7 +36,7 @@ export default class AdminCustomizeContainer extends Container {
       isAllReplyShown: false,
       isSearchScopeChildrenAsDefault: false,
       currentCustomizeTitle: '',
-      currentCustomizeHeader: '',
+      currentCustomizeNoscript: '',
       currentCustomizeCss: '',
       currentCustomizeScript: '',
     };
@@ -73,7 +73,7 @@ export default class AdminCustomizeContainer extends Container {
         isAllReplyShown: customizeParams.isAllReplyShown,
         isSearchScopeChildrenAsDefault: customizeParams.isSearchScopeChildrenAsDefault,
         currentCustomizeTitle: customizeParams.customizeTitle,
-        currentCustomizeHeader: customizeParams.customizeHeader,
+        currentCustomizeNoscript: customizeParams.customizeNoscript,
         currentCustomizeCss: customizeParams.customizeCss,
         currentCustomizeScript: customizeParams.customizeScript,
       });
@@ -160,8 +160,8 @@ export default class AdminCustomizeContainer extends Container {
   /**
    * Change customize Html header
    */
-  changeCustomizeHeader(inputValue) {
-    this.setState({ currentCustomizeHeader: inputValue });
+  changeCustomizeNoscript(inputValue) {
+    this.setState({ currentCustomizeNoscript: inputValue });
   }
 
   /**
@@ -235,18 +235,14 @@ export default class AdminCustomizeContainer extends Container {
     }
   }
 
-  /**
-   * Update customHeader
-   * @memberOf AdminCustomizeContainer
-   */
-  async updateCustomizeHeader() {
+  async updateCustomizeNoscript() {
     try {
-      const response = await apiv3Put('/customize-setting/customize-header', {
-        customizeHeader: this.state.currentCustomizeHeader,
+      const response = await apiv3Put('/customize-setting/customize-noscript', {
+        customizeNoscript: this.state.currentCustomizeNoscript,
       });
       const { customizedParams } = response.data;
       this.setState({
-        currentCustomizeHeader: customizedParams.customizeHeader,
+        currentCustomizeNoscript: customizedParams.customizeNoscript,
       });
     }
     catch (err) {

+ 8 - 8
packages/app/src/components/Admin/Customize/Customize.jsx

@@ -12,9 +12,9 @@ import { withUnstatedContainers } from '../../UnstatedUtils';
 
 import CustomizeCssSetting from './CustomizeCssSetting';
 import CustomizeFunctionSetting from './CustomizeFunctionSetting';
-import CustomizeHeaderSetting from './CustomizeHeaderSetting';
 import CustomizeLayoutSetting from './CustomizeLayoutSetting';
 import CustomizeLogoSetting from './CustomizeLogoSetting';
+import CustomizeNoscriptSetting from './CustomizeNoscriptSetting';
 import CustomizeScriptSetting from './CustomizeScriptSetting';
 import CustomizeSidebarSetting from './CustomizeSidebarSetting';
 import CustomizeThemeSetting from './CustomizeThemeSetting';
@@ -44,10 +44,13 @@ function Customize(props) {
   return (
     <div data-testid="admin-customize">
       <div className="mb-5">
-        <CustomizeLayoutSetting />
+        <CustomizeThemeSetting />
       </div>
       <div className="mb-5">
-        <CustomizeThemeSetting />
+        <CustomizeLogoSetting />
+      </div>
+      <div className="mb-5">
+        <CustomizeLayoutSetting />
       </div>
       <div className="mb-5">
         <CustomizeSidebarSetting />
@@ -59,16 +62,13 @@ function Customize(props) {
         <CustomizeTitle />
       </div>
       <div className="mb-5">
-        <CustomizeHeaderSetting />
+        <CustomizeScriptSetting />
       </div>
       <div className="mb-5">
         <CustomizeCssSetting />
       </div>
       <div className="mb-5">
-        <CustomizeScriptSetting />
-      </div>
-      <div className="mb-5">
-        <CustomizeLogoSetting />
+        <CustomizeNoscriptSetting />
       </div>
     </div>
   );

+ 3 - 0
packages/app/src/components/Admin/Customize/CustomizeCssSetting.tsx

@@ -45,13 +45,16 @@ const CustomizeCssSetting = (props: Props): JSX.Element => {
             <textarea
               className="form-control"
               name="customizeCss"
+              rows={8}
               value={adminCustomizeContainer.state.currentCustomizeCss || ''}
               onChange={(e) => { adminCustomizeContainer.changeCustomizeCss(e.target.value) }}
             />
+            {/* disabled in v6.0.0 temporarily -- 2022.12.19 Yuki Takei
             <p className="form-text text-muted text-right">
               <i className="fa fa-fw fa-keyboard-o" aria-hidden="true" />
               {t('admin:customize_settings.ctrl_space')}
             </p>
+            */}
           </div>
 
           <AdminUpdateButtonRow onClick={onClickSubmit} disabled={adminCustomizeContainer.state.retrieveError != null} />

+ 33 - 21
packages/app/src/components/Admin/Customize/CustomizeHeaderSetting.tsx → packages/app/src/components/Admin/Customize/CustomizeNoscriptSetting.tsx

@@ -1,6 +1,8 @@
 import React, { useCallback } from 'react';
 
 import { useTranslation } from 'next-i18next';
+import { PrismAsyncLight } from 'react-syntax-highlighter';
+import { oneDark } from 'react-syntax-highlighter/dist/cjs/styles/prism';
 import { Card, CardBody } from 'reactstrap';
 
 import AdminCustomizeContainer from '~/client/services/AdminCustomizeContainer';
@@ -13,15 +15,15 @@ type Props = {
   adminCustomizeContainer: AdminCustomizeContainer
 }
 
-const CustomizeHeaderSetting = (props: Props): JSX.Element => {
+const CustomizeNoscriptSetting = (props: Props): JSX.Element => {
 
   const { adminCustomizeContainer } = props;
   const { t } = useTranslation();
 
   const onClickSubmit = useCallback(async() => {
     try {
-      await adminCustomizeContainer.updateCustomizeHeader();
-      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_settings.custom_header'), ns: 'commons' }));
+      await adminCustomizeContainer.updateCustomizeNoscript();
+      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_settings.custom_noscript'), ns: 'commons' }));
     }
     catch (err) {
       toastError(err);
@@ -32,38 +34,48 @@ const CustomizeHeaderSetting = (props: Props): JSX.Element => {
     <React.Fragment>
       <div className="row">
         <div className="col-12">
-          <h2 className="admin-setting-header">{t('admin:customize_settings.custom_header')}</h2>
+          <h2 className="admin-setting-header">{t('admin:customize_settings.custom_noscript')}</h2>
 
           <Card className="card well my-3">
             <CardBody className="px-0 py-2">
               <span
                 // eslint-disable-next-line react/no-danger
-                dangerouslySetInnerHTML={{ __html: t('admin:customize_settings.custom_header_detail') }}
+                dangerouslySetInnerHTML={{ __html: t('admin:customize_settings.custom_noscript_detail') }}
               />
             </CardBody>
           </Card>
-          <div className="form-text text-muted">
-            { t('Example') }:
-            <pre>
-              {/* eslint-disable-next-line react/no-unescaped-entities */}
-              <code className="text-wrap">&lt;script src="https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@9.13.0/build/languages/yaml.min.js"
-                defer&gt;&lt;/script&gt;
-              </code>
-            </pre>
-          </div>
 
           <div className="form-group">
             <textarea
               className="form-control"
-              name="customizeHeader"
-              value={adminCustomizeContainer.state.currentCustomizeHeader || ''}
-              onChange={(e) => { adminCustomizeContainer.changeCustomizeHeader(e.target.value) }}
+              name="customizeNoscript"
+              rows={8}
+              value={adminCustomizeContainer.state.currentCustomizeNoscript || ''}
+              onChange={(e) => { adminCustomizeContainer.changeCustomizeNoscript(e.target.value) }}
             />
-            <p className="form-text text-muted text-right">
+            {/* disabled in v6.0.0 temporarily -- 2022.12.19 Yuki Takei
+            <span className="form-text text-muted text-right">
               <i className="fa fa-fw fa-keyboard-o" aria-hidden="true"></i>
               {t('admin:customize_settings.ctrl_space')}
-            </p>
+            </span>
+            */}
+          </div>
+
+          <a className="text-muted"
+            data-toggle="collapse" href="#collapseExampleHtml" role="button" aria-expanded="false" aria-controls="collapseExampleHtml">
+            <i className="fa fa-fw fa-chevron-right" aria-hidden="true"></i>
+            Example for Google Tag Manager
+          </a>
+          <div className="collapse" id="collapseExampleHtml">
+            <PrismAsyncLight style={oneDark} language={'javascript'}
+            >
+              {`<iframe src="https://www.googletagmanager.com/ns.html?id=GTM-XXXXXXX"
+  height="0"
+  width="0"
+  style="display:none;visibility:hidden"></iframe>`}
+            </PrismAsyncLight>
           </div>
+
           <AdminUpdateButtonRow onClick={onClickSubmit} disabled={adminCustomizeContainer.state.retrieveError != null} />
         </div>
       </div>
@@ -72,6 +84,6 @@ const CustomizeHeaderSetting = (props: Props): JSX.Element => {
 
 };
 
-const CustomizeHeaderSettingWrapper = withUnstatedContainers(CustomizeHeaderSetting, [AdminCustomizeContainer]);
+const CustomizeNoscriptSettingWrapper = withUnstatedContainers(CustomizeNoscriptSetting, [AdminCustomizeContainer]);
 
-export default CustomizeHeaderSettingWrapper;
+export default CustomizeNoscriptSettingWrapper;

+ 27 - 44
packages/app/src/components/Admin/Customize/CustomizeScriptSetting.tsx

@@ -1,6 +1,8 @@
 import React, { useCallback } from 'react';
 
 import { useTranslation } from 'next-i18next';
+import { PrismAsyncLight } from 'react-syntax-highlighter';
+import { oneDark } from 'react-syntax-highlighter/dist/cjs/styles/prism';
 import { Card, CardBody } from 'reactstrap';
 
 import AdminCustomizeContainer from '~/client/services/AdminCustomizeContainer';
@@ -28,14 +30,6 @@ const CustomizeScriptSetting = (props: Props): JSX.Element => {
     }
   }, [t, adminCustomizeContainer]);
 
-  const getExampleCode = useCallback(() => {
-    return `console.log($('.main-container'));
-    window.addEventListener('load', (event) => {
-      console.log('config: ', appContainer.config);
-    });
-    `;
-  }, []);
-
   return (
     <React.Fragment>
       <div className="row">
@@ -48,51 +42,40 @@ const CustomizeScriptSetting = (props: Props): JSX.Element => {
             </CardBody>
           </Card>
 
-          <div className="form-text text-muted">
-            Placeholders:<br />
-            (Available after <code>load</code> event)
-          </div>
-          <table className="table table-borderless table-sm form-text text-muted offset-1 col-11">
-            <tbody>
-              <tr>
-                <th className="text-right"><code>$</code></th>
-                <td>jQuery instance</td>
-              </tr>
-              <tr>
-                <th className="text-right"><code>appContainer</code></th>
-                <td>GROWI App <a href="https://github.com/jamiebuilds/unstated">unstated container</a></td>
-              </tr>
-              <tr>
-                <th className="text-right"><code>growiRenderer</code></th>
-                <td>GROWI Renderer origin instance</td>
-              </tr>
-              <tr>
-                <th className="text-right"><code>growiPlugin</code></th>
-                <td>GROWI Plugin Manager instance</td>
-              </tr>
-              <tr>
-                <th className="text-right"><code>Crowi</code></th>
-                <td>Crowi legacy instance (jQuery based)</td>
-              </tr>
-            </tbody>
-          </table>
-
-          <div className="form-text text-muted">
-            Examples:
-            <pre><code className='language-javascript'>{getExampleCode()}</code></pre>
-          </div>
-
           <div className="form-group">
             <textarea
               className="form-control"
               name="customizeScript"
+              rows={8}
               value={adminCustomizeContainer.state.currentCustomizeScript || ''}
               onChange={(e) => { adminCustomizeContainer.changeCustomizeScript(e.target.value) }}
             />
-            <p className="form-text text-muted text-right">
+            {/* disabled in v6.0.0 temporarily -- 2022.12.19 Yuki Takei
+            <span className="form-text text-muted text-right">
               <i className="fa fa-fw fa-keyboard-o" aria-hidden="true" />
               {t('admin:customize_settings.ctrl_space')}
-            </p>
+            </span>
+            */}
+          </div>
+
+          <a className="text-muted"
+            data-toggle="collapse" href="#collapseExampleScript" role="button" aria-expanded="false" aria-controls="collapseExampleScript">
+            <i className="fa fa-fw fa-chevron-right" aria-hidden="true"></i>
+            Example for Google Tag Manager
+          </a>
+          <div className="collapse" id="collapseExampleScript">
+            <PrismAsyncLight style={oneDark} language={'javascript'}
+            >
+              {`(function(w,d,s,l,i){
+w[l]=w[l]||[];
+w[l].push({'gtm.start':new Date().getTime(),event:'gtm.js'});
+var f=d.getElementsByTagName(s)[0],
+  j=d.createElement(s),
+  dl=l!='dataLayer'?'&l='+l:'';
+j.async=true;
+j.src='https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
+})(window,document,'script','dataLayer','GTM-XXXXXX');`}
+            </PrismAsyncLight>
           </div>
 
           <AdminUpdateButtonRow onClick={onClickSubmit} disabled={adminCustomizeContainer.state.retrieveError != null} />

+ 3 - 3
packages/app/src/interfaces/activity.ts

@@ -112,7 +112,7 @@ const ACTION_ADMIN_SIDEBAR_UPDATE = 'ADMIN_SIDEBAR_UPDATE';
 const ACTION_ADMIN_FUNCTION_UPDATE = 'ADMIN_FUNCTION_UPDATE';
 const ACTION_ADMIN_CODE_HIGHLIGHT_UPDATE = 'ADMIN_CODE_HIGHLIGHT_UPDATE';
 const ACTION_ADMIN_CUSTOM_TITLE_UPDATE = 'ADMIN_CUSTOM_TITLE_UPDATE';
-const ACTION_ADMIN_CUSTOM_HTML_HEADER_UPDATE = 'ADMIN_CUSTOM_HTML_HEADER_UPDATE';
+const ACTION_ADMIN_CUSTOM_NOSCRIPT_UPDATE = 'ADMIN_CUSTOM_NOSCRIPT_UPDATE';
 const ACTION_ADMIN_CUSTOM_CSS_UPDATE = 'ADMIN_CUSTOM_CSS_UPDATE';
 const ACTION_ADMIN_CUSTOM_SCRIPT_UPDATE = 'ADMIN_CUSTOM_SCRIPT_UPDATE';
 const ACTION_ADMIN_ARCHIVE_DATA_UPLOAD = 'ADMIN_ARCHIVE_DATA_UPLOAD';
@@ -291,7 +291,7 @@ export const SupportedAction = {
   ACTION_ADMIN_FUNCTION_UPDATE,
   ACTION_ADMIN_CODE_HIGHLIGHT_UPDATE,
   ACTION_ADMIN_CUSTOM_TITLE_UPDATE,
-  ACTION_ADMIN_CUSTOM_HTML_HEADER_UPDATE,
+  ACTION_ADMIN_CUSTOM_NOSCRIPT_UPDATE,
   ACTION_ADMIN_CUSTOM_CSS_UPDATE,
   ACTION_ADMIN_CUSTOM_SCRIPT_UPDATE,
   ACTION_ADMIN_ARCHIVE_DATA_UPLOAD,
@@ -478,7 +478,7 @@ export const LargeActionGroup = {
   ACTION_ADMIN_FUNCTION_UPDATE,
   ACTION_ADMIN_CODE_HIGHLIGHT_UPDATE,
   ACTION_ADMIN_CUSTOM_TITLE_UPDATE,
-  ACTION_ADMIN_CUSTOM_HTML_HEADER_UPDATE,
+  ACTION_ADMIN_CUSTOM_NOSCRIPT_UPDATE,
   ACTION_ADMIN_CUSTOM_CSS_UPDATE,
   ACTION_ADMIN_CUSTOM_SCRIPT_UPDATE,
   ACTION_ADMIN_ARCHIVE_DATA_UPLOAD,

+ 34 - 10
packages/app/src/pages/_document.page.tsx

@@ -73,7 +73,9 @@ const HeadersForGrowiPlugin = (props: HeadersForGrowiPluginProps): JSX.Element =
 
 interface GrowiDocumentProps {
   theme: string,
-  customCss: string;
+  customScript: string | null,
+  customCss: string | null,
+  customNoscript: string | null,
   presetThemesManifest: ViteManifest,
   pluginThemeHref: string | undefined,
   pluginResourceEntries: GrowiPluginResourceEntries;
@@ -88,7 +90,9 @@ class GrowiDocument extends Document<GrowiDocumentInitialProps> {
     const { configManager, customizeService, pluginService } = crowi;
 
     const theme = configManager.getConfig('crowi', 'customize:theme');
-    const customCss: string = customizeService.getCustomCss();
+    const customScript: string | null = customizeService.getCustomScript();
+    const customCss: string | null = customizeService.getCustomCss();
+    const customNoscript: string | null = customizeService.getCustomNoscript();
 
     // import preset-themes manifest
     const presetThemesManifest = await import('@growi/preset-themes/dist/themes/manifest.json').then(imported => imported.default);
@@ -100,28 +104,46 @@ class GrowiDocument extends Document<GrowiDocumentInitialProps> {
     return {
       ...initialProps,
       theme,
+      customScript,
       customCss,
+      customNoscript,
       presetThemesManifest,
       pluginThemeHref,
       pluginResourceEntries,
     };
   }
 
+  renderCustomScript(customScript: string | null): JSX.Element {
+    if (customScript == null || customScript.length === 0) {
+      return <></>;
+    }
+    return <script id="customScript" dangerouslySetInnerHTML={{ __html: customScript }} />;
+  }
+
+  renderCustomCss(customCss: string | null): JSX.Element {
+    if (customCss == null || customCss.length === 0) {
+      return <></>;
+    }
+    return <style dangerouslySetInnerHTML={{ __html: customCss }} />;
+  }
+
+  renderCustomNoscript(customNoscript: string | null): JSX.Element {
+    if (customNoscript == null || customNoscript.length === 0) {
+      return <></>;
+    }
+    return <noscript dangerouslySetInnerHTML={{ __html: customNoscript }} />;
+  }
+
   override render(): JSX.Element {
     const {
-      customCss, theme, presetThemesManifest, pluginThemeHref, pluginResourceEntries,
+      customCss, customScript, customNoscript,
+      theme, presetThemesManifest, pluginThemeHref, pluginResourceEntries,
     } = this.props;
 
     return (
       <Html>
         <Head>
-          <style>
-            {customCss}
-          </style>
-          {/*
-          {renderScriptTagsByGroup('basis')}
-          {renderStyleTagsByGroup('basis')}
-          */}
+          {this.renderCustomScript(customScript)}
           <link rel='preload' href="/static/fonts/PressStart2P-latin.woff2" as="font" type="font/woff2" />
           <link rel='preload' href="/static/fonts/PressStart2P-latin-ext.woff2" as="font" type="font/woff2" />
           <link rel='preload' href="/static/fonts/Lato-Regular-latin.woff2" as="font" type="font/woff2" />
@@ -131,8 +153,10 @@ class GrowiDocument extends Document<GrowiDocumentInitialProps> {
           <HeadersForThemes theme={theme}
             presetThemesManifest={presetThemesManifest} pluginThemeHref={pluginThemeHref} />
           <HeadersForGrowiPlugin pluginResourceEntries={pluginResourceEntries} />
+          {this.renderCustomCss(customCss)}
         </Head>
         <body>
+          {this.renderCustomNoscript(customNoscript)}
           <Main />
           <NextScript />
         </body>

+ 1 - 1
packages/app/src/server/models/config.ts

@@ -118,7 +118,7 @@ export const defaultCrowiConfigs: { [key: string]: any } = {
 
   'customize:css' : undefined,
   'customize:script' : undefined,
-  'customize:header' : undefined,
+  'customize:noscript' : undefined,
   'customize:title' : undefined,
   'customize:highlightJsStyle' : 'github',
   'customize:highlightJsStyleBorder' : false,

+ 22 - 22
packages/app/src/server/routes/apiv3/customize-setting.js

@@ -77,11 +77,11 @@ const multer = require('multer');
  *        properties:
  *          customizeTitle:
  *            type: string
- *      CustomizeHeader:
- *        description: CustomizeHeader
+ *      CustomizeNoscript:
+ *        description: CustomizeNoscript
  *        type: object
  *        properties:
- *          customizeHeader:
+ *          customizeNoscript:
  *            type: string
  *      CustomizeCss:
  *        description: CustomizeCss
@@ -131,20 +131,20 @@ module.exports = (crowi) => {
     customizeTitle: [
       body('customizeTitle').isString(),
     ],
-    customizeHeader: [
-      body('customizeHeader').isString(),
-    ],
     highlight: [
       body('highlightJsStyle').isString().isIn([
         'github', 'github-gist', 'atom-one-light', 'xcode', 'vs', 'atom-one-dark', 'hybrid', 'monokai', 'tomorrow-night', 'vs2015',
       ]),
       body('highlightJsStyleBorder').isBoolean(),
     ],
+    customizeScript: [
+      body('customizeScript').isString(),
+    ],
     customizeCss: [
       body('customizeCss').isString(),
     ],
-    customizeScript: [
-      body('customizeScript').isString(),
+    customizeNoscript: [
+      body('customizeNoscript').isString(),
     ],
     logo: [
       body('isDefaultLogo').isBoolean().optional({ nullable: true }),
@@ -186,9 +186,9 @@ module.exports = (crowi) => {
       styleName: await crowi.configManager.getConfig('crowi', 'customize:highlightJsStyle'),
       styleBorder: await crowi.configManager.getConfig('crowi', 'customize:highlightJsStyleBorder'),
       customizeTitle: await crowi.configManager.getConfig('crowi', 'customize:title'),
-      customizeHeader: await crowi.configManager.getConfig('crowi', 'customize:header'),
-      customizeCss: await crowi.configManager.getConfig('crowi', 'customize:css'),
       customizeScript: await crowi.configManager.getConfig('crowi', 'customize:script'),
+      customizeCss: await crowi.configManager.getConfig('crowi', 'customize:css'),
+      customizeNoscript: await crowi.configManager.getConfig('crowi', 'customize:noscript'),
     };
 
     return res.apiv3({ customizeParams });
@@ -531,43 +531,43 @@ module.exports = (crowi) => {
   /**
    * @swagger
    *
-   *    /customize-setting/customizeHeader:
+   *    /customize-setting/customize-noscript:
    *      put:
    *        tags: [CustomizeSetting]
-   *        operationId: updateCustomizeHeaderCustomizeSetting
-   *        summary: /customize-setting/customizeHeader
-   *        description: Update customizeHeader
+   *        operationId: updateCustomizeNoscriptCustomizeSetting
+   *        summary: /customize-setting/customize-noscript
+   *        description: Update customizeNoscript
    *        requestBody:
    *          required: true
    *          content:
    *            application/json:
    *              schema:
-   *                $ref: '#/components/schemas/CustomizeHeader'
+   *                $ref: '#/components/schemas/CustomizeNoscript'
    *        responses:
    *          200:
    *            description: Succeeded to update customize header
    *            content:
    *              application/json:
    *                schema:
-   *                  $ref: '#/components/schemas/CustomizeHeader'
+   *                  $ref: '#/components/schemas/CustomizeNoscript'
    */
-  router.put('/customize-header', loginRequiredStrictly, adminRequired, addActivity, validator.customizeHeader, apiV3FormValidator, async(req, res) => {
+  router.put('/customize-noscript', loginRequiredStrictly, adminRequired, addActivity, validator.customizeNoscript, apiV3FormValidator, async(req, res) => {
     const requestParams = {
-      'customize:header': req.body.customizeHeader,
+      'customize:noscript': req.body.customizeNoscript,
     };
     try {
       await crowi.configManager.updateConfigsInTheSameNamespace('crowi', requestParams);
       const customizedParams = {
-        customizeHeader: await crowi.configManager.getConfig('crowi', 'customize:header'),
+        customizeNoscript: await crowi.configManager.getConfig('crowi', 'customize:noscript'),
       };
-      const parameters = { action: SupportedAction.ACTION_ADMIN_CUSTOM_HTML_HEADER_UPDATE };
+      const parameters = { action: SupportedAction.ACTION_ADMIN_CUSTOM_NOSCRIPT_UPDATE };
       activityEvent.emit('update', res.locals.activity._id, parameters);
       return res.apiv3({ customizedParams });
     }
     catch (err) {
-      const msg = 'Error occurred in updating customizeHeader';
+      const msg = 'Error occurred in updating customizeNoscript';
       logger.error('Error', err);
-      return res.apiv3Err(new ErrorV3(msg, 'update-customizeHeader-failed'));
+      return res.apiv3Err(new ErrorV3(msg, 'update-customizeNoscript-failed'));
     }
   });
 

+ 8 - 5
packages/app/src/server/service/customize.ts

@@ -1,12 +1,13 @@
 // eslint-disable-next-line no-unused-vars
+import { DevidedPagePath } from '@growi/core';
 import uglifycss from 'uglifycss';
 
-import { DevidedPagePath } from '@growi/core';
 import loggerFactory from '~/utils/logger';
 
 import S2sMessage from '../models/vo/s2s-message';
-import { S2sMessageHandlable } from './s2s-messaging/handlable';
+
 import ConfigManager from './config-manager';
+import { S2sMessageHandlable } from './s2s-messaging/handlable';
 
 const logger = loggerFactory('growi:service:CustomizeService');
 
@@ -80,8 +81,6 @@ class CustomizeService implements S2sMessageHandlable {
    * initialize custom css strings
    */
   initCustomCss() {
-    const uglifycss = require('uglifycss');
-
     const rawCss = this.configManager.getConfig('crowi', 'customize:css') || '';
 
     // uglify and store
@@ -95,7 +94,11 @@ class CustomizeService implements S2sMessageHandlable {
   }
 
   getCustomScript() {
-    return this.configManager.getConfig('crowi', 'customize:script') || '';
+    return this.configManager.getConfig('crowi', 'customize:script');
+  }
+
+  getCustomNoscript() {
+    return this.configManager.getConfig('crowi', 'customize:noscript');
   }
 
   initCustomTitle() {