Просмотр исходного кода

Merge pull request #7352 from weseek/imprv/ssr

feat: Server Side Rendering
Yuki Takei 3 лет назад
Родитель
Сommit
c24a731479

+ 0 - 84
packages/app/src/components/Page/PageContents.tsx

@@ -1,84 +0,0 @@
-import React, { useEffect } from 'react';
-
-import { useTranslation } from 'next-i18next';
-
-import { useUpdateStateAfterSave } from '~/client/services/page-operation';
-import { useDrawioModalLauncherForView } from '~/client/services/side-effects/drawio-modal-launcher-for-view';
-import { useHandsontableModalLauncherForView } from '~/client/services/side-effects/handsontable-modal-launcher-for-view';
-import { toastSuccess, toastError } from '~/client/util/toastr';
-import { useSWRxCurrentPage } from '~/stores/page';
-import { useViewOptions } from '~/stores/renderer';
-import { registerGrowiFacade } from '~/utils/growi-facade';
-import loggerFactory from '~/utils/logger';
-
-import RevisionRenderer from './RevisionRenderer';
-
-
-const logger = loggerFactory('growi:Page');
-
-
-export const PageContents = (): JSX.Element => {
-  const { t } = useTranslation();
-
-  const { data: currentPage } = useSWRxCurrentPage();
-  const updateStateAfterSave = useUpdateStateAfterSave(currentPage?._id);
-
-  const { data: rendererOptions, mutate: mutateRendererOptions } = useViewOptions();
-
-  // register to facade
-  useEffect(() => {
-    registerGrowiFacade({
-      markdownRenderer: {
-        optionsMutators: {
-          viewOptionsMutator: mutateRendererOptions,
-        },
-      },
-    });
-  }, [mutateRendererOptions]);
-
-  useHandsontableModalLauncherForView({
-    onSaveSuccess: () => {
-      toastSuccess(t('toaster.save_succeeded'));
-
-      updateStateAfterSave?.();
-    },
-    onSaveError: (error) => {
-      toastError(error);
-    },
-  });
-
-  useDrawioModalLauncherForView({
-    onSaveSuccess: () => {
-      toastSuccess(t('toaster.save_succeeded'));
-
-      updateStateAfterSave?.();
-    },
-    onSaveError: (error) => {
-      toastError(error);
-    },
-  });
-
-
-  if (currentPage == null || rendererOptions == null) {
-    const entries = Object.entries({
-      currentPage, rendererOptions,
-    })
-      .map(([key, value]) => [key, value == null ? 'null' : undefined])
-      .filter(([, value]) => value != null);
-
-    logger.warn('Some of materials are missing.', Object.fromEntries(entries));
-
-    return <></>;
-  }
-
-  const { _id: revisionId, body: markdown } = currentPage.revision;
-
-  return (
-    <>
-      { revisionId != null && (
-        <RevisionRenderer rendererOptions={rendererOptions} markdown={markdown} />
-      )}
-    </>
-  );
-
-};

+ 39 - 0
packages/app/src/components/Page/PageContentsUtilities.tsx

@@ -0,0 +1,39 @@
+import { useTranslation } from 'next-i18next';
+
+import { useUpdateStateAfterSave } from '~/client/services/page-operation';
+import { useDrawioModalLauncherForView } from '~/client/services/side-effects/drawio-modal-launcher-for-view';
+import { useHandsontableModalLauncherForView } from '~/client/services/side-effects/handsontable-modal-launcher-for-view';
+import { toastSuccess, toastError } from '~/client/util/toastr';
+import { useCurrentPageId } from '~/stores/context';
+
+
+export const PageContentsUtilities = (): null => {
+  const { t } = useTranslation();
+
+  const { data: pageId } = useCurrentPageId();
+  const updateStateAfterSave = useUpdateStateAfterSave(pageId);
+
+  useHandsontableModalLauncherForView({
+    onSaveSuccess: () => {
+      toastSuccess(t('toaster.save_succeeded'));
+
+      updateStateAfterSave?.();
+    },
+    onSaveError: (error) => {
+      toastError(error);
+    },
+  });
+
+  useDrawioModalLauncherForView({
+    onSaveSuccess: () => {
+      toastSuccess(t('toaster.save_succeeded'));
+
+      updateStateAfterSave?.();
+    },
+    onSaveError: (error) => {
+      toastError(error);
+    },
+  });
+
+  return null;
+};

+ 50 - 36
packages/app/src/components/Page/PageView.tsx

@@ -1,13 +1,17 @@
-import React, { useMemo } from 'react';
+import React, { useEffect, useMemo } from 'react';
 
 
 import { type IPagePopulatedToShowRevision, pagePathUtils } from '@growi/core';
 import { type IPagePopulatedToShowRevision, pagePathUtils } from '@growi/core';
 import dynamic from 'next/dynamic';
 import dynamic from 'next/dynamic';
 
 
-
+import type { RendererConfig } from '~/interfaces/services/renderer';
+import { generateSSRViewOptions } from '~/services/renderer/renderer';
 import {
 import {
   useIsForbidden, useIsIdenticalPath, useIsNotCreatable, useIsNotFound,
   useIsForbidden, useIsIdenticalPath, useIsNotCreatable, useIsNotFound,
 } from '~/stores/context';
 } from '~/stores/context';
+import { useSWRxCurrentPage } from '~/stores/page';
+import { useViewOptions } from '~/stores/renderer';
 import { useIsMobile } from '~/stores/ui';
 import { useIsMobile } from '~/stores/ui';
+import { registerGrowiFacade } from '~/utils/growi-facade';
 
 
 import type { CommentsProps } from '../Comments';
 import type { CommentsProps } from '../Comments';
 import { MainPane } from '../Layout/MainPane';
 import { MainPane } from '../Layout/MainPane';
@@ -17,7 +21,7 @@ import type { PageSideContentsProps } from '../PageSideContents';
 import { UserInfo } from '../User/UserInfo';
 import { UserInfo } from '../User/UserInfo';
 import type { UsersHomePageFooterProps } from '../UsersHomePageFooter';
 import type { UsersHomePageFooterProps } from '../UsersHomePageFooter';
 
 
-import { PageContents } from './PageContents';
+import RevisionRenderer from './RevisionRenderer';
 
 
 import styles from './PageView.module.scss';
 import styles from './PageView.module.scss';
 
 
@@ -29,35 +33,48 @@ const NotCreatablePage = dynamic(() => import('../NotCreatablePage').then(mod =>
 const ForbiddenPage = dynamic(() => import('../ForbiddenPage'), { ssr: false });
 const ForbiddenPage = dynamic(() => import('../ForbiddenPage'), { ssr: false });
 const NotFoundPage = dynamic(() => import('../NotFoundPage'), { ssr: false });
 const NotFoundPage = dynamic(() => import('../NotFoundPage'), { ssr: false });
 const PageSideContents = dynamic<PageSideContentsProps>(() => import('../PageSideContents').then(mod => mod.PageSideContents), { ssr: false });
 const PageSideContents = dynamic<PageSideContentsProps>(() => import('../PageSideContents').then(mod => mod.PageSideContents), { ssr: false });
+const PageContentsUtilities = dynamic(() => import('./PageContentsUtilities').then(mod => mod.PageContentsUtilities), { ssr: false });
 const Comments = dynamic<CommentsProps>(() => import('../Comments').then(mod => mod.Comments), { ssr: false });
 const Comments = dynamic<CommentsProps>(() => import('../Comments').then(mod => mod.Comments), { ssr: false });
 const UsersHomePageFooter = dynamic<UsersHomePageFooterProps>(() => import('../UsersHomePageFooter')
 const UsersHomePageFooter = dynamic<UsersHomePageFooterProps>(() => import('../UsersHomePageFooter')
   .then(mod => mod.UsersHomePageFooter), { ssr: false });
   .then(mod => mod.UsersHomePageFooter), { ssr: false });
-
-const IdenticalPathPage = (): JSX.Element => {
-  const IdenticalPathPage = dynamic(() => import('../IdenticalPathPage').then(mod => mod.IdenticalPathPage), { ssr: false });
-  return <IdenticalPathPage />;
-};
+const IdenticalPathPage = dynamic(() => import('../IdenticalPathPage').then(mod => mod.IdenticalPathPage), { ssr: false });
 
 
 
 
 type Props = {
 type Props = {
   pagePath: string,
   pagePath: string,
-  page?: IPagePopulatedToShowRevision,
-  ssrBody?: JSX.Element,
+  rendererConfig: RendererConfig,
+  initialPage?: IPagePopulatedToShowRevision,
 }
 }
 
 
 export const PageView = (props: Props): JSX.Element => {
 export const PageView = (props: Props): JSX.Element => {
   const {
   const {
-    pagePath, page, ssrBody,
+    pagePath, initialPage, rendererConfig,
   } = props;
   } = props;
 
 
-  const pageId = page?._id;
-
   const { data: isIdenticalPathPage } = useIsIdenticalPath();
   const { data: isIdenticalPathPage } = useIsIdenticalPath();
   const { data: isForbidden } = useIsForbidden();
   const { data: isForbidden } = useIsForbidden();
   const { data: isNotCreatable } = useIsNotCreatable();
   const { data: isNotCreatable } = useIsNotCreatable();
-  const { data: isNotFound } = useIsNotFound();
+  const { data: isNotFoundMeta } = useIsNotFound();
   const { data: isMobile } = useIsMobile();
   const { data: isMobile } = useIsMobile();
 
 
+  const { data: pageBySWR } = useSWRxCurrentPage();
+  const { data: viewOptions, mutate: mutateRendererOptions } = useViewOptions();
+
+  const page = pageBySWR ?? initialPage;
+  const isNotFound = isNotFoundMeta || page == null;
+  const isUsersHomePagePath = isUsersHomePage(pagePath);
+
+  // register to facade
+  useEffect(() => {
+    registerGrowiFacade({
+      markdownRenderer: {
+        optionsMutators: {
+          viewOptionsMutator: mutateRendererOptions,
+        },
+      },
+    });
+  }, [mutateRendererOptions]);
+
   const specialContents = useMemo(() => {
   const specialContents = useMemo(() => {
     if (isIdenticalPathPage) {
     if (isIdenticalPathPage) {
       return <IdenticalPathPage />;
       return <IdenticalPathPage />;
@@ -68,10 +85,7 @@ export const PageView = (props: Props): JSX.Element => {
     if (isNotCreatable) {
     if (isNotCreatable) {
       return <NotCreatablePage />;
       return <NotCreatablePage />;
     }
     }
-    if (isNotFound) {
-      return <NotFoundPage path={pagePath} />;
-    }
-  }, [isForbidden, isIdenticalPathPage, isNotCreatable, isNotFound, pagePath]);
+  }, [isForbidden, isIdenticalPathPage, isNotCreatable]);
 
 
   const sideContents = !isNotFound && !isNotCreatable
   const sideContents = !isNotFound && !isNotCreatable
     ? (
     ? (
@@ -79,13 +93,11 @@ export const PageView = (props: Props): JSX.Element => {
     )
     )
     : null;
     : null;
 
 
-  const footerContents = !isIdenticalPathPage && !isNotFound && page != null
+  const footerContents = !isIdenticalPathPage && !isNotFound
     ? (
     ? (
       <>
       <>
-        { pageId != null && pagePath != null && (
-          <Comments pageId={pageId} pagePath={pagePath} revision={page.revision} />
-        ) }
-        { pagePath != null && isUsersHomePage(pagePath) && (
+        <Comments pageId={page._id} pagePath={pagePath} revision={page.revision} />
+        { isUsersHomePagePath && (
           <UsersHomePageFooter creatorId={page.creator._id}/>
           <UsersHomePageFooter creatorId={page.creator._id}/>
         ) }
         ) }
         <PageContentFooter page={page} />
         <PageContentFooter page={page} />
@@ -93,19 +105,21 @@ export const PageView = (props: Props): JSX.Element => {
     )
     )
     : null;
     : null;
 
 
-  const isUsersHomePagePath = isUsersHomePage(pagePath);
+  const Contents = () => {
+    if (isNotFound) {
+      return <NotFoundPage path={pagePath} />;
+    }
+
+    const rendererOptions = viewOptions ?? generateSSRViewOptions(rendererConfig, pagePath);
+    const markdown = page.revision.body;
 
 
-  const contents = specialContents != null
-    ? <></>
-    // TODO: show SSR body
-    // : (() => {
-    //   const PageContents = dynamic(() => import('./PageContents').then(mod => mod.PageContents), {
-    //     ssr: false,
-    //     // loading: () => ssrBody ?? <></>,
-    //   });
-    //   return <PageContents />;
-    // })();
-    : <PageContents />;
+    return (
+      <>
+        <PageContentsUtilities />
+        <RevisionRenderer rendererOptions={rendererOptions} markdown={markdown} />
+      </>
+    );
+  };
 
 
   return (
   return (
     <MainPane
     <MainPane
@@ -119,7 +133,7 @@ export const PageView = (props: Props): JSX.Element => {
         <>
         <>
           { isUsersHomePagePath && <UserInfo author={page?.creator} /> }
           { isUsersHomePagePath && <UserInfo author={page?.creator} /> }
           <div className={`mb-5 ${isMobile ? `page-mobile ${styles['page-mobile']}` : ''}`}>
           <div className={`mb-5 ${isMobile ? `page-mobile ${styles['page-mobile']}` : ''}`}>
-            { contents }
+            <Contents />
           </div>
           </div>
         </>
         </>
       ) }
       ) }

+ 1 - 1
packages/app/src/components/PageAlert/PageAlerts.tsx

@@ -4,12 +4,12 @@ import dynamic from 'next/dynamic';
 
 
 import { useIsNotFound } from '~/stores/context';
 import { useIsNotFound } from '~/stores/context';
 
 
-import { FixPageGrantAlert } from './FixPageGrantAlert';
 import { OldRevisionAlert } from './OldRevisionAlert';
 import { OldRevisionAlert } from './OldRevisionAlert';
 import { PageGrantAlert } from './PageGrantAlert';
 import { PageGrantAlert } from './PageGrantAlert';
 import { PageRedirectedAlert } from './PageRedirectedAlert';
 import { PageRedirectedAlert } from './PageRedirectedAlert';
 import { PageStaleAlert } from './PageStaleAlert';
 import { PageStaleAlert } from './PageStaleAlert';
 
 
+const FixPageGrantAlert = dynamic(() => import('./FixPageGrantAlert').then(mod => mod.FixPageGrantAlert), { ssr: false });
 // dynamic import because TrashPageAlert uses localStorageMiddleware
 // dynamic import because TrashPageAlert uses localStorageMiddleware
 const TrashPageAlert = dynamic(() => import('./TrashPageAlert').then(mod => mod.TrashPageAlert), { ssr: false });
 const TrashPageAlert = dynamic(() => import('./TrashPageAlert').then(mod => mod.TrashPageAlert), { ssr: false });
 
 

+ 6 - 8
packages/app/src/components/PageSideContents.tsx

@@ -5,7 +5,6 @@ import { useTranslation } from 'next-i18next';
 import { Link } from 'react-scroll';
 import { Link } from 'react-scroll';
 
 
 import { DEFAULT_AUTO_SCROLL_OPTS } from '~/client/util/smooth-scroll';
 import { DEFAULT_AUTO_SCROLL_OPTS } from '~/client/util/smooth-scroll';
-import { useCurrentPathname } from '~/stores/context';
 import { useDescendantsPageListModal } from '~/stores/modal';
 import { useDescendantsPageListModal } from '~/stores/modal';
 
 
 import CountBadge from './Common/CountBadge';
 import CountBadge from './Common/CountBadge';
@@ -20,27 +19,26 @@ const { isTopPage, isUsersHomePage } = pagePathUtils;
 
 
 
 
 export type PageSideContentsProps = {
 export type PageSideContentsProps = {
-  page?: IPageHasId,
+  page: IPageHasId,
   isSharedUser?: boolean,
   isSharedUser?: boolean,
 }
 }
 
 
 export const PageSideContents = (props: PageSideContentsProps): JSX.Element => {
 export const PageSideContents = (props: PageSideContentsProps): JSX.Element => {
   const { t } = useTranslation();
   const { t } = useTranslation();
 
 
-  const { data: currentPathname } = useCurrentPathname();
   const { open: openDescendantPageListModal } = useDescendantsPageListModal();
   const { open: openDescendantPageListModal } = useDescendantsPageListModal();
 
 
   const { page, isSharedUser } = props;
   const { page, isSharedUser } = props;
 
 
-  const pagePath = page?.path ?? currentPathname;
-  const isTopPagePath = isTopPage(pagePath ?? '');
-  const isUsersHomePagePath = isUsersHomePage(pagePath ?? '');
+  const pagePath = page.path;
+  const isTopPagePath = isTopPage(pagePath);
+  const isUsersHomePagePath = isUsersHomePage(pagePath);
 
 
   return (
   return (
     <>
     <>
       {/* Page list */}
       {/* Page list */}
       <div className={`grw-page-accessories-control ${styles['grw-page-accessories-control']}`}>
       <div className={`grw-page-accessories-control ${styles['grw-page-accessories-control']}`}>
-        { pagePath != null && !isSharedUser && (
+        { !isSharedUser && (
           <button
           <button
             type="button"
             type="button"
             className="btn btn-block btn-outline-secondary grw-btn-page-accessories rounded-pill d-flex justify-content-between align-items-center"
             className="btn btn-block btn-outline-secondary grw-btn-page-accessories rounded-pill d-flex justify-content-between align-items-center"
@@ -57,7 +55,7 @@ export const PageSideContents = (props: PageSideContentsProps): JSX.Element => {
       </div>
       </div>
 
 
       {/* Comments */}
       {/* Comments */}
-      { page != null && !isTopPagePath && (
+      { !isTopPagePath && (
         <div className={`mt-2 grw-page-accessories-control ${styles['grw-page-accessories-control']}`}>
         <div className={`mt-2 grw-page-accessories-control ${styles['grw-page-accessories-control']}`}>
           <Link to={'page-comments'} offset={-100} {...DEFAULT_AUTO_SCROLL_OPTS}>
           <Link to={'page-comments'} offset={-100} {...DEFAULT_AUTO_SCROLL_OPTS}>
             <button
             <button

+ 0 - 58
packages/app/src/components/ShareLink/ShareLinkPageContents.tsx

@@ -1,58 +0,0 @@
-import React, { useEffect } from 'react';
-
-import type { IPagePopulatedToShowRevision } from '@growi/core';
-
-import { useViewOptions } from '~/stores/renderer';
-import { registerGrowiFacade } from '~/utils/growi-facade';
-import loggerFactory from '~/utils/logger';
-
-import RevisionRenderer from '../Page/RevisionRenderer';
-
-
-const logger = loggerFactory('growi:Page');
-
-
-export type ShareLinkPageContentsProps = {
-  page?: IPagePopulatedToShowRevision,
-}
-
-export const ShareLinkPageContents = (props: ShareLinkPageContentsProps): JSX.Element => {
-  const { page } = props;
-
-  const { data: rendererOptions, mutate: mutateRendererOptions } = useViewOptions();
-
-  // register to facade
-  useEffect(() => {
-    registerGrowiFacade({
-      markdownRenderer: {
-        optionsMutators: {
-          viewOptionsMutator: mutateRendererOptions,
-        },
-      },
-    });
-  }, [mutateRendererOptions]);
-
-
-  if (page == null || rendererOptions == null) {
-    const entries = Object.entries({
-      page, rendererOptions,
-    })
-      .map(([key, value]) => [key, value == null ? 'null' : undefined])
-      .filter(([, value]) => value != null);
-
-    logger.warn('Some of materials are missing.', Object.fromEntries(entries));
-
-    return <></>;
-  }
-
-  const { _id: revisionId, body: markdown } = page.revision;
-
-  return (
-    <>
-      { revisionId != null && (
-        <RevisionRenderer rendererOptions={rendererOptions} markdown={markdown} />
-      )}
-    </>
-  );
-
-};

+ 124 - 0
packages/app/src/components/ShareLink/ShareLinkPageView.tsx

@@ -0,0 +1,124 @@
+import React, { useEffect, useMemo } from 'react';
+
+import type { IPagePopulatedToShowRevision } from '@growi/core';
+import dynamic from 'next/dynamic';
+
+import type { RendererConfig } from '~/interfaces/services/renderer';
+import { IShareLinkHasId } from '~/interfaces/share-link';
+import { generateSSRViewOptions } from '~/services/renderer/renderer';
+import { useIsNotFound } from '~/stores/context';
+import { useViewOptions } from '~/stores/renderer';
+import { registerGrowiFacade } from '~/utils/growi-facade';
+import loggerFactory from '~/utils/logger';
+
+import { MainPane } from '../Layout/MainPane';
+import RevisionRenderer from '../Page/RevisionRenderer';
+import ShareLinkAlert from '../Page/ShareLinkAlert';
+import { PageSideContentsProps } from '../PageSideContents';
+
+
+const logger = loggerFactory('growi:Page');
+
+
+const PageSideContents = dynamic<PageSideContentsProps>(() => import('../PageSideContents').then(mod => mod.PageSideContents), { ssr: false });
+const ForbiddenPage = dynamic(() => import('../ForbiddenPage'), { ssr: false });
+
+
+type Props = {
+  pagePath: string,
+  rendererConfig: RendererConfig,
+  page?: IPagePopulatedToShowRevision,
+  shareLink?: IShareLinkHasId,
+  isExpired: boolean,
+  disableLinkSharing: boolean,
+}
+
+export const ShareLinkPageView = (props: Props): JSX.Element => {
+  const {
+    pagePath, rendererConfig,
+    page, shareLink,
+    isExpired, disableLinkSharing,
+  } = props;
+
+  const { data: isNotFoundMeta } = useIsNotFound();
+
+  const { data: viewOptions, mutate: mutateRendererOptions } = useViewOptions();
+
+  // register to facade
+  useEffect(() => {
+    registerGrowiFacade({
+      markdownRenderer: {
+        optionsMutators: {
+          viewOptionsMutator: mutateRendererOptions,
+        },
+      },
+    });
+  }, [mutateRendererOptions]);
+
+  const isNotFound = isNotFoundMeta || page == null || shareLink == null;
+
+  const specialContents = useMemo(() => {
+    if (disableLinkSharing) {
+      return <ForbiddenPage isLinkSharingDisabled={props.disableLinkSharing} />;
+    }
+  }, [disableLinkSharing, props.disableLinkSharing]);
+
+  const sideContents = !isNotFound
+    ? (
+      <PageSideContents page={page} />
+    )
+    : null;
+
+
+  const Contents = () => {
+    if (isNotFound) {
+      return <></>;
+    }
+
+    if (isExpired) {
+      return (
+        <>
+          <h2 className="text-muted mt-4">
+            <i className="icon-ban" aria-hidden="true" />
+            <span> Page is expired</span>
+          </h2>
+        </>
+      );
+    }
+
+    const rendererOptions = viewOptions ?? generateSSRViewOptions(rendererConfig, pagePath);
+    const markdown = page.revision.body;
+
+    return (
+      <>
+        <RevisionRenderer rendererOptions={rendererOptions} markdown={markdown} />
+      </>
+    );
+  };
+
+  return (
+    <MainPane
+      sideContents={sideContents}
+    >
+      { specialContents }
+      { specialContents == null && (
+        <>
+          { isNotFound && (
+            <h2 className="text-muted mt-4">
+              <i className="icon-ban" aria-hidden="true" />
+              <span> Page is not found</span>
+            </h2>
+          ) }
+          { !isNotFound && (
+            <>
+              <ShareLinkAlert expiredAt={shareLink.expiredAt} createdAt={shareLink.createdAt} />
+              <div className="mb-5">
+                <Contents />
+              </div>
+            </>
+          ) }
+        </>
+      ) }
+    </MainPane>
+  );
+};

+ 2 - 9
packages/app/src/pages/[[...path]].page.tsx

@@ -21,7 +21,6 @@ import superjson from 'superjson';
 
 
 import { useCurrentGrowiLayoutFluidClassName, useEditorModeClassName } from '~/client/services/layout';
 import { useCurrentGrowiLayoutFluidClassName, useEditorModeClassName } from '~/client/services/layout';
 import { PageView } from '~/components/Page/PageView';
 import { PageView } from '~/components/Page/PageView';
-import RevisionRenderer from '~/components/Page/RevisionRenderer';
 import { DrawioViewerScript } from '~/components/Script/DrawioViewerScript';
 import { DrawioViewerScript } from '~/components/Script/DrawioViewerScript';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import type { EditorConfig } from '~/interfaces/editor-settings';
 import type { EditorConfig } from '~/interfaces/editor-settings';
@@ -32,7 +31,6 @@ import type { IUserUISettings } from '~/interfaces/user-ui-settings';
 import type { PageModel, PageDocument } from '~/server/models/page';
 import type { PageModel, PageDocument } from '~/server/models/page';
 import type { PageRedirectModel } from '~/server/models/page-redirect';
 import type { PageRedirectModel } from '~/server/models/page-redirect';
 import type { UserUISettingsModel } from '~/server/models/user-ui-settings';
 import type { UserUISettingsModel } from '~/server/models/user-ui-settings';
-import { generateSSRViewOptions } from '~/services/renderer/renderer';
 import { useEditingMarkdown } from '~/stores/editor';
 import { useEditingMarkdown } from '~/stores/editor';
 import { useHasDraftOnHackmd, usePageIdOnHackmd, useRevisionIdHackmdSynced } from '~/stores/hackmd';
 import { useHasDraftOnHackmd, usePageIdOnHackmd, useRevisionIdHackmdSynced } from '~/stores/hackmd';
 import { useSWRxCurrentPage, useSWRxIsGrantNormalized } from '~/stores/page';
 import { useSWRxCurrentPage, useSWRxIsGrantNormalized } from '~/stores/page';
@@ -298,10 +296,6 @@ const Page: NextPageWithLayout<Props> = (props: Props) => {
 
 
   const title = generateCustomTitleForPage(props, pagePath);
   const title = generateCustomTitleForPage(props, pagePath);
 
 
-  // TODO: show SSR body
-  // const rendererOptions = generateSSRViewOptions(props.rendererConfig, pagePath);
-  // const ssrBody = <RevisionRenderer rendererOptions={rendererOptions} markdown={revisionBody ?? ''} />;
-
   return (
   return (
     <>
     <>
       <Head>
       <Head>
@@ -325,9 +319,8 @@ const Page: NextPageWithLayout<Props> = (props: Props) => {
           pageView={
           pageView={
             <PageView
             <PageView
               pagePath={pagePath}
               pagePath={pagePath}
-              page={pageWithMeta?.data}
-              // TODO: show SSR body
-              // ssrBody={ssrBody}
+              initialPage={pageWithMeta?.data}
+              rendererConfig={props.rendererConfig}
             />
             />
           }
           }
         />
         />

+ 23 - 91
packages/app/src/pages/share/[[...path]].page.tsx

@@ -5,27 +5,21 @@ import {
   GetServerSideProps, GetServerSidePropsContext,
   GetServerSideProps, GetServerSidePropsContext,
 } from 'next';
 } from 'next';
 import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
 import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
-import dynamic from 'next/dynamic';
 import Head from 'next/head';
 import Head from 'next/head';
 import superjson from 'superjson';
 import superjson from 'superjson';
 
 
 import { useCurrentGrowiLayoutFluidClassName } from '~/client/services/layout';
 import { useCurrentGrowiLayoutFluidClassName } from '~/client/services/layout';
-import { MainPane } from '~/components/Layout/MainPane';
 import { ShareLinkLayout } from '~/components/Layout/ShareLinkLayout';
 import { ShareLinkLayout } from '~/components/Layout/ShareLinkLayout';
 import GrowiContextualSubNavigationSubstance from '~/components/Navbar/GrowiContextualSubNavigation';
 import GrowiContextualSubNavigationSubstance from '~/components/Navbar/GrowiContextualSubNavigation';
-import RevisionRenderer from '~/components/Page/RevisionRenderer';
-import ShareLinkAlert from '~/components/Page/ShareLinkAlert';
-import type { PageSideContentsProps } from '~/components/PageSideContents';
 import { DrawioViewerScript } from '~/components/Script/DrawioViewerScript';
 import { DrawioViewerScript } from '~/components/Script/DrawioViewerScript';
-import type { ShareLinkPageContentsProps } from '~/components/ShareLink/ShareLinkPageContents';
+import { ShareLinkPageView } from '~/components/ShareLink/ShareLinkPageView';
 import { SupportedAction, SupportedActionType } from '~/interfaces/activity';
 import { SupportedAction, SupportedActionType } from '~/interfaces/activity';
 import { CrowiRequest } from '~/interfaces/crowi-request';
 import { CrowiRequest } from '~/interfaces/crowi-request';
 import { RendererConfig } from '~/interfaces/services/renderer';
 import { RendererConfig } from '~/interfaces/services/renderer';
 import { IShareLinkHasId } from '~/interfaces/share-link';
 import { IShareLinkHasId } from '~/interfaces/share-link';
 import type { PageDocument } from '~/server/models/page';
 import type { PageDocument } from '~/server/models/page';
-import { generateSSRViewOptions } from '~/services/renderer/renderer';
 import {
 import {
-  useCurrentUser, useCurrentPageId, useRendererConfig, useIsSearchPage, useCurrentPathname,
+  useCurrentUser, useCurrentPageId, useRendererConfig, useIsSearchPage, useCurrentPathname, useIsNotFound,
   useShareLinkId, useIsSearchServiceConfigured, useIsSearchServiceReachable, useIsSearchScopeChildrenAsDefault, useDrawioUri, useIsContainerFluid,
   useShareLinkId, useIsSearchServiceConfigured, useIsSearchServiceReachable, useIsSearchScopeChildrenAsDefault, useDrawioUri, useIsContainerFluid,
 } from '~/stores/context';
 } from '~/stores/context';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
@@ -37,13 +31,10 @@ import {
 
 
 const logger = loggerFactory('growi:next-page:share');
 const logger = loggerFactory('growi:next-page:share');
 
 
-const PageSideContents = dynamic<PageSideContentsProps>(() => import('~/components/PageSideContents').then(mod => mod.PageSideContents), { ssr: false });
-// const Comments = dynamic(() => import('~/components/Comments').then(mod => mod.Comments), { ssr: false });
-const ForbiddenPage = dynamic(() => import('~/components/ForbiddenPage'), { ssr: false });
-
 type Props = CommonProps & {
 type Props = CommonProps & {
   shareLinkRelatedPage?: IShareLinkRelatedPage,
   shareLinkRelatedPage?: IShareLinkRelatedPage,
   shareLink?: IShareLinkHasId,
   shareLink?: IShareLinkHasId,
+  isNotFound: boolean,
   isExpired: boolean,
   isExpired: boolean,
   disableLinkSharing: boolean,
   disableLinkSharing: boolean,
   isSearchServiceConfigured: boolean,
   isSearchServiceConfigured: boolean,
@@ -73,16 +64,16 @@ superjson.registerCustom<IShareLinkRelatedPage, string>(
 // GrowiContextualSubNavigation for shared page
 // GrowiContextualSubNavigation for shared page
 // get page info from props not to send request 'GET /page' from client
 // get page info from props not to send request 'GET /page' from client
 type GrowiContextualSubNavigationForSharedPageProps = {
 type GrowiContextualSubNavigationForSharedPageProps = {
-  currentPage?: IPagePopulatedToShowRevision,
+  page?: IPagePopulatedToShowRevision,
   isLinkSharingDisabled: boolean,
   isLinkSharingDisabled: boolean,
 }
 }
 
 
 const GrowiContextualSubNavigationForSharedPage = (props: GrowiContextualSubNavigationForSharedPageProps): JSX.Element => {
 const GrowiContextualSubNavigationForSharedPage = (props: GrowiContextualSubNavigationForSharedPageProps): JSX.Element => {
-  const { currentPage, isLinkSharingDisabled } = props;
-  if (currentPage == null) { return <></> }
+  const { page, isLinkSharingDisabled } = props;
+
   return (
   return (
     <div data-testid="grw-contextual-sub-nav">
     <div data-testid="grw-contextual-sub-nav">
-      <GrowiContextualSubNavigationSubstance currentPage={currentPage} isLinkSharingDisabled={isLinkSharingDisabled}/>
+      <GrowiContextualSubNavigationSubstance currentPage={page} isLinkSharingDisabled={isLinkSharingDisabled}/>
     </div>
     </div>
   );
   );
 };
 };
@@ -90,6 +81,7 @@ const GrowiContextualSubNavigationForSharedPage = (props: GrowiContextualSubNavi
 const SharedPage: NextPageWithLayout<Props> = (props: Props) => {
 const SharedPage: NextPageWithLayout<Props> = (props: Props) => {
   useCurrentPathname(props.shareLink?.relatedPage.path);
   useCurrentPathname(props.shareLink?.relatedPage.path);
   useIsSearchPage(false);
   useIsSearchPage(false);
+  useIsNotFound(props.isNotFound);
   useShareLinkId(props.shareLink?._id);
   useShareLinkId(props.shareLink?._id);
   useCurrentPageId(props.shareLink?.relatedPage._id);
   useCurrentPageId(props.shareLink?.relatedPage._id);
   useCurrentUser(props.currentUser);
   useCurrentUser(props.currentUser);
@@ -103,43 +95,10 @@ const SharedPage: NextPageWithLayout<Props> = (props: Props) => {
 
 
   const growiLayoutFluidClass = useCurrentGrowiLayoutFluidClassName();
   const growiLayoutFluidClass = useCurrentGrowiLayoutFluidClassName();
 
 
-  const isNotFound = props.shareLink == null || props.shareLink.relatedPage == null || props.shareLink.relatedPage.isEmpty;
-  const isShowSharedPage = !props.disableLinkSharing && !isNotFound && !props.isExpired;
-  const shareLink = props.shareLink;
-
   const pagePath = props.shareLinkRelatedPage?.path ?? '';
   const pagePath = props.shareLinkRelatedPage?.path ?? '';
-  const revisionBody = props.shareLinkRelatedPage?.revision.body;
 
 
   const title = generateCustomTitleForPage(props, pagePath);
   const title = generateCustomTitleForPage(props, pagePath);
 
 
-  // TODO: show SSR body
-  // const rendererOptions = generateSSRViewOptions(props.rendererConfig, pagePath);
-  // const ssrBody = <RevisionRenderer rendererOptions={rendererOptions} markdown={revisionBody ?? ''} />;
-
-  const sideContents = shareLink != null
-    ? <PageSideContents page={shareLink.relatedPage} />
-    : <></>;
-
-  // const footerContents = shareLink != null && isPopulated(shareLink.relatedPage.revision)
-  //   ? (
-  //     <>
-  //       <Comments pageId={shareLink._id} pagePath={shareLink.relatedPage.path} revision={shareLink.relatedPage.revision} />
-  //     </>
-  //   )
-  //   : <></>;
-
-  const contents = (() => {
-    const ShareLinkPageContents = dynamic<ShareLinkPageContentsProps>(
-      () => import('~/components/ShareLink/ShareLinkPageContents').then(mod => mod.ShareLinkPageContents),
-      {
-        ssr: false,
-        // TODO: show SSR body
-        // loading: () => ssrBody,
-      },
-    );
-    return <ShareLinkPageContents page={props.shareLinkRelatedPage} />;
-  })();
-
   return (
   return (
     <>
     <>
       <Head>
       <Head>
@@ -148,50 +107,19 @@ const SharedPage: NextPageWithLayout<Props> = (props: Props) => {
 
 
       <div className={`dynamic-layout-root ${growiLayoutFluidClass} h-100 d-flex flex-column justify-content-between`}>
       <div className={`dynamic-layout-root ${growiLayoutFluidClass} h-100 d-flex flex-column justify-content-between`}>
         <header className="py-0 position-relative">
         <header className="py-0 position-relative">
-          {isShowSharedPage
-          && <GrowiContextualSubNavigationForSharedPage currentPage={props.shareLinkRelatedPage} isLinkSharingDisabled={props.disableLinkSharing} />}
+          <GrowiContextualSubNavigationForSharedPage page={props.shareLinkRelatedPage} isLinkSharingDisabled={props.disableLinkSharing} />
         </header>
         </header>
 
 
         <div id="grw-fav-sticky-trigger" className="sticky-top"></div>
         <div id="grw-fav-sticky-trigger" className="sticky-top"></div>
 
 
-        <MainPane
-          sideContents={sideContents}
-          // footerContents={footerContents}
-        >
-          { props.disableLinkSharing && (
-            <div className="mt-4">
-              <ForbiddenPage isLinkSharingDisabled={props.disableLinkSharing} />
-            </div>
-          )}
-
-          { (isNotFound && !props.disableLinkSharing) && (
-            <div className="container-lg">
-              <h2 className="text-muted mt-4">
-                <i className="icon-ban" aria-hidden="true" />
-                <span> Page is not found</span>
-              </h2>
-            </div>
-          )}
-
-          { (props.isExpired && !props.disableLinkSharing && shareLink != null) && (
-            <div className="container-lg">
-              <ShareLinkAlert expiredAt={shareLink.expiredAt} createdAt={shareLink.createdAt} />
-              <h2 className="text-muted mt-4">
-                <i className="icon-ban" aria-hidden="true" />
-                <span> Page is expired</span>
-              </h2>
-            </div>
-          )}
-
-          {(isShowSharedPage && shareLink != null) && (
-            <>
-              <ShareLinkAlert expiredAt={shareLink.expiredAt} createdAt={shareLink.createdAt} />
-              <div className="mb-5">
-                { contents }
-              </div>
-            </>
-          )}
-        </MainPane>
+        <ShareLinkPageView
+          pagePath={pagePath}
+          rendererConfig={props.rendererConfig}
+          page={props.shareLinkRelatedPage}
+          shareLink={props.shareLink}
+          isExpired={props.isExpired}
+          disableLinkSharing={props.disableLinkSharing}
+        />
 
 
       </div>
       </div>
     </>
     </>
@@ -210,7 +138,7 @@ SharedPage.getLayout = function getLayout(page) {
 function injectServerConfigurations(context: GetServerSidePropsContext, props: Props): void {
 function injectServerConfigurations(context: GetServerSidePropsContext, props: Props): void {
   const req: CrowiRequest = context.req as CrowiRequest;
   const req: CrowiRequest = context.req as CrowiRequest;
   const { crowi } = req;
   const { crowi } = req;
-  const { configManager, searchService, xssService } = crowi;
+  const { configManager, searchService } = crowi;
 
 
   props.disableLinkSharing = configManager.getConfig('crowi', 'security:disableLinkSharing');
   props.disableLinkSharing = configManager.getConfig('crowi', 'security:disableLinkSharing');
 
 
@@ -288,7 +216,11 @@ export const getServerSideProps: GetServerSideProps = async(context: GetServerSi
   try {
   try {
     const ShareLinkModel = crowi.model('ShareLink');
     const ShareLinkModel = crowi.model('ShareLink');
     const shareLink = await ShareLinkModel.findOne({ _id: params.linkId }).populate('relatedPage');
     const shareLink = await ShareLinkModel.findOne({ _id: params.linkId }).populate('relatedPage');
-    if (shareLink != null) {
+    if (shareLink == null) {
+      props.isNotFound = true;
+    }
+    else {
+      props.isNotFound = false;
       props.shareLinkRelatedPage = await shareLink.relatedPage.populateDataToShowRevision();
       props.shareLinkRelatedPage = await shareLink.relatedPage.populateDataToShowRevision();
       props.isExpired = shareLink.isExpired();
       props.isExpired = shareLink.isExpired();
       props.shareLink = shareLink.toObject();
       props.shareLink = shareLink.toObject();

+ 3 - 3
packages/app/src/services/renderer/rehype-plugins/relocate-toc.ts

@@ -1,6 +1,6 @@
-import rehypeToc, { HtmlElementNode } from 'rehype-toc';
-import { Plugin } from 'unified';
-import { Node } from 'unist';
+import rehypeToc, { type HtmlElementNode } from 'rehype-toc';
+import type { Plugin } from 'unified';
+import type { Node } from 'unist';
 
 
 type StoreTocPluginParams = {
 type StoreTocPluginParams = {
   storeTocNode: (toc: HtmlElementNode) => void,
   storeTocNode: (toc: HtmlElementNode) => void,

+ 8 - 8
packages/app/src/services/renderer/renderer.tsx

@@ -1,26 +1,26 @@
 // allow only types to import from react
 // allow only types to import from react
-import { ComponentType } from 'react';
+import type { ComponentType } from 'react';
 
 
 import { isClient } from '@growi/core';
 import { isClient } from '@growi/core';
 import * as drawioPlugin from '@growi/remark-drawio';
 import * as drawioPlugin from '@growi/remark-drawio';
 import growiDirective from '@growi/remark-growi-directive';
 import growiDirective from '@growi/remark-growi-directive';
 import { Lsx, LsxImmutable } from '@growi/remark-lsx/components';
 import { Lsx, LsxImmutable } from '@growi/remark-lsx/components';
 import * as lsxGrowiPlugin from '@growi/remark-lsx/services/renderer';
 import * as lsxGrowiPlugin from '@growi/remark-lsx/services/renderer';
-import { Schema as SanitizeOption } from 'hast-util-sanitize';
-import { SpecialComponents } from 'react-markdown/lib/ast-to-react';
-import { NormalComponents } from 'react-markdown/lib/complex-types';
-import { ReactMarkdownOptions } from 'react-markdown/lib/react-markdown';
+import type { Schema as SanitizeOption } from 'hast-util-sanitize';
+import type { SpecialComponents } from 'react-markdown/lib/ast-to-react';
+import type { NormalComponents } from 'react-markdown/lib/complex-types';
+import type { ReactMarkdownOptions } from 'react-markdown/lib/react-markdown';
 import katex from 'rehype-katex';
 import katex from 'rehype-katex';
 import raw from 'rehype-raw';
 import raw from 'rehype-raw';
 import sanitize, { defaultSchema as sanitizeDefaultSchema } from 'rehype-sanitize';
 import sanitize, { defaultSchema as sanitizeDefaultSchema } from 'rehype-sanitize';
 import slug from 'rehype-slug';
 import slug from 'rehype-slug';
-import { HtmlElementNode } from 'rehype-toc';
+import type { HtmlElementNode } from 'rehype-toc';
 import breaks from 'remark-breaks';
 import breaks from 'remark-breaks';
 import emoji from 'remark-emoji';
 import emoji from 'remark-emoji';
 import gfm from 'remark-gfm';
 import gfm from 'remark-gfm';
 import math from 'remark-math';
 import math from 'remark-math';
 import deepmerge from 'ts-deepmerge';
 import deepmerge from 'ts-deepmerge';
-import { PluggableList, Pluggable, PluginTuple } from 'unified';
+import type { PluggableList, Pluggable, PluginTuple } from 'unified';
 
 
 
 
 import { CodeBlock } from '~/components/ReactMarkdownComponents/CodeBlock';
 import { CodeBlock } from '~/components/ReactMarkdownComponents/CodeBlock';
@@ -30,7 +30,7 @@ import { NextLink } from '~/components/ReactMarkdownComponents/NextLink';
 import { Table } from '~/components/ReactMarkdownComponents/Table';
 import { Table } from '~/components/ReactMarkdownComponents/Table';
 import { TableWithEditButton } from '~/components/ReactMarkdownComponents/TableWithEditButton';
 import { TableWithEditButton } from '~/components/ReactMarkdownComponents/TableWithEditButton';
 import { RehypeSanitizeOption } from '~/interfaces/rehype';
 import { RehypeSanitizeOption } from '~/interfaces/rehype';
-import { RendererConfig } from '~/interfaces/services/renderer';
+import type { RendererConfig } from '~/interfaces/services/renderer';
 import { registerGrowiFacade } from '~/utils/growi-facade';
 import { registerGrowiFacade } from '~/utils/growi-facade';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 

+ 24 - 8
packages/app/src/stores/renderer.tsx

@@ -2,12 +2,11 @@ import {
   useCallback, useEffect, useRef,
   useCallback, useEffect, useRef,
 } from 'react';
 } from 'react';
 
 
-import { HtmlElementNode } from 'rehype-toc';
-import useSWR, { SWRResponse } from 'swr';
-import useSWRImmutable from 'swr/immutable';
+import type { HtmlElementNode } from 'rehype-toc';
+import useSWR, { type SWRResponse } from 'swr';
 
 
 import {
 import {
-  RendererOptions,
+  type RendererOptions,
   generateSimpleViewOptions, generatePreviewOptions,
   generateSimpleViewOptions, generatePreviewOptions,
   generateViewOptions, generateTocOptions,
   generateViewOptions, generateTocOptions,
 } from '~/services/renderer/renderer';
 } from '~/services/renderer/renderer';
@@ -50,7 +49,10 @@ export const useViewOptions = (): SWRResponse<RendererOptions, Error> => {
       return optionsGenerator(currentPagePath, rendererConfig, storeTocNodeHandler);
       return optionsGenerator(currentPagePath, rendererConfig, storeTocNodeHandler);
     },
     },
     {
     {
+      keepPreviousData: true,
       fallbackData: isAllDataValid ? generateViewOptions(currentPagePath, rendererConfig, storeTocNodeHandler) : undefined,
       fallbackData: isAllDataValid ? generateViewOptions(currentPagePath, rendererConfig, storeTocNodeHandler) : undefined,
+      revalidateOnFocus: false,
+      revalidateOnReconnect: false,
     },
     },
   );
   );
 };
 };
@@ -62,13 +64,16 @@ export const useTocOptions = (): SWRResponse<RendererOptions, Error> => {
 
 
   const isAllDataValid = currentPagePath != null && rendererConfig != null && tocNode != null;
   const isAllDataValid = currentPagePath != null && rendererConfig != null && tocNode != null;
 
 
-  return useSWRImmutable(
+  return useSWR(
     isAllDataValid
     isAllDataValid
       ? ['tocOptions', currentPagePath, tocNode, rendererConfig]
       ? ['tocOptions', currentPagePath, tocNode, rendererConfig]
       : null,
       : null,
     ([, , tocNode, rendererConfig]) => generateTocOptions(rendererConfig, tocNode),
     ([, , tocNode, rendererConfig]) => generateTocOptions(rendererConfig, tocNode),
     {
     {
+      keepPreviousData: true,
       fallbackData: isAllDataValid ? generateTocOptions(rendererConfig, tocNode) : undefined,
       fallbackData: isAllDataValid ? generateTocOptions(rendererConfig, tocNode) : undefined,
+      revalidateOnFocus: false,
+      revalidateOnReconnect: false,
     },
     },
   );
   );
 };
 };
@@ -89,7 +94,10 @@ export const usePreviewOptions = (): SWRResponse<RendererOptions, Error> => {
       return optionsGenerator(rendererConfig, pagePath);
       return optionsGenerator(rendererConfig, pagePath);
     },
     },
     {
     {
+      keepPreviousData: true,
       fallbackData: isAllDataValid ? generatePreviewOptions(rendererConfig, currentPagePath) : undefined,
       fallbackData: isAllDataValid ? generatePreviewOptions(rendererConfig, currentPagePath) : undefined,
+      revalidateOnFocus: false,
+      revalidateOnReconnect: false,
     },
     },
   );
   );
 };
 };
@@ -100,7 +108,7 @@ export const useCommentForCurrentPageOptions = (): SWRResponse<RendererOptions,
 
 
   const isAllDataValid = currentPagePath != null && rendererConfig != null;
   const isAllDataValid = currentPagePath != null && rendererConfig != null;
 
 
-  return useSWRImmutable(
+  return useSWR(
     isAllDataValid
     isAllDataValid
       ? ['commentPreviewOptions', rendererConfig, currentPagePath]
       ? ['commentPreviewOptions', rendererConfig, currentPagePath]
       : null,
       : null,
@@ -111,12 +119,15 @@ export const useCommentForCurrentPageOptions = (): SWRResponse<RendererOptions,
       rendererConfig.isEnabledLinebreaksInComments,
       rendererConfig.isEnabledLinebreaksInComments,
     ),
     ),
     {
     {
+      keepPreviousData: true,
       fallbackData: isAllDataValid ? generateSimpleViewOptions(
       fallbackData: isAllDataValid ? generateSimpleViewOptions(
         rendererConfig,
         rendererConfig,
         currentPagePath,
         currentPagePath,
         undefined,
         undefined,
         rendererConfig.isEnabledLinebreaksInComments,
         rendererConfig.isEnabledLinebreaksInComments,
       ) : undefined,
       ) : undefined,
+      revalidateOnFocus: false,
+      revalidateOnReconnect: false,
     },
     },
   );
   );
 };
 };
@@ -127,13 +138,15 @@ export const useSelectedPagePreviewOptions = (pagePath: string, highlightKeyword
 
 
   const isAllDataValid = rendererConfig != null;
   const isAllDataValid = rendererConfig != null;
 
 
-  return useSWRImmutable(
+  return useSWR(
     isAllDataValid
     isAllDataValid
       ? ['selectedPagePreviewOptions', rendererConfig, pagePath, highlightKeywords]
       ? ['selectedPagePreviewOptions', rendererConfig, pagePath, highlightKeywords]
       : null,
       : null,
     ([, rendererConfig, pagePath, highlightKeywords]) => generateSimpleViewOptions(rendererConfig, pagePath, highlightKeywords),
     ([, rendererConfig, pagePath, highlightKeywords]) => generateSimpleViewOptions(rendererConfig, pagePath, highlightKeywords),
     {
     {
       fallbackData: isAllDataValid ? generateSimpleViewOptions(rendererConfig, pagePath, highlightKeywords) : undefined,
       fallbackData: isAllDataValid ? generateSimpleViewOptions(rendererConfig, pagePath, highlightKeywords) : undefined,
+      revalidateOnFocus: false,
+      revalidateOnReconnect: false,
     },
     },
   );
   );
 };
 };
@@ -146,13 +159,16 @@ export const useCustomSidebarOptions = (): SWRResponse<RendererOptions, Error> =
 
 
   const isAllDataValid = rendererConfig != null;
   const isAllDataValid = rendererConfig != null;
 
 
-  return useSWRImmutable(
+  return useSWR(
     isAllDataValid
     isAllDataValid
       ? ['customSidebarOptions', rendererConfig]
       ? ['customSidebarOptions', rendererConfig]
       : null,
       : null,
     ([, rendererConfig]) => generateSimpleViewOptions(rendererConfig, '/'),
     ([, rendererConfig]) => generateSimpleViewOptions(rendererConfig, '/'),
     {
     {
+      keepPreviousData: true,
       fallbackData: isAllDataValid ? generateSimpleViewOptions(rendererConfig, '/') : undefined,
       fallbackData: isAllDataValid ? generateSimpleViewOptions(rendererConfig, '/') : undefined,
+      revalidateOnFocus: false,
+      revalidateOnReconnect: false,
     },
     },
   );
   );
 };
 };

+ 9 - 9
packages/app/src/stores/ui.tsx

@@ -1,24 +1,24 @@
-import { RefObject, useCallback, useEffect } from 'react';
+import { type RefObject, useCallback, useEffect } from 'react';
 
 
 import {
 import {
-  isClient, isServer, pagePathUtils, Nullable, PageGrant,
+  isClient, isServer, pagePathUtils, type Nullable, PageGrant,
 } from '@growi/core';
 } from '@growi/core';
-import { withUtils, SWRResponseWithUtils } from '@growi/core/src/utils/with-utils';
+import { withUtils, type SWRResponseWithUtils } from '@growi/core/src/utils/with-utils';
 import { Breakpoint, addBreakpointListener, cleanupBreakpointListener } from '@growi/ui';
 import { Breakpoint, addBreakpointListener, cleanupBreakpointListener } from '@growi/ui';
-import { HtmlElementNode } from 'rehype-toc';
+import type { HtmlElementNode } from 'rehype-toc';
 import type SimpleBar from 'simplebar-react';
 import type SimpleBar from 'simplebar-react';
 import {
 import {
-  useSWRConfig, SWRResponse, Key,
+  useSWRConfig, type SWRResponse, type Key,
 } from 'swr';
 } from 'swr';
 import useSWRImmutable from 'swr/immutable';
 import useSWRImmutable from 'swr/immutable';
 
 
-import { IFocusable } from '~/client/interfaces/focusable';
+import type { IFocusable } from '~/client/interfaces/focusable';
 import { useUserUISettings } from '~/client/services/user-ui-settings';
 import { useUserUISettings } from '~/client/services/user-ui-settings';
 import { apiv3Get, apiv3Put } from '~/client/util/apiv3-client';
 import { apiv3Get, apiv3Put } from '~/client/util/apiv3-client';
-import { IPageGrantData } from '~/interfaces/page';
-import { ISidebarConfig } from '~/interfaces/sidebar-config';
+import type { IPageGrantData } from '~/interfaces/page';
+import type { ISidebarConfig } from '~/interfaces/sidebar-config';
 import { SidebarContentsType } from '~/interfaces/ui';
 import { SidebarContentsType } from '~/interfaces/ui';
-import { UpdateDescCountData } from '~/interfaces/websocket';
+import type { UpdateDescCountData } from '~/interfaces/websocket';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 import {
 import {