Explorar o código

implement share link page route and reorganize common props generation

Yuki Takei hai 7 meses
pai
achega
2b7c89c56e
Modificáronse 34 ficheiros con 634 adicións e 493 borrados
  1. 2 3
      apps/app/src/client/components/DescendantsPageList.tsx
  2. 2 2
      apps/app/src/client/components/DescendantsPageListModal.tsx
  3. 7 8
      apps/app/src/client/components/Navbar/GrowiContextualSubNavigation.tsx
  4. 2 2
      apps/app/src/client/components/Navbar/GrowiNavbarBottom.tsx
  5. 2 3
      apps/app/src/client/components/PageAccessoriesModal/PageAccessoriesModal.tsx
  6. 2 3
      apps/app/src/client/components/PageControls/PageControls.tsx
  7. 4 7
      apps/app/src/client/components/ReactMarkdownComponents/DrawioViewerWithEditButton.tsx
  8. 4 6
      apps/app/src/client/components/ReactMarkdownComponents/Header.tsx
  9. 4 6
      apps/app/src/client/components/ReactMarkdownComponents/TableWithEditButton.tsx
  10. 3 3
      apps/app/src/client/components/Sidebar/Sidebar.tsx
  11. 2 2
      apps/app/src/client/services/side-effects/drawio-modal-launcher-for-view.ts
  12. 2 2
      apps/app/src/client/services/side-effects/handsontable-modal-launcher-for-view.ts
  13. 3 3
      apps/app/src/components/PageView/PageView.tsx
  14. 10 9
      apps/app/src/components/ShareLinkPageView/ShareLinkPageView.tsx
  15. 8 4
      apps/app/src/pages/[[...path]]/index.page.tsx
  16. 8 0
      apps/app/src/pages/[[...path]]/types.ts
  17. 2 2
      apps/app/src/pages/[[...path]]/use-shallow-routing.ts
  18. 40 0
      apps/app/src/pages/common-props/commons.ts
  19. 1 1
      apps/app/src/pages/common-props/index.ts
  20. 1 1
      apps/app/src/pages/general-page/index.ts
  21. 6 40
      apps/app/src/pages/general-page/type-guards.ts
  22. 2 13
      apps/app/src/pages/general-page/types.ts
  23. 1 0
      apps/app/src/pages/share/[[...path]]/consts/index.ts
  24. 78 186
      apps/app/src/pages/share/[[...path]]/index.page.tsx
  25. 90 0
      apps/app/src/pages/share/[[...path]]/page-data-props.ts
  26. 145 0
      apps/app/src/pages/share/[[...path]]/server-side-props.ts
  27. 13 0
      apps/app/src/pages/share/[[...path]]/types.ts
  28. 22 12
      apps/app/src/states/page/hooks.ts
  29. 19 43
      apps/app/src/states/page/hydrate.ts
  30. 13 6
      apps/app/src/states/page/internal-atoms.ts
  31. 126 105
      apps/app/src/states/page/use-fetch-current-page.ts
  32. 0 12
      apps/app/src/stores-universal/context.tsx
  33. 2 2
      apps/app/src/stores/page.tsx
  34. 8 7
      apps/app/src/stores/ui.tsx

+ 2 - 3
apps/app/src/client/components/DescendantsPageList.tsx

@@ -11,8 +11,7 @@ import { useTranslation } from 'next-i18next';
 import { toastSuccess } from '~/client/util/toastr';
 import type { IPagingResult } from '~/interfaces/paging-result';
 import type { OnDeletedFunction, OnPutBackedFunction } from '~/interfaces/ui';
-import { useIsGuestUser, useIsReadOnlyUser } from '~/states/context';
-import { useIsSharedUser } from '~/stores-universal/context';
+import { useIsGuestUser, useIsReadOnlyUser, useIsSharedUser } from '~/states/context';
 import {
   mutatePageTree,
   useSWRxPageInfoForList, useSWRxPageList, mutateRecentlyUpdated,
@@ -134,7 +133,7 @@ export const DescendantsPageList = (props: DescendantsPageListProps): JSX.Elemen
 
   const [activePage, setActivePage] = useState(1);
 
-  const { data: isSharedUser } = useIsSharedUser();
+  const [isSharedUser] = useIsSharedUser();
 
   const { data: pagingResult, error, mutate } = useSWRxPageList(isSharedUser ? null : path, activePage, limit);
 

+ 2 - 2
apps/app/src/client/components/DescendantsPageListModal.tsx

@@ -10,7 +10,7 @@ import {
   Modal, ModalHeader, ModalBody,
 } from 'reactstrap';
 
-import { useIsSharedUser } from '~/stores-universal/context';
+import { useIsSharedUser } from '~/states/context';
 import { useDescendantsPageListModal } from '~/stores/modal';
 import { useIsDeviceLargerThanLg } from '~/stores/ui';
 
@@ -31,7 +31,7 @@ export const DescendantsPageListModal = (): JSX.Element => {
   const [activeTab, setActiveTab] = useState('pagelist');
   const [isWindowExpanded, setIsWindowExpanded] = useState(false);
 
-  const { data: isSharedUser } = useIsSharedUser();
+  const [isSharedUser] = useIsSharedUser();
 
   const { data: status, close } = useDescendantsPageListModal();
 

+ 7 - 8
apps/app/src/client/components/Navbar/GrowiContextualSubNavigation.tsx

@@ -24,14 +24,15 @@ import { GroundGlassBar } from '~/components/Navbar/GroundGlassBar';
 import { usePageBulkExportSelectModal } from '~/features/page-bulk-export/client/stores/modal';
 import type { OnDuplicatedFunction, OnRenamedFunction, OnDeletedFunction } from '~/interfaces/ui';
 import { useShouldExpandContent } from '~/services/layout/use-should-expand-content';
-import { useIsGuestUser, useIsReadOnlyUser } from '~/states/context';
+import { useIsGuestUser, useIsReadOnlyUser, useIsSharedUser } from '~/states/context';
 import { useCurrentPathname, useCurrentUser } from '~/states/global';
 import { useCurrentPageId, useFetchCurrentPage } from '~/states/page';
+import { useShareLinkId } from '~/states/page/hooks';
 import {
+  useDisableLinkSharing,
   useIsBulkExportPagesEnabled, useIsLocalAccountRegistrationEnabled, useIsUploadEnabled,
 } from '~/states/server-configurations';
 import { useEditorMode } from '~/states/ui/editor';
-import { useIsSharedUser, useShareLinkId } from '~/stores-universal/context';
 import {
   usePageAccessoriesModal, PageAccessoriesModalContents, type IPageForPageDuplicateModal,
   usePageDuplicateModal, usePageRenameModal, usePageDeleteModal, usePagePresentationModal,
@@ -80,7 +81,7 @@ const PageOperationMenuItems = (props: PageOperationMenuItemsProps): JSX.Element
 
   const [isGuestUser] = useIsGuestUser();
   const [isReadOnlyUser] = useIsReadOnlyUser();
-  const { data: isSharedUser } = useIsSharedUser();
+  const [isSharedUser] = useIsSharedUser();
   const [isBulkExportPagesEnabled] = useIsBulkExportPagesEnabled();
   const [isUploadEnabled] = useIsUploadEnabled();
 
@@ -244,7 +245,6 @@ const CreateTemplateMenuItems = (props: CreateTemplateMenuItemsProps): JSX.Eleme
 
 type GrowiContextualSubNavigationProps = {
   currentPage?: IPagePopulatedToShowRevision | null,
-  isLinkSharingDisabled?: boolean,
 };
 
 const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps): JSX.Element => {
@@ -255,7 +255,7 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
 
   const router = useRouter();
 
-  const { data: shareLinkId } = useShareLinkId();
+  const [shareLinkId] = useShareLinkId();
   const { fetchCurrentPage } = useFetchCurrentPage();
 
   const [currentPathname] = useCurrentPathname();
@@ -270,7 +270,8 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
   const [isGuestUser] = useIsGuestUser();
   const [isReadOnlyUser] = useIsReadOnlyUser();
   const [isLocalAccountRegistrationEnabled] = useIsLocalAccountRegistrationEnabled();
-  const { data: isSharedUser } = useIsSharedUser();
+  const [isLinkSharingDisabled] = useDisableLinkSharing();
+  const [isSharedUser] = useIsSharedUser();
 
   const shouldExpandContent = useShouldExpandContent(currentPage);
 
@@ -292,8 +293,6 @@ const GrowiContextualSubNavigation = (props: GrowiContextualSubNavigationProps):
 
   const [isPageTemplateModalShown, setIsPageTempleteModalShown] = useState(false);
 
-  const { isLinkSharingDisabled } = props;
-
   const duplicateItemClickedHandler = useCallback(async(page: IPageForPageDuplicateModal) => {
     const duplicatedHandler: OnDuplicatedFunction = (fromPath, toPath) => {
       router.push(toPath);

+ 2 - 2
apps/app/src/client/components/Navbar/GrowiNavbarBottom.tsx

@@ -2,9 +2,9 @@ import React, { useCallback, type JSX } from 'react';
 
 import { GroundGlassBar } from '~/components/Navbar/GroundGlassBar';
 import { useSearchModal } from '~/features/search/client/stores/search';
+import { useIsSearchPage } from '~/states/context';
 import { useCurrentPagePath } from '~/states/page';
 import { useDrawerOpened } from '~/states/ui/sidebar';
-import { useIsSearchPage } from '~/stores-universal/context';
 import { usePageCreateModal } from '~/stores/modal';
 
 import styles from './GrowiNavbarBottom.module.scss';
@@ -15,7 +15,7 @@ export const GrowiNavbarBottom = (): JSX.Element => {
   const [isDrawerOpened, setIsDrawerOpened] = useDrawerOpened();
   const { open: openCreateModal } = usePageCreateModal();
   const [currentPagePath] = useCurrentPagePath();
-  const { data: isSearchPage } = useIsSearchPage();
+  const [isSearchPage] = useIsSearchPage();
   const { open: openSearchModal } = useSearchModal();
 
   const searchButtonClickHandler = useCallback(() => {

+ 2 - 3
apps/app/src/client/components/PageAccessoriesModal/PageAccessoriesModal.tsx

@@ -6,9 +6,8 @@ import {
   Modal, ModalBody, ModalHeader,
 } from 'reactstrap';
 
-import { useIsGuestUser, useIsReadOnlyUser } from '~/states/context';
+import { useIsGuestUser, useIsReadOnlyUser, useIsSharedUser } from '~/states/context';
 import { useDisableLinkSharing } from '~/states/server-configurations';
-import { useIsSharedUser } from '~/stores-universal/context';
 import { usePageAccessoriesModal, PageAccessoriesModalContents } from '~/stores/modal';
 import { useIsDeviceLargerThanLg } from '~/stores/ui';
 
@@ -32,7 +31,7 @@ export const PageAccessoriesModal = (): JSX.Element => {
 
   const [isWindowExpanded, setIsWindowExpanded] = useState(false);
 
-  const { data: isSharedUser } = useIsSharedUser();
+  const [isSharedUser] = useIsSharedUser();
   const [isGuestUser] = useIsGuestUser();
   const [isReadOnlyUser] = useIsReadOnlyUser();
   const [isLinkSharingDisabled] = useDisableLinkSharing();

+ 2 - 3
apps/app/src/client/components/PageControls/PageControls.tsx

@@ -18,13 +18,12 @@ import {
 } from '~/client/services/page-operation';
 import { toastError } from '~/client/util/toastr';
 import OpenDefaultAiAssistantButton from '~/features/openai/client/components/AiAssistant/OpenDefaultAiAssistantButton';
-import { useIsGuestUser, useIsReadOnlyUser } from '~/states/context';
+import { useIsGuestUser, useIsReadOnlyUser, useIsSearchPage } from '~/states/context';
 import { useCurrentPagePath } from '~/states/page';
 import { useIsUsersHomepageDeletionEnabled } from '~/states/server-configurations';
 import {
   EditorMode, useEditorMode,
 } from '~/states/ui/editor';
-import { useIsSearchPage } from '~/stores-universal/context';
 import { useTagEditModal, type IPageForPageDuplicateModal } from '~/stores/modal';
 import {
   useIsDeviceLargerThanMd, usePageControlsX,
@@ -137,7 +136,7 @@ const PageControlsSubstance = (props: PageControlsSubstanceProps): JSX.Element =
   const [isReadOnlyUser] = useIsReadOnlyUser();
   const { editorMode } = useEditorMode();
   const { data: isDeviceLargerThanMd } = useIsDeviceLargerThanMd();
-  const { data: isSearchPage } = useIsSearchPage();
+  const [isSearchPage] = useIsSearchPage();
   const [isUsersHomepageDeletionEnabled] = useIsUsersHomepageDeletionEnabled();
   const [currentPagePath] = useCurrentPagePath();
 

+ 4 - 7
apps/app/src/client/components/ReactMarkdownComponents/DrawioViewerWithEditButton.tsx

@@ -9,17 +9,14 @@ import {
 } from '@growi/remark-drawio';
 import { useTranslation } from 'next-i18next';
 
+import { useIsGuestUser, useIsReadOnlyUser, useIsSharedUser } from '~/states/context';
 import { useIsRevisionOutdated } from '~/states/page';
-import {
-  useIsSharedUser, useShareLinkId,
-} from '~/stores-universal/context';
+import { useShareLinkId } from '~/states/page/hooks';
 import { useCurrentPageYjsData } from '~/stores/yjs';
 
 import '@growi/remark-drawio/dist/style.css';
 import styles from './DrawioViewerWithEditButton.module.scss';
 
-import { useIsGuestUser, useIsReadOnlyUser } from '~/states/context';
-
 
 declare global {
   // eslint-disable-next-line vars-on-top, no-var
@@ -34,8 +31,8 @@ export const DrawioViewerWithEditButton = React.memo((props: DrawioViewerProps):
 
   const [isGuestUser] = useIsGuestUser();
   const [isReadOnlyUser] = useIsReadOnlyUser();
-  const { data: isSharedUser } = useIsSharedUser();
-  const { data: shareLinkId } = useShareLinkId();
+  const [isSharedUser] = useIsSharedUser();
+  const [shareLinkId] = useShareLinkId();
   const [isRevisionOutdated] = useIsRevisionOutdated();
   const { data: currentPageYjsData } = useCurrentPageYjsData();
 

+ 4 - 6
apps/app/src/client/components/ReactMarkdownComponents/Header.tsx

@@ -8,10 +8,8 @@ import type { Element } from 'hast';
 import { useRouter } from 'next/router';
 
 import { NextLink } from '~/components/ReactMarkdownComponents/NextLink';
-import { useIsGuestUser, useIsReadOnlyUser } from '~/states/context';
-import {
-  useIsSharedUser, useShareLinkId,
-} from '~/stores-universal/context';
+import { useIsGuestUser, useIsReadOnlyUser, useIsSharedUser } from '~/states/context';
+import { useShareLinkId } from '~/states/page/hooks';
 import { useCurrentPageYjsData } from '~/stores/yjs';
 import loggerFactory from '~/utils/logger';
 
@@ -67,8 +65,8 @@ export const Header = (props: HeaderProps): JSX.Element => {
 
   const [isGuestUser] = useIsGuestUser();
   const [isReadOnlyUser] = useIsReadOnlyUser();
-  const { data: isSharedUser } = useIsSharedUser();
-  const { data: shareLinkId } = useShareLinkId();
+  const [isSharedUser] = useIsSharedUser();
+  const [shareLinkId] = useShareLinkId();
   const { data: currentPageYjsData, isLoading: isLoadingCurrentPageYjsData } = useCurrentPageYjsData();
 
   const router = useRouter();

+ 4 - 6
apps/app/src/client/components/ReactMarkdownComponents/TableWithEditButton.tsx

@@ -4,11 +4,9 @@ import type EventEmitter from 'events';
 
 import type { Element } from 'hast';
 
-import { useIsGuestUser, useIsReadOnlyUser } from '~/states/context';
+import { useIsGuestUser, useIsReadOnlyUser, useIsSharedUser } from '~/states/context';
 import { useIsRevisionOutdated } from '~/states/page';
-import {
-  useIsSharedUser, useShareLinkId,
-} from '~/stores-universal/context';
+import { useShareLinkId } from '~/states/page/hooks';
 import { useCurrentPageYjsData } from '~/stores/yjs';
 
 import styles from './TableWithEditButton.module.scss';
@@ -29,8 +27,8 @@ const TableWithEditButtonNoMemorized = (props: TableWithEditButtonProps): JSX.El
 
   const [isGuestUser] = useIsGuestUser();
   const [isReadOnlyUser] = useIsReadOnlyUser();
-  const { data: isSharedUser } = useIsSharedUser();
-  const { data: shareLinkId } = useShareLinkId();
+  const [isSharedUser] = useIsSharedUser();
+  const [shareLinkId] = useShareLinkId();
   const [isRevisionOutdated] = useIsRevisionOutdated();
   const { data: currentPageYjsData } = useCurrentPageYjsData();
 

+ 3 - 3
apps/app/src/client/components/Sidebar/Sidebar.tsx

@@ -8,7 +8,9 @@ import SimpleBar from 'simplebar-react';
 import { useIsomorphicLayoutEffect } from 'usehooks-ts';
 
 import { SidebarMode } from '~/interfaces/ui';
+import { useIsSearchPage } from '~/states/context';
 import { useDeviceLargerThanXl } from '~/states/ui/device';
+import { EditorMode, useEditorMode } from '~/states/ui/editor';
 import {
   useDrawerOpened,
   usePreferCollapsedMode,
@@ -16,8 +18,6 @@ import {
   useCollapsedContentsOpened,
   useCurrentProductNavWidth,
 } from '~/states/ui/sidebar';
-import { useIsSearchPage } from '~/stores-universal/context';
-import { EditorMode, useEditorMode } from '~/states/ui/editor';
 import {
   useSidebarScrollerRef,
   useIsDeviceLargerThanMd,
@@ -232,7 +232,7 @@ export const Sidebar = (): JSX.Element => {
     isDrawerMode, isCollapsedMode, isDockMode,
   } = useSidebarMode();
 
-  const { data: isSearchPage } = useIsSearchPage();
+  const [isSearchPage] = useIsSearchPage();
   const { editorMode } = useEditorMode();
   const { data: isMdSize } = useIsDeviceLargerThanMd();
   const [isXlSize] = useDeviceLargerThanXl();

+ 2 - 2
apps/app/src/client/services/side-effects/drawio-modal-launcher-for-view.ts

@@ -8,7 +8,7 @@ import type { DrawioEditByViewerProps } from '@growi/remark-drawio';
 import { replaceDrawioInMarkdown } from '~/client/components/Page/markdown-drawio-util-for-view';
 import { extractRemoteRevisionDataFromErrorObj, useUpdatePage } from '~/client/services/update-page';
 import { useCurrentPageData } from '~/states/page';
-import { useShareLinkId } from '~/stores-universal/context';
+import { useShareLinkId } from '~/states/page/hooks';
 import { useConflictDiffModal, useDrawioModal } from '~/stores/modal';
 import { type RemoteRevisionData, useSetRemoteLatestPageData } from '~/stores/remote-latest-page';
 import loggerFactory from '~/utils/logger';
@@ -28,7 +28,7 @@ export const useDrawioModalLauncherForView = (opts?: {
   onSaveError?: (error: any) => void,
 }): void => {
 
-  const { data: shareLinkId } = useShareLinkId();
+  const [shareLinkId] = useShareLinkId();
 
   const [currentPage] = useCurrentPageData();
 

+ 2 - 2
apps/app/src/client/services/side-effects/handsontable-modal-launcher-for-view.ts

@@ -8,7 +8,7 @@ import type { MarkdownTable } from '@growi/editor';
 import { getMarkdownTableFromLine, replaceMarkdownTableInMarkdown } from '~/client/components/Page/markdown-table-util-for-view';
 import { extractRemoteRevisionDataFromErrorObj, useUpdatePage } from '~/client/services/update-page';
 import { useCurrentPageData } from '~/states/page';
-import { useShareLinkId } from '~/stores-universal/context';
+import { useShareLinkId } from '~/states/page/hooks';
 import { useHandsontableModal, useConflictDiffModal } from '~/stores/modal';
 import { type RemoteRevisionData, useSetRemoteLatestPageData } from '~/stores/remote-latest-page';
 import loggerFactory from '~/utils/logger';
@@ -28,7 +28,7 @@ export const useHandsontableModalLauncherForView = (opts?: {
   onSaveError?: (error: any) => void,
 }): void => {
 
-  const { data: shareLinkId } = useShareLinkId();
+  const [shareLinkId] = useShareLinkId();
 
   const [currentPage] = useCurrentPageData();
 

+ 3 - 3
apps/app/src/components/PageView/PageView.tsx

@@ -1,5 +1,5 @@
-import React, {
-  useEffect, useMemo, useRef, type JSX,
+import {
+  useEffect, useMemo, useRef, memo, type JSX,
 } from 'react';
 
 import { isUsersHomepage } from '@growi/core/dist/utils/page-path-utils';
@@ -40,7 +40,7 @@ type Props = {
   className?: string,
 }
 
-export const PageView = React.memo((props: Props): JSX.Element => {
+export const PageView = memo((props: Props): JSX.Element => {
   const renderStartTime = performance.now();
 
   const commentsContainerRef = useRef<HTMLDivElement>(null);

+ 10 - 9
apps/app/src/components/ShareLinkPageView/ShareLinkPageView.tsx

@@ -1,6 +1,5 @@
-import { useMemo, type JSX } from 'react';
+import { useMemo, memo, type JSX } from 'react';
 
-import type { IPagePopulatedToShowRevision } from '@growi/core';
 import { useSlidesByFrontmatter } from '@growi/presentation/dist/services';
 import dynamic from 'next/dynamic';
 
@@ -9,7 +8,7 @@ import type { RendererConfig } from '~/interfaces/services/renderer';
 import type { IShareLinkHasId } from '~/interfaces/share-link';
 import { useShouldExpandContent } from '~/services/layout/use-should-expand-content';
 import { generateSSRViewOptions } from '~/services/renderer/renderer';
-import { usePageNotFound } from '~/states/page';
+import { useCurrentPageData, usePageNotFound } from '~/states/page';
 import { useViewOptions } from '~/stores/renderer';
 import loggerFactory from '~/utils/logger';
 
@@ -20,7 +19,7 @@ import RevisionRenderer from '../PageView/RevisionRenderer';
 import ShareLinkAlert from './ShareLinkAlert';
 
 
-const logger = loggerFactory('growi:Page');
+const logger = loggerFactory('growi:components:ShareLinkPageView');
 
 
 const PageSideContents = dynamic(() => import('~/client/components/PageSideContents').then(mod => mod.PageSideContents), { ssr: false });
@@ -30,21 +29,22 @@ const SlideRenderer = dynamic(() => import('~/client/components/Page/SlideRender
 type Props = {
   pagePath: string,
   rendererConfig: RendererConfig,
-  page?: IPagePopulatedToShowRevision,
   shareLink?: IShareLinkHasId,
-  isExpired: boolean,
+  isExpired?: boolean,
   disableLinkSharing: boolean,
 }
 
-export const ShareLinkPageView = (props: Props): JSX.Element => {
+export const ShareLinkPageView = memo((props: Props): JSX.Element => {
   const {
     pagePath, rendererConfig,
-    page, shareLink,
+    shareLink,
     isExpired, disableLinkSharing,
   } = props;
 
   const [isNotFoundMeta] = usePageNotFound();
 
+  const [page] = useCurrentPageData();
+
   const { data: viewOptions } = useViewOptions();
 
   const shouldExpandContent = useShouldExpandContent(page);
@@ -128,4 +128,5 @@ export const ShareLinkPageView = (props: Props): JSX.Element => {
       ) }
     </PageViewLayout>
   );
-};
+});
+ShareLinkPageView.displayName = 'ShareLinkPageView';

+ 8 - 4
apps/app/src/pages/[[...path]]/index.page.tsx

@@ -32,9 +32,8 @@ import { useHydrateSidebarAtoms } from '~/states/ui/sidebar/hydrate';
 import { useSWRMUTxCurrentPageYjsData } from '~/stores/yjs';
 
 import type { NextPageWithLayout } from '../_app.page';
-import type {
-  Props, InitialProps, SameRouteEachProps,
-} from '../general-page';
+import type { CommonEachProps, UserUISettingsProps } from '../common-props';
+import type { InitialProps, SidebarConfigProps } from '../general-page';
 import { useInitialCSRFetch, useSameRouteNavigation } from '../general-page';
 import { registerPageToShowRevisionWithMeta } from '../general-page/superjson';
 import { NextjsRoutingType, detectNextjsRoutingType } from '../utils/nextjs-routing-utils';
@@ -42,6 +41,7 @@ import { useCustomTitleForPage } from '../utils/page-title-customization';
 
 import { NEXT_JS_ROUTING_PAGE } from './consts';
 import { getServerSidePropsForInitial, getServerSidePropsForSameRoute } from './server-side-props';
+import type { EachProps } from './types';
 import { useShallowRouting } from './use-shallow-routing';
 
 // call superjson custom register
@@ -74,10 +74,14 @@ const ConflictDiffModal = dynamic(() => import('~/client/components/PageEditor/C
 const EditablePageEffects = dynamic(() => import('~/client/components/Page/EditablePageEffects').then(mod => mod.EditablePageEffects), { ssr: false });
 
 
-const isInitialProps = (props: Props): props is (InitialProps & SameRouteEachProps) => {
+type SameRouteEachProps = CommonEachProps & EachProps;
+type Props = SameRouteEachProps | (SameRouteEachProps & InitialProps & UserUISettingsProps & SidebarConfigProps);
+
+const isInitialProps = (props: Props): props is (UserUISettingsProps & SidebarConfigProps & InitialProps & SameRouteEachProps) => {
   return 'isNextjsRoutingTypeInitial' in props && props.isNextjsRoutingTypeInitial;
 };
 
+
 const Page: NextPageWithLayout<Props> = (props: Props) => {
 
   // register global EventEmitter

+ 8 - 0
apps/app/src/pages/[[...path]]/types.ts

@@ -0,0 +1,8 @@
+export type EachProps = {
+  redirectFrom?: string;
+
+  isIdenticalPathPage: boolean,
+
+  templateTagData?: string[],
+  templateBodyData?: string,
+}

+ 2 - 2
apps/app/src/pages/[[...path]]/use-shallow-routing.ts

@@ -3,13 +3,13 @@ import { useEffect, useRef } from 'react';
 import { isClient } from '@growi/core/dist/utils';
 import { useRouter } from 'next/router';
 
-import type { Props } from '../general-page';
+import type { CommonEachProps } from '../common-props';
 
 /**
  * Custom hook for syncing pathname by Shallow Routing
  * Optimized to minimize unnecessary router operations and re-renders
  */
-export const useShallowRouting = (props: Props): void => {
+export const useShallowRouting = (props: CommonEachProps): void => {
   const router = useRouter();
   const lastPathnameRef = useRef<string>();
 

+ 40 - 0
apps/app/src/pages/common-props/commons.ts

@@ -3,6 +3,10 @@ import type { GetServerSideProps, GetServerSidePropsContext } from 'next';
 
 import type { CrowiRequest } from '~/interfaces/crowi-request';
 import { getGrowiVersion } from '~/utils/growi-version';
+import loggerFactory from '~/utils/logger';
+
+const logger = loggerFactory('growi:pages:common-props:commons');
+
 
 export type CommonInitialProps = {
   isNextjsRoutingTypeInitial: true,
@@ -51,6 +55,42 @@ export type CommonEachProps = {
   redirectDestination?: string | null,
 };
 
+/**
+ * Type guard for SameRouteEachProps validation
+ * Lightweight validation for same-route navigation
+ */
+export function isValidCommonEachRouteProps(props: unknown): props is CommonEachProps {
+  if (typeof props !== 'object' || props === null) {
+    logger.warn('isValidSameRouteProps: props is not an object or is null');
+    return false;
+  }
+
+  const p = props as Record<string, unknown>;
+
+  // Essential properties validation
+  if (typeof p.nextjsRoutingPage !== 'string' && p.nextjsRoutingPage !== null) {
+    logger.warn('isValidSameRouteProps: nextjsRoutingPage is not a string or null', { nextjsRoutingPage: p.nextjsRoutingPage });
+    return false;
+  }
+  if (typeof p.currentPathname !== 'string') {
+    logger.warn('isValidSameRouteProps: currentPathname is not a string', { currentPathname: p.currentPathname });
+    return false;
+  }
+  if (typeof p.csrfToken !== 'string') {
+    logger.warn('isValidSameRouteProps: csrfToken is not a string', { csrfToken: p.csrfToken });
+    return false;
+  }
+  if (typeof p.isMaintenanceMode !== 'boolean') {
+    logger.warn('isValidSameRouteProps: isMaintenanceMode is not a boolean', { isMaintenanceMode: p.isMaintenanceMode });
+    return false;
+  }
+  if (typeof p.isIdenticalPathPage !== 'boolean') {
+    logger.warn('isValidSameRouteProps: isIdenticalPathPage is not a boolean', { isIdenticalPathPage: p.isIdenticalPathPage });
+    return false;
+  }
+
+  return true;
+}
 
 export const getServerSideCommonEachProps: GetServerSideProps<Omit<CommonEachProps, 'nextjsRoutingPage'>> = async(context: GetServerSidePropsContext) => {
   const req = context.req as CrowiRequest;

+ 1 - 1
apps/app/src/pages/common-props/index.ts

@@ -1,6 +1,6 @@
 export {
   type CommonInitialProps, getServerSideCommonInitialProps,
-  type CommonEachProps, getServerSideCommonEachProps,
+  type CommonEachProps, getServerSideCommonEachProps, isValidCommonEachRouteProps,
 } from './commons';
 export { getServerSideI18nProps } from './i18n';
 export { getServerSideUserUISettingsProps, type UserUISettingsProps } from './user-ui-settings';

+ 1 - 1
apps/app/src/pages/general-page/index.ts

@@ -1,6 +1,6 @@
 export { getServerSideSidebarConfigProps, getServerSideRendererConfigProps, getServerSideConfigurationProps } from './configuration-props';
 export { getActivityAction } from './get-activity-action';
 export type * from './types';
-export { isValidInitialAndSameRouteProps, isValidSameRouteProps } from './type-guards';
+export { isValidInitialAndSameRouteProps } from './type-guards';
 export { useInitialCSRFetch } from './use-initial-skip-ssr-fetch';
 export { useSameRouteNavigation } from './use-same-route-navigation';

+ 6 - 40
apps/app/src/pages/general-page/type-guards.ts

@@ -1,53 +1,19 @@
 import loggerFactory from '~/utils/logger';
 
-import type { InitialProps, SameRouteEachProps } from '.';
+import type { CommonEachProps } from '../common-props';
+import { isValidCommonEachRouteProps } from '../common-props';
 
-const logger = loggerFactory('growi:pages:general-page:type-guards');
-
-/**
- * Type guard for SameRouteEachProps validation
- * Lightweight validation for same-route navigation
- */
-export function isValidSameRouteProps(props: unknown): props is SameRouteEachProps {
-  if (typeof props !== 'object' || props === null) {
-    logger.warn('isValidSameRouteProps: props is not an object or is null');
-    return false;
-  }
-
-  const p = props as Record<string, unknown>;
-
-  // Essential properties validation
-  if (typeof p.nextjsRoutingPage !== 'string' && p.nextjsRoutingPage !== null) {
-    logger.warn('isValidSameRouteProps: nextjsRoutingPage is not a string or null', { nextjsRoutingPage: p.nextjsRoutingPage });
-    return false;
-  }
-  if (typeof p.currentPathname !== 'string') {
-    logger.warn('isValidSameRouteProps: currentPathname is not a string', { currentPathname: p.currentPathname });
-    return false;
-  }
-  if (typeof p.csrfToken !== 'string') {
-    logger.warn('isValidSameRouteProps: csrfToken is not a string', { csrfToken: p.csrfToken });
-    return false;
-  }
-  if (typeof p.isMaintenanceMode !== 'boolean') {
-    logger.warn('isValidSameRouteProps: isMaintenanceMode is not a boolean', { isMaintenanceMode: p.isMaintenanceMode });
-    return false;
-  }
-  if (typeof p.isIdenticalPathPage !== 'boolean') {
-    logger.warn('isValidSameRouteProps: isIdenticalPathPage is not a boolean', { isIdenticalPathPage: p.isIdenticalPathPage });
-    return false;
-  }
+import type { InitialProps } from './types';
 
-  return true;
-}
+const logger = loggerFactory('growi:pages:general-page:type-guards');
 
 /**
  * Type guard for InitialProps & SameRouteEachProps validation
  * First validates SameRouteEachProps, then checks InitialProps-specific properties
  */
-export function isValidInitialAndSameRouteProps(props: unknown): props is InitialProps & SameRouteEachProps {
+export function isValidInitialAndSameRouteProps(props: unknown): props is InitialProps & CommonEachProps {
   // First, validate SameRouteEachProps
-  if (!isValidSameRouteProps(props)) {
+  if (!isValidCommonEachRouteProps(props)) {
     logger.warn('isValidInitialAndSameRouteProps: SameRouteEachProps validation failed');
     return false;
   }

+ 2 - 13
apps/app/src/pages/general-page/types.ts

@@ -7,7 +7,7 @@ import type { ISidebarConfig } from '~/interfaces/sidebar-config';
 import type { PageDocument } from '~/server/models/page';
 import type { ServerConfigurationHyderateArgs } from '~/states/server-configurations/hydrate';
 
-import type { CommonEachProps, CommonInitialProps, UserUISettingsProps } from '../common-props';
+import type { CommonInitialProps } from '../common-props';
 
 export type IPageToShowRevisionWithMeta = IDataWithMeta<IPagePopulatedToShowRevision & PageDocument, IPageInfo>;
 
@@ -23,7 +23,7 @@ export type ServerConfigurationProps = {
   serverConfig: ServerConfigurationHyderateArgs,
 }
 
-export type InitialProps = CommonInitialProps & UserUISettingsProps & SidebarConfigProps & RendererConfigProps & ServerConfigurationProps & {
+export type InitialProps = CommonInitialProps & RendererConfigProps & ServerConfigurationProps & {
   pageWithMeta: IPageToShowRevisionWithMeta | null,
   skipSSR?: boolean,
 
@@ -32,14 +32,3 @@ export type InitialProps = CommonInitialProps & UserUISettingsProps & SidebarCon
   isForbidden: boolean,
   isNotCreatable: boolean,
 }
-
-export type SameRouteEachProps = CommonEachProps & {
-  redirectFrom?: string;
-
-  isIdenticalPathPage: boolean,
-
-  templateTagData?: string[],
-  templateBodyData?: string,
-}
-
-export type Props = SameRouteEachProps | (InitialProps & SameRouteEachProps);

+ 1 - 0
apps/app/src/pages/share/[[...path]]/consts/index.ts

@@ -0,0 +1 @@
+export const NEXT_JS_ROUTING_PAGE = 'share/[[...path]]';

+ 78 - 186
apps/app/src/pages/share/[[...path]]/index.page.tsx

@@ -1,129 +1,88 @@
-import React, { useEffect, type JSX } from 'react';
+import type { ReactNode, JSX } from 'react';
+import React, { useEffect } from 'react';
 
-import { type IPagePopulatedToShowRevision, getIdForRef } from '@growi/core';
 import type {
   GetServerSideProps, GetServerSidePropsContext,
 } from 'next';
-import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
 import dynamic from 'next/dynamic';
 import Head from 'next/head';
-import superjson from 'superjson';
 
 import { ShareLinkLayout } from '~/components/Layout/ShareLinkLayout';
 import { DrawioViewerScript } from '~/components/Script/DrawioViewerScript';
 import { ShareLinkPageView } from '~/components/ShareLinkPageView';
 import type { SupportedActionType } from '~/interfaces/activity';
 import { SupportedAction } from '~/interfaces/activity';
-import type { CrowiRequest } from '~/interfaces/crowi-request';
-import { RegistrationMode } from '~/interfaces/registration-mode';
-import type { RendererConfig } from '~/interfaces/services/renderer';
-import type { IShareLinkHasId } from '~/interfaces/share-link';
-import type { PageDocument, PageModel } from '~/server/models/page';
-import ShareLink from '~/server/models/share-link';
-import { useHydrateSharedPageAtoms } from '~/states/hydrate/page';
-import { useCurrentPageData, useFetchCurrentPage } from '~/states/page';
+import type { CommonEachProps } from '~/pages/common-props';
+import { NextjsRoutingType, detectNextjsRoutingType } from '~/pages/utils/nextjs-routing-utils';
+import { useCustomTitleForPage } from '~/pages/utils/page-title-customization';
+import { useIsSearchPage, useIsSharedUser } from '~/states/context';
 import {
-  useCurrentUser, useRendererConfig, useIsSearchPage, useCurrentPathname,
-  useShareLinkId, useIsSearchServiceConfigured, useIsSearchServiceReachable, useIsSearchScopeChildrenAsDefault, useIsContainerFluid, useIsEnabledMarp,
-  useIsLocalAccountRegistrationEnabled, useShowPageSideAuthors,
-} from '~/stores-universal/context';
+  useCurrentPageData, useCurrentPagePath,
+} from '~/states/page';
+import { useHydratePageAtoms } from '~/states/page/hydrate';
+import { useDisableLinkSharing, useRendererConfig } from '~/states/server-configurations';
+import { useHydrateServerConfigurationAtoms } from '~/states/server-configurations/hydrate';
 import loggerFactory from '~/utils/logger';
 
 import type { NextPageWithLayout } from '../../_app.page';
-import type { CommonProps } from '../../common-props';
-import {
-  getServerSideCommonProps, generateCustomTitleForPage, getNextI18NextConfig, skipSSR, addActivity,
-} from '../../common-props';
+import type { InitialProps } from '../../general-page';
+import { useInitialCSRFetch } from '../../general-page';
+import { registerPageToShowRevisionWithMeta } from '../../general-page/superjson';
 
+import { NEXT_JS_ROUTING_PAGE } from './consts';
+import { getServerSidePropsForInitial, getServerSidePropsForSameRoute } from './server-side-props';
+import type { ShareLinkPageProps } from './types';
 
-const GrowiContextualSubNavigationSubstance = dynamic(() => import('~/client/components/Navbar/GrowiContextualSubNavigation'), { ssr: false });
+// call superjson custom register
+registerPageToShowRevisionWithMeta();
 
 
-const logger = loggerFactory('growi:next-page:share');
+const GrowiContextualSubNavigation = dynamic(() => import('~/client/components/Navbar/GrowiContextualSubNavigation'), { ssr: false });
 
-type Props = CommonProps & {
-  shareLinkRelatedPage?: IShareLinkRelatedPage,
-  shareLink?: IShareLinkHasId,
-  isNotFound: boolean,
-  isExpired: boolean,
-  disableLinkSharing: boolean,
-  isSearchServiceConfigured: boolean,
-  isSearchServiceReachable: boolean,
-  isSearchScopeChildrenAsDefault: boolean,
-  showPageSideAuthors: boolean,
-  isEnabledMarp: boolean,
-  isLocalAccountRegistrationEnabled: boolean,
-  drawioUri: string | null,
-  rendererConfig: RendererConfig,
-  skipSSR: boolean,
-  ssrMaxRevisionBodyLength: number,
-};
 
-type IShareLinkRelatedPage = IPagePopulatedToShowRevision & PageDocument;
-
-superjson.registerCustom<IShareLinkRelatedPage, string>(
-  {
-    isApplicable: (v): v is IShareLinkRelatedPage => {
-      return v != null
-        && v.toObject != null;
-    },
-    serialize: (v) => { return superjson.stringify(v.toObject()) },
-    deserialize: (v) => { return superjson.parse(v) },
-  },
-  'IShareLinkRelatedPageTransformer',
-);
-
-// GrowiContextualSubNavigation for shared page
-// get page info from props not to send request 'GET /page' from client
-type GrowiContextualSubNavigationForSharedPageProps = {
-  page?: IPagePopulatedToShowRevision,
-  isLinkSharingDisabled: boolean,
-}
+const logger = loggerFactory('growi:next-page:share');
 
-const GrowiContextualSubNavigationForSharedPage = (props: GrowiContextualSubNavigationForSharedPageProps): JSX.Element => {
-  const { page, isLinkSharingDisabled } = props;
+type Props = ShareLinkPageProps &
+  (CommonEachProps | (CommonEachProps & InitialProps));
 
-  return (
-    <GrowiContextualSubNavigationSubstance currentPage={page} isLinkSharingDisabled={isLinkSharingDisabled} />
-  );
+const isInitialProps = (props: Props): props is (ShareLinkPageProps & InitialProps & CommonEachProps) => {
+  return 'isNextjsRoutingTypeInitial' in props && props.isNextjsRoutingTypeInitial;
 };
 
 const SharedPage: NextPageWithLayout<Props> = (props: Props) => {
-  useHydrateSharedPageAtoms({
-    pageId: props.shareLinkRelatedPage?._id,
-    isNotFound: props.isNotFound,
+
+  const { shareLink, isExpired } = props;
+
+  // Initialize Jotai atoms with initial data - must be called unconditionally
+  const pageData = isInitialProps(props) ? props.pageWithMeta?.data : undefined;
+  useHydratePageAtoms(pageData, {
+    shareLinkId: props.shareLink?._id,
   });
 
   const [currentPage] = useCurrentPageData();
-  useCurrentPathname(props.shareLink?.relatedPage.path);
-  useIsSearchPage(false);
-  useShareLinkId(props.shareLink?._id);
-  useCurrentUser(props.currentUser);
-  useRendererConfig(props.rendererConfig);
-  useIsSearchServiceConfigured(props.isSearchServiceConfigured);
-  useIsSearchServiceReachable(props.isSearchServiceReachable);
-  useIsSearchScopeChildrenAsDefault(props.isSearchScopeChildrenAsDefault);
-  useIsEnabledMarp(props.rendererConfig.isEnabledMarp);
-  useIsLocalAccountRegistrationEnabled(props.isLocalAccountRegistrationEnabled);
-  useShowPageSideAuthors(props.showPageSideAuthors);
-  useIsContainerFluid(props.isContainerFluid);
-
-  const { fetchCurrentPage } = useFetchCurrentPage();
+  const [currentPagePath] = useCurrentPagePath();
+  const [rendererConfig] = useRendererConfig();
+  const [, setIsSharedUser] = useIsSharedUser();
+  const [, setIsSearchPage] = useIsSearchPage();
+  const [isLinkSharingDisabled] = useDisableLinkSharing();
 
-  useEffect(() => {
-    if (!props.skipSSR) {
-      return;
-    }
+  // Use custom hooks for navigation and routing
+  // useSameRouteNavigation();
 
-    if (props.shareLink?.relatedPage._id != null && !props.isNotFound) {
-      fetchCurrentPage();
-    }
-  }, [fetchCurrentPage, props.isNotFound, props.shareLink?.relatedPage._id, props.skipSSR]);
+  // If initial props and skipSSR, fetch page data on client-side
+  useInitialCSRFetch(isInitialProps(props) && props.skipSSR);
 
+  // Initialize atom values
+  useEffect(() => {
+    setIsSharedUser(true);
+    setIsSearchPage(false);
+  }, [setIsSharedUser, setIsSearchPage]);
 
-  const pagePath = props.shareLinkRelatedPage?.path ?? '';
+  // If the data on the page changes without router.push, pageWithMeta remains old because getServerSideProps() is not executed
+  // So preferentially take page data from useSWRxCurrentPage
+  const pagePath = currentPagePath ?? props.currentPathname;
 
-  const title = generateCustomTitleForPage(props, pagePath);
+  const title = useCustomTitleForPage(pagePath);
 
   return (
     <>
@@ -133,15 +92,14 @@ const SharedPage: NextPageWithLayout<Props> = (props: Props) => {
 
       <div className="dynamic-layout-root justify-content-between">
 
-        <GrowiContextualSubNavigationForSharedPage page={currentPage ?? props.shareLinkRelatedPage} isLinkSharingDisabled={props.disableLinkSharing} />
+        <GrowiContextualSubNavigation currentPage={currentPage} />
 
         <ShareLinkPageView
           pagePath={pagePath}
-          rendererConfig={props.rendererConfig}
-          page={currentPage ?? props.shareLinkRelatedPage}
-          shareLink={props.shareLink}
-          isExpired={props.isExpired}
-          disableLinkSharing={props.disableLinkSharing}
+          rendererConfig={rendererConfig}
+          shareLink={shareLink}
+          isExpired={isExpired}
+          disableLinkSharing={isLinkSharingDisabled}
         />
 
       </div>
@@ -149,62 +107,29 @@ const SharedPage: NextPageWithLayout<Props> = (props: Props) => {
   );
 };
 
+type LayoutProps = Props & {
+  children?: ReactNode
+}
+
+const Layout = ({ children, ...props }: LayoutProps): JSX.Element => {
+  // Hydrate sidebar atoms with server-side data - must be called unconditionally
+  const initialProps = isInitialProps(props) ? props : undefined;
+  useHydrateServerConfigurationAtoms(initialProps?.serverConfig, initialProps?.rendererConfig);
+
+  return <ShareLinkLayout>{children}</ShareLinkLayout>;
+};
+
 SharedPage.getLayout = function getLayout(page) {
   return (
     <>
       <DrawioViewerScript drawioUri={page.props.rendererConfig.drawioUri} />
-      <ShareLinkLayout>{page}</ShareLinkLayout>
+      <Layout {...page.props}>
+        {page}
+      </Layout>
     </>
   );
 };
 
-function injectServerConfigurations(context: GetServerSidePropsContext, props: Props): void {
-  const req: CrowiRequest = context.req as CrowiRequest;
-  const { crowi } = req;
-  const { configManager, searchService } = crowi;
-
-  props.disableLinkSharing = configManager.getConfig('security:disableLinkSharing');
-
-  props.isSearchServiceConfigured = searchService.isConfigured;
-  props.isSearchServiceReachable = searchService.isReachable;
-  props.isSearchScopeChildrenAsDefault = configManager.getConfig('customize:isSearchScopeChildrenAsDefault');
-
-  props.drawioUri = configManager.getConfig('app:drawioUri');
-
-  props.showPageSideAuthors = configManager.getConfig('customize:showPageSideAuthors');
-
-  props.isLocalAccountRegistrationEnabled = crowi.passportService.isLocalStrategySetup
-    && configManager.getConfig('security:registrationMode') !== RegistrationMode.CLOSED;
-
-  props.rendererConfig = {
-    isSharedPage: true,
-    isEnabledLinebreaks: configManager.getConfig('markdown:isEnabledLinebreaks'),
-    isEnabledLinebreaksInComments: configManager.getConfig('markdown:isEnabledLinebreaksInComments'),
-    isEnabledMarp: configManager.getConfig('customize:isEnabledMarp'),
-    adminPreferredIndentSize: configManager.getConfig('markdown:adminPreferredIndentSize'),
-    isIndentSizeForced: configManager.getConfig('markdown:isIndentSizeForced'),
-
-    drawioUri: configManager.getConfig('app:drawioUri'),
-    plantumlUri: configManager.getConfig('app:plantumlUri'),
-
-    // XSS Options
-    isEnabledXssPrevention: configManager.getConfig('markdown:rehypeSanitize:isEnabledPrevention'),
-    sanitizeType: configManager.getConfig('markdown:rehypeSanitize:option'),
-    customTagWhitelist: crowi.configManager.getConfig('markdown:rehypeSanitize:tagNames'),
-    customAttrWhitelist: configManager.getConfig('markdown:rehypeSanitize:attributes') != null
-      ? JSON.parse(configManager.getConfig('markdown:rehypeSanitize:attributes'))
-      : undefined,
-    highlightJsStyleBorder: crowi.configManager.getConfig('customize:highlightJsStyleBorder'),
-  };
-
-  props.ssrMaxRevisionBodyLength = configManager.getConfig('app:ssrMaxRevisionBodyLength');
-}
-
-async function injectNextI18NextConfigurations(context: GetServerSidePropsContext, props: Props, namespacesRequired?: string[] | undefined): Promise<void> {
-  const nextI18NextConfig = await getNextI18NextConfig(serverSideTranslations, context, namespacesRequired);
-  props._nextI18Next = nextI18NextConfig._nextI18Next;
-}
-
 function getAction(props: Props): SupportedActionType {
   let action: SupportedActionType;
   if (props.isExpired) {
@@ -219,50 +144,17 @@ function getAction(props: Props): SupportedActionType {
 
   return action;
 }
-export const getServerSideProps: GetServerSideProps = async(context: GetServerSidePropsContext) => {
-  const req = context.req as CrowiRequest;
-  const { crowi, params } = req;
-  const result = await getServerSideCommonProps(context);
 
-  if (!('props' in result)) {
-    throw new Error('invalid getSSP result');
-  }
-  const props: Props = result.props as Props;
-
-  try {
-    const shareLink = await ShareLink.findOne({ _id: params.linkId }).populate('relatedPage');
-    if (shareLink == null) {
-      props.isNotFound = true;
-    }
-    else {
-      props.isNotFound = false;
-      props.isExpired = shareLink.isExpired();
-      props.shareLink = shareLink.toObject();
-
-      // retrieve Page
-      const Page = crowi.model('Page') as PageModel;
-      const relatedPage = await Page.findOne({ _id: getIdForRef(shareLink.relatedPage) });
-      // determine whether skip SSR
-      const ssrMaxRevisionBodyLength = crowi.configManager.getConfig('app:ssrMaxRevisionBodyLength');
-
-      if (relatedPage != null) {
-        props.skipSSR = await skipSSR(relatedPage, ssrMaxRevisionBodyLength);
-        // populate
-        props.shareLinkRelatedPage = await relatedPage.populateDataToShowRevision(props.skipSSR); // shouldExcludeBody = skipSSR
-      }
-    }
-  }
-  catch (err) {
-    logger.error(err);
-  }
+export const getServerSideProps: GetServerSideProps<ShareLinkPageProps> = async(context: GetServerSidePropsContext) => {
+  // detect Next.js routing type
+  const nextjsRoutingType = detectNextjsRoutingType(context, NEXT_JS_ROUTING_PAGE);
 
-  injectServerConfigurations(context, props);
-  await injectNextI18NextConfigurations(context, props);
-  await addActivity(context, getAction(props));
+  if (nextjsRoutingType === NextjsRoutingType.INITIAL) {
+    return getServerSidePropsForInitial(context);
+  }
 
-  return {
-    props,
-  };
+  // Lightweight props for same-route navigation
+  return getServerSidePropsForSameRoute(context);
 };
 
 export default SharedPage;

+ 90 - 0
apps/app/src/pages/share/[[...path]]/page-data-props.ts

@@ -0,0 +1,90 @@
+import { getIdForRef } from '@growi/core';
+import type { IPage } from '@growi/core';
+import type { model } from 'mongoose';
+import type { GetServerSidePropsContext, GetServerSidePropsResult } from 'next';
+
+import type { CrowiRequest } from '~/interfaces/crowi-request';
+import type { IShareLink } from '~/interfaces/share-link';
+import type { PageModel } from '~/server/models/page';
+import type { ShareLinkModel } from '~/server/models/share-link';
+
+import type { InitialProps } from '../../general-page';
+
+import type { ShareLinkPageProps } from './types';
+
+
+let mongooseModel: typeof model;
+let Page: PageModel;
+let ShareLink: ShareLinkModel;
+
+export const getPageDataForInitial = async(context: GetServerSidePropsContext):
+    Promise<GetServerSidePropsResult<Pick<InitialProps, 'isNotFound' | 'pageWithMeta' | 'skipSSR'> & ShareLinkPageProps>> => {
+
+  const req = context.req as CrowiRequest;
+  const { crowi, params } = req;
+
+  if (mongooseModel == null) {
+    mongooseModel = (await import('mongoose')).model;
+  }
+  if (Page == null) {
+    Page = mongooseModel<IPage, PageModel>('Page');
+  }
+  if (ShareLink == null) {
+    ShareLink = mongooseModel<IShareLink, ShareLinkModel>('ShareLink');
+  }
+
+  const shareLink = await ShareLink.findOne({ _id: params.linkId }).populate('relatedPage');
+
+  // not found
+  if (shareLink == null) {
+    return {
+      props: {
+        isNotFound: true,
+        pageWithMeta: null,
+      },
+    };
+  }
+
+  // expired
+  if (shareLink.isExpired()) {
+    return {
+      props: {
+        isNotFound: false,
+        pageWithMeta: null,
+        isExpired: true,
+      },
+    };
+  }
+
+  // retrieve Page
+  const relatedPage = await Page.findOne({ _id: getIdForRef(shareLink.relatedPage) });
+
+  // not found
+  if (relatedPage == null) {
+    return {
+      props: {
+        isNotFound: true,
+        pageWithMeta: null,
+      },
+    };
+  }
+
+  // Handle existing page
+  const ssrMaxRevisionBodyLength = crowi.configManager.getConfig('app:ssrMaxRevisionBodyLength');
+
+  // Check if SSR should be skipped
+  const latestRevisionBodyLength = await relatedPage.getLatestRevisionBodyLength();
+  const skipSSR = latestRevisionBodyLength != null && ssrMaxRevisionBodyLength < latestRevisionBodyLength;
+
+  const populatedPage = await relatedPage.populateDataToShowRevision(skipSSR);
+
+  return {
+    props: {
+      isNotFound: false,
+      pageWithMeta: { data: populatedPage },
+      skipSSR,
+      isExpired: false,
+      shareLink: shareLink.toObject(),
+    },
+  };
+};

+ 145 - 0
apps/app/src/pages/share/[[...path]]/server-side-props.ts

@@ -0,0 +1,145 @@
+import type { GetServerSidePropsContext, GetServerSidePropsResult } from 'next';
+
+import {
+  getServerSideI18nProps, getServerSideCommonInitialProps, getServerSideCommonEachProps,
+} from '../../common-props';
+import type { InitialProps } from '../../general-page';
+import {
+  getServerSideConfigurationProps, getServerSideRendererConfigProps,
+  getActivityAction, isValidInitialAndSameRouteProps,
+} from '../../general-page';
+import { addActivity } from '../../utils/activity';
+import { mergeGetServerSidePropsResults } from '../../utils/server-side-props';
+
+import { NEXT_JS_ROUTING_PAGE } from './consts';
+import { getPageDataForInitial } from './page-data-props';
+import type { ShareLinkPageProps } from './types';
+
+
+const nextjsRoutingProps = {
+  props: {
+    nextjsRoutingPage: NEXT_JS_ROUTING_PAGE,
+  },
+};
+
+const basisProps = {
+  props: {
+    isNotCreatable: true,
+    isForbidden: false,
+    isIdenticalPathPage: false,
+  },
+};
+
+export async function getServerSidePropsForInitial(context: GetServerSidePropsContext):
+    Promise<GetServerSidePropsResult<InitialProps & ShareLinkPageProps>> {
+
+  //
+  // STAGE 1
+  //
+
+  const commonEachPropsResult = await getServerSideCommonEachProps(context);
+  // Handle early return cases (redirect/notFound)
+  if ('redirect' in commonEachPropsResult || 'notFound' in commonEachPropsResult) {
+    return commonEachPropsResult;
+  }
+  const commonEachProps = await commonEachPropsResult.props;
+
+  // Handle redirect destination from common props
+  if (commonEachProps.redirectDestination != null) {
+    return {
+      redirect: {
+        permanent: false,
+        destination: commonEachProps.redirectDestination,
+      },
+    };
+  }
+
+  //
+  // STAGE 2
+  //
+
+  const [
+    commonInitialResult,
+    serverConfigResult,
+    rendererConfigResult,
+    i18nPropsResult,
+    pageDataResult,
+  ] = await Promise.all([
+    getServerSideCommonInitialProps(context),
+    getServerSideConfigurationProps(context),
+    getServerSideRendererConfigProps(context),
+    getServerSideI18nProps(context, ['translation']),
+    getPageDataForInitial(context),
+  ]);
+
+  // Merge all results in a type-safe manner (using sequential merging)
+  const mergedResult = mergeGetServerSidePropsResults(commonEachPropsResult,
+    mergeGetServerSidePropsResults(commonInitialResult,
+      mergeGetServerSidePropsResults(serverConfigResult,
+        mergeGetServerSidePropsResults(rendererConfigResult,
+          mergeGetServerSidePropsResults(i18nPropsResult,
+            mergeGetServerSidePropsResults(pageDataResult,
+              mergeGetServerSidePropsResults(nextjsRoutingProps, basisProps)))))));
+
+  // Check for early return (redirect/notFound)
+  if ('redirect' in mergedResult || 'notFound' in mergedResult) {
+    return mergedResult;
+  }
+
+  const mergedProps = await mergedResult.props;
+
+  // Type-safe props validation AFTER skipSSR is properly set
+  if (!isValidInitialAndSameRouteProps(mergedProps)) {
+    throw new Error('Invalid merged props structure');
+  }
+
+  await addActivity(context, getActivityAction(mergedProps));
+  return mergedResult;
+}
+
+export async function getServerSidePropsForSameRoute(context: GetServerSidePropsContext): Promise<GetServerSidePropsResult<SameRouteEachProps>> {
+  //
+  // STAGE 1
+  //
+
+  const commonEachPropsResult = await getServerSideCommonEachProps(context);
+  // Handle early return cases (redirect/notFound)
+  if ('redirect' in commonEachPropsResult || 'notFound' in commonEachPropsResult) {
+    return commonEachPropsResult;
+  }
+  const commonEachProps = await commonEachPropsResult.props;
+
+  // Handle redirect destination from common props
+  if (commonEachProps.redirectDestination != null) {
+    return {
+      redirect: {
+        permanent: false,
+        destination: commonEachProps.redirectDestination,
+      },
+    };
+  }
+
+  //
+  // STAGE 2
+  //
+
+  // Merge results in a type-safe manner
+  const mergedResult = mergeGetServerSidePropsResults(commonEachPropsResult,
+    mergeGetServerSidePropsResults({
+      props: {
+        isIdenticalPathPage: false,
+      },
+    }, nextjsRoutingProps));
+
+  // -- TODO: persist activity
+
+  // const mergedProps = await mergedResult.props;
+
+  // // Type-safe props validation AFTER skipSSR is properly set
+  // if (!isValidSameRouteProps(mergedProps)) {
+  //   throw new Error('Invalid same route props structure');
+  // }
+
+  // await addActivity(context, getActivityAction(mergedProps));
+  return mergedResult;
+}

+ 13 - 0
apps/app/src/pages/share/[[...path]]/types.ts

@@ -0,0 +1,13 @@
+import type { IShareLinkHasId } from '~/interfaces/share-link';
+
+export type ShareLinkPageProps = {
+  isNotFound: true,
+  isExpired: undefined
+  shareLink: undefined,
+} | {
+  isExpired: true,
+  shareLink: undefined,
+} | {
+  isExpired: false,
+  shareLink: IShareLinkHasId,
+};

+ 22 - 12
apps/app/src/states/page/hooks.ts

@@ -5,21 +5,21 @@ import { useCurrentPathname } from '../global';
 import type { UseAtom } from '../helper';
 
 import {
-  currentPageIdAtom,
   currentPageDataAtom,
+  currentPageIdAtom,
   currentPagePathAtom,
-  pageNotFoundAtom,
-  pageNotCreatableAtom,
+  isRevisionOutdatedAtom,
+  isTrashPageAtom,
   latestRevisionAtom,
-  // New atoms for enhanced functionality
-  remoteRevisionIdAtom,
+  pageNotCreatableAtom,
+  pageNotFoundAtom,
   remoteRevisionBodyAtom,
-  remoteRevisionLastUpdateUserAtom,
+  remoteRevisionIdAtom,
   remoteRevisionLastUpdatedAtAtom,
-  isTrashPageAtom,
-  isRevisionOutdatedAtom,
-  templateTagsAtom,
+  remoteRevisionLastUpdateUserAtom,
+  shareLinkIdAtom,
   templateBodyAtom,
+  templateTagsAtom,
 } from './internal-atoms';
 
 /**
@@ -48,6 +48,10 @@ export const useLatestRevision = (): UseAtom<typeof latestRevisionAtom> => {
   return useAtom(latestRevisionAtom);
 };
 
+export const useShareLinkId = (): UseAtom<typeof shareLinkIdAtom> => {
+  return useAtom(shareLinkIdAtom);
+};
+
 export const useTemplateTags = (): UseAtom<typeof templateTagsAtom> => {
   return useAtom(templateTagsAtom);
 };
@@ -61,15 +65,21 @@ export const useRemoteRevisionId = (): UseAtom<typeof remoteRevisionIdAtom> => {
   return useAtom(remoteRevisionIdAtom);
 };
 
-export const useRemoteRevisionBody = (): UseAtom<typeof remoteRevisionBodyAtom> => {
+export const useRemoteRevisionBody = (): UseAtom<
+  typeof remoteRevisionBodyAtom
+> => {
   return useAtom(remoteRevisionBodyAtom);
 };
 
-export const useRemoteRevisionLastUpdateUser = (): UseAtom<typeof remoteRevisionLastUpdateUserAtom> => {
+export const useRemoteRevisionLastUpdateUser = (): UseAtom<
+  typeof remoteRevisionLastUpdateUserAtom
+> => {
   return useAtom(remoteRevisionLastUpdateUserAtom);
 };
 
-export const useRemoteRevisionLastUpdatedAt = (): UseAtom<typeof remoteRevisionLastUpdatedAtAtom> => {
+export const useRemoteRevisionLastUpdatedAt = (): UseAtom<
+  typeof remoteRevisionLastUpdatedAtAtom
+> => {
   return useAtom(remoteRevisionLastUpdatedAtAtom);
 };
 

+ 19 - 43
apps/app/src/states/page/hydrate.ts

@@ -2,15 +2,16 @@ import type { IPagePopulatedToShowRevision } from '@growi/core';
 import { useHydrateAtoms } from 'jotai/utils';
 
 import {
-  currentPageIdAtom,
   currentPageDataAtom,
-  pageNotFoundAtom,
+  currentPageIdAtom,
   latestRevisionAtom,
-  templateTagsAtom,
-  templateBodyAtom,
-  remoteRevisionIdAtom,
-  remoteRevisionBodyAtom,
   pageNotCreatableAtom,
+  pageNotFoundAtom,
+  remoteRevisionBodyAtom,
+  remoteRevisionIdAtom,
+  shareLinkIdAtom,
+  templateBodyAtom,
+  templateTagsAtom,
 } from './internal-atoms';
 
 /**
@@ -37,14 +38,15 @@ import {
  * });
  */
 export const useHydratePageAtoms = (
-    page: IPagePopulatedToShowRevision | undefined,
-    options?: {
-      isNotFound?: boolean;
-      isNotCreatable?: boolean;
-      isLatestRevision?: boolean;
-      templateTags?: string[];
-      templateBody?: string;
-    },
+  page: IPagePopulatedToShowRevision | undefined,
+  options?: {
+    isNotFound?: boolean;
+    isNotCreatable?: boolean;
+    isLatestRevision?: boolean;
+    shareLinkId?: string;
+    templateTags?: string[];
+    templateBody?: string;
+  },
 ): void => {
   useHydrateAtoms([
     // Core page state - automatically extract from page object
@@ -54,6 +56,9 @@ export const useHydratePageAtoms = (
     [pageNotCreatableAtom, options?.isNotCreatable ?? false],
     [latestRevisionAtom, options?.isLatestRevision ?? true],
 
+    // ShareLink page state
+    [shareLinkIdAtom, options?.shareLinkId],
+
     // Template data - from options (not auto-extracted from page)
     [templateTagsAtom, options?.templateTags ?? []],
     [templateBodyAtom, options?.templateBody ?? ''],
@@ -63,32 +68,3 @@ export const useHydratePageAtoms = (
     [remoteRevisionBodyAtom, page?.revision?.body],
   ]);
 };
-
-/**
- * Hook for hydrating shared page atoms with server-side data
- * This is a simplified version that focuses on the most common use case:
- * hydrating with page ID and not found status
- * @param args
- */
-export const useHydrateSharedPageAtoms = (
-    args: {
-      pageId: string | undefined;
-      isNotFound: boolean;
-    },
-): void => {
-  useHydrateAtoms([
-    // Core page state - automatically extract from page object
-    [currentPageIdAtom, args.pageId],
-    [currentPageDataAtom, undefined],
-    [pageNotFoundAtom, args.isNotFound],
-    [latestRevisionAtom, true],
-
-    // Template data - from options (not auto-extracted from page)
-    [templateTagsAtom, []],
-    [templateBodyAtom, ''],
-
-    // Remote revision data - auto-extracted from page.revision
-    [remoteRevisionIdAtom, undefined],
-    [remoteRevisionBodyAtom, undefined],
-  ]);
-};

+ 13 - 6
apps/app/src/states/page/internal-atoms.ts

@@ -14,6 +14,9 @@ export const pageNotFoundAtom = atom(false);
 export const pageNotCreatableAtom = atom(false);
 export const latestRevisionAtom = atom(true);
 
+// ShareLink page state atoms (internal)
+export const shareLinkIdAtom = atom<string>();
+
 // Fetch state atoms (internal)
 export const pageLoadingAtom = atom(false);
 export const pageErrorAtom = atom<Error | null>(null);
@@ -84,12 +87,16 @@ export const setTemplateContentAtom = atom(
 
 export const setRemoteRevisionDataAtom = atom(
   null,
-  (get, set, data: {
-    id?: string;
-    body?: string;
-    lastUpdateUser?: IUserHasId;
-    lastUpdatedAt?: Date;
-  }) => {
+  (
+    get,
+    set,
+    data: {
+      id?: string;
+      body?: string;
+      lastUpdateUser?: IUserHasId;
+      lastUpdatedAt?: Date;
+    },
+  ) => {
     if (data.id !== undefined) {
       set(remoteRevisionIdAtom, data.id);
     }

+ 126 - 105
apps/app/src/states/page/use-fetch-current-page.ts

@@ -1,142 +1,163 @@
-import { useCallback } from 'react';
-
 import type { IPagePopulatedToShowRevision } from '@growi/core';
 import { isClient } from '@growi/core/dist/utils';
-import { isCreatablePage, isPermalink } from '@growi/core/dist/utils/page-path-utils';
+import {
+  isCreatablePage,
+  isPermalink,
+} from '@growi/core/dist/utils/page-path-utils';
 import { removeHeadingSlash } from '@growi/core/dist/utils/path-utils';
 import { useAtomValue } from 'jotai';
 import { useAtomCallback } from 'jotai/utils';
+import { useCallback } from 'react';
 
 import { apiv3Get } from '~/client/util/apiv3-client';
-import { useShareLinkId } from '~/stores-universal/context';
 
 import {
-  currentPageDataAtom, currentPageIdAtom, pageErrorAtom, pageLoadingAtom, pageNotCreatableAtom, pageNotFoundAtom,
+  currentPageDataAtom,
+  currentPageIdAtom,
+  pageErrorAtom,
+  pageLoadingAtom,
+  pageNotCreatableAtom,
+  pageNotFoundAtom,
+  shareLinkIdAtom,
 } from './internal-atoms';
 
 type FetchPageArgs = {
-  path?: string,
-  pageId?: string,
-  revisionId?: string,
-}
+  path?: string;
+  pageId?: string;
+  revisionId?: string;
+};
 
 /**
  * Simplified page fetching hook using Jotai state management
  * All state is managed through atoms for consistent global state
  */
 export const useFetchCurrentPage = (): {
-  fetchCurrentPage: (args?: FetchPageArgs) => Promise<IPagePopulatedToShowRevision | null>,
-  isLoading: boolean,
-  error: Error | null,
+  fetchCurrentPage: (
+    args?: FetchPageArgs,
+  ) => Promise<IPagePopulatedToShowRevision | null>;
+  isLoading: boolean;
+  error: Error | null;
 } => {
-  const { data: shareLinkId } = useShareLinkId();
+  const shareLinkId = useAtomValue(shareLinkIdAtom);
 
-  // Use atoms for state instead of local state
   const isLoading = useAtomValue(pageLoadingAtom);
   const error = useAtomValue(pageErrorAtom);
 
   const fetchCurrentPage = useAtomCallback(
-    useCallback(async(get, set, args?: FetchPageArgs): Promise<IPagePopulatedToShowRevision | null> => {
-      const currentPageId = get(currentPageIdAtom);
-      const currentPageData = get(currentPageDataAtom);
+    useCallback(
+      async (
+        get,
+        set,
+        args?: FetchPageArgs,
+      ): Promise<IPagePopulatedToShowRevision | null> => {
+        const currentPageId = get(currentPageIdAtom);
+        const currentPageData = get(currentPageDataAtom);
+
+        // Process path first to handle permalinks
+        let decodedPath: string | undefined;
+        if (args?.path != null) {
+          try {
+            decodedPath = decodeURIComponent(args.path);
+          } catch (e) {
+            decodedPath = args.path;
+          }
+        }
 
-      // Process path first to handle permalinks
-      let decodedPath: string | undefined;
-      if (args?.path != null) {
-        try {
-          decodedPath = decodeURIComponent(args.path);
+        // Guard clause to prevent unnecessary fetching
+        if (args?.pageId != null && args.pageId === currentPageId) {
+          return currentPageData ?? null;
         }
-        catch (e) {
-          decodedPath = args.path;
+        if (decodedPath != null) {
+          if (
+            isPermalink(decodedPath) &&
+            removeHeadingSlash(decodedPath) === currentPageId
+          ) {
+            return currentPageData ?? null;
+          }
+          if (decodedPath === currentPageData?.path) {
+            return currentPageData ?? null;
+          }
         }
-      }
-
-      // Guard clause to prevent unnecessary fetching
-      if (args?.pageId != null && args.pageId === currentPageId) {
-        return currentPageData ?? null;
-      }
-      if (decodedPath != null) {
-        if (isPermalink(decodedPath) && removeHeadingSlash(decodedPath) === currentPageId) {
-          return currentPageData ?? null;
+
+        set(pageLoadingAtom, true);
+        set(pageErrorAtom, null);
+
+        // determine parameters
+        const pageId = args?.pageId;
+        const revisionId =
+          args?.revisionId ??
+          (isClient()
+            ? new URLSearchParams(window.location.search).get('revisionId')
+            : undefined);
+
+        // params for API
+        const params: {
+          path?: string;
+          pageId?: string;
+          revisionId?: string;
+          shareLinkId?: string;
+        } = {};
+        if (shareLinkId != null) {
+          params.shareLinkId = shareLinkId;
         }
-        if (decodedPath === currentPageData?.path) {
-          return currentPageData ?? null;
+        if (revisionId != null) {
+          params.revisionId = revisionId;
         }
-      }
-
-      set(pageLoadingAtom, true);
-      set(pageErrorAtom, null);
-
-      // determine parameters
-      const pageId = args?.pageId;
-      const revisionId = args?.revisionId ?? (isClient() ? new URLSearchParams(window.location.search).get('revisionId') : undefined);
-
-      // params for API
-      const params: { path?: string, pageId?: string, revisionId?: string, shareLinkId?: string } = {};
-      if (shareLinkId != null) {
-        params.shareLinkId = shareLinkId;
-      }
-      if (revisionId != null) {
-        params.revisionId = revisionId;
-      }
-
-      // priority: pageId > permalink > path
-      if (pageId != null) {
-        params.pageId = pageId;
-      }
-      else if (decodedPath != null && isPermalink(decodedPath)) {
-        params.pageId = removeHeadingSlash(decodedPath);
-      }
-      else if (decodedPath != null) {
-        params.path = decodedPath;
-      }
-      // if args is empty, get from global state
-      else if (currentPageId != null) {
-        params.pageId = currentPageId;
-      }
-      else if (isClient()) {
-        try {
-          params.path = decodeURIComponent(window.location.pathname);
+
+        // priority: pageId > permalink > path
+        if (pageId != null) {
+          params.pageId = pageId;
+        } else if (decodedPath != null && isPermalink(decodedPath)) {
+          params.pageId = removeHeadingSlash(decodedPath);
+        } else if (decodedPath != null) {
+          params.path = decodedPath;
         }
-        catch (e) {
-          params.path = window.location.pathname;
+        // if args is empty, get from global state
+        else if (currentPageId != null) {
+          params.pageId = currentPageId;
+        } else if (isClient()) {
+          try {
+            params.path = decodeURIComponent(window.location.pathname);
+          } catch (e) {
+            params.path = window.location.pathname;
+          }
+        } else {
+          // TODO: https://github.com/weseek/growi/pull/9118
+          // throw new Error('Either path or pageId must be provided when not in a browser environment');
+          set(pageLoadingAtom, false);
+          return null;
         }
-      }
-      else {
-        // TODO: https://github.com/weseek/growi/pull/9118
-        // throw new Error('Either path or pageId must be provided when not in a browser environment');
-        set(pageLoadingAtom, false);
-        return null;
-      }
-
-      try {
-        const { data } = await apiv3Get<{ page: IPagePopulatedToShowRevision }>('/page', params);
-        const { page: newData } = data;
-
-        set(currentPageDataAtom, newData);
-        set(currentPageIdAtom, newData._id);
-        set(pageNotFoundAtom, false);
-        set(pageNotCreatableAtom, false);
-
-        return newData;
-      }
-      catch (err) {
-        set(pageErrorAtom, err as Error);
-
-        const apiError = err as any; // eslint-disable-line @typescript-eslint/no-explicit-any
-        if (apiError.response?.status === 404) {
-          set(pageNotFoundAtom, true);
-          if (params.path != null) {
-            set(pageNotCreatableAtom, !isCreatablePage(params.path));
+
+        try {
+          const { data } = await apiv3Get<{
+            page: IPagePopulatedToShowRevision;
+          }>('/page', params);
+          const { page: newData } = data;
+
+          set(currentPageDataAtom, newData);
+          set(currentPageIdAtom, newData._id);
+          set(pageNotFoundAtom, false);
+          set(pageNotCreatableAtom, false);
+
+          return newData;
+        } catch (err) {
+          set(pageErrorAtom, err as Error);
+
+          const apiError = err as any; // eslint-disable-line @typescript-eslint/no-explicit-any
+          if (apiError.response?.status === 404) {
+            set(pageNotFoundAtom, true);
+            if (params.path != null) {
+              set(pageNotCreatableAtom, !isCreatablePage(params.path));
+            }
           }
+        } finally {
+          set(pageLoadingAtom, false);
         }
-      }
-      finally {
-        set(pageLoadingAtom, false);
-      }
 
-      return null;
-    }, [shareLinkId]),
+        return null;
+      },
+      [shareLinkId],
+    ),
   );
 
   return { fetchCurrentPage, isLoading, error };

+ 0 - 12
apps/app/src/stores-universal/context.tsx

@@ -18,22 +18,10 @@ declare global {
 type Nullable<T> = T | null;
 
 
-export const useIsSharedUser = (initialData?: boolean): SWRResponse<boolean, Error> => {
-  return useContextSWR<boolean, Error>('isSharedUser', initialData);
-};
-
-export const useShareLinkId = (initialData?: string): SWRResponse<string, Error> => {
-  return useContextSWR('shareLinkId', initialData);
-};
-
 export const useRegistrationWhitelist = (initialData?: Nullable<string[]>): SWRResponse<Nullable<string[]>, Error> => {
   return useContextSWR<Nullable<string[]>, Error>('registrationWhitelist', initialData);
 };
 
-export const useIsSearchPage = (initialData?: Nullable<boolean>) : SWRResponse<Nullable<boolean>, Error> => {
-  return useContextSWR<Nullable<boolean>, Error>('isSearchPage', initialData);
-};
-
 export const useIsMailerSetup = (initialData?: boolean): SWRResponse<boolean, Error> => {
   return useContextSWR('isMailerSetup', initialData);
 };

+ 2 - 2
apps/app/src/stores/page.tsx

@@ -20,7 +20,7 @@ import type { IPagePathWithDescendantCount } from '~/interfaces/page';
 import type { IRecordApplicableGrant, IResCurrentGrantData } from '~/interfaces/page-grant';
 import { useIsGuestUser, useIsReadOnlyUser } from '~/states/context';
 import { usePageNotFound } from '~/states/page';
-import { useShareLinkId } from '~/stores-universal/context';
+import { useShareLinkId } from '~/states/page/hooks';
 import type { AxiosResponse } from '~/utils/axios';
 
 import type { IPageTagsInfo } from '../interfaces/tag';
@@ -53,7 +53,7 @@ export const useSWRxPageByPath = (path?: string, config?: SWRConfiguration): SWR
 };
 
 export const useSWRxTagsInfo = (pageId: Nullable<string>, config?: SWRConfiguration): SWRResponse<IPageTagsInfo | null, Error> => {
-  const { data: shareLinkId } = useShareLinkId();
+  const [shareLinkId] = useShareLinkId();
 
   const endpoint = `/pages.getPageTag?pageId=${pageId}`;
 

+ 8 - 7
apps/app/src/stores/ui.tsx

@@ -17,15 +17,15 @@ import useSWRImmutable from 'swr/immutable';
 
 import type { IPageSelectedGrant } from '~/interfaces/page';
 import type { UpdateDescCountData } from '~/interfaces/websocket';
-import { useIsEditable, useIsIdenticalPath, useIsReadOnlyUser } from '~/states/context';
+import {
+  useIsEditable, useIsIdenticalPath, useIsReadOnlyUser, useIsSharedUser,
+} from '~/states/context';
 import { useCurrentUser } from '~/states/global';
 import {
   usePageNotFound, useCurrentPagePath, useIsTrashPage, useCurrentPageId,
 } from '~/states/page';
+import { useShareLinkId } from '~/states/page/hooks';
 import { EditorMode, useEditorMode } from '~/states/ui/editor';
-import {
-  useIsSharedUser, useShareLinkId,
-} from '~/stores-universal/context';
 import loggerFactory from '~/utils/logger';
 
 import { useStaticSWR } from './use-static-swr';
@@ -257,7 +257,7 @@ export const useIsAbleToShowPageManagement = (): SWRResponse<boolean, Error> =>
   const [currentPageId] = useCurrentPageId();
   const [isNotFound] = usePageNotFound();
   const [_isTrashPage] = useIsTrashPage();
-  const { data: _isSharedUser } = useIsSharedUser();
+  const [_isSharedUser] = useIsSharedUser();
 
   const pageId = currentPageId;
   const includesUndefined = [pageId, _isTrashPage, _isSharedUser, isNotFound].some(v => v === undefined);
@@ -279,7 +279,8 @@ export const useIsAbleToShowTagLabel = (): SWRResponse<boolean, Error> => {
   const [currentPagePath] = useCurrentPagePath();
   const [isIdenticalPath] = useIsIdenticalPath();
   const { editorMode } = useEditorMode();
-  const { data: shareLinkId } = useShareLinkId();
+  const [shareLinkId] = useShareLinkId();
+
 
   const includesUndefined = [currentPagePath, isIdenticalPath, isNotFound, editorMode].some(v => v === undefined);
 
@@ -296,7 +297,7 @@ export const useIsAbleToShowTagLabel = (): SWRResponse<boolean, Error> => {
 export const useIsAbleToChangeEditorMode = (): SWRResponse<boolean, Error> => {
   const key = 'isAbleToChangeEditorMode';
   const [isEditable] = useIsEditable();
-  const { data: isSharedUser } = useIsSharedUser();
+  const [isSharedUser] = useIsSharedUser();
 
   const includesUndefined = [isEditable, isSharedUser].some(v => v === undefined);