@myst-theme/site 0.1.37 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myst-theme/site",
3
- "version": "0.1.37",
3
+ "version": "0.2.0",
4
4
  "main": "./src/index.ts",
5
5
  "types": "./src/index.ts",
6
6
  "files": [
@@ -16,16 +16,16 @@
16
16
  "dependencies": {
17
17
  "@headlessui/react": "^1.7.13",
18
18
  "@heroicons/react": "^2.0.14",
19
- "@myst-theme/diagrams": "^0.1.37",
20
- "@myst-theme/frontmatter": "^0.1.37",
21
- "@myst-theme/jupyter": "^0.1.37",
22
- "@myst-theme/providers": "^0.1.37",
19
+ "@myst-theme/diagrams": "^0.2.0",
20
+ "@myst-theme/frontmatter": "^0.2.0",
21
+ "@myst-theme/jupyter": "^0.2.0",
22
+ "@myst-theme/providers": "^0.2.0",
23
23
  "classnames": "^2.3.2",
24
24
  "lodash.throttle": "^4.1.1",
25
25
  "myst-common": "^0.0.16",
26
26
  "myst-config": "^0.0.14",
27
- "myst-demo": "^0.1.37",
28
- "myst-to-react": "^0.1.37",
27
+ "myst-demo": "^0.2.0",
28
+ "myst-to-react": "^0.2.0",
29
29
  "nbtx": "^0.2.3",
30
30
  "node-cache": "^5.1.2",
31
31
  "node-fetch": "^2.6.7",
@@ -1,13 +1,16 @@
1
1
  import { useParse, DEFAULT_RENDERERS } from 'myst-to-react';
2
- import type { GenericParent, SourceFileKind } from 'myst-common';
2
+ import { SourceFileKind } from 'myst-common';
3
+ import type { GenericParent } from 'myst-common';
3
4
  import { useNodeRenderers } from '@myst-theme/providers';
4
5
  import classNames from 'classnames';
5
6
  import { ClearCell, RunCell } from './ComputeControls';
6
7
 
7
8
  function isACodeCell(node: GenericParent) {
8
9
  return (
10
+ node &&
9
11
  node.type === 'block' &&
10
- node.children.length === 2 &&
12
+ node.children &&
13
+ node.children?.length === 2 &&
11
14
  node.children[0].type === 'code' &&
12
15
  node.children[1].type === 'output'
13
16
  );
@@ -50,16 +53,15 @@ function Block({
50
53
  }
51
54
 
52
55
  export function ContentBlocks({
53
- name,
54
- pageKind,
55
56
  mdast,
57
+ pageKind = SourceFileKind.Article,
56
58
  className,
57
59
  }: {
58
- name: string;
59
- pageKind: SourceFileKind;
60
60
  mdast: GenericParent;
61
+ pageKind?: SourceFileKind;
61
62
  className?: string;
62
63
  }) {
64
+ if (!mdast) return null;
63
65
  const blocks = mdast.children as GenericParent[];
64
66
  return (
65
67
  <>
@@ -55,12 +55,9 @@ async function mystLiveReloadConnect(config: { onOpen?: () => void; port?: strin
55
55
  }
56
56
 
57
57
  // Inspired by the LiveReload component in Remix
58
- export const ContentReload =
59
- process.env.NODE_ENV !== 'development'
60
- ? () => null
61
- : ({ port }: { port?: string | number }) => {
62
- useEffect(() => {
63
- mystLiveReloadConnect({ port });
64
- }, []);
65
- return null;
66
- };
58
+ export function ContentReload({ port }: { port?: string | number }) {
59
+ useEffect(() => {
60
+ mystLiveReloadConnect({ port });
61
+ }, []);
62
+ return null;
63
+ }
@@ -1,7 +1,7 @@
1
1
  import { useNavigation } from '@remix-run/react';
2
2
  import classNames from 'classnames';
3
3
  import throttle from 'lodash.throttle';
4
- import { useCallback, useEffect, useRef, useState } from 'react';
4
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
5
5
 
6
6
  const SELECTOR = [1, 2, 3, 4, 5, 6].map((n) => `main h${n}`).join(', ');
7
7
  const HIGHLIGHT_CLASS = 'highlight';
@@ -176,7 +176,7 @@ export function useOutlineHeight<T extends HTMLElement = HTMLElement>() {
176
176
  return () => {
177
177
  window.removeEventListener('scroll', handleScroll);
178
178
  };
179
- }, [container, outline, transitionState]);
179
+ }, [container.current, outline.current, transitionState]);
180
180
  return { container, outline };
181
181
  }
182
182
 
@@ -191,13 +191,18 @@ export const DocumentOutline = ({
191
191
  className?: string;
192
192
  }) => {
193
193
  const { activeId, headings, highlight } = useHeaders();
194
- if (headings.length <= 1) return <nav suppressHydrationWarning />;
194
+ if (headings.length <= 1 || !onClient) {
195
+ return <nav suppressHydrationWarning />;
196
+ }
195
197
  return (
196
198
  <nav
197
199
  ref={outlineRef}
198
200
  aria-label="Document Outline"
199
- suppressHydrationWarning
200
- className={classNames('not-prose transition-opacity overflow-y-auto', className)}
201
+ className={classNames(
202
+ 'not-prose overflow-y-auto hidden',
203
+ 'transition-opacity duration-700', // Animation on load
204
+ className,
205
+ )}
201
206
  style={{
202
207
  top: top ?? 0,
203
208
  maxHeight: `calc(100vh - ${(top ?? 0) + 20}px)`,
@@ -206,7 +211,7 @@ export const DocumentOutline = ({
206
211
  <div className="text-slate-900 mb-4 text-sm leading-6 dark:text-slate-100 uppercase">
207
212
  In this article
208
213
  </div>
209
- {onClient && <Headings headings={headings} activeId={activeId} highlight={highlight} />}
214
+ <Headings headings={headings} activeId={activeId} highlight={highlight} />
210
215
  </nav>
211
216
  );
212
217
  };
@@ -1,4 +1,4 @@
1
- import { Link } from '@remix-run/react';
1
+ import { useLinkProvider } from '@myst-theme/providers';
2
2
 
3
3
  export function ExternalOrInternalLink({
4
4
  to,
@@ -15,6 +15,7 @@ export function ExternalOrInternalLink({
15
15
  title?: string;
16
16
  children: React.ReactNode;
17
17
  }) {
18
+ const Link = useLinkProvider();
18
19
  if (to.startsWith('http') || isStatic) {
19
20
  return (
20
21
  <a href={to} className={className} target="_blank" rel="noopener noreferrer" title={title}>
@@ -1,18 +1,17 @@
1
1
  import classNames from 'classnames';
2
- import { Link } from '@remix-run/react';
3
2
  import ArrowLeftIcon from '@heroicons/react/24/outline/ArrowLeftIcon';
4
3
  import ArrowRightIcon from '@heroicons/react/24/outline/ArrowRightIcon';
5
4
  import type { FooterLinks, NavigationLink } from '../types';
6
- import { useUrlbase, withUrlbase } from '@myst-theme/providers';
7
- import { useEffect, useState } from 'react';
5
+ import { useLinkProvider, useBaseurl, withBaseurl } from '@myst-theme/providers';
8
6
 
9
7
  const FooterLink = ({ title, url, group, right }: NavigationLink & { right?: boolean }) => {
10
- const urlbase = useUrlbase();
8
+ const baseurl = useBaseurl();
9
+ const Link = useLinkProvider();
11
10
  return (
12
11
  <Link
13
12
  prefetch="intent"
14
13
  className="group flex-1 p-4 block border font-normal hover:border-blue-600 dark:hover:border-blue-400 no-underline hover:text-blue-600 dark:hover:text-blue-400 text-gray-600 dark:text-gray-100 border-gray-200 dark:border-gray-500 rounded shadow-sm hover:shadow-lg dark:shadow-neutral-700"
15
- to={withUrlbase(url, urlbase)}
14
+ to={withBaseurl(url, baseurl)}
16
15
  >
17
16
  <div className="flex align-middle h-full">
18
17
  {right && (
@@ -1,4 +1,4 @@
1
- import { useTransition } from '@remix-run/react';
1
+ import { useNavigation } from '@remix-run/react';
2
2
  import { useEffect, useMemo, useState } from 'react';
3
3
  import classNames from 'classnames';
4
4
 
@@ -6,7 +6,7 @@ import classNames from 'classnames';
6
6
  * Show a loading progess bad if the load takes more than 150ms
7
7
  */
8
8
  function useLoading() {
9
- const transitionState = useTransition().state;
9
+ const transitionState = useNavigation().state;
10
10
  const ref = useMemo<{ start?: NodeJS.Timeout; finish?: NodeJS.Timeout }>(() => ({}), []);
11
11
  const [showLoading, setShowLoading] = useState(false);
12
12
 
@@ -1,8 +1,14 @@
1
1
  import React, { useEffect, useRef } from 'react';
2
2
  import classNames from 'classnames';
3
- import { NavLink, useParams, useLocation, useNavigation } from '@remix-run/react';
3
+ import { useLocation, useNavigation } from '@remix-run/react';
4
4
  import type { SiteManifest } from 'myst-config';
5
- import { useNavOpen, useSiteManifest, useUrlbase, withUrlbase } from '@myst-theme/providers';
5
+ import {
6
+ useNavLinkProvider,
7
+ useNavOpen,
8
+ useSiteManifest,
9
+ useBaseurl,
10
+ withBaseurl,
11
+ } from '@myst-theme/providers';
6
12
  import { getProjectHeadings } from '../../loaders';
7
13
  import type { Heading } from '../../types';
8
14
 
@@ -26,8 +32,9 @@ const HeadingLink = ({
26
32
  children: React.ReactNode;
27
33
  }) => {
28
34
  const { pathname } = useLocation();
35
+ const NavLink = useNavLinkProvider();
29
36
  const exact = pathname === path;
30
- const urlbase = useUrlbase();
37
+ const baseurl = useBaseurl();
31
38
  const [, setOpen] = useNavOpen();
32
39
  return (
33
40
  <NavLink
@@ -44,7 +51,7 @@ const HeadingLink = ({
44
51
  !isActive,
45
52
  })
46
53
  }
47
- to={withUrlbase(path, urlbase)}
54
+ to={withBaseurl(path, baseurl)}
48
55
  suppressHydrationWarning // The pathname is not defined on the server always.
49
56
  onClick={() => {
50
57
  // Close the nav panel if it is open
@@ -138,14 +145,20 @@ export const TableOfContents = ({
138
145
  projectSlug?: string;
139
146
  footer?: React.ReactNode;
140
147
  }) => {
148
+ const footerRef = useRef<HTMLDivElement>(null);
141
149
  const [open] = useNavOpen();
142
150
  const config = useSiteManifest();
143
- const { folder, project } = useParams();
144
- const resolvedProjectSlug = projectSlug || (folder ?? project);
145
151
  if (!config) return null;
146
- const headings = getProjectHeadings(config, resolvedProjectSlug, {
152
+ const headings = getProjectHeadings(config, projectSlug, {
147
153
  addGroups: false,
148
154
  });
155
+ useEffect(() => {
156
+ setTimeout(() => {
157
+ if (!footerRef.current) return;
158
+ footerRef.current.style.opacity = '1';
159
+ footerRef.current.style.transform = 'none';
160
+ }, 500);
161
+ }, [footerRef]);
149
162
  if (!headings) return null;
150
163
  return (
151
164
  <div
@@ -175,9 +188,16 @@ export const TableOfContents = ({
175
188
  aria-label="Table of Contents"
176
189
  className="flex-grow overflow-y-auto transition-opacity mt-6 pb-3 ml-3 xl:ml-0 mr-3"
177
190
  >
178
- <Headings folder={resolvedProjectSlug} headings={headings} sections={config?.projects} />
191
+ <Headings folder={projectSlug} headings={headings} sections={config?.projects} />
179
192
  </nav>
180
- {footer && <div className="flex-none py-4">{footer}</div>}
193
+ {footer && (
194
+ <div
195
+ className="flex-none py-4 opacity-0 transition-all duration-700 translate-y-6"
196
+ ref={footerRef}
197
+ >
198
+ {footer}
199
+ </div>
200
+ )}
181
201
  </div>
182
202
  </div>
183
203
  );
@@ -1,4 +1,4 @@
1
- import { Link, NavLink } from '@remix-run/react';
1
+ import { NavLink } from '@remix-run/react';
2
2
  import { Fragment } from 'react';
3
3
  import classNames from 'classnames';
4
4
  import { Menu, Transition } from '@headlessui/react';
@@ -7,7 +7,13 @@ import MenuIcon from '@heroicons/react/24/solid/Bars3Icon';
7
7
  import ChevronDownIcon from '@heroicons/react/24/solid/ChevronDownIcon';
8
8
  import type { SiteManifest, SiteNavItem } from 'myst-config';
9
9
  import { ThemeButton } from './ThemeButton';
10
- import { useNavOpen, useSiteManifest } from '@myst-theme/providers';
10
+ import {
11
+ useBaseurl,
12
+ useLinkProvider,
13
+ useNavOpen,
14
+ useSiteManifest,
15
+ withBaseurl,
16
+ } from '@myst-theme/providers';
11
17
  import { LoadingBar } from './Loading';
12
18
 
13
19
  export const DEFAULT_NAV_HEIGHT = 60;
@@ -25,6 +31,7 @@ function ExternalOrInternalLink({
25
31
  nav?: boolean;
26
32
  prefetch?: 'intent' | 'render' | 'none';
27
33
  }) {
34
+ const Link = useLinkProvider();
28
35
  const staticClass = typeof className === 'function' ? className({ isActive: false }) : className;
29
36
  if (to.startsWith('http') || to.startsWith('mailto:')) {
30
37
  return (
@@ -176,11 +183,13 @@ function ActionMenu({ actions }: { actions?: SiteManifest['actions'] }) {
176
183
  }
177
184
 
178
185
  function HomeLink({ logo, logoText, name }: { logo?: string; logoText?: string; name?: string }) {
186
+ const Link = useLinkProvider();
187
+ const baseurl = useBaseurl();
179
188
  const nothingSet = !logo && !logoText;
180
189
  return (
181
190
  <Link
182
191
  className="flex items-center dark:text-white w-fit ml-3 md:ml-5 xl:ml-7"
183
- to="/"
192
+ to={withBaseurl('/', baseurl)}
184
193
  prefetch="intent"
185
194
  >
186
195
  {logo && (
@@ -16,7 +16,8 @@ export function getProject(
16
16
  config?: SiteManifest,
17
17
  projectSlug?: string,
18
18
  ): ManifestProject | undefined {
19
- if (!projectSlug || !config) return undefined;
19
+ if (!config) return undefined;
20
+ if (!projectSlug) return config.projects?.[0];
20
21
  const project = config.projects?.find((p) => p.slug === projectSlug);
21
22
  return project;
22
23
  }
@@ -32,12 +33,12 @@ export function getProjectHeadings(
32
33
  {
33
34
  title: project.title,
34
35
  slug: project.index,
35
- path: `/${project.slug}`,
36
+ path: project.slug ? `/${project.slug}` : '/',
36
37
  level: 'index',
37
38
  },
38
39
  ...project.pages.map((p) => {
39
40
  if (!('slug' in p)) return p;
40
- return { ...p, path: `/${project.slug}/${p.slug}` };
41
+ return { ...p, path: projectSlug ? `/${project.slug}/${p.slug}` : `/${p.slug}` };
41
42
  }),
42
43
  ];
43
44
  if (opts.addGroups) {
@@ -37,11 +37,7 @@ export const ArticlePage = React.memo(function ({ article }: { article: PageLoad
37
37
  </EnableCompute>
38
38
  )}
39
39
  </div>
40
- <ContentBlocks
41
- name={article.slug}
42
- pageKind={article.kind}
43
- mdast={article.mdast as GenericParent}
44
- />
40
+ <ContentBlocks pageKind={article.kind} mdast={article.mdast as GenericParent} />
45
41
  <Bibliography />
46
42
  {!hide_footer_links && <FooterLinksBlock links={article.footer} />}
47
43
  </NotebookProvider>
@@ -0,0 +1,8 @@
1
+ export function Error404() {
2
+ return (
3
+ <>
4
+ <h1>No Site Found - 404</h1>
5
+ <p>No website is available at this url, or an error occurred. Please double check the url.</p>
6
+ </>
7
+ );
8
+ }
@@ -1,6 +1,6 @@
1
1
  import type { SiteManifest } from 'myst-config';
2
2
  import type { SiteLoader } from '../types';
3
- import { SiteProvider, Theme, ThemeProvider } from '@myst-theme/providers';
3
+ import { BaseUrlProvider, SiteProvider, Theme, ThemeProvider } from '@myst-theme/providers';
4
4
  import {
5
5
  Links,
6
6
  LiveReload,
@@ -8,32 +8,45 @@ import {
8
8
  Outlet,
9
9
  Scripts,
10
10
  ScrollRestoration,
11
- useCatch,
12
11
  useLoaderData,
13
- Link as RemixLink,
12
+ Link,
13
+ NavLink,
14
14
  } from '@remix-run/react';
15
15
  import { ContentReload, renderers } from '../components';
16
16
  import { Analytics } from '../seo';
17
- import { ErrorSiteNotFound } from './ErrorSiteNotFound';
17
+ import { Error404 } from './Error404';
18
18
  import classNames from 'classnames';
19
19
  import { ThebeCoreProvider } from 'thebe-react';
20
20
  import { ConfiguredThebeServerProvider } from '@myst-theme/jupyter';
21
21
 
22
22
  export function Document({
23
23
  children,
24
+ scripts,
24
25
  theme,
25
26
  config,
26
27
  title,
27
- CONTENT_CDN_PORT,
28
28
  scrollTopClass = 'scroll-p-20',
29
+ staticBuild,
30
+ baseurl,
29
31
  }: {
30
32
  children: React.ReactNode;
33
+ scripts?: React.ReactNode;
31
34
  theme: Theme;
32
35
  config?: SiteManifest;
33
36
  title?: string;
34
- CONTENT_CDN_PORT?: number | string;
35
37
  scrollTopClass?: string;
38
+ staticBuild?: boolean;
39
+ baseurl?: string;
36
40
  }) {
41
+ const links = staticBuild
42
+ ? {
43
+ Link: (props: any) => <Link {...{ ...props, reloadDocument: true }} />,
44
+ NavLink: (props: any) => <NavLink {...{ ...props, reloadDocument: true }} />,
45
+ }
46
+ : {
47
+ Link: Link as any,
48
+ NavLink: NavLink as any,
49
+ };
37
50
  return (
38
51
  <html lang="en" className={classNames(theme, scrollTopClass)}>
39
52
  <head>
@@ -48,54 +61,56 @@ export function Document({
48
61
  />
49
62
  </head>
50
63
  <body className="m-0 transition-colors duration-500 bg-white dark:bg-stone-900">
51
- <ThemeProvider theme={theme} renderers={renderers} Link={RemixLink as any}>
52
- <ThebeCoreProvider>
53
- <SiteProvider config={config}>
54
- <ConfiguredThebeServerProvider>{children}</ConfiguredThebeServerProvider>
55
- </SiteProvider>
56
- </ThebeCoreProvider>
64
+ <ThemeProvider theme={theme} renderers={renderers} {...links}>
65
+ <BaseUrlProvider baseurl={baseurl}>
66
+ <ThebeCoreProvider>
67
+ <SiteProvider config={config}>
68
+ <ConfiguredThebeServerProvider>{children}</ConfiguredThebeServerProvider>
69
+ </SiteProvider>
70
+ </ThebeCoreProvider>
71
+ </BaseUrlProvider>
57
72
  </ThemeProvider>
58
73
  <ScrollRestoration />
59
74
  <Scripts />
60
- <LiveReload />
61
- <ContentReload port={CONTENT_CDN_PORT} />
75
+ {!staticBuild && <LiveReload />}
76
+ {scripts}
62
77
  </body>
63
78
  </html>
64
79
  );
65
80
  }
66
81
 
67
82
  export function App() {
68
- const { theme, config, CONTENT_CDN_PORT } = useLoaderData<SiteLoader>();
83
+ const { theme, config } = useLoaderData<SiteLoader>();
69
84
  return (
70
- <Document theme={theme} config={config} CONTENT_CDN_PORT={CONTENT_CDN_PORT}>
85
+ <Document theme={theme} config={config}>
71
86
  <Outlet />
72
87
  </Document>
73
88
  );
74
89
  }
75
90
 
76
- export function AppCatchBoundary() {
77
- const caught = useCatch();
91
+ export function AppWithReload() {
92
+ const { theme, config, CONTENT_CDN_PORT, MODE, BASE_URL } = useLoaderData<SiteLoader>();
78
93
  return (
79
- <Document theme={Theme.light} title={caught.statusText}>
80
- <article className="content">
81
- <main className="article-grid article-subgrid-gap col-screen">
82
- <ErrorSiteNotFound />
83
- </main>
84
- </article>
94
+ <Document
95
+ theme={theme}
96
+ config={config}
97
+ scripts={MODE === 'static' ? undefined : <ContentReload port={CONTENT_CDN_PORT} />}
98
+ staticBuild={MODE === 'static'}
99
+ baseurl={BASE_URL}
100
+ >
101
+ <Outlet />
85
102
  </Document>
86
103
  );
87
104
  }
88
105
 
89
- export function AppDebugErrorBoundary({ error }: { error: { message: string; stack: string } }) {
106
+ export function AppCatchBoundary() {
90
107
  return (
91
- <Document theme={Theme.light} title="Error">
92
- <div className="mt-16">
108
+ <Document theme={Theme.light}>
109
+ <article className="article">
93
110
  <main className="article-grid article-subgrid-gap col-screen">
94
- <h1>An Error Occurred</h1>
95
- <code>{error.message}</code>
96
- <pre>{error.stack}</pre>
111
+ <Error404 />
97
112
  </main>
98
- </div>
113
+ </article>
99
114
  </Document>
100
115
  );
101
116
  }
@@ -1,5 +1,5 @@
1
1
  export { ErrorProjectNotFound } from './ErrorProjectNotFound';
2
2
  export { ErrorDocumentNotFound } from './ErrorDocumentNotFound';
3
- export { ErrorSiteNotFound } from './ErrorSiteNotFound';
3
+ export { Error404 } from './Error404';
4
4
  export { ArticlePage, ArticlePageCatchBoundary, ProjectPageCatchBoundary } from './Article';
5
- export { App, Document, AppCatchBoundary, AppDebugErrorBoundary } from './Root';
5
+ export { App, AppWithReload, Document, AppCatchBoundary } from './Root';
@@ -145,14 +145,15 @@ export function getSiteSlugs(
145
145
  const slugs =
146
146
  site.projects
147
147
  ?.map((project) => {
148
+ const projectSlug = project.slug ? `/${project.slug}` : '';
148
149
  const pages = project.pages
149
150
  .filter((page): page is ManifestProjectItem => 'slug' in page)
150
- .map((page) => `${baseurl}/${project.slug}/${page.slug}`);
151
+ .map((page) => `${baseurl}${projectSlug}/${page.slug}`);
151
152
  if (opts?.excludeIndex) return [...pages];
152
153
  return [
153
154
  opts?.explicitIndex
154
- ? `${baseurl}/${project.slug}/${project.index}`
155
- : `${baseurl}/${project.slug}`,
155
+ ? `${baseurl}${projectSlug}/${project.index}`
156
+ : `${baseurl}${projectSlug}`,
156
157
  ...pages,
157
158
  ];
158
159
  })
package/src/types.ts CHANGED
@@ -17,6 +17,8 @@ export type SiteLoader = {
17
17
  theme: Theme;
18
18
  config?: SiteManifest;
19
19
  CONTENT_CDN_PORT?: string | number;
20
+ MODE?: 'app' | 'static';
21
+ BASE_URL?: string;
20
22
  };
21
23
 
22
24
  export type NavigationLink = {
@@ -38,6 +40,7 @@ export type PageLoader = {
38
40
  sha256: string;
39
41
  slug: string;
40
42
  domain: string; // This is written in at render time in the site
43
+ project: string; // This is written in at render time in the site
41
44
  frontmatter: PageFrontmatter;
42
45
  mdast: Root;
43
46
  references: References;
@@ -1,30 +0,0 @@
1
- export function ErrorSiteNotFound() {
2
- return (
3
- <>
4
- <h1>No Site Found</h1>
5
- <p>No website is available at this url, please double check the url.</p>
6
- <h3>What&apos;s next?</h3>
7
- <p>
8
- If you are expecting to see <span className="font-semibold">your website</span> here and you
9
- think that something has gone wrong, please send an email to{' '}
10
- <a
11
- href={`mailto:support@curvenote.com?subject=Website%20Unavailable&body=${encodeURIComponent(
12
- `My website is not available. 😥`,
13
- )}`}
14
- >
15
- support@curvenote.com
16
- </a>
17
- , or let us know on our <a href="https://slack.curvenote.dev">community slack</a>, and
18
- we&apos;ll help out.
19
- </p>
20
- <p>
21
- Or create a new temporary website from Markdown and Jupyter Notebooks using{' '}
22
- <a href="https://try.curvenote.com">try.curvenote.com</a>.
23
- </p>
24
- <p>
25
- Or find out more about Curvenote&apos;s scientific writing, collaboration and publishing
26
- tools at <a href="https://curvenote.com">curvenote.com</a>.
27
- </p>
28
- </>
29
- );
30
- }