diff --git a/app/[[...path]]/page.tsx b/app/[[...path]]/page.tsx index 70872af034763..972e8e16c77cd 100644 --- a/app/[[...path]]/page.tsx +++ b/app/[[...path]]/page.tsx @@ -7,7 +7,7 @@ import {apiCategories} from 'sentry-docs/build/resolveOpenAPI'; import {ApiCategoryPage} from 'sentry-docs/components/apiCategoryPage'; import {ApiPage} from 'sentry-docs/components/apiPage'; import {DocPage} from 'sentry-docs/components/docPage'; -import {Home} from 'sentry-docs/components/home'; +import Home from 'sentry-docs/components/home'; import {Include} from 'sentry-docs/components/include'; import {PageLoadMetrics} from 'sentry-docs/components/pageLoadMetrics'; import {PlatformContent} from 'sentry-docs/components/platformContent'; @@ -87,8 +87,6 @@ export default async function Page(props: {params: Promise<{path?: string[]}>}) const pageNode = nodeForPath(rootNode, params.path ?? ''); if (!pageNode) { - // eslint-disable-next-line no-console - console.warn('no page node', params.path); return notFound(); } @@ -123,8 +121,6 @@ export default async function Page(props: {params: Promise<{path?: string[]}>}) doc = await getFileBySlugWithCache(`develop-docs/${params.path?.join('/') ?? ''}`); } catch (e) { if (e.code === 'ENOENT') { - // eslint-disable-next-line no-console - console.error('ENOENT', params.path); return notFound(); } throw e; @@ -184,8 +180,6 @@ export default async function Page(props: {params: Promise<{path?: string[]}>}) doc = await getFileBySlugWithCache(`docs/${pageNode.path}`); } catch (e) { if (e.code === 'ENOENT') { - // eslint-disable-next-line no-console - console.error('ENOENT', pageNode.path); return notFound(); } throw e; diff --git a/app/not-found.tsx b/app/not-found.tsx index 7d5eb07076fdc..c310826f16cc3 100644 --- a/app/not-found.tsx +++ b/app/not-found.tsx @@ -4,7 +4,7 @@ import {useEffect, useState} from 'react'; import {Button} from '@radix-ui/themes'; import {usePathname} from 'next/navigation'; -import {Header} from 'sentry-docs/components/header'; +import Header from 'sentry-docs/components/header'; import {Search} from 'sentry-docs/components/search'; import {DocMetrics} from 'sentry-docs/metrics'; @@ -39,7 +39,7 @@ export default function NotFound() { const reportUrl = `https://github.com/getsentry/sentry-docs/issues/new?template=issue-platform-404.yml&title=🔗 404 Error&url=${brokenUrl}`; return (
-
+

Page Not Found

We couldn't find the page you were looking for :(

diff --git a/src/components/TopNav.tsx b/src/components/TopNav.tsx new file mode 100644 index 0000000000000..b933a2c55f96e --- /dev/null +++ b/src/components/TopNav.tsx @@ -0,0 +1,9 @@ +import {extractPlatforms, getDocsRootNode} from 'sentry-docs/docTree'; + +import TopNavClient from './TopNavClient'; + +export default async function TopNav() { + const rootNode = await getDocsRootNode(); + const platforms = extractPlatforms(rootNode); + return ; +} diff --git a/src/components/TopNavClient.tsx b/src/components/TopNavClient.tsx new file mode 100644 index 0000000000000..fb0eeb1a7388a --- /dev/null +++ b/src/components/TopNavClient.tsx @@ -0,0 +1,697 @@ +'use client'; +import {useEffect, useRef, useState} from 'react'; +import ReactDOM from 'react-dom'; +import Link from 'next/link'; +import {usePathname} from 'next/navigation'; + +import {Platform} from 'sentry-docs/types'; + +import platformSelectorStyles from './platformSelector/style.module.scss'; + +import {PlatformSelector} from './platformSelector'; + +const productSections = [ + {label: 'Sentry Basics', href: '/product/sentry-basics/'}, + {label: 'AI in Sentry', href: '/product/ai-in-sentry/'}, + {label: 'Insights', href: '/product/insights/'}, + {label: 'User Feedback', href: '/product/user-feedback/'}, + {label: 'Uptime Monitoring', href: '/product/uptime-monitoring/'}, + {label: 'Dashboards', href: '/product/dashboards/'}, + {label: 'Projects', href: '/product/projects/'}, + {label: 'Explore', href: '/product/explore/'}, + {label: 'Issues', href: '/product/issues/'}, + {label: 'Alerts', href: '/product/alerts/'}, + {label: 'Crons', href: '/product/crons/'}, + {label: 'Releases', href: '/product/releases/'}, + {label: 'Relay', href: '/product/relay/'}, + {label: 'Sentry MCP', href: '/product/sentry-mcp/'}, + {label: 'Sentry Toolbar', href: '/product/sentry-toolbar/'}, + {label: 'Stats', href: '/product/stats/'}, + {label: 'Codecov', href: '/product/codecov/'}, + {label: 'Onboarding', href: '/product/onboarding/'}, +]; + +const mainSections = [ + {label: 'SDKS', href: '/platforms/'}, + { + label: 'PRODUCT', + href: '/product/', + dropdown: productSections, + }, + { + label: 'CONCEPTS', + href: '/concepts/', + dropdown: [ + {label: 'Key Terms', href: '/concepts/key-terms/'}, + {label: 'Search', href: '/concepts/search/'}, + {label: 'Migration', href: '/concepts/migration/'}, + {label: 'Data Management', href: '/concepts/data-management/'}, + {label: 'Sentry CLI', href: '/cli/'}, + ], + }, + { + label: 'ADMIN', + href: '/organization/', + dropdown: [ + {label: 'Account Settings', href: '/account/'}, + {label: 'Organization Settings', href: '/organization/'}, + {label: 'Pricing & Billing', href: '/pricing'}, + ], + }, + {label: 'API', href: '/api/'}, + {label: 'SECURITY, LEGAL, & PII', href: '/security-legal-pii/'}, +]; + +// Add a helper hook for portal dropdown positioning +function useDropdownPosition(triggerRef, open) { + const [position, setPosition] = useState({top: 0, left: 0, width: 0}); + useEffect(() => { + function updatePosition() { + if (triggerRef.current && open) { + const rect = triggerRef.current.getBoundingClientRect(); + setPosition({ + top: rect.bottom + window.scrollY, + left: rect.left + window.scrollX, + width: rect.width, + }); + } + } + updatePosition(); + if (open) { + window.addEventListener('resize', updatePosition); + window.addEventListener('scroll', updatePosition, true); + } + return () => { + window.removeEventListener('resize', updatePosition); + window.removeEventListener('scroll', updatePosition, true); + }; + }, [triggerRef, open]); + return position; +} + +export default function TopNavClient({platforms}: {platforms: Platform[]}) { + const [platformDropdownOpen, setPlatformDropdownOpen] = useState(false); + const [platformDropdownByClick, setPlatformDropdownByClick] = useState(false); + const platformBtnRef = useRef(null); + const platformDropdownRef = useRef(null); + const pathname = usePathname(); + const closeTimers = useRef<{ + admin?: NodeJS.Timeout; + concepts?: NodeJS.Timeout; + products?: NodeJS.Timeout; + sdks?: NodeJS.Timeout; + }>({}); + const [productsDropdownOpen, setProductsDropdownOpen] = useState(false); + const [conceptsDropdownOpen, setConceptsDropdownOpen] = useState(false); + const [adminDropdownOpen, setAdminDropdownOpen] = useState(false); + const productsBtnRef = useRef(null); + const conceptsBtnRef = useRef(null); + const adminBtnRef = useRef(null); + const productsDropdownRef = useRef(null); + const conceptsDropdownRef = useRef(null); + const adminDropdownRef = useRef(null); + const navRef = useRef(null); + const [canScrollLeft, setCanScrollLeft] = useState(false); + const [canScrollRight, setCanScrollRight] = useState(false); + + // Close dropdowns on outside click if opened by click + useEffect(() => { + function handleClick(e: MouseEvent) { + if (platformDropdownOpen && platformDropdownByClick) { + if ( + !platformBtnRef.current?.contains(e.target as Node) && + !platformDropdownRef.current?.contains(e.target as Node) + ) { + setPlatformDropdownOpen(false); + setPlatformDropdownByClick(false); + } + } + if (productsDropdownOpen) { + if ( + !productsBtnRef.current?.contains(e.target as Node) && + !productsDropdownRef.current?.contains(e.target as Node) + ) { + setProductsDropdownOpen(false); + } + } + if (conceptsDropdownOpen) { + if ( + !conceptsBtnRef.current?.contains(e.target as Node) && + !conceptsDropdownRef.current?.contains(e.target as Node) + ) { + setConceptsDropdownOpen(false); + } + } + if (adminDropdownOpen) { + if ( + !adminBtnRef.current?.contains(e.target as Node) && + !adminDropdownRef.current?.contains(e.target as Node) + ) { + setAdminDropdownOpen(false); + } + } + } + document.addEventListener('mousedown', handleClick); + return () => document.removeEventListener('mousedown', handleClick); + }, [ + platformDropdownOpen, + platformDropdownByClick, + productsDropdownOpen, + conceptsDropdownOpen, + adminDropdownOpen, + ]); + + useEffect(() => { + function updateScrollState() { + const nav = navRef.current; + if (!nav) return; + setCanScrollLeft(nav.scrollLeft > 0); + setCanScrollRight(nav.scrollLeft + nav.clientWidth < nav.scrollWidth - 1); + } + updateScrollState(); + const nav = navRef.current; + if (nav) { + nav.addEventListener('scroll', updateScrollState); + } + window.addEventListener('resize', updateScrollState); + return () => { + if (nav) nav.removeEventListener('scroll', updateScrollState); + window.removeEventListener('resize', updateScrollState); + }; + }, []); + + function scrollNavBy(amount: number) { + const nav = navRef.current; + if (nav) { + nav.scrollBy({left: amount, behavior: 'smooth'}); + } + } + + // For each dropdown, use the hook and portal rendering + // Example for Products: + const productsPosition = useDropdownPosition(productsBtnRef, productsDropdownOpen); + const sdksPosition = useDropdownPosition(platformBtnRef, platformDropdownOpen); + const conceptsPosition = useDropdownPosition(conceptsBtnRef, conceptsDropdownOpen); + const adminPosition = useDropdownPosition(adminBtnRef, adminDropdownOpen); + + return ( +
+
+ {canScrollLeft && ( + + )} + {canScrollRight && ( + + )} +
+
    + {mainSections.map(section => ( +
  • + {section.label === 'PRODUCTS' ? ( +
    { + clearTimeout(closeTimers.current.products); + setProductsDropdownOpen(true); + setConceptsDropdownOpen(false); + setAdminDropdownOpen(false); + setPlatformDropdownOpen(false); + }} + onMouseLeave={() => { + closeTimers.current.products = setTimeout(() => { + setProductsDropdownOpen(false); + }, 150); + }} + > + +
    + ) : section.label === 'CONCEPTS' ? ( +
    { + clearTimeout(closeTimers.current.concepts); + setConceptsDropdownOpen(true); + setProductsDropdownOpen(false); + setAdminDropdownOpen(false); + setPlatformDropdownOpen(false); + }} + onMouseLeave={() => { + closeTimers.current.concepts = setTimeout(() => { + setConceptsDropdownOpen(false); + }, 150); + }} + > + +
    + ) : section.label === 'ADMIN' ? ( +
    { + clearTimeout(closeTimers.current.admin); + setAdminDropdownOpen(true); + setProductsDropdownOpen(false); + setConceptsDropdownOpen(false); + setPlatformDropdownOpen(false); + }} + onMouseLeave={() => { + closeTimers.current.admin = setTimeout(() => { + setAdminDropdownOpen(false); + }, 150); + }} + > + +
    + ) : ( + + {section.label} + + )} +
  • + ))} +
+
+
+ {/* Portal-based dropdowns */} + {productsDropdownOpen && + ReactDOM.createPortal( +
e.stopPropagation()} + onMouseEnter={() => { + clearTimeout(closeTimers.current.products); + }} + onMouseLeave={() => { + closeTimers.current.products = setTimeout(() => { + setProductsDropdownOpen(false); + }, 150); + }} + > + + {productSections.map(product => ( + + {product.label} + + ))} +
, + document.body + )} + {platformDropdownOpen && + ReactDOM.createPortal( +
e.stopPropagation()} + onMouseEnter={() => { + clearTimeout(closeTimers.current.sdks); + }} + onMouseLeave={() => { + closeTimers.current.sdks = setTimeout(() => { + setPlatformDropdownOpen(false); + setPlatformDropdownByClick(false); + }, 150); + }} + > + + +
, + document.body + )} + {conceptsDropdownOpen && + ReactDOM.createPortal( +
e.stopPropagation()} + onMouseEnter={() => { + clearTimeout(closeTimers.current.concepts); + }} + onMouseLeave={() => { + closeTimers.current.concepts = setTimeout(() => { + setConceptsDropdownOpen(false); + }, 150); + }} + > + + {mainSections + .find(s => s.label === 'CONCEPTS') + ?.dropdown?.map(dropdown => ( + + {dropdown.label} + + ))} +
, + document.body + )} + {adminDropdownOpen && + ReactDOM.createPortal( +
e.stopPropagation()} + onMouseEnter={() => { + clearTimeout(closeTimers.current.admin); + }} + onMouseLeave={() => { + closeTimers.current.admin = setTimeout(() => { + setAdminDropdownOpen(false); + }, 150); + }} + > + + {mainSections + .find(s => s.label === 'ADMIN') + ?.dropdown?.map(dropdown => ( + + {dropdown.label} + + ))} +
, + document.body + )} + +
+ ); +} diff --git a/src/components/card.tsx b/src/components/card.tsx index b17746da26ed1..a266aa63aba00 100644 --- a/src/components/card.tsx +++ b/src/components/card.tsx @@ -19,7 +19,7 @@ export function Card({ }) { return ( -
+
-
- +
{sidebar ?? ( )} -
+
{ + clearTimeout(timeout); + timeout = setTimeout(() => func(...args), wait); + }; +} + +/** Find the scrollable parent element */ +function getScrollableParent(element: Element | null): Element | null { + if (!element) return null; + + let parent = element.parentElement; + while (parent) { + const style = window.getComputedStyle(parent); + const overflowY = style.overflowY; + if (overflowY === 'auto' || overflowY === 'scroll') { + return parent; + } + parent = parent.parentElement; + } + return null; +} + /** Make sure the active link is visible in the sidebar */ export function ScrollActiveLink({activeLinkSelector}: Props) { useEffect(() => { - const sidebar = document.querySelector('[data-sidebar-link]')?.closest('aside'); - if (!sidebar) { + const sidebarLink = document.querySelector('[data-sidebar-link]'); + const scrollContainer = getScrollableParent(sidebarLink); + if (!scrollContainer) { const noOp = () => {}; return noOp; } const onLinkClick = (e: Event) => { const target = e.target as HTMLElement; - if (target.hasAttribute('data-sidebar-link')) { - const top = target.getBoundingClientRect().top; - sessionStorage.setItem('sidebar-link-position', top.toString()); + if ( + target.hasAttribute('data-sidebar-link') || + target.closest('[data-sidebar-link]') + ) { + const link = target.hasAttribute('data-sidebar-link') + ? target + : target.closest('[data-sidebar-link]'); + if (link) { + const top = link.getBoundingClientRect().top; + sessionStorage.setItem('sidebar-link-position', top.toString()); + } } }; - sidebar.addEventListener('click', onLinkClick); + scrollContainer.addEventListener('click', onLinkClick); // track active link position on scroll as well const onSidebarScroll = debounce(() => { const activeLink = document.querySelector(activeLinkSelector); @@ -32,28 +63,37 @@ export function ScrollActiveLink({activeLinkSelector}: Props) { } }, 50); - sidebar.addEventListener('scroll', onSidebarScroll); + scrollContainer.addEventListener('scroll', onSidebarScroll); return () => { - sidebar.removeEventListener('click', onLinkClick); - sidebar.removeEventListener('scroll', onSidebarScroll); + scrollContainer.removeEventListener('click', onLinkClick); + scrollContainer.removeEventListener('scroll', onSidebarScroll); }; }, [activeLinkSelector]); useEffect(() => { - const activeLink = document.querySelector(activeLinkSelector); - const sidebar = activeLink?.closest('aside')!; - if (!activeLink || !sidebar) { + const activeLink = document.querySelector(activeLinkSelector) as HTMLElement | null; + if (!activeLink) { + return; + } + + const scrollContainer = getScrollableParent(activeLink); + if (!scrollContainer) { return; } + const previousBoundingRectTop = sessionStorage.getItem('sidebar-link-position'); const currentBoundingRectTop = activeLink.getBoundingClientRect().top; - // scroll the sidebar to make sure the active link is visible & has the same position as when it was clicked - if (!previousBoundingRectTop) { - return; + + // If we have a previous position, try to maintain the same visual position + if (previousBoundingRectTop) { + const scrollX = 0; + const scrollY = + scrollContainer.scrollTop + currentBoundingRectTop - +previousBoundingRectTop; + scrollContainer.scrollTo(scrollX, scrollY); + } else { + // No previous position - scroll the active link into view (centered in sidebar) + activeLink.scrollIntoView({block: 'center', behavior: 'instant'}); } - const scrollX = 0; - const scrollY = sidebar.scrollTop + currentBoundingRectTop - +previousBoundingRectTop; - sidebar?.scrollTo(scrollX, scrollY); }, [activeLinkSelector]); // don't render anything, just exist as a client-side component for the useEffect. return null; diff --git a/src/components/header.tsx b/src/components/header.tsx index 3cb5557500960..d51812f887b90 100644 --- a/src/components/header.tsx +++ b/src/components/header.tsx @@ -1,17 +1,21 @@ 'use client'; +import {useCallback, useState} from 'react'; import {HamburgerMenuIcon} from '@radix-ui/react-icons'; +import {Button} from '@radix-ui/themes'; import Image from 'next/image'; import Link from 'next/link'; import SentryLogoSVG from 'sentry-docs/logos/sentry-logo-dark.svg'; +import {Platform} from 'sentry-docs/types'; import sidebarStyles from './sidebar/style.module.scss'; -import {MobileMenu} from './mobileMenu'; -import {NavLink} from './navlink'; +import {MagicIcon} from './cutomIcons/magic'; +import {useHomeSearchVisibility} from './homeSearchVisibility'; import {Search} from './search'; import {ThemeToggle} from './theme-toggle'; +import TopNavClient from './TopNavClient'; export const sidebarToggleId = sidebarStyles['navbar-menu-toggle']; @@ -19,20 +23,35 @@ type Props = { pathname: string; searchPlatforms: string[]; noSearch?: boolean; + platforms?: Platform[]; useStoredSearchPlatforms?: boolean; }; -export function Header({ +export default function Header({ pathname, searchPlatforms, noSearch, useStoredSearchPlatforms, + platforms = [], }: Props) { + const isHomePage = pathname === '/'; + const [homeSearchVisible, setHomeSearchVisible] = useState(true); + + // Listen for home search visibility changes + useHomeSearchVisibility( + useCallback((isVisible: boolean) => { + setHomeSearchVisible(isVisible); + }, []) + ); + + // Show header search if: not on home page, OR on home page but home search is scrolled out of view + const showHeaderSearch = !isHomePage || !homeSearchVisible; + return (
{/* define a header-height variable for consumption by other components */} - -
+ )} + {/* Mobile: show Ask AI button and theme toggle */} +
+ + +
); } diff --git a/src/components/homeSearchVisibility.tsx b/src/components/homeSearchVisibility.tsx new file mode 100644 index 0000000000000..6695c6240d6f5 --- /dev/null +++ b/src/components/homeSearchVisibility.tsx @@ -0,0 +1,60 @@ +'use client'; + +import {useEffect, useRef} from 'react'; + +// Custom event to communicate search visibility across components +const SEARCH_VISIBILITY_EVENT = 'home-search-visibility'; + +export function useHomeSearchVisible() { + // This hook is used by the header to know if home search is visible + if (typeof window === 'undefined') { + return true; // SSR default + } + return (window as any).__homeSearchVisible ?? true; +} + +export function HomeSearchObserver({children}: {children: React.ReactNode}) { + const ref = useRef(null); + + useEffect(() => { + const element = ref.current; + if (!element) { + return undefined; + } + + const observer = new IntersectionObserver( + ([entry]) => { + const isVisible = entry.isIntersecting; + (window as any).__homeSearchVisible = isVisible; + window.dispatchEvent( + new CustomEvent(SEARCH_VISIBILITY_EVENT, {detail: {isVisible}}) + ); + }, + { + threshold: 0, + rootMargin: '-64px 0px 0px 0px', // Account for header height + } + ); + + observer.observe(element); + return () => observer.disconnect(); + }, []); + + return
{children}
; +} + +export function useHomeSearchVisibility(callback: (isVisible: boolean) => void) { + useEffect(() => { + const handler = (e: CustomEvent<{isVisible: boolean}>) => { + callback(e.detail.isVisible); + }; + + window.addEventListener(SEARCH_VISIBILITY_EVENT as any, handler as EventListener); + return () => { + window.removeEventListener( + SEARCH_VISIBILITY_EVENT as any, + handler as EventListener + ); + }; + }, [callback]); +} diff --git a/src/components/mobileMenu/index.tsx b/src/components/mobileMenu/index.tsx index 300566be95488..61b20824293f3 100644 --- a/src/components/mobileMenu/index.tsx +++ b/src/components/mobileMenu/index.tsx @@ -8,17 +8,23 @@ import {Search} from 'sentry-docs/components/search'; import styles from './styles.module.scss'; -import {ThemeToggle} from '../theme-toggle'; - type Props = { pathname: string; searchPlatforms: string[]; }; export function MobileMenu({pathname, searchPlatforms}: Props) { + const mainSections = [ + {label: 'Products', href: '/product/sentry'}, + {label: 'SDKs', href: '/platforms/'}, + {label: 'Concepts & Reference', href: '/concepts/'}, + {label: 'Admin', href: '/organization/'}, + {label: 'API', href: '/api/'}, + {label: 'Security, Legal, & PII', href: '/security-legal-pii/'}, + ]; + return (
- + )} +
+ {guides.map(guide => ( + + ))} + + ); + } return ( {/* This is a hack. The Label allows us to have a clickable button inside the item without triggering its selection */} @@ -295,13 +419,17 @@ function PlatformItem({ - + {guides.map(guide => { - return ; + return ; })} ); @@ -340,20 +468,63 @@ function PlatformItem({ type GuideItemProps = { guide: (PlatformGuide | PlatformIntegration) & {isLastGuide: boolean}; + dropdownStyle?: boolean; + listOnly?: boolean; }; -function GuideItem({guide}: GuideItemProps) { +function GuideItem({guide, dropdownStyle = false, listOnly = false}: GuideItemProps) { + if (listOnly) { + return ( +
{ + if (typeof window !== 'undefined') { + window.location.href = guide.url; + } + }} + > + + + {/* replace dots with zero width space + period to allow text wrapping before periods + without breaking words in weird places + */} + {(guide.title ?? guide.name ?? guide.key).replace(/\./g, '\u200B.')} + +
+ ); + } return ( - +
diff --git a/src/components/search/search.module.scss b/src/components/search/search.module.scss index 6b564adc036c5..2314d378f682f 100644 --- a/src/components/search/search.module.scss +++ b/src/components/search/search.module.scss @@ -17,24 +17,25 @@ } .search { - --sgs-bg-color: var(--gray-2); + --sgs-bg-color: #ffffff; --sgs-color-border: var(--desatPurple12); --sgs-color-white: #ffffff; --sgs-color-progress-indicator: var(--desatPurple1); - --sgs-color-result-heading-background: var(--desatPurple4); + --sgs-color-result-heading-background: var(--accent-purple); --sgs-color-result-heading-text: #ffffff; - --sgs-color-hit-text: var(--desatPurple1); - --sgs-color-hit-highlight: var(--flame0); - --sgs-color-hit-context: var(--desatPurple4); - --sgs-color-hit-hover-background: var(--lightestPurpleBackground); - --sgs-color-expand-results-background: var(--lightestPurpleBackground); - --sgs-color-expand-results-text: var(--desatPurple6); + --sgs-color-hit-text: var(--gray-12); + --sgs-color-hit-highlight: var(--accent-purple); + --sgs-color-hit-context: var(--gray-11); + --sgs-color-hit-hover-background: var(--gray-a3); + --sgs-color-expand-results-background: var(--gray-a3); + --sgs-color-expand-results-text: var(--gray-11); position: relative; box-sizing: border-box; + width: 100%; } .search-bar { @@ -42,6 +43,7 @@ flex-direction: row; align-items: center; gap: 1rem; + width: 100%; @media screen and (max-width: 768px) { flex-direction: column; @@ -77,7 +79,6 @@ border-color: var(--accent-purple); box-shadow: 0 0 0 0.2rem var(--accent-purple-light); background-color: var(--gray-a1); - min-width: 20rem; } &::placeholder { color: var(--foreground-secondary); @@ -87,6 +88,8 @@ .input-wrapper { position: relative; width: 100%; + flex: 1; + min-width: 0; } .search-hotkey { @@ -106,23 +109,29 @@ .sgs-search-results { position: absolute; margin-top: 1rem; - z-index: 5; - box-shadow: var(--shadow-6); - border-radius: 0.25rem; - background-color: var(--sgs-bg-color); + z-index: 100; + box-shadow: 0 4px 24px 0 rgba(0, 0, 0, 0.12), 0 2px 8px 0 rgba(0, 0, 0, 0.08); + border-radius: 1rem; + background-color: #ffffff; font-size: 0.875rem; max-height: 80vh; display: flex; flex-direction: column; overflow: hidden; width: 500px; - border: 1.5px solid var(--gray-a4); + border: 1px solid var(--gray-a3); @media screen and (max-width: 768px) { width: calc(100vw - 4 * 1rem); left: auto; } + :global(.dark) & { + background-color: var(--gray-2); + box-shadow: 0 4px 24px 0 rgba(0, 0, 0, 0.4), 0 2px 8px 0 rgba(0, 0, 0, 0.3); + border: 1px solid var(--gray-5); + } + :global(.logo) { margin-top: 0.75rem; margin-bottom: 0.5rem; @@ -150,10 +159,12 @@ .sgs-hit-list { list-style: none; margin: 0; - padding: 0.25rem; + padding: 0.25rem 0.5rem; &.sgs-offsite { background-color: var(--sgs-color-hit-hover-background); + border-radius: 0.5rem; + margin: 0.25rem 0.5rem; .sgs-hit-item > a:hover { background-color: var(--sgs-bg-color); @@ -163,6 +174,7 @@ .sgs-hit-item { scroll-margin: 10px; + margin: 0.25rem; mark { color: var(--sgs-color-hit-highlight); @@ -174,9 +186,10 @@ display: block; text-decoration: none; color: var(--sgs-color-hit-text); - padding: 0.75rem; line-height: 1.5; + border-radius: 0.5rem; + transition: background-color 0.15s ease; } a:hover, @@ -193,7 +206,7 @@ .sgs-ai { color: var(--sgs-color-hit-text); - padding: 0.25rem; + padding: 0.5rem; &-button { padding: 0.75rem; @@ -202,6 +215,8 @@ align-items: center; gap: 0.75rem; color: inherit; + border-radius: 0.5rem; + transition: background-color 0.15s ease; &:hover, .sgs-ai-focused & { diff --git a/src/components/sidebar/MobileSidebarNav.tsx b/src/components/sidebar/MobileSidebarNav.tsx new file mode 100644 index 0000000000000..0cc4bd0cbf05a --- /dev/null +++ b/src/components/sidebar/MobileSidebarNav.tsx @@ -0,0 +1,120 @@ +'use client'; + +import {Fragment, useState} from 'react'; +import {ChevronDownIcon} from '@radix-ui/react-icons'; +import Link from 'next/link'; +import {usePathname} from 'next/navigation'; + +const productSections = [ + {label: 'Sentry Basics', href: '/product/sentry-basics/'}, + {label: 'AI in Sentry', href: '/product/ai-in-sentry/'}, + {label: 'Insights', href: '/product/insights/'}, + {label: 'User Feedback', href: '/product/user-feedback/'}, + {label: 'Uptime Monitoring', href: '/product/uptime-monitoring/'}, + {label: 'Dashboards', href: '/product/dashboards/'}, + {label: 'Projects', href: '/product/projects/'}, + {label: 'Explore', href: '/product/explore/'}, + {label: 'Issues', href: '/product/issues/'}, + {label: 'Alerts', href: '/product/alerts/'}, + {label: 'Crons', href: '/product/crons/'}, + {label: 'Releases', href: '/product/releases/'}, + {label: 'Relay', href: '/product/relay/'}, + {label: 'Sentry MCP', href: '/product/sentry-mcp/'}, + {label: 'Sentry Toolbar', href: '/product/sentry-toolbar/'}, + {label: 'Stats', href: '/product/stats/'}, + {label: 'Codecov', href: '/product/codecov/'}, + {label: 'Onboarding', href: '/product/onboarding/'}, +]; + +const conceptsSections = [ + {label: 'Key Terms', href: '/concepts/key-terms/'}, + {label: 'Search', href: '/concepts/search/'}, + {label: 'Migration', href: '/concepts/migration/'}, + {label: 'Data Management', href: '/concepts/data-management/'}, + {label: 'Sentry CLI', href: '/cli/'}, +]; + +const adminSections = [ + {label: 'Account Settings', href: '/account/'}, + {label: 'Organization Settings', href: '/organization/'}, + {label: 'Pricing & Billing', href: '/pricing'}, +]; + +const mainSections = [ + {label: 'SDKs', href: '/platforms/'}, + {label: 'Product', href: '/product/', dropdown: productSections}, + {label: 'Concepts', href: '/concepts/', dropdown: conceptsSections}, + {label: 'Admin', href: '/organization/', dropdown: adminSections}, + {label: 'API', href: '/api/'}, + {label: 'Security, Legal, & PII', href: '/security-legal-pii/'}, +]; + +export function MobileSidebarNav() { + const pathname = usePathname(); + const [expandedSection, setExpandedSection] = useState(null); + + const toggleSection = (label: string) => { + setExpandedSection(prev => (prev === label ? null : label)); + }; + + const isActive = (href: string) => pathname?.startsWith(href); + + return ( +
+ {/* Main navigation sections */} + +
+ ); +} diff --git a/src/components/sidebar/SidebarMoreLinks.tsx b/src/components/sidebar/SidebarMoreLinks.tsx new file mode 100644 index 0000000000000..420aa416f32aa --- /dev/null +++ b/src/components/sidebar/SidebarMoreLinks.tsx @@ -0,0 +1,63 @@ +'use client'; + +import {Fragment, useState} from 'react'; +import {ChevronDownIcon, ChevronRightIcon} from '@radix-ui/react-icons'; + +import styles from './style.module.scss'; + +import {SidebarLink} from './sidebarLink'; + +export function SidebarMoreLinks() { + const [isExpanded, setIsExpanded] = useState(false); + + return ( +
    +
  • +
      + {/* Always visible links */} + + + + {/* Collapsible "More" section - styled to match SidebarLink */} + + {isExpanded && ( + + + + + + + + )} +
    +
  • +
+ ); +} diff --git a/src/components/sidebar/dynamicNav.tsx b/src/components/sidebar/dynamicNav.tsx index a94c9b6c717d4..293f716b9dff5 100644 --- a/src/components/sidebar/dynamicNav.tsx +++ b/src/components/sidebar/dynamicNav.tsx @@ -296,7 +296,7 @@ export function DynamicNav({ } const {path} = serverContext(); - const isActive = path.join('/').indexOf(root) === 0; + const isActive = getUnversionedPath(path, false) === root; const linkPath = `/${path.join('/')}/`; const unversionedPath = getUnversionedPath(path, false); @@ -320,7 +320,7 @@ export function DynamicNav({ return (
  • {header} - {(!collapsible || isActive) && entity.children && ( + {entity.children && entity.children.length > 0 && (!collapsible || isActive) && (
      {isPlatformSidebar && ( { - const platformPageForCurrentPath = - nodeForPath(rootNode, [ - 'platforms', - platform.name, - // take the :path in /platforms/:platformName/:path - // or /platforms/:platformName/guides/:guideName/:path when we're in a guide - ...path.slice(currentGuide ? 4 : 2), - ]) || - // try to go one page higher, example: go to /usage/ from /usage/something - nodeForPath(rootNode, [ - 'platforms', - platform.name, - ...path.slice(currentGuide ? 4 : 2, path.length - 1), - ]); - - return { - ...platform, - url: - platformPageForCurrentPath && !platformPageForCurrentPath.missing - ? '/' + platformPageForCurrentPath.path + '/' - : platform.url, - guides: platform.guides.map(guide => { - const guidePageForCurrentPath = nodeForPath(rootNode, [ + // Only show the platform selector and sidebar for SDKs/platforms section + if (path[0] === 'platforms') { + const currentPlatform = getCurrentPlatform(rootNode, path); + const currentGuide = getCurrentGuide(rootNode, path); + const platforms: Platform[] = !rootNode + ? [] + : extractPlatforms(rootNode).map(platform => { + const platformPageForCurrentPath = + nodeForPath(rootNode, [ 'platforms', platform.name, - 'guides', - guide.name, ...path.slice(currentGuide ? 4 : 2), + ]) || + nodeForPath(rootNode, [ + 'platforms', + platform.name, + ...path.slice(currentGuide ? 4 : 2, path.length - 1), ]); - return guidePageForCurrentPath && !guidePageForCurrentPath.missing - ? { - ...guide, - url: '/' + guidePageForCurrentPath.path + '/', - } - : guide; - }), - }; - }); + return { + ...platform, + url: + platformPageForCurrentPath && !platformPageForCurrentPath.missing + ? '/' + platformPageForCurrentPath.path + '/' + : platform.url, + guides: platform.guides.map(guide => { + const guidePageForCurrentPath = nodeForPath(rootNode, [ + 'platforms', + platform.name, + 'guides', + guide.name, + ...path.slice(currentGuide ? 4 : 2), + ]); + return guidePageForCurrentPath && !guidePageForCurrentPath.missing + ? { + ...guide, + url: '/' + guidePageForCurrentPath.path + '/', + } + : guide; + }), + }; + }); + + return ( + + ); + } + + // For all other sections, just show the sidebar navigation (no platform selector) return ( ); diff --git a/src/components/sidebar/productSidebar.tsx b/src/components/sidebar/productSidebar.tsx index 0425a4a0440c1..d392e63efc59d 100644 --- a/src/components/sidebar/productSidebar.tsx +++ b/src/components/sidebar/productSidebar.tsx @@ -1,7 +1,6 @@ import {nodeForPath} from 'sentry-docs/docTree'; import {DynamicNav, toTree} from './dynamicNav'; -import {SidebarLink, SidebarSeparator} from './sidebarLink'; import {NavNode, ProductSidebarProps} from './types'; import {docNodeToNavNode, getNavNodes} from './utils'; @@ -19,7 +18,6 @@ export function ProductSidebar({rootNode, items}: ProductSidebarProps) {
        {items.map(item => { const tree = itemTree(item.root); - return ( tree && ( ) ); })}
      - -
        -
      • -
          - - - - - -
        -
      • -
      + {/* External links menu removed from here */}
  • ); } diff --git a/src/components/sidebar/sidebarLink.tsx b/src/components/sidebar/sidebarLink.tsx index 0ecd33382f58c..498dfe391ddc7 100644 --- a/src/components/sidebar/sidebarLink.tsx +++ b/src/components/sidebar/sidebarLink.tsx @@ -13,12 +13,14 @@ export function SidebarLink({ collapsible, onClick, topLevel = false, + className, beta = false, isNew = false, }: { href: string; title: string; beta?: boolean; + className?: string; collapsible?: boolean; isActive?: boolean; isNew?: boolean; @@ -34,7 +36,7 @@ export function SidebarLink({ onClick={onClick} className={`${styles['sidebar-link']} ${isActive ? 'active' : ''} ${ topLevel ? styles['sidebar-link-top-level'] : '' - }`} + } ${className ?? ''}`} data-sidebar-link >
    diff --git a/src/components/sidebar/sidebarNavigation.tsx b/src/components/sidebar/sidebarNavigation.tsx index 88de1485506fc..1110423fce00e 100644 --- a/src/components/sidebar/sidebarNavigation.tsx +++ b/src/components/sidebar/sidebarNavigation.tsx @@ -9,58 +9,17 @@ import {SidebarSeparator} from './sidebarLink'; import {NavNode} from './types'; import {docNodeToNavNode, getNavNodes} from './utils'; -/** a root of `"some-root"` maps to the `/some-root/` url */ -// todo: we should probably get rid of this -const productSidebarItems = [ - { - title: 'Account Settings', - root: 'account', - }, - { - title: 'Organization Settings', - root: 'organization', - }, - { - title: 'Product Walkthroughs', - root: 'product', - }, - { - title: 'Pricing & Billing', - root: 'pricing', - }, - { - title: 'Sentry CLI', - root: 'cli', - }, - { - title: 'Sentry API', - root: 'api', - }, - { - title: 'Security, Legal, & PII', - root: 'security-legal-pii', - }, - { - title: 'Concepts & Reference', - root: 'concepts', - }, - { - title: 'Documentation Changelog', - root: 'changelog', - }, -]; - export async function SidebarNavigation({path}: {path: string[]}) { const rootNode = await getDocsRootNode(); - // product docs and platform-redirect page - if ( - productSidebarItems.some(el => el.root === path[0]) || - path[0] === 'platform-redirect' - ) { - return ; + + // Product section: just show the sidebar for /product/ and its children + if (path[0] === 'product') { + return ( + + ); } - // /platforms/:platformName/guides/:guideName + // SDKs/Platforms if (path[0] === 'platforms') { const platformName = path[1]; const guideName = path[3]; @@ -76,12 +35,100 @@ export async function SidebarNavigation({path}: {path: string[]}) { )} - ); } - // contributing pages + // Concepts & Reference + if (path[0] === 'concepts') { + return ( +
      + + + + + +
    + ); + } + + // Admin Settings + if (path[0] === 'organization' || path[0] === 'account' || path[0] === 'pricing') { + const adminItems = [ + {title: 'Account Settings', root: 'account'}, + {title: 'Organization Settings', root: 'organization'}, + {title: 'Pricing & Billing', root: 'pricing'}, + ]; + return ; + } + + // Security, Legal, & PII + if (path[0] === 'security-legal-pii') { + return ( +
      + +
    + ); + } + + // API Reference + if (path[0] === 'api') { + return ( +
      + +
    + ); + } + + // Contributing pages if (path[0] === 'contributing') { const contribNode = nodeForPath(rootNode, 'contributing'); if (contribNode) { @@ -98,6 +145,74 @@ export async function SidebarNavigation({path}: {path: string[]}) { } } + // Sentry CLI (standalone route) + if (path[0] === 'cli') { + return ( +
      + + + + + +
    + ); + } + + // Documentation Changelog + if (path[0] === 'changelog') { + const changelogNode = nodeForPath(rootNode, 'changelog'); + if (changelogNode) { + const changelogNodes: NavNode[] = getNavNodes([changelogNode], docNodeToNavNode); + return ( +
      + +
    + ); + } + // Return empty sidebar if no changelog node exists + return
      ; + } + // This should never happen, all cases need to be handled above throw new Error(`Unknown path: ${path.join('/')} - cannot render sidebar`); } diff --git a/src/components/sidebar/style.module.scss b/src/components/sidebar/style.module.scss index 3c6815b2782ae..9a26386e7029d 100644 --- a/src/components/sidebar/style.module.scss +++ b/src/components/sidebar/style.module.scss @@ -7,6 +7,7 @@ } } .sidebar { + margin-top: 0px; --sidebar-item-bg-hover: var(--accent-purple-light); --sidebar-item-color: var(--accent-purple); /* Light mode: use pure white background */ @@ -21,7 +22,6 @@ display: none; flex-shrink: 0; height: 100vh; - overflow-y: auto; /* Minimal, accessible scrollbar styling */ /* Firefox */ @@ -156,6 +156,16 @@ } } +.sidebar-main { + flex: 1; + overflow: auto; +} + +.sidebar-external-links { + flex: 0 0 auto; + padding-bottom: 0; +} + .sidebar-link-content { display: flex; align-items: center; diff --git a/src/docTree.ts b/src/docTree.ts index e32ce6122a2ba..f79f5aebabcc0 100644 --- a/src/docTree.ts +++ b/src/docTree.ts @@ -146,28 +146,30 @@ function frontmatterToTree(frontmatter: FrontMatter[]): DocNode { rootNode.children.push(node); slugMap[slug] = node; } else { - const parentSlug = slugParts.slice(0, slugParts.length - 1).join('/'); - let parent: DocNode | undefined = slugMap[parentSlug]; - if (!parent) { - const grandparentSlug = slugParts.slice(0, slugParts.length - 2).join('/'); - const grandparent = slugMap[grandparentSlug]; - if (!grandparent) { - throw new Error('missing parent and grandparent: ' + parentSlug); - } - parent = { + let parent: DocNode | undefined; + // Walk up the tree and create missing parents as needed + for (let i = slugParts.length - 1; i > 0; i--) { + const parentSlug = slugParts.slice(0, i).join('/'); + parent = slugMap[parentSlug]; + if (parent) break; + + // Create missing parent node + const grandparentSlug = slugParts.slice(0, i - 1).join('/'); + const grandparent = slugMap[grandparentSlug] || rootNode; + const missingParent: DocNode = { path: parentSlug, - slug: slugParts[slugParts.length - 2], + slug: slugParts[i - 1], frontmatter: { - slug: slugParts[slugParts.length - 2], - // not ideal + slug: slugParts[i - 1], title: '', }, parent: grandparent, children: [], missing: true, }; - grandparent.children.push(parent); - slugMap[parentSlug] = parent; + grandparent.children.push(missingParent); + slugMap[parentSlug] = missingParent; + parent = missingParent; } const node = { path: slug, @@ -178,7 +180,7 @@ function frontmatterToTree(frontmatter: FrontMatter[]): DocNode { missing: false, sourcePath: doc.sourcePath, }; - parent.children.push(node); + parent!.children.push(node); slugMap[slug] = node; } }); diff --git a/src/imgs/Linkedin-1128x191.png b/src/imgs/Linkedin-1128x191.png new file mode 100644 index 0000000000000..f6e404e086083 Binary files /dev/null and b/src/imgs/Linkedin-1128x191.png differ diff --git a/src/imgs/ai-sentry-hero.png b/src/imgs/ai-sentry-hero.png new file mode 100644 index 0000000000000..cca064cc84cb9 Binary files /dev/null and b/src/imgs/ai-sentry-hero.png differ diff --git a/src/imgs/background-gradient-afternoon.png b/src/imgs/background-gradient-afternoon.png new file mode 100644 index 0000000000000..dcf5c2ae48036 Binary files /dev/null and b/src/imgs/background-gradient-afternoon.png differ diff --git a/src/imgs/error-monitoring-hero.png b/src/imgs/error-monitoring-hero.png new file mode 100644 index 0000000000000..9003d88382117 Binary files /dev/null and b/src/imgs/error-monitoring-hero.png differ diff --git a/src/imgs/pink-shape-06.png b/src/imgs/pink-shape-06.png new file mode 100644 index 0000000000000..ac41a4a1e5151 Binary files /dev/null and b/src/imgs/pink-shape-06.png differ diff --git a/src/imgs/squiggle.svg b/src/imgs/squiggle.svg new file mode 100644 index 0000000000000..d41e49ab3b858 --- /dev/null +++ b/src/imgs/squiggle.svg @@ -0,0 +1,614 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/imgs/yellow-shape-05.png b/src/imgs/yellow-shape-05.png new file mode 100644 index 0000000000000..a7a62885b1efa Binary files /dev/null and b/src/imgs/yellow-shape-05.png differ diff --git a/src/imgs/yellow-shape-06.png b/src/imgs/yellow-shape-06.png new file mode 100644 index 0000000000000..c606603115c3f Binary files /dev/null and b/src/imgs/yellow-shape-06.png differ diff --git a/src/imgs/yellow-shape-08.png b/src/imgs/yellow-shape-08.png new file mode 100644 index 0000000000000..98da0da4d6ff5 Binary files /dev/null and b/src/imgs/yellow-shape-08.png differ diff --git a/src/imgs/yellow-shape-13.png b/src/imgs/yellow-shape-13.png new file mode 100644 index 0000000000000..67756a7117d55 Binary files /dev/null and b/src/imgs/yellow-shape-13.png differ diff --git a/src/mdx.ts b/src/mdx.ts index 1d053c3233831..d2f329c65fb2c 100644 --- a/src/mdx.ts +++ b/src/mdx.ts @@ -15,7 +15,7 @@ import { createBrotliCompress, createBrotliDecompress, } from 'node:zlib'; -import {limitFunction} from 'p-limit'; +import pLimit from 'p-limit'; import rehypeAutolinkHeadings from 'rehype-autolink-headings'; import rehypePresetMinify from 'rehype-preset-minify'; import rehypePrismDiff from 'rehype-prism-diff'; @@ -240,29 +240,27 @@ export async function getDevDocsFrontMatterUncached(): Promise { const folder = 'develop-docs'; const docsPath = path.join(root, folder); const files = await getAllFilesRecursively(docsPath); + const limit = pLimit(FILE_CONCURRENCY_LIMIT); const frontMatters = ( await Promise.all( - files.map( - limitFunction( - async file => { - const fileName = file.slice(docsPath.length + 1); - if (path.extname(fileName) !== '.md' && path.extname(fileName) !== '.mdx') { - return undefined; - } + files.map(file => + limit(async () => { + const fileName = file.slice(docsPath.length + 1); + if (path.extname(fileName) !== '.md' && path.extname(fileName) !== '.mdx') { + return undefined; + } - const source = await readFile(file, 'utf8'); - const {data: frontmatter} = matter(source); - return { - ...(frontmatter as FrontMatter), - slug: fileName.replace(/\/index.mdx?$/, '').replace(/\.mdx?$/, ''), - sourcePath: path.join(folder, fileName), - }; - }, - {concurrency: FILE_CONCURRENCY_LIMIT} - ) + const source = await readFile(file, 'utf8'); + const {data: frontmatter} = matter(source); + return { + ...(frontmatter as FrontMatter), + slug: fileName.replace(/\/index.mdx?$/, '').replace(/\.mdx?$/, ''), + sourcePath: path.join(folder, fileName), + }; + }) ) ) - ).filter(isNotNil); + ).filter(isNotNil) as FrontMatter[]; return frontMatters; } @@ -279,30 +277,28 @@ async function getAllFilesFrontMatter(): Promise { const docsPath = path.join(root, 'docs'); const files = await getAllFilesRecursively(docsPath); const allFrontMatter: FrontMatter[] = []; + const limit = pLimit(FILE_CONCURRENCY_LIMIT); await Promise.all( - files.map( - limitFunction( - async file => { - const fileName = file.slice(docsPath.length + 1); - if (path.extname(fileName) !== '.md' && path.extname(fileName) !== '.mdx') { - return; - } + files.map(file => + limit(async () => { + const fileName = file.slice(docsPath.length + 1); + if (path.extname(fileName) !== '.md' && path.extname(fileName) !== '.mdx') { + return; + } - if (fileName.indexOf('/common/') !== -1) { - return; - } + if (fileName.indexOf('/common/') !== -1) { + return; + } - const source = await readFile(file, 'utf8'); - const {data: frontmatter} = matter(source); - allFrontMatter.push({ - ...(frontmatter as FrontMatter), - slug: formatSlug(fileName), - sourcePath: path.join('docs', fileName), - }); - }, - {concurrency: FILE_CONCURRENCY_LIMIT} - ) + const source = await readFile(file, 'utf8'); + const {data: frontmatter} = matter(source); + allFrontMatter.push({ + ...(frontmatter as FrontMatter), + slug: formatSlug(fileName), + sourcePath: path.join('docs', fileName), + }); + }) ) ); @@ -339,50 +335,44 @@ async function getAllFilesFrontMatter(): Promise { ); const commonFiles = await Promise.all( - commonFileNames.map( - limitFunction( - async commonFileName => { - const source = await readFile(commonFileName, 'utf8'); - const {data: frontmatter} = matter(source); - return {commonFileName, frontmatter: frontmatter as FrontMatter}; - }, - {concurrency: FILE_CONCURRENCY_LIMIT} - ) + commonFileNames.map(commonFileName => + limit(async () => { + const source = await readFile(commonFileName, 'utf8'); + const {data: frontmatter} = matter(source); + return {commonFileName, frontmatter: frontmatter as FrontMatter}; + }) ) ); await Promise.all( - commonFiles.map( - limitFunction( - async f => { - if (!isSupported(f.frontmatter, platformName)) { - return; - } + commonFiles.map(f => + limit(async () => { + if (!isSupported(f.frontmatter, platformName)) { + return; + } - const subpath = f.commonFileName.slice(commonPath.length + 1); - const slug = f.commonFileName - .slice(docsPath.length + 1) - .replace(/\/common\//, '/'); - const noFrontMatter = ( - await Promise.allSettled([ - access(path.join(docsPath, slug)), - access(path.join(docsPath, slug.replace('/index.mdx', '.mdx'))), - ]) - ).every(r => r.status === 'rejected'); - if (noFrontMatter) { - let frontmatter = f.frontmatter; - if (subpath === 'index.mdx') { - frontmatter = {...frontmatter, ...platformFrontmatter}; - } - allFrontMatter.push({ - ...frontmatter, - slug: formatSlug(slug), - sourcePath: 'docs/' + f.commonFileName.slice(docsPath.length + 1), - }); + const subpath = f.commonFileName.slice(commonPath.length + 1); + const slug = f.commonFileName + .slice(docsPath.length + 1) + .replace(/\/common\//, '/'); + const noFrontMatter = ( + await Promise.allSettled([ + access(path.join(docsPath, slug)), + access(path.join(docsPath, slug.replace('/index.mdx', '.mdx'))), + ]) + ).every(r => r.status === 'rejected'); + if (noFrontMatter) { + let frontmatter = f.frontmatter; + if (subpath === 'index.mdx') { + frontmatter = {...frontmatter, ...platformFrontmatter}; } - }, - {concurrency: FILE_CONCURRENCY_LIMIT} - ) + allFrontMatter.push({ + ...frontmatter, + slug: formatSlug(slug), + sourcePath: 'docs/' + f.commonFileName.slice(docsPath.length + 1), + }); + } + }) ) ); @@ -412,40 +402,37 @@ async function getAllFilesFrontMatter(): Promise { } await Promise.all( - commonFiles.map( - limitFunction( - async f => { - if (!isSupported(f.frontmatter, platformName, guideName)) { - return; - } - - const subpath = f.commonFileName.slice(commonPath.length + 1); - const slug = path.join( - 'platforms', - platformName, - 'guides', - guideName, - subpath - ); - try { - await access(path.join(docsPath, slug)); - return; - } catch { - // pass - } - - let frontmatter = f.frontmatter; - if (subpath === 'index.mdx') { - frontmatter = {...frontmatter, ...guideFrontmatter}; - } - allFrontMatter.push({ - ...frontmatter, - slug: formatSlug(slug), - sourcePath: 'docs/' + f.commonFileName.slice(docsPath.length + 1), - }); - }, - {concurrency: FILE_CONCURRENCY_LIMIT} - ) + commonFiles.map(f => + limit(async () => { + if (!isSupported(f.frontmatter, platformName, guideName)) { + return; + } + + const subpath = f.commonFileName.slice(commonPath.length + 1); + const slug = path.join( + 'platforms', + platformName, + 'guides', + guideName, + subpath + ); + try { + await access(path.join(docsPath, slug)); + return; + } catch { + // pass + } + + let frontmatter = f.frontmatter; + if (subpath === 'index.mdx') { + frontmatter = {...frontmatter, ...guideFrontmatter}; + } + allFrontMatter.push({ + ...frontmatter, + slug: formatSlug(slug), + sourcePath: 'docs/' + f.commonFileName.slice(docsPath.length + 1), + }); + }) ) ); } diff --git a/src/seer-image.jpg b/src/seer-image.jpg new file mode 100644 index 0000000000000..47fb3a79025b7 Binary files /dev/null and b/src/seer-image.jpg differ