diff --git a/apps/docs/components/home-card.tsx b/apps/docs/components/home-card.tsx index 3603a13b..eb744236 100644 --- a/apps/docs/components/home-card.tsx +++ b/apps/docs/components/home-card.tsx @@ -8,8 +8,8 @@ function Card(props: { icon: string; }) { return ( -
- +
+
{props.children}
{props.right}
diff --git a/apps/docs/pages/docs/config/index.tsx b/apps/docs/pages/docs/config/index.tsx index ace9cd0a..7492ee5b 100644 --- a/apps/docs/pages/docs/config/index.tsx +++ b/apps/docs/pages/docs/config/index.tsx @@ -8,12 +8,12 @@ import { export default function LandingPage() { return ( - + Simple type-safe configuration - Configure with json files or environment variables, without losing - type-safety + Configure your app with JSON files or environment variables without + losing type-safety @@ -23,14 +23,17 @@ export default function LandingPage() { - - Effortlessly create beautiful documentation sites with just markdown. + + Define your config structure using strictly typed and validated + schemas. - - Effortlessly create beautiful documentation sites with just markdown. + + Easily configure applications in the cloud using multiple environment + agnostic sources. - - Effortlessly create beautiful documentation sites with just markdown. + + Supports a wide range of configuration sources including JSON files, + environment variables and CLI arguments. diff --git a/apps/docs/pages/docs/guider/api-reference/components/guider-content-footer.mdx b/apps/docs/pages/docs/guider/api-reference/components/guider-content-footer.mdx index eb1a768c..bf790e1a 100644 --- a/apps/docs/pages/docs/guider/api-reference/components/guider-content-footer.mdx +++ b/apps/docs/pages/docs/guider/api-reference/components/guider-content-footer.mdx @@ -2,8 +2,12 @@ React component that will show the currently configured content footer. -It respects configured `contentFooter` in the layout settings. Read more [about partials](../theme/settings.mdx#about-partials). +It respects the configured `contentFooter` in the layout settings. Read more [about partials](../theme/settings.mdx#about-partials). +Will show one of: + - Nothing if the content footer is set to be hidden. + - The default content footer with the configured settings. + - The custom component if the content footer has been overriden in the partials. ## Example diff --git a/apps/docs/pages/docs/guider/api-reference/components/guider-header.mdx b/apps/docs/pages/docs/guider/api-reference/components/guider-header.mdx index 276e9275..774ca843 100644 --- a/apps/docs/pages/docs/guider/api-reference/components/guider-header.mdx +++ b/apps/docs/pages/docs/guider/api-reference/components/guider-header.mdx @@ -2,8 +2,12 @@ React component that will show the currently configured navigation partial. -It respects configured `navigation` in the layout settings. Read more [about partials](../theme/settings.mdx#about-partials). +It respects the configured `navigation` in the layout settings. Read more [about partials](../theme/settings.mdx#about-partials). +Will show one of: + - Nothing if the navigation bar is set to be hidden. + - The default navigation bar with the configured settings. + - The custom component if the navigation bar has been overriden in the partials. ## Example diff --git a/apps/docs/pages/docs/guider/api-reference/components/guider-layout.mdx b/apps/docs/pages/docs/guider/api-reference/components/guider-layout.mdx index a77ee0c6..a6d879db 100644 --- a/apps/docs/pages/docs/guider/api-reference/components/guider-layout.mdx +++ b/apps/docs/pages/docs/guider/api-reference/components/guider-layout.mdx @@ -2,10 +2,14 @@ React component that will show the currently configured layout partial. -It respects configured `layout` in the layout settings. Read more [about partials](../theme/settings.mdx#about-partials). +It respects the configured `layout` in the layout settings. Read more [about partials](../theme/settings.mdx#about-partials). The primary use of this component is to make a non-MDX page look like an MDX page. Don't use it if you're already in an MDX File. +Will show one of: + - The default layout with the configured settings. + - The custom component if the layout has been overriden in the partials. + ## Example ```tsx @@ -55,7 +59,7 @@ function GuiderLayout(props); - The children of the react component, it will be displayed as the page contents. + The children of the React component, it will be displayed as the page contents. diff --git a/apps/docs/pages/docs/guider/api-reference/components/guider-logo.mdx b/apps/docs/pages/docs/guider/api-reference/components/guider-logo.mdx index 1dbb8c07..1d918629 100644 --- a/apps/docs/pages/docs/guider/api-reference/components/guider-logo.mdx +++ b/apps/docs/pages/docs/guider/api-reference/components/guider-logo.mdx @@ -2,8 +2,12 @@ React component that will show the currently configured logo. -It respects configured `logo` in the layout settings. Read more [about partials](../theme/settings.mdx#about-partials). +It respects the configured `logo` in the layout settings. Read more [about partials](../theme/settings.mdx#about-partials). +Will show one of: + - Nothing if the logo is set to be hidden. + - The default logo with the configured settings. + - The custom component if the logo has been overriden in the partials. ## Example diff --git a/apps/docs/pages/docs/guider/api-reference/components/guider-page-end.mdx b/apps/docs/pages/docs/guider/api-reference/components/guider-page-end.mdx new file mode 100644 index 00000000..feca6894 --- /dev/null +++ b/apps/docs/pages/docs/guider/api-reference/components/guider-page-end.mdx @@ -0,0 +1,23 @@ +# `` + +React component that will show the currently configured page end. + +It respects the configured `pageEnd` in the layout settings. Read more [about partials](../theme/settings.mdx#about-partials). + +Will show one of: + - Nothing if the page end is set to be hidden. + - The default page end with the configured settings. + - The custom component if the page end has been overriden in the partials. + +## Example + +```tsx + +``` + + +## Reference + +```tsx +function GuiderPageEnd(): ReactNode; +``` diff --git a/apps/docs/pages/docs/guider/api-reference/components/guider-page-footer.mdx b/apps/docs/pages/docs/guider/api-reference/components/guider-page-footer.mdx index 28ff6ca4..2fc8c2ac 100644 --- a/apps/docs/pages/docs/guider/api-reference/components/guider-page-footer.mdx +++ b/apps/docs/pages/docs/guider/api-reference/components/guider-page-footer.mdx @@ -2,8 +2,12 @@ React component that will show the currently configured page footer. -It respects configured `pageFooter` in the layout settings. Read more [about partials](../theme/settings.mdx#about-partials). +It respects the configured `pageFooter` in the layout settings. Read more [about partials](../theme/settings.mdx#about-partials). +Will show one of: + - Nothing if the page footer is set to be hidden. + - The default page footer with the configured settings. + - The custom component if the page footer has been overriden in the partials. ## Example diff --git a/apps/docs/pages/docs/guider/api-reference/components/guider-sidebar.mdx b/apps/docs/pages/docs/guider/api-reference/components/guider-sidebar.mdx index cf47f551..282991ff 100644 --- a/apps/docs/pages/docs/guider/api-reference/components/guider-sidebar.mdx +++ b/apps/docs/pages/docs/guider/api-reference/components/guider-sidebar.mdx @@ -2,8 +2,12 @@ React component that will show the currently configured sidebar partial. -It respects configured `sidebar` in the layout settings. Read more [about partials](../theme/settings.mdx#about-partials). +It respects the configured `sidebar` in the layout settings. Read more [about partials](../theme/settings.mdx#about-partials). +Will show one of: + - Nothing if the sidebar is set to be hidden. + - The default sidebar with the configured settings. + - The custom component if the sidebar has been overriden in the partials. ## Example diff --git a/apps/docs/pages/docs/guider/api-reference/components/guider-toc.mdx b/apps/docs/pages/docs/guider/api-reference/components/guider-toc.mdx index f0213840..86a34e57 100644 --- a/apps/docs/pages/docs/guider/api-reference/components/guider-toc.mdx +++ b/apps/docs/pages/docs/guider/api-reference/components/guider-toc.mdx @@ -2,8 +2,12 @@ React component that will show the currently configured table of contents. -It respects configured `toc` in the layout settings. Read more [about partials](../theme/settings.mdx#about-partials). +It respects the configured `toc` in the layout settings. Read more [about partials](../theme/settings.mdx#about-partials). +Will show one of: + - Nothing if the table of contents is set to be hidden. + - The default table of contents with the configured settings. + - The custom component if the table of contents has been overriden in the partials. ## Example diff --git a/apps/docs/pages/docs/guider/api-reference/functions/create-not-found-page.mdx b/apps/docs/pages/docs/guider/api-reference/functions/create-not-found-page.mdx index 125a51d2..187f96a4 100644 --- a/apps/docs/pages/docs/guider/api-reference/functions/create-not-found-page.mdx +++ b/apps/docs/pages/docs/guider/api-reference/functions/create-not-found-page.mdx @@ -5,7 +5,7 @@ This function creates a premade 404 page. You don't have to use this 404 page, it does nothing special. Feel free to make your own. - This has be put in `/pages/404.tsx` to work, you can't choose a different location or it won't work. + This has be put in `/pages/404.tsx` to work, if you use a different location it won't work. diff --git a/apps/docs/pages/docs/guider/api-reference/functions/create-redirect.mdx b/apps/docs/pages/docs/guider/api-reference/functions/create-redirect.mdx index 3091a724..d33c566f 100644 --- a/apps/docs/pages/docs/guider/api-reference/functions/create-redirect.mdx +++ b/apps/docs/pages/docs/guider/api-reference/functions/create-redirect.mdx @@ -2,6 +2,7 @@ This utility function is an easy way to make redirects work in static site generation mode. +When it's used, it creates a dummy page that redirects when loaded. ## Example diff --git a/apps/docs/pages/docs/guider/api-reference/functions/use-guider-page.mdx b/apps/docs/pages/docs/guider/api-reference/functions/use-guider-page.mdx index 8421cc75..3084d915 100644 --- a/apps/docs/pages/docs/guider/api-reference/functions/use-guider-page.mdx +++ b/apps/docs/pages/docs/guider/api-reference/functions/use-guider-page.mdx @@ -50,6 +50,11 @@ function useGuiderPage(): GuiderPageContext; See [the `useGuider()` page](./use-guider.mdx) for more info. + + The navigational context of the page, use it to get the current links in the sidebar. + + See [the `useGuider()` page](./use-guider.mdx) for more info. + Data for the current page content. @@ -59,19 +64,19 @@ function useGuiderPage(): GuiderPageContext; - The site field in the Frontmatter + The site field in the Frontmatter. - The directory field in the Frontmatter + The directory field in the Frontmatter. - The layout field in the Frontmatter + The layout field in the Frontmatter. - The title field in the Frontmatter + The title field in the Frontmatter. - The description field in the Frontmatter + The description field in the Frontmatter. diff --git a/apps/docs/pages/docs/guider/api-reference/functions/use-guider.mdx b/apps/docs/pages/docs/guider/api-reference/functions/use-guider.mdx index 50c68313..d7cafff6 100644 --- a/apps/docs/pages/docs/guider/api-reference/functions/use-guider.mdx +++ b/apps/docs/pages/docs/guider/api-reference/functions/use-guider.mdx @@ -76,7 +76,7 @@ function useGuider(metaConf): GuiderContext; - The ID of the layout + The ID of the layout. The populated layout settings for this layout, read more [about layout settings](../theme/settings.mdx). @@ -95,7 +95,7 @@ function useGuider(metaConf): GuiderContext; - The ID of the directory + The ID of the directory. The ID of the layout that needs to be shown for pages in this directory. @@ -198,5 +198,20 @@ function useGuider(metaConf): GuiderContext; + + The navigational context of the page, use it to get the current links in the sidebar. + + + + The previous page link, according to the order of the sidebar links. It can be null + + + The current page link. It can be null if it isn't present in the sidebar. + + + The next page link, according to the order of the sidebar links. It can be null. + + + diff --git a/apps/docs/pages/docs/guider/api-reference/theme/settings.mdx b/apps/docs/pages/docs/guider/api-reference/theme/settings.mdx index 709dd26a..e2f301d4 100644 --- a/apps/docs/pages/docs/guider/api-reference/theme/settings.mdx +++ b/apps/docs/pages/docs/guider/api-reference/theme/settings.mdx @@ -47,6 +47,9 @@ When you overwrite a partial, you can use `useGuiderPage()` to get the current p The page footer partial. Check [this section](#about-partials) for information on how to use partials. + + The page end partial. Check [this section](#about-partials) for information on how to use partials. + The logo partial. Check [this section](#about-partials) for information on how to use partials. diff --git a/apps/docs/pages/docs/guider/guides/advanced/customizing-layout.mdx b/apps/docs/pages/docs/guider/guides/advanced/customizing-layout.mdx index 3e8a1d8b..ce711eb2 100644 --- a/apps/docs/pages/docs/guider/guides/advanced/customizing-layout.mdx +++ b/apps/docs/pages/docs/guider/guides/advanced/customizing-layout.mdx @@ -11,6 +11,7 @@ Here are all possibilities: - `sidebar`: The sidebar navigation menu. - `navigation`: The header navigation menu. - `contentFooter`: The content footer, placed right below content. + - `pageEnd`: The navigation links below the content, but above the content footer. - `pageFooter`: The page footer, placed at the very bottom of the page. Disabled by default. - `logo`: The logo, shown in the navigation bar and page footer. - `pageLayout`: The entire page, cannot be turned off. diff --git a/apps/docs/pages/docs/guider/guides/advanced/header.mdx b/apps/docs/pages/docs/guider/guides/advanced/header.mdx index 32a4a35b..5f9ffcfc 100644 --- a/apps/docs/pages/docs/guider/guides/advanced/header.mdx +++ b/apps/docs/pages/docs/guider/guides/advanced/header.mdx @@ -6,7 +6,7 @@ Learn about configuring the page header. If you are looking for ways to customise the navigation links in the header, you can [read more here](/docs/guider/guides/config/navigation). -## Github widget +## GitHub widget To showcase your GitHub repository with its stars and forks, you can configure a GitHub widget. diff --git a/apps/docs/pages/docs/guider/guides/config/common/api-ref.mdx b/apps/docs/pages/docs/guider/guides/config/common/api-ref.mdx index e333fd0f..6447a2fd 100644 --- a/apps/docs/pages/docs/guider/guides/config/common/api-ref.mdx +++ b/apps/docs/pages/docs/guider/guides/config/common/api-ref.mdx @@ -1,5 +1,68 @@ # API reference + Docs - -This article is a stub, please help by contributing to the docs. - +A common requirement for expansive documentation sites is having both guides and an API reference. + +API reference is a lot more technical and usually isn't very helpful for beginners, but massively helpful if you know the library already and want to fact check something. + +The guides is almost the opposite: It's very friendly for beginners, but lacks the completeness of an API reference. + + +## Adding the two sections to your project + +The recommended way to add an API reference next to your guides is to use tabs. + +Tabs is a configuration option for a Guider site that allows for easy switching between two contexts. + + + + ### Adding the pages to sub-directories + + First, you have to make sure that your pages are divided in sub-directories. Like so: + - `/pages/guides` All your MDX files or pages for your guides + - `/pages/api` All your MDX files or pages for your API reference + + + ### Configuring your tabs + + Your site configuration needs to be split into two `directory()` instances and need to have the configured tabs. Like so: + + ```tsx title="theme.config.tsx" showLineNumbers + import { directories, link, defineTheme } from "@neato/guider/theme" + + export default defineTheme({ + tabs: [ + link("Guides", "/guides"), + link("API reference", "/api"), + ] + directories: [ + directory('guides', { + sidebar: [ + // your sidebar items + ], + }), + directory('api', { + sidebar: [ + // your sidebar items + ], + }) + ] + }) + ``` + + + ### Creating meta files + + To make your pages tied to your `directory()` instances (and tabs), you'll need to make two meta files for each folder. + + ```json title="/pages/guides/_meta.json" + { + "directory": "guides" + } + ``` + ```json title="/pages/api/_meta.json" + { + "directory": "api" + } + ``` + + diff --git a/apps/docs/pages/docs/guider/guides/config/common/blog.mdx b/apps/docs/pages/docs/guider/guides/config/common/blog.mdx deleted file mode 100644 index fd1bbf10..00000000 --- a/apps/docs/pages/docs/guider/guides/config/common/blog.mdx +++ /dev/null @@ -1,5 +0,0 @@ -# Blog posts + Docs - - -This article is a stub, please help by contributing to the docs. - diff --git a/apps/docs/pages/docs/guider/guides/deploy/cloudflare.mdx b/apps/docs/pages/docs/guider/guides/deploy/cloudflare.mdx index 5e59e80b..b668baaa 100644 --- a/apps/docs/pages/docs/guider/guides/deploy/cloudflare.mdx +++ b/apps/docs/pages/docs/guider/guides/deploy/cloudflare.mdx @@ -11,7 +11,7 @@ Follow the steps below to deploy a Guider project to Cloudflare Pages. ### Make a new Application - Go to [the cloudflare dashboard](https://dash.cloudflare.com) on under "Workers & Pages" click "Create application". + Go to [the Cloudflare dashboard](https://dash.cloudflare.com) on under "Workers & Pages" click "Create application". ### Select your repository diff --git a/apps/docs/pages/docs/guider/guides/deploy/github-pages.mdx b/apps/docs/pages/docs/guider/guides/deploy/github-pages.mdx index 8d11e3f2..fc06a1f2 100644 --- a/apps/docs/pages/docs/guider/guides/deploy/github-pages.mdx +++ b/apps/docs/pages/docs/guider/guides/deploy/github-pages.mdx @@ -1,10 +1,10 @@ -# Github Pages +# GitHub Pages -A Github Action to deploy a Guider documentation site. +A GitHub Action to deploy a Guider documentation site. ## Prerequisites -- Enable Github Pages, go to `Your repo > Settings > Pages` and set `Source` to `Github Actions`. +- Enable GitHub Pages, go to `Your repo > Settings > Pages` and set `Source` to `GitHub Actions`. ## The workflow diff --git a/apps/docs/pages/docs/guider/guides/deploy/netlify.mdx b/apps/docs/pages/docs/guider/guides/deploy/netlify.mdx index c77cb980..caecad1a 100644 --- a/apps/docs/pages/docs/guider/guides/deploy/netlify.mdx +++ b/apps/docs/pages/docs/guider/guides/deploy/netlify.mdx @@ -1,4 +1,4 @@ -# Vercel +# Netlify A guide to deploy a Guider project to Netlify. @@ -26,7 +26,7 @@ Follow the steps below to deploy a Guider project to Netlify. ### Deploy - Click the `deploy` button and wait until your site has been deployed. + Click the `Deploy` button and wait until your site has been deployed. Netlify has linked this deploy with your branch, meaning that once new changes are pushed. Netlify will automatically update your site. diff --git a/apps/docs/pages/docs/guider/guides/getting-started/migration/from-docus.mdx b/apps/docs/pages/docs/guider/guides/getting-started/migration/from-docus.mdx index 68916054..0d130aeb 100644 --- a/apps/docs/pages/docs/guider/guides/getting-started/migration/from-docus.mdx +++ b/apps/docs/pages/docs/guider/guides/getting-started/migration/from-docus.mdx @@ -33,16 +33,16 @@ Migrating can be a long journey, this guide aims to create a clear set of steps ## Migration steps -1. **Make a guider project:** Make a new guider project by following [this guide](../installation.mdx). -2. **Copy your markdown files over:** Copy over all Markdown files into the pages directory of the new project. - - Remove the numbers prefixed on front of your files and folders. The order is now in the theme config. - - Replace the component directives with MDX React components. - - Update frontmatter to set the layout ID instead of modifying the layout settings directly. See [this page](../../advanced/customizing-layout.mdx). - - Replace title in the frontmatter to a link in the theme config. -3. **Migrate _dir.yml files:** dir files don't exist in Guider. Check these steps: - - title goes in a `group()` in the theme config. - - replace `navigation.redirect` with a `createRedirect()` in a index.tsx - - title template can be set globally, not a per directory basis. -4. **Migrate app.config.ts:** The settings in Guider go into the theme config, along with your sidebar contents. -5. **Migrate vue components to react:** Guider is based in react, so to have custom components work you will need to port them. -6. **Finished:** That should be everything to migrate to Guider, congratulations! + 1. **Make a guider project:** Make a new guider project by following [this guide](../installation.mdx). + 2. **Copy your markdown files over:** Copy over all Markdown files into the pages directory of the new project. + - Remove the numbers prefixed on front of your files and folders. The order is now in the theme config. + - Replace the component directives with MDX React components. + - Update frontmatter to set the layout ID instead of modifying the layout settings directly. See [this page](../../advanced/customizing-layout.mdx). + - Replace title in the frontmatter to a link in the theme config. + 3. **Migrate _dir.yml files:** dir files don't exist in Guider. Check these steps: + - title goes in a `group()` in the theme config. + - replace `navigation.redirect` with a `createRedirect()` in a index.tsx + - title template can be set globally, not a per directory basis. + 4. **Migrate app.config.ts:** The settings in Guider go into the theme config, along with your sidebar contents. + 5. **Migrate vue components to react:** Guider is based in react, so to have custom components work you will need to port them. + 6. **Finished:** That should be everything to migrate to Guider, congratulations! diff --git a/apps/docs/pages/docs/guider/index.tsx b/apps/docs/pages/docs/guider/index.tsx index 2a2b8bad..6d50691e 100644 --- a/apps/docs/pages/docs/guider/index.tsx +++ b/apps/docs/pages/docs/guider/index.tsx @@ -8,15 +8,15 @@ import { export default function LandingPage() { return ( - + Just went out of alpha! Documentation that looks great out of the box - Flexible but beautiful documentation, easy to write and easier to - extend. Exactly what you need everytime. + Flexible but beautiful documentation powered by NextJS — easy to write + and easier to extend. @@ -26,14 +26,17 @@ export default function LandingPage() { - - Effortlessly create beautiful documentation sites with just markdown. + + Effortlessly create beautiful documentation sites using Markdown or + MDX files. - - Effortlessly create beautiful documentation sites with just markdown. + + Guider doesn't make assumptions about your site. Use it by itself or + include it as part of a larger project. - - Effortlessly create beautiful documentation sites with just markdown. + + Comes out the box with a ready-to-go theme, but can easily be made to + look exactly like what you have envisioned. diff --git a/apps/docs/pages/docs/guider/writing/components/fields.mdx b/apps/docs/pages/docs/guider/writing/components/fields.mdx index 12c4c398..1600fedd 100644 --- a/apps/docs/pages/docs/guider/writing/components/fields.mdx +++ b/apps/docs/pages/docs/guider/writing/components/fields.mdx @@ -48,10 +48,10 @@ You can also use the `{:tsx}` component to group properties A field with a type display, great for documenting types or structures. - The title of the field + The title of the field. - The type of the field + The type of the field. Mark this field as required. diff --git a/apps/docs/pages/docs/guider/writing/components/tabs.mdx b/apps/docs/pages/docs/guider/writing/components/tabs.mdx index 13ae36e8..78c84aa3 100644 --- a/apps/docs/pages/docs/guider/writing/components/tabs.mdx +++ b/apps/docs/pages/docs/guider/writing/components/tabs.mdx @@ -96,7 +96,7 @@ The Tabs component The tab items, must have the same number of items as there are tabs. - The storage key, to store the preferences against. See [this section](#storing-the-tab-choice) for usage. + The storage key, to store the preferences under. See [this section](#storing-the-tab-choice) for usage. The tabs to display. diff --git a/apps/docs/pages/docs/guider/writing/markdown/making-pages.mdx b/apps/docs/pages/docs/guider/writing/markdown/making-pages.mdx index f31eb69c..51649c54 100644 --- a/apps/docs/pages/docs/guider/writing/markdown/making-pages.mdx +++ b/apps/docs/pages/docs/guider/writing/markdown/making-pages.mdx @@ -73,6 +73,6 @@ export default function MyPage() { ## Writing in MD or MDX -Guider uses most of [Github Flavoured Markdown](https://github.github.com/gfm/). If you are familiar with it, you can use everything you know. +Guider uses most of [GitHub Flavoured Markdown](https://github.github.com/gfm/). If you are familiar with it, you can use everything you know. If you aren't familiar, you can refer to [Basic text](/docs/guider/writing/basic-text) to learn how to write in Markdown. diff --git a/apps/docs/pages/index.tsx b/apps/docs/pages/index.tsx index 7bb44afb..166d2915 100644 --- a/apps/docs/pages/index.tsx +++ b/apps/docs/pages/index.tsx @@ -15,21 +15,21 @@ export default function LandingPage() { title: '@neato/guider', description: 'Flexible documentation that looks good out of the box.', href: '/docs/guider', - icon: 'mdi:cube-outline', + icon: 'ic:round-menu-book', }, { title: '@neato/config', description: 'NodeJS configuration loader with strict typing and autocomplete.', href: '/docs/config', - icon: 'mdi:cube-outline', + icon: 'material-symbols:settings-heart-rounded', }, ]; return ( - NeatoJS — A collection of JS tools that you will want. + NeatoJS — A collection of libraries made to simplify Tools that follow the philosophy of doing only one thing, and doing it diff --git a/apps/docs/pages/showcase.tsx b/apps/docs/pages/showcase.tsx index cb1ffe94..1884d4dd 100644 --- a/apps/docs/pages/showcase.tsx +++ b/apps/docs/pages/showcase.tsx @@ -5,22 +5,30 @@ import type { ShowcaseTag, ShowcaseType } from 'components/showcase-card'; import { ShowcaseCard, ShowcaseCardContainer } from 'components/showcase-card'; import pretendoImg from 'public/showcases/pretendo.png'; import mwAccountImg from 'public/showcases/movie-web-account.png'; +import mwDocsImg from 'public/showcases/movie-web-docs.png'; const showcases: ShowcaseType[] = [ { title: 'Pretendo', - description: 'Uses Guider for protocol documentation.', + description: 'Uses Guider to document their protocols and libaries.', href: 'https://developer.pretendo.network/', imageUrl: pretendoImg.src, tags: ['guider'], }, { - title: 'movie-web account', + title: 'movie-web backend', description: 'Uses Config for their account service.', href: 'https://github.com/movie-web/backend/', imageUrl: mwAccountImg.src, tags: ['config'], }, + { + title: 'movie-web docs', + description: 'Uses Guider for their documentation', + href: 'https://github.com/movie-web/docs/', + imageUrl: mwDocsImg.src, + tags: ['guider'], + }, ]; export default function ShowcasePage() { diff --git a/apps/docs/public/showcases/movie-web-docs.png b/apps/docs/public/showcases/movie-web-docs.png new file mode 100644 index 00000000..05c7217a Binary files /dev/null and b/apps/docs/public/showcases/movie-web-docs.png differ diff --git a/apps/docs/theme.config.tsx b/apps/docs/theme.config.tsx index c458016f..0e198d66 100644 --- a/apps/docs/theme.config.tsx +++ b/apps/docs/theme.config.tsx @@ -46,7 +46,7 @@ const gdWriting = (url: string) => `/docs/guider/writing${url}`; const gdApi = (url: string) => `/docs/guider/api-reference${url}`; const starLinks = [ - link('Github', 'https://github.com/mrjvs/neatojs', { + link('GitHub', 'https://github.com/mrjvs/neatojs', { style: 'star', newTab: true, icon: 'akar-icons:github-fill', @@ -121,7 +121,6 @@ export default defineTheme([ gdGuides('/config/common/multi-docs'), ), link('API reference + docs', gdGuides('/config/common/api-ref')), - link('Blog posts + docs', gdGuides('/config/common/blog')), ]), ]), group('Advanced', [ @@ -135,10 +134,10 @@ export default defineTheme([ link('Deep-dive concepts', gdGuides('/advanced/deep-dive')), ]), group('Deploying', [ - link('Github Pages', gdGuides('/deploy/github-pages')), + link('GitHub Pages', gdGuides('/deploy/github-pages')), link('Netlify', gdGuides('/deploy/netlify')), link('Vercel', gdGuides('/deploy/vercel')), - link('Cloudflare pages', gdGuides('/deploy/cloudflare')), + link('Cloudflare Pages', gdGuides('/deploy/cloudflare')), link('Docker', gdGuides('/deploy/docker')), ]), ], @@ -245,6 +244,7 @@ export default defineTheme([ '', gdApi('/components/guider-page-footer'), ), + link('', gdApi('/components/guider-page-end')), ]), ], }), diff --git a/packages/guider/package.json b/packages/guider/package.json index 5c6f35c3..19fc003d 100644 --- a/packages/guider/package.json +++ b/packages/guider/package.json @@ -1,6 +1,6 @@ { "name": "@neato/guider", - "version": "0.1.5", + "version": "1.0.0", "description": "Beautiful documentation sites, without all the hassle", "main": "./dist/index.js", "type": "module", @@ -75,6 +75,7 @@ "@types/color": "^3.0.6", "@types/extra-watch-webpack-plugin": "^1.0.6", "@types/git-url-parse": "^9.0.3", + "@types/mdast": "^4.0.3", "@types/react": "18.2.56", "@types/webpack": "^5.28.5", "autoprefixer": "^10.4.17", @@ -94,14 +95,17 @@ "@mdx-js/mdx": "^3.0.1", "@shikijs/transformers": "^1.1.7", "@theguild/remark-npm2yarn": "^0.3.0", + "@types/unist": "^3.0.2", "@vcarl/remark-headings": "^0.1.0", "approximate-number": "^2.1.1", "classnames": "^2.5.1", "color": "^4.2.3", "extra-watch-webpack-plugin": "^1.0.3", + "flexsearch": "^0.7.43", "git-url-parse": "^14.0.0", "glob": "^10.3.10", "gray-matter": "^4.0.3", + "mdast-util-phrasing": "^4.1.0", "next-seo": "^6.5.0", "react-helmet-async": "^2.0.4", "rehype-extract-excerpt": "^0.3.1", @@ -113,6 +117,8 @@ "shiki": "^1.1.7", "tailwindcss-themer": "^4.0.0", "type-fest": "^4.10.3", + "unified": "^11.0.4", + "unist-util-visit": "^5.0.0", "webpack-virtual-modules": "^0.6.1", "zustand": "^4.5.2" } diff --git a/packages/guider/src/client/components/markdown/image.tsx b/packages/guider/src/client/components/markdown/image.tsx new file mode 100644 index 00000000..4e591815 --- /dev/null +++ b/packages/guider/src/client/components/markdown/image.tsx @@ -0,0 +1,17 @@ +import classNames from 'classnames'; +import { useRouter } from 'next/router'; +import type { MarkdownProps } from './types'; + +export function MarkdownImage(props: MarkdownProps) { + const router = useRouter(); + const srcInput = props.attrs.src; + const src = srcInput?.startsWith('/') ? router.basePath + srcInput : srcInput; + + return ( + + ); +} diff --git a/packages/guider/src/client/components/markdown/index.tsx b/packages/guider/src/client/components/markdown/index.tsx index b33b832f..63c8db6f 100644 --- a/packages/guider/src/client/components/markdown/index.tsx +++ b/packages/guider/src/client/components/markdown/index.tsx @@ -13,6 +13,7 @@ import { MarkdownH6, } from './headings'; import { MarkdownHr } from './hr'; +import { MarkdownImage } from './image'; import { MarkdownItalic, MarkdownStrike, MarkdownStrong } from './inline'; import { MarkdownLi, MarkdownOl, MarkdownUl } from './lists'; import { MarkdownLink, MarkdownParagraph } from './paragraph'; @@ -126,5 +127,8 @@ export function useMDXComponents() { ); return
{props.children}
; }, + img(props: ElementProps) { + return ; + }, }; } diff --git a/packages/guider/src/client/components/markdown/lists.tsx b/packages/guider/src/client/components/markdown/lists.tsx index b34eb4e1..82b0cd7d 100644 --- a/packages/guider/src/client/components/markdown/lists.tsx +++ b/packages/guider/src/client/components/markdown/lists.tsx @@ -19,7 +19,7 @@ export function MarkdownUl(props: MarkdownProps) { export function MarkdownLi(props: MarkdownProps) { return (
  • - + {props.children} diff --git a/packages/guider/src/client/components/public/button.tsx b/packages/guider/src/client/components/public/button.tsx index 723983fb..67bb66f4 100644 --- a/packages/guider/src/client/components/public/button.tsx +++ b/packages/guider/src/client/components/public/button.tsx @@ -20,9 +20,9 @@ const buttonStyles = { export function Button(props: ButtonProps) { const classes = classNames( buttonStyles[props.type ?? 'primary'], - 'gd-bg-gradient-to-b gd-text-opacity-90 gd-px-4 gd-py-2 gd-rounded-md gd-border', - 'gd-transition-[background,transform]', - 'active:gd-scale-105', + 'gd-bg-gradient-to-b gd-text-opacity-80 gd-px-7 gd-py-2.5 gd-rounded-xl gd-border', + 'gd-transition-[background-image,transform]', + 'active:gd-scale-105 gd-inline-block gd-text-center', props.className, ); diff --git a/packages/guider/src/client/components/public/hero.tsx b/packages/guider/src/client/components/public/hero.tsx index fea263b8..66a0b62f 100644 --- a/packages/guider/src/client/components/public/hero.tsx +++ b/packages/guider/src/client/components/public/hero.tsx @@ -51,7 +51,11 @@ function Badge(props: { } function Actions(props: { children: React.ReactNode }) { - return
    {props.children}
    ; + return ( +
    + {props.children} +
    + ); } function HeroFunc(props: { children: React.ReactNode }) { diff --git a/packages/guider/src/client/components/utils/activelink.tsx b/packages/guider/src/client/components/utils/activelink.tsx index eb2832a3..6345ea2e 100644 --- a/packages/guider/src/client/components/utils/activelink.tsx +++ b/packages/guider/src/client/components/utils/activelink.tsx @@ -5,6 +5,33 @@ import { useMemo, useCallback, useState, useEffect } from 'react'; import classNames from 'classnames'; import { usePathname } from 'next/navigation.js'; +export function isRouteAtive( + link: string, + currentPathname: string, + exact?: boolean, +): boolean { + const linkPathname = new URL(link, 'https://example.com').pathname; + const activePathname = new URL(currentPathname, 'https://example.com') + .pathname; + + const linkPathArr = linkPathname.split('/').filter(Boolean); + const activePathArr = activePathname.split('/').filter(Boolean); + + if (exact) { + const exactMatch = linkPathArr.join('/') === activePathArr.join('/'); + return exactMatch; + } + + let matches = true; + for (let i = 0; i < linkPathArr.length; i++) { + if (linkPathArr[i] !== activePathArr[i]) { + matches = false; + } + } + + return matches; +} + export function useAreRoutesActive( ops: { as?: string; @@ -16,25 +43,7 @@ export function useAreRoutesActive( const isActiveCheck = useCallback( (href: string, exact?: boolean) => { - const linkPathname = new URL(href, location.href).pathname; - const activePathname = new URL(pathName, location.href).pathname; - - const linkPathArr = linkPathname.split('/').filter(Boolean); - const activePathArr = activePathname.split('/').filter(Boolean); - - if (exact) { - const exactMatch = linkPathArr.join('/') === activePathArr.join('/'); - return exactMatch; - } - - let matches = true; - for (let i = 0; i < linkPathArr.length; i++) { - if (linkPathArr[i] !== activePathArr[i]) { - matches = false; - } - } - - return matches; + return isRouteAtive(href, pathName, exact); }, [pathName], ); diff --git a/packages/guider/src/client/hooks/use-guider.tsx b/packages/guider/src/client/hooks/use-guider.tsx index 9d3aaa67..a7b2e27a 100644 --- a/packages/guider/src/client/hooks/use-guider.tsx +++ b/packages/guider/src/client/hooks/use-guider.tsx @@ -3,6 +3,7 @@ import { useRouter } from 'next/router.js'; import type { MetaConf, MetaMapItem } from '../../theme'; import { mergeLayoutSettings } from '../../theme/components/layout'; import { sites, metaMap } from '../virtuals'; +import { getCurrentPageContext } from '../utils/navigational-buttons'; export function getPage(pageUrl: string) { const matches = metaMap @@ -28,6 +29,7 @@ export function getGuiderContext(pageUrl: string, pageMeta: MetaConf = {}) { if (!layout) throw new Error('No layout found'); const settings = mergeLayoutSettings(layout.settings, dir.settings); + const navContext = getCurrentPageContext(pageUrl, dir); return { metaMap, @@ -35,6 +37,7 @@ export function getGuiderContext(pageUrl: string, pageMeta: MetaConf = {}) { directory: dir, layout, site, + navContext, }; } diff --git a/packages/guider/src/client/partials/content-footer/content-footer.tsx b/packages/guider/src/client/partials/content-footer/content-footer.tsx index 8fb83475..11500c6a 100644 --- a/packages/guider/src/client/partials/content-footer/content-footer.tsx +++ b/packages/guider/src/client/partials/content-footer/content-footer.tsx @@ -4,6 +4,8 @@ import { useGuiderPage } from '../../hooks/use-guider-page'; import type { SocialTypes } from '../../../theme/components/social'; import { GithubEditLink, useEditLink } from './github-edit-link'; +const guiderDocumentationLink = 'https://neatojs.com/docs/guider'; + const iconMap: Record = { discord: 'ic:twotone-discord', github: 'mdi:github', @@ -44,7 +46,15 @@ export function ContentFooterInternal() { ) : null}
    {site.contentFooter?.text ?? copyright}{' '} - Powered by Guider + {' '} + + Powered by Guider +
  • {editUrl ? ( diff --git a/packages/guider/src/client/partials/header/header.tsx b/packages/guider/src/client/partials/header/header.tsx index 8b5a9806..35b30187 100644 --- a/packages/guider/src/client/partials/header/header.tsx +++ b/packages/guider/src/client/partials/header/header.tsx @@ -9,6 +9,7 @@ import { HeaderNav } from './nav'; import { HeaderDropdown } from './dropdown'; import { SidebarMobileNav } from './sidebar-mobile-nav'; import { TopMobileNav } from './top-mobile-nav'; +import { HeaderSearch } from './search'; export function HeaderInternal() { const ctx = useContext(GuiderLayoutContext); @@ -51,6 +52,7 @@ export function HeaderInternal() { ) : null} +
    diff --git a/packages/guider/src/client/partials/header/search/button.tsx b/packages/guider/src/client/partials/header/search/button.tsx new file mode 100644 index 00000000..c59eeac0 --- /dev/null +++ b/packages/guider/src/client/partials/header/search/button.tsx @@ -0,0 +1,20 @@ +import { Icon } from '../../../components/icon'; + +export function SearchButton(props: { onClick?: () => void }) { + return ( + + ); +} diff --git a/packages/guider/src/client/partials/header/search/content.ts b/packages/guider/src/client/partials/header/search/content.ts new file mode 100644 index 00000000..8b305f40 --- /dev/null +++ b/packages/guider/src/client/partials/header/search/content.ts @@ -0,0 +1,148 @@ +import FlexSearch from 'flexsearch'; +import { useRouter } from 'next/router'; +import { useCallback, useEffect, useState } from 'react'; + +type ContentDocument = { + mainDoc: FlexSearch.Document< + { + id: number; + url: string; + title: string; + content: string; + }, + never[] + >; +}; + +type SearchData = Record< + string, + { + sections: { + heading?: { id: string; depth: number; text: string }; + content: string; + }[]; + } +>; + +export type SearchResult = { + id: string; + type: 'section'; + title: string; + content: string; + url: string; +}; + +let contentPromise: Promise | null = null; +let contentDocument: ContentDocument | null = null; + +function loadDocument(basePath: string, key: string): Promise { + if (contentPromise) return contentPromise; + const promise = fetchDocument(basePath, key); + contentPromise = promise; + return promise; +} + +async function fetchDocument(basePath: string, key: string) { + const res = await fetch( + `${basePath}/_next/static/chunks/guider-data-${key}.json`, + ); + const searchData = (await res.json()) as SearchData; + const searchDocument: ContentDocument['mainDoc'] = new FlexSearch.Document({ + cache: 100, + tokenize: 'full', + document: { + id: 'id', + index: ['title', 'content'], + store: ['content', 'url', 'title'], + }, + context: { + resolution: 9, + depth: 2, + bidirectional: true, + }, + }); + + let pageId = 0; + for (const [url, data] of Object.entries(searchData)) { + for (const section of data.sections) { + pageId++; + searchDocument.add({ + id: pageId, + title: section.heading?.text ?? '', + url: url + (section.heading ? `#${section.heading.id}` : ''), + content: section.content, + }); + } + } + + contentDocument = { + mainDoc: searchDocument, + }; +} + +export function usePreloadSearch(key: string) { + const { basePath } = useRouter(); + useEffect(() => { + void loadDocument(basePath, key); + }, [basePath, key]); +} + +export function useSearch(key: string) { + const { basePath } = useRouter(); + const [query, setQuery] = useState(''); + const [resultLoading, setResultLoading] = useState(false); + const [resultLoadingError, setResultLoadingError] = useState(false); + const [results, setResults] = useState(null); + + const doSearch = useCallback( + async (contentKey: string, searchQuery: string) => { + setResultLoading(true); + setResultLoadingError(false); + try { + await loadDocument(basePath, contentKey); + const doc = contentDocument; + if (!doc) throw new Error('Doc not loaded'); + const docResults = + doc.mainDoc.search(searchQuery, 5, { + enrich: true, + suggest: true, + })?.[0]?.result ?? []; + setResults( + docResults.map((res): SearchResult => { + return { + type: 'section', + id: res.id.toString(), + title: res.doc.title, + content: res.doc.content, + url: res.doc.url, + }; + }), + ); + } catch (err) { + // eslint-disable-next-line no-console -- needs to be logged to console + console.error('Failed to search', err); + setResultLoadingError(true); + setResultLoading(false); + return; + } + setResultLoading(false); + }, + [basePath], + ); + + const setQueryAndSearch = useCallback( + (data: string) => { + setQuery(data); + void doSearch(key, data); + }, + [key, doSearch], + ); + + return { + query, + setQuery: setQueryAndSearch, + results, + loading: resultLoading, + error: resultLoadingError, + }; +} diff --git a/packages/guider/src/client/partials/header/search/index.tsx b/packages/guider/src/client/partials/header/search/index.tsx new file mode 100644 index 00000000..a2aea67e --- /dev/null +++ b/packages/guider/src/client/partials/header/search/index.tsx @@ -0,0 +1,66 @@ +import { Fragment, useEffect, useRef, useState } from 'react'; +import { Transition } from '@headlessui/react'; +import { SearchButton } from './button'; +import { SearchModal } from './modal'; +import { usePreloadSearch } from './content'; + +function UpdateHead(props: { active?: boolean }) { + useEffect(() => { + if (props.active) + document.body.setAttribute('data-header-search-open', 'true'); + return () => { + document.body.removeAttribute('data-header-search-open'); + }; + }); + return null; +} + +export function HeaderSearch() { + const [open, setOpen] = useState(false); + const openRef = useRef(open); + const searchKey = 'default'; + usePreloadSearch(searchKey); + + useEffect(() => { + openRef.current = open; + }, [open]); + + useEffect(() => { + const listener = (e: KeyboardEvent) => { + if (openRef.current && e.key === 'Escape') { + setOpen(false); + e.preventDefault(); + return; + } + if (e.key === 'k' && e.ctrlKey) { + setOpen(true); + e.preventDefault(); + } + }; + document.addEventListener('keydown', listener); + return () => { + document.removeEventListener('keydown', listener); + }; + }, []); + + return ( +
    + { + setOpen(true); + }} + /> + + +
    + { + setOpen(false); + }} + /> +
    +
    +
    + ); +} diff --git a/packages/guider/src/client/partials/header/search/modal.tsx b/packages/guider/src/client/partials/header/search/modal.tsx new file mode 100644 index 00000000..723237cf --- /dev/null +++ b/packages/guider/src/client/partials/header/search/modal.tsx @@ -0,0 +1,38 @@ +import { Fragment } from 'react'; +import { Transition } from '@headlessui/react'; +import { SearchScreen } from './screen'; + +export function SearchModal(props: { + onClose?: () => void; + searchKey: string; +}) { + return ( +
    + +
    + +
    + +
    + +
    +
    +
    + ); +} diff --git a/packages/guider/src/client/partials/header/search/screen.tsx b/packages/guider/src/client/partials/header/search/screen.tsx new file mode 100644 index 00000000..8d063f4d --- /dev/null +++ b/packages/guider/src/client/partials/header/search/screen.tsx @@ -0,0 +1,154 @@ +import Link from 'next/link'; +import { Combobox } from '@headlessui/react'; +import { useRouter } from 'next/router'; +import { useCallback } from 'react'; +import classNames from 'classnames'; +import { Icon } from '../../../components/icon'; +import type { SearchResult } from './content'; +import { useSearch } from './content'; + +const iconMap = { + page: 'tabler:file-filled', + section: 'majesticons:text', +} as const; + +function SearchMessage(props: { title: string; text: string; icon: string }) { + return ( +
    +
    + +
    +

    + {props.title} +

    +

    {props.text}

    +
    + ); +} + +function SearchResults(props: { results: SearchResult[] }) { + return ( +
    + {props.results.map((v) => { + return ( + + {({ active }) => ( + +
    + +
    +
    +

    + {v.title} +

    +

    + {v.content} +

    +
    +
    + +
    + + )} +
    + ); + })} +
    + ); +} + +export function SearchScreen(props: { + searchKey: string; + onClose?: () => void; +}) { + const router = useRouter(); + const { error, loading, results, query, setQuery } = useSearch( + props.searchKey, + ); + + const onChange = useCallback( + (e?: SearchResult) => { + if (e) void router.push(e.url); + props.onClose?.(); + }, + [router, props.onClose], + ); + + return ( + +
    +
    + { + setQuery(e.target.value); + }} + type="text" + autoFocus + /> +
    + +
    +
    + + {loading ? ( +

    + Loading... +

    + ) : error ? ( + + ) : query.length > 0 && results && results.length === 0 ? ( + + ) : query.length > 0 && results ? ( + + ) : null} +
    +
    +
    + ); +} diff --git a/packages/guider/src/client/partials/layout/breadcrumb.tsx b/packages/guider/src/client/partials/layout/breadcrumb.tsx new file mode 100644 index 00000000..b2e5d71c --- /dev/null +++ b/packages/guider/src/client/partials/layout/breadcrumb.tsx @@ -0,0 +1,12 @@ +import { useGuiderPage } from '../../hooks/use-guider-page'; + +export function Breadcrumb() { + const { navContext } = useGuiderPage(); + + if (!navContext.current?.group) return null; + return ( +

    + {navContext.current.group} +

    + ); +} diff --git a/packages/guider/src/client/partials/layout/layout.tsx b/packages/guider/src/client/partials/layout/layout.tsx index 7216ca07..d52b0f97 100644 --- a/packages/guider/src/client/partials/layout/layout.tsx +++ b/packages/guider/src/client/partials/layout/layout.tsx @@ -6,6 +6,8 @@ import { GuiderToc } from '../toc'; import { GuiderContentFooter } from '../content-footer'; import { GuiderpageFooter } from '../page-footer'; import { useGuiderPage } from '../../hooks/use-guider-page'; +import { GuiderPageEnd } from '../page-end'; +import { Breadcrumb } from './breadcrumb'; export type GuiderLayoutProps = { children?: ReactNode; @@ -39,8 +41,10 @@ export function LayoutInternal(props: GuiderLayoutProps) { >
    -
    +
    + {props.children} +
    ; +} diff --git a/packages/guider/src/client/partials/page-end/page-end.tsx b/packages/guider/src/client/partials/page-end/page-end.tsx new file mode 100644 index 00000000..61add462 --- /dev/null +++ b/packages/guider/src/client/partials/page-end/page-end.tsx @@ -0,0 +1,62 @@ +import Link from 'next/link'; +import classNames from 'classnames'; +import { useGuiderPage } from '../../hooks/use-guider-page'; +import { Icon } from '../../components/icon'; + +function PageEndLink(props: { to: string; title: string; flip?: boolean }) { + return ( + + {!props.flip ? ( + + ) : null} + {props.title} + {props.flip ? ( + + ) : null} + + ); +} + +export function PageEndInternal() { + const { navContext } = useGuiderPage(); + + const hasContent = navContext.prev || navContext.next; + if (!hasContent) return null; + + return ( +
    +
    + {navContext.prev ? ( + + ) : null} +
    +
    + {navContext.next ? ( + + ) : null} +
    +
    + ); +} diff --git a/packages/guider/src/client/partials/toc/toc.tsx b/packages/guider/src/client/partials/toc/toc.tsx index 0181eb50..5e12efd0 100644 --- a/packages/guider/src/client/partials/toc/toc.tsx +++ b/packages/guider/src/client/partials/toc/toc.tsx @@ -10,7 +10,11 @@ function TocLink(props: { onClick?: () => void; }) { return ( -

    +

    { e.preventDefault(); @@ -34,7 +38,7 @@ function TocLink(props: { export function TocInternal() { const ctx = useContext(GuiderLayoutContext); const headings = useMemo( - () => [...(ctx?.headings ?? [])].slice(1).filter((v) => v.depth <= 2), + () => [...(ctx?.headings ?? [])].slice(1).filter((v) => v.depth <= 3), [ctx?.headings], ); const ids = useMemo(() => headings.map((v) => v.data.id), [headings]); diff --git a/packages/guider/src/client/utils/navigational-buttons.ts b/packages/guider/src/client/utils/navigational-buttons.ts new file mode 100644 index 00000000..6e49d709 --- /dev/null +++ b/packages/guider/src/client/utils/navigational-buttons.ts @@ -0,0 +1,67 @@ +import type { + DirectoryComponent, + LinkComponent, + NestableLinkComponent, +} from 'src/theme'; +import { isRouteAtive } from '../components/utils/activelink'; + +type SidebarLink = LinkComponent | NestableLinkComponent; +type WrappedSidebarLink = { + item: SidebarLink; + group?: string; +}; +type DeepArray = DeepArray[] | T; + +function wrapLink(item: SidebarLink, group?: string): WrappedSidebarLink { + return { + group, + item, + }; +} + +function flattenSidebar(items: DirectoryComponent['sidebar'], group?: string) { + return items + .map((item): DeepArray => { + if (item.type === 'link') return wrapLink(item, group); + if (item.type === 'group') return flattenSidebar(item.items, item.title); + if (item.type === 'nested-link' && item.to) + return [wrapLink(item, group), ...flattenSidebar(item.items, group)]; + if (item.type === 'nested-link' && !item.to) + return flattenSidebar(item.items, group); + return []; + }) + .filter((v) => { + if (Array.isArray(v)) return true; + if (v.item.type === 'link' || v.item.type === 'nested-link') return true; + return false; + }); +} + +export function getCurrentPageContext( + pagePathName: string, + dir: DirectoryComponent, +) { + const navMap = flattenSidebar(dir.sidebar).flat(4) as WrappedSidebarLink[]; + + for (let i = 0; i < navMap.length; i++) { + const item = navMap[i]; + const isActive = isRouteAtive( + item.item.to ?? '/', + pagePathName, + item.item.exact ?? false, + ); + if (!isActive) continue; + + return { + prev: i > 0 ? navMap[i - 1] : null, + current: item, + next: i + 1 < navMap.length ? navMap[i + 1] : null, + }; + } + + return { + prev: null, + current: null, + next: null, + }; +} diff --git a/packages/guider/src/index.ts b/packages/guider/src/index.ts index 49a05cb4..9b0bf35c 100644 --- a/packages/guider/src/index.ts +++ b/packages/guider/src/index.ts @@ -2,6 +2,7 @@ import type { NextConfig } from 'next'; import ExtraWatchWebpackPlugin from 'extra-watch-webpack-plugin'; import { GuiderPlugin } from './webpack/plugin/plugin'; import type { GuiderInitConfig } from './types'; +import { GuiderSearchPlugin } from './webpack/search'; export { getGuiderPluginCache } from './webpack/plugin/plugin'; @@ -10,6 +11,7 @@ export function guider(initConfig: GuiderInitConfig) { ...initConfig, }; const guiderPlugin = new GuiderPlugin(guiderConfig); + const searchPlugin = new GuiderSearchPlugin(); function withGuider(nextConfig: NextConfig = {}): NextConfig { const extraWatchers = new ExtraWatchWebpackPlugin({ @@ -17,6 +19,10 @@ export function guider(initConfig: GuiderInitConfig) { }); return { ...nextConfig, + images: { + ...(nextConfig.images ?? {}), + unoptimized: nextConfig?.images?.unoptimized ?? true, + }, transpilePackages: [ '@neato/guider', ...(nextConfig.transpilePackages ?? []), @@ -28,6 +34,7 @@ export function guider(initConfig: GuiderInitConfig) { webpack(config, options) { if (!config.plugins) config.plugins = []; config.plugins.push(guiderPlugin); + config.plugins.push(searchPlugin); config.plugins.push(extraWatchers); config.module.rules.push({ diff --git a/packages/guider/src/styles/global.css b/packages/guider/src/styles/global.css index 852ede6c..7c89a2c6 100644 --- a/packages/guider/src/styles/global.css +++ b/packages/guider/src/styles/global.css @@ -16,7 +16,7 @@ body[data-header-dropdown-open] { } } -body[data-stop-overflow="true"] { +body[data-stop-overflow="true"], body[data-header-search-open="true"] { @apply gd-overflow-hidden; scrollbar-gutter: stable; } diff --git a/packages/guider/src/theme/components/layout.ts b/packages/guider/src/theme/components/layout.ts index 27aad9a3..fbd821fa 100644 --- a/packages/guider/src/theme/components/layout.ts +++ b/packages/guider/src/theme/components/layout.ts @@ -54,6 +54,7 @@ const baseLayoutConfig: PopulatedLayoutSettings = { navigationState: true, backgroundPatternState: false, logoState: true, + pageEndState: true, }; function extractState( diff --git a/packages/guider/src/theme/components/settings.ts b/packages/guider/src/theme/components/settings.ts index e17954fe..05499f56 100644 --- a/packages/guider/src/theme/components/settings.ts +++ b/packages/guider/src/theme/components/settings.ts @@ -41,6 +41,7 @@ export type LayoutSettings = { navigation: ToggleablePartial; contentFooter: ToggleablePartial; pageFooter: ToggleablePartial; + pageEnd: ToggleablePartial; logo: ToggleablePartial; pageLayout?: Partial; backgroundPattern: ToggleSetting | BackgroundPatterns; @@ -83,6 +84,7 @@ export type PopulatedLayoutSettings = { navigationState: ToggleSetting; contentFooterState: ToggleSetting; pageFooterState: ToggleSetting; + pageEndState: ToggleSetting; logoState: ToggleSetting; pageLayoutComponent?: Partial; @@ -93,4 +95,5 @@ export type PopulatedLayoutSettings = { contentFooterComponent?: Partial; pageFooterComponent?: Partial; logoComponent?: Partial; + pageEndComponent?: Partial; }; diff --git a/packages/guider/src/webpack/loader/index.ts b/packages/guider/src/webpack/loader/index.ts index 3a119026..bb1b1209 100644 --- a/packages/guider/src/webpack/loader/index.ts +++ b/packages/guider/src/webpack/loader/index.ts @@ -23,7 +23,7 @@ async function loader( if (directories.pagesDir) context.addContextDependency(directories.pagesDir); if (type === 'virtual') return virtualLoader(getGuiderPluginCache()); - if (type === 'mdx') return mdLoader(source); + if (type === 'mdx') return (await mdLoader(source)).script; throw new Error(`Loader used with incorrect type (${type})`); } diff --git a/packages/guider/src/webpack/loader/md-loader.ts b/packages/guider/src/webpack/loader/md-loader.ts index 82995566..741c98a3 100644 --- a/packages/guider/src/webpack/loader/md-loader.ts +++ b/packages/guider/src/webpack/loader/md-loader.ts @@ -8,6 +8,10 @@ import rehypeExtractExcerpt from 'rehype-extract-excerpt'; import remarkLinkRewrite from 'remark-link-rewrite'; import { remarkNpm2Yarn } from '@theguild/remark-npm2yarn'; import remarkGfm from 'remark-gfm'; +import { SKIP, visit } from 'unist-util-visit'; +import type { Heading, HeadingData, Root } from 'mdast'; +import { phrasing } from 'mdast-util-phrasing'; +import type { VFileWithOutput } from 'unified'; import { transformerNotationDiff, transformerNotationHighlight, @@ -18,7 +22,12 @@ import { const EXPORT_FOOTER = 'export default '; -export async function mdLoader(source: string): Promise { +interface Section { + heading?: { id: string; depth: Heading['depth']; text: string }; + content: string; +} + +export async function mdLoader(source: string) { const meta = grayMatter(source); const file = await compile(source, { jsx: true, @@ -66,6 +75,67 @@ export async function mdLoader(source: string): Promise { }, }, ], + () => { + return (root: Root, vFile: VFileWithOutput) => { + const sections: Section[] = []; + + let currentSection: Section | undefined; + let previousParentNode: any; + + visit(root, (node, _, parent) => { + if (node.type === 'heading') { + if (currentSection) { + sections.push(currentSection); + } + + const heading = node; + const id = (heading.data as HeadingData & { id: string })?.id; + const depth = heading.depth; + let text = ''; + visit(heading, ['text', 'inlineCode'], (hChild: any) => { + text += hChild.value; + }); + + currentSection = { + heading: { + id, + depth, + text, + }, + content: '', + }; + + return SKIP; + } + + const types = ['text', 'inlineCode']; + + if (types.includes(node.type)) { + if (!currentSection) { + currentSection = { + heading: undefined, + content: '', + }; + } + + if ( + previousParentNode && + previousParentNode !== parent && + !phrasing(previousParentNode) + ) { + currentSection.content += ' '; + } + currentSection.content += (node as any).value; + + previousParentNode = parent; + } + }); + + if (currentSection) sections.push(currentSection); + + vFile.data = { ...vFile.data, sections }; + }; + }, ], rehypePlugins: [ rehypeExtractExcerpt, @@ -98,7 +168,7 @@ export async function mdLoader(source: string): Promise { excerpt: file.data.excerpt, }; - return ` + const script = ` import { createMdxPage } from "@neato/guider/client"; ${finalMdxCode} @@ -110,4 +180,11 @@ export async function mdLoader(source: string): Promise { export default createMdxPage(__guiderPageOptions); `; + + return { + script, + searchData: { + sections: file.data.sections, + }, + }; } diff --git a/packages/guider/src/webpack/plugin/collector.ts b/packages/guider/src/webpack/plugin/collector.ts index 4c1ed078..fd399896 100644 --- a/packages/guider/src/webpack/plugin/collector.ts +++ b/packages/guider/src/webpack/plugin/collector.ts @@ -35,9 +35,7 @@ function normalizePathSeparator(path: string): string { return path.replace(pathSeparatorRegex, '/'); } -async function filePathToSitePath( - filePath: string, -): Promise { +async function filePathToSitePath(filePath: string): Promise { let strippedPath = dirname(relative('./pages', filePath)); const fileContents = await readFile(filePath, 'utf-8'); const parsedContents = JSON.parse(fileContents); diff --git a/packages/guider/src/webpack/search/index.ts b/packages/guider/src/webpack/search/index.ts new file mode 100644 index 00000000..9ae5e255 --- /dev/null +++ b/packages/guider/src/webpack/search/index.ts @@ -0,0 +1,83 @@ +import { relative, sep } from 'node:path'; +import { readFile } from 'node:fs/promises'; +import { createHash } from 'node:crypto'; +import webpack from 'webpack'; +import type { Compiler } from 'webpack'; +import { glob } from 'glob'; +import { mdLoader } from '../loader/md-loader'; + +const pluginName = 'GuiderSearchPlugin'; +const defaultKey = 'default'; + +const pathSeparatorRegex = RegExp(`\\${sep}`, 'g'); +function normalizePathSeparator(path: string): string { + return path.replace(pathSeparatorRegex, '/'); +} + +async function filePathToPageData(filePath: string) { + let strippedPath = relative('./pages', filePath).replace(/.mdx?$/g, ''); + const fileContents = await readFile(filePath, 'utf-8'); + + strippedPath = normalizePathSeparator(strippedPath); + + return { + sitePath: `/${strippedPath}`, + fileContents, + }; +} + +function generateChecksum(str: string) { + return createHash('md5').update(str, 'utf8').digest('hex'); +} + +const cache: Record = {}; + +export class GuiderSearchPlugin { + apply(compiler: Compiler) { + compiler.hooks.make.tap(pluginName, (compilation) => { + compilation.hooks.processAssets.tapAsync( + { + name: pluginName, + stage: webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONS, + }, + async (assets, callback) => { + try { + const dataBuckets: Record = {}; + + const mdxPages = await glob('pages/**/*.{mdx,md}'); + const pageData = await Promise.all( + mdxPages.map(async (filePath) => { + const sitePathData = await filePathToPageData(filePath); + const hash = generateChecksum(sitePathData.fileContents); + const key = `${filePath}-${hash}`; + const compiled = + cache[key] ?? (await mdLoader(sitePathData.fileContents)); + cache[key] = compiled; + return { + searchData: compiled.searchData, + sitePath: sitePathData.sitePath, + }; + }), + ); + + for (const page of pageData) { + const key = defaultKey; + dataBuckets[key] ??= {}; + dataBuckets[key][page.sitePath] = page.searchData; + } + + for (const [fileKey, content] of Object.entries(dataBuckets)) { + assets[`static/chunks/guider-data-${fileKey}.json`] = + new webpack.sources.RawSource(JSON.stringify(content)); + } + } catch (err) { + callback(err as Error); + return; + } + + callback(); + }, + ); + }); + } +} diff --git a/packages/guider/tsup.config.ts b/packages/guider/tsup.config.ts index a5d1e8a3..8c0461c3 100644 --- a/packages/guider/tsup.config.ts +++ b/packages/guider/tsup.config.ts @@ -8,7 +8,7 @@ export default defineConfig([ outExtension: () => ({ js: '.js', dts: '.d.ts' }), dts: true, bundle: true, - external: ['@neato/guider', '@neato/guider/shim.guider.virtual'], + external: ['@neato/guider', '@neato/guider/shim.guider.virtual', 'webpack'], }, { name: 'guider-loader', diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4b65a689..6c130bb7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -173,6 +173,9 @@ importers: '@theguild/remark-npm2yarn': specifier: ^0.3.0 version: 0.3.0 + '@types/unist': + specifier: ^3.0.2 + version: 3.0.2 '@vcarl/remark-headings': specifier: ^0.1.0 version: 0.1.0 @@ -188,6 +191,9 @@ importers: extra-watch-webpack-plugin: specifier: ^1.0.3 version: 1.0.3 + flexsearch: + specifier: ^0.7.43 + version: 0.7.43 git-url-parse: specifier: ^14.0.0 version: 14.0.0 @@ -197,6 +203,9 @@ importers: gray-matter: specifier: ^4.0.3 version: 4.0.3 + mdast-util-phrasing: + specifier: ^4.1.0 + version: 4.1.0 next-seo: specifier: ^6.5.0 version: 6.5.0(next@14.1.0)(react-dom@18.2.0)(react@18.2.0) @@ -230,6 +239,12 @@ importers: type-fest: specifier: ^4.10.3 version: 4.10.3 + unified: + specifier: ^11.0.4 + version: 11.0.4 + unist-util-visit: + specifier: ^5.0.0 + version: 5.0.0 webpack-virtual-modules: specifier: ^0.6.1 version: 0.6.1 @@ -255,6 +270,9 @@ importers: '@types/git-url-parse': specifier: ^9.0.3 version: 9.0.3 + '@types/mdast': + specifier: ^4.0.3 + version: 4.0.3 '@types/react': specifier: 18.2.56 version: 18.2.56 @@ -3102,6 +3120,10 @@ packages: resolution: {integrity: sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==} dev: true + /flexsearch@0.7.43: + resolution: {integrity: sha512-c5o/+Um8aqCSOXGcZoqZOm+NqtVwNsvVpWv6lfmSclU954O3wvQKxxK8zj74fPaSJbXpSLTs4PRhh+wnoCXnKg==} + dev: false + /for-each@0.3.3: resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} dependencies: