diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..61d0e33c --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,45 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Next.js: debug server-side", + "type": "node-terminal", + "request": "launch", + "command": "pnpm dev" + }, + { + "name": "Next.js: debug client-side", + "type": "chrome", + "request": "launch", + "url": "http://localhost:3000" + }, + { + "name": "Next.js: debug client-side (Firefox)", + "type": "firefox", + "request": "launch", + "url": "http://localhost:3000", + "reAttach": true, + "pathMappings": [ + { + "url": "webpack://_N_E", + "path": "${workspaceFolder}" + } + ] + }, + { + "name": "Next.js: debug full stack", + "type": "node", + "request": "launch", + "program": "${workspaceFolder}/node_modules/.bin/next", + "runtimeArgs": ["--inspect"], + "skipFiles": ["/**"], + "serverReadyAction": { + "action": "debugWithChrome", + "killOnServerStop": true, + "pattern": "- Local:.+(https?://.+)", + "uriFormat": "%s", + "webRoot": "${workspaceFolder}" + } + } + ] +} diff --git a/README.md b/README.md index 1356b7b6..f73f9998 100644 --- a/README.md +++ b/README.md @@ -37,34 +37,10 @@ Formatting - checks for spaces, line length, etc. i.e prettier Both are run against staged files on commit. If it's failing for a good reason and you need to bypass you can use the `--no-verify` or `-n` flag. `git commit -nm "my message"` -## Documentation Docs +## Editing content -Docs are MDX in the `docs` folder. Here are a couple things you should know! - -1. Our MDX supports [Github Flavored Markdown](https://github.github.com/gfm/) (GFM). -2. Images should be stored along side the doc that uses them. -3. Shared Images can be stored in a shared folder @ `docs/images` -4. [Callouts](https://github.com/lin-stephanie/rehype-callouts?tab=readme-ov-file#rehype-callouts): These are similar to block quotes or an aside but for various warnings, info, pro times, etc. -5. Code Blocks: - - - Required - 1. Specify a language: ` ```js ` or `` `const inlineCode = [1,2,3];{:js}` `` - - Commands for a users terminal = `bash` - - env files = `ini` - - JavaScript = `js` - - TypeScript = `ts` - - GraphQL = `gql` - - JSON = `json` - - For a full list see: https://shiki.style/languages - 2. Add [line numbers](https://rehype-pretty.pages.dev/#line-numbers) to any complex code. `ini` and `bash` don't need to show line numbers generally. ` ```js showLineNumbers` - 3. Complete files should have a [file names](https://rehype-pretty.pages.dev/#titles) ` ```js title="pages/_app.js` - - Optional - - 1. Lines can be [highlighted](https://rehype-pretty.pages.dev/#highlight-lines) in code blocks ` ```js {1,3-5}`. There are a variety of advanced highlighting methods, see: https://rehype-pretty.pages.dev/#highlight-lines - 2. Lines may be [diffed](https://shiki.style/packages/transformers#transformernotationdiff) in a code block: - - ```js - console.log('hewwo') // [!code --] - console.log('hello') // [!code ++] - console.log('goodbye') - ``` +- Docs: [Located in the `faustjs` Repo under `/docs`](https://github.com/wpengine/faustjs/tree/canary/docs)\ +- Blog Posts & Pages: [Located in the headless WordPress CMS](https://cms.faustjs.org/wp-admin) +- [Home Page](src/pages/index.jsx) +- [Showcase](src/pages/showcase/index.jsx) +- [Main Navigation](src/components/primary-nav.jsx) diff --git a/next.config.mjs b/next.config.mjs index 3a375d15..ebba0ab6 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,17 +1,8 @@ -import { env } from "node:process"; import { withFaust, getWpHostname } from "@faustwp/core"; -import createMDX from "@next/mdx"; -import { transformerNotationDiff } from "@shikijs/transformers"; import { withWPEConfig } from "@wpengine/atlas-next"; import { createSecureHeaders } from "next-secure-headers"; -import recmaNextjsStaticProps from "recma-nextjs-static-props"; -import rehypeCallouts from "rehype-callouts"; -import rehypeMdxImportMedia from "rehype-mdx-import-media"; -import { rehypePrettyCode } from "rehype-pretty-code"; -import rehypeSlug from "rehype-slug"; -import remarkGfm from "remark-gfm"; import redirectsOldSite from "./redirects-old-site.mjs"; -import smartSearchPlugin from "./src/lib/smart-search-plugin.mjs"; +import { DOCS_PATH } from "./src/lib/remote-mdx-files.mjs"; const newRedirects = [ { @@ -50,6 +41,11 @@ const nextConfig = { hostname: getWpHostname(), pathname: "/**", }, + { + protocol: "https", + hostname: "raw.githubusercontent.com", + pathname: DOCS_PATH + "/**", + }, ], }, i18n: { @@ -66,43 +62,6 @@ const nextConfig = { }, ]; }, - webpack: (config, { isServer }) => { - if (isServer) { - config.plugins.push( - smartSearchPlugin({ - endpoint: env.NEXT_PUBLIC_SEARCH_ENDPOINT, - accessToken: env.NEXT_SEARCH_ACCESS_TOKEN, - }), - ); - } - - return config; - }, }; -const withMDX = createMDX({ - options: { - recmaPlugins: [recmaNextjsStaticProps], - remarkPlugins: [remarkGfm], - rehypePlugins: [ - rehypeMdxImportMedia, - rehypeSlug, - rehypeCallouts, - [ - rehypePrettyCode, - { - transformers: [ - transformerNotationDiff({ - matchAlgorithm: "v3", - }), - ], - theme: "github-dark-dimmed", - defaultLang: "plaintext", - bypassInlineCode: false, - }, - ], - ], - }, -}); - -export default withWPEConfig(withFaust(withMDX(nextConfig))); +export default withWPEConfig(withFaust(nextConfig)); diff --git a/package.json b/package.json index cdd352d5..d9823509 100644 --- a/package.json +++ b/package.json @@ -4,12 +4,15 @@ "scripts": { "dev": "faust dev", "build": "faust build", - "postbuild": "next-sitemap --config next-sitemap.config.mjs", + "postbuild": "concurrently \"pnpm build:sitemap\" \"pnpm build:search\"", "generate": "faust generatePossibleTypes", "stylesheet": "faust generateGlobalStylesheet", "start": "faust start", "test:format": "prettier . --check", "test:lint": "eslint --quiet", + "test:search": "NODE_ENV=test node --env-file=.env.local ./scripts/smart-search.mjs", + "build:search": "node ./scripts/smart-search.mjs", + "build:sitemap": "next-sitemap --config next-sitemap.config.mjs", "lint": "eslint --fix", "format": "prettier . --write", "prepare": "husky" @@ -23,35 +26,42 @@ "@heroicons/react": "^2.2.0", "@icons-pack/react-simple-icons": "^12.0.0", "@js-temporal/polyfill": "^0.4.4", - "@mdx-js/loader": "^3.0.1", - "@mdx-js/react": "^3.0.1", - "@next/mdx": "^15.1.7", + "@jsdevtools/rehype-url-inspector": "^2.0.2", + "@octokit/core": "^6.1.4", "@shikijs/transformers": "^3.0.0", "@sindresorhus/slugify": "^2.2.1", "@wpengine/atlas-next": "^2.0.0", - "classnames": "^2.5.1", "date-fns": "^4.1.0", "downshift": "^9.0.8", "feed": "^4.2.2", "graphql": "^16.10.0", - "html-to-text": "^9.0.5", "http-status-codes": "^2.3.0", "lodash.debounce": "^4.0.8", "next": "^15.1.7", + "next-mdx-remote-client": "^2.1.1", "next-sitemap": "^4.2.3", "react": "^19.0.0", "react-dom": "^19.0.0", - "recma-nextjs-static-props": "^2.0.1", "rehype-callouts": "^2.0.1", - "rehype-mdx-import-media": "^1.2.0", + "rehype-external-links": "^3.0.0", "rehype-pretty-code": "^0.14.0", "rehype-slug": "^6.0.0", + "remark-flexible-toc": "^1.1.1", + "remark-frontmatter": "^5.0.0", "remark-gfm": "^4.0.1", - "shiki": "^3.0.0" + "remark-parse": "^11.0.0", + "remark-smartypants": "^3.0.2", + "remark-stringify": "^11.0.0", + "shiki": "^3.0.0", + "strip-markdown": "^6.0.0", + "unified": "^11.0.5", + "unist-util-visit": "^5.0.0", + "vfile-matter": "^5.0.0" }, "devDependencies": { "@tailwindcss/postcss": "^4.0.7", "@tailwindcss/typography": "^0.5.15", + "concurrently": "^9.1.2", "eslint": "^9.20.1", "eslint-config-neon": "^0.2.4", "eslint-plugin-mdx": "^3.1.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e26348c0..a9df50ce 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -40,15 +40,12 @@ importers: '@js-temporal/polyfill': specifier: ^0.4.4 version: 0.4.4 - '@mdx-js/loader': - specifier: ^3.0.1 - version: 3.1.0(acorn@8.14.0)(webpack@5.96.1) - '@mdx-js/react': - specifier: ^3.0.1 - version: 3.1.0(@types/react@18.3.12)(react@19.0.0) - '@next/mdx': - specifier: ^15.1.7 - version: 15.1.7(@mdx-js/loader@3.1.0(acorn@8.14.0)(webpack@5.96.1))(@mdx-js/react@3.1.0(@types/react@18.3.12)(react@19.0.0)) + '@jsdevtools/rehype-url-inspector': + specifier: ^2.0.2 + version: 2.0.2 + '@octokit/core': + specifier: ^6.1.4 + version: 6.1.4 '@shikijs/transformers': specifier: ^3.0.0 version: 3.0.0 @@ -58,9 +55,6 @@ importers: '@wpengine/atlas-next': specifier: ^2.0.0 version: 2.0.0(next@15.1.7(@babel/core@7.25.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(node-fetch@2.7.0) - classnames: - specifier: ^2.5.1 - version: 2.5.1 date-fns: specifier: ^4.1.0 version: 4.1.0 @@ -73,9 +67,6 @@ importers: graphql: specifier: ^16.10.0 version: 16.10.0 - html-to-text: - specifier: ^9.0.5 - version: 9.0.5 http-status-codes: specifier: ^2.3.0 version: 2.3.0 @@ -85,6 +76,9 @@ importers: next: specifier: ^15.1.7 version: 15.1.7(@babel/core@7.25.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + next-mdx-remote-client: + specifier: ^2.1.1 + version: 2.1.1(@types/react@18.3.12)(acorn@8.14.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) next-sitemap: specifier: ^4.2.3 version: 4.2.3(next@15.1.7(@babel/core@7.25.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)) @@ -94,27 +88,51 @@ importers: react-dom: specifier: ^19.0.0 version: 19.0.0(react@19.0.0) - recma-nextjs-static-props: - specifier: ^2.0.1 - version: 2.0.1 rehype-callouts: specifier: ^2.0.1 version: 2.0.1 - rehype-mdx-import-media: - specifier: ^1.2.0 - version: 1.2.0 + rehype-external-links: + specifier: ^3.0.0 + version: 3.0.0 rehype-pretty-code: specifier: ^0.14.0 version: 0.14.0(patch_hash=5daf4a646bac7bc7afda28dbc7a0a2cbe5ef4d7c0eb08f622a7f4c12d743fa4f)(shiki@3.0.0) rehype-slug: specifier: ^6.0.0 version: 6.0.0 + remark-flexible-toc: + specifier: ^1.1.1 + version: 1.1.1 + remark-frontmatter: + specifier: ^5.0.0 + version: 5.0.0 remark-gfm: specifier: ^4.0.1 version: 4.0.1 + remark-parse: + specifier: ^11.0.0 + version: 11.0.0 + remark-smartypants: + specifier: ^3.0.2 + version: 3.0.2 + remark-stringify: + specifier: ^11.0.0 + version: 11.0.0 shiki: specifier: ^3.0.0 version: 3.0.0 + strip-markdown: + specifier: ^6.0.0 + version: 6.0.0 + unified: + specifier: ^11.0.5 + version: 11.0.5 + unist-util-visit: + specifier: ^5.0.0 + version: 5.0.0 + vfile-matter: + specifier: ^5.0.0 + version: 5.0.0 devDependencies: '@tailwindcss/postcss': specifier: ^4.0.7 @@ -122,6 +140,9 @@ importers: '@tailwindcss/typography': specifier: ^0.5.15 version: 0.5.16(tailwindcss@4.0.7) + concurrently: + specifier: ^9.1.2 + version: 9.1.2 eslint: specifier: ^9.20.1 version: 9.20.1(jiti@2.4.2) @@ -1175,13 +1196,9 @@ packages: resolution: {integrity: sha512-2X6bvghJ/JAoZO52lbgyAPFj8uCflhTo2g7nkFzEQdXd/D8rEeD4HtmTEpmtGCva260fcd66YNXBOYdnmHqSOg==} engines: {node: '>=12'} - '@mdx-js/loader@3.1.0': - resolution: {integrity: sha512-xU/lwKdOyfXtQGqn3VnJjlDrmKXEvMi1mgYxVmukEUtVycIz1nh7oQ40bKTd4cA7rLStqu0740pnhGYxGoqsCg==} - peerDependencies: - webpack: '>=5' - peerDependenciesMeta: - webpack: - optional: true + '@jsdevtools/rehype-url-inspector@2.0.2': + resolution: {integrity: sha512-iQL2lb+nOoT+5jFz1nfOa3oBJPF2JyBtPQgGmrRhxLT3QZAGIMLz/mh/9mUmk7FfKF5NB8xq0KoLn5F4/QHpyA==} + engines: {node: '>=10'} '@mdx-js/mdx@3.1.0': resolution: {integrity: sha512-/QxEhPAvGwbQmy1Px8F899L5Uc2KZ6JtXwlCgJmjSTBedwOZkByYcBG4GceIGPXRDsmfxhHazuS+hlOShRLeDw==} @@ -1207,17 +1224,6 @@ packages: '@next/eslint-plugin-next@15.1.6': resolution: {integrity: sha512-+slMxhTgILUntZDGNgsKEYHUvpn72WP1YTlkmEhS51vnVd7S9jEEy0n9YAMcI21vUG4akTw9voWH02lrClt/yw==} - '@next/mdx@15.1.7': - resolution: {integrity: sha512-olVOjKA1K8b7/cu0zqWecVkwyCUnB9xlKXxB/CeCRoZYlH0zluLHwhWBX0PR9yf3CG7eNLrK+PfuPBF+LdWODQ==} - peerDependencies: - '@mdx-js/loader': '>=0.15.0' - '@mdx-js/react': '>=0.15.0' - peerDependenciesMeta: - '@mdx-js/loader': - optional: true - '@mdx-js/react': - optional: true - '@next/swc-darwin-arm64@15.1.7': resolution: {integrity: sha512-hPFwzPJDpA8FGj7IKV3Yf1web3oz2YsR8du4amKw8d+jAOHfYHYFpMkoF6vgSY4W6vB29RtZEklK9ayinGiCmQ==} engines: {node: '>= 10'} @@ -1309,6 +1315,36 @@ packages: resolution: {integrity: sha512-xhfYPXoV5Dy4UkY0D+v2KkwvnDfiA/8Mt3sWCGI/hM03NsYIH8ZaG6QzS9x7pje5vHZBZJ2v6VRFVTWACnqcmQ==} engines: {node: ^16.14.0 || >=18.0.0} + '@octokit/auth-token@5.1.2': + resolution: {integrity: sha512-JcQDsBdg49Yky2w2ld20IHAlwr8d/d8N6NiOXbtuoPCqzbsiJgF633mVUw3x4mo0H5ypataQIX7SFu3yy44Mpw==} + engines: {node: '>= 18'} + + '@octokit/core@6.1.4': + resolution: {integrity: sha512-lAS9k7d6I0MPN+gb9bKDt7X8SdxknYqAMh44S5L+lNqIN2NuV8nvv3g8rPp7MuRxcOpxpUIATWprO0C34a8Qmg==} + engines: {node: '>= 18'} + + '@octokit/endpoint@10.1.3': + resolution: {integrity: sha512-nBRBMpKPhQUxCsQQeW+rCJ/OPSMcj3g0nfHn01zGYZXuNDvvXudF/TYY6APj5THlurerpFN4a/dQAIAaM6BYhA==} + engines: {node: '>= 18'} + + '@octokit/graphql@8.2.1': + resolution: {integrity: sha512-n57hXtOoHrhwTWdvhVkdJHdhTv0JstjDbDRhJfwIRNfFqmSo1DaK/mD2syoNUoLCyqSjBpGAKOG0BuwF392slw==} + engines: {node: '>= 18'} + + '@octokit/openapi-types@23.0.1': + resolution: {integrity: sha512-izFjMJ1sir0jn0ldEKhZ7xegCTj/ObmEDlEfpFrx4k/JyZSMRHbO3/rBwgE7f3m2DHt+RrNGIVw4wSmwnm3t/g==} + + '@octokit/request-error@6.1.7': + resolution: {integrity: sha512-69NIppAwaauwZv6aOzb+VVLwt+0havz9GT5YplkeJv7fG7a40qpLt/yZKyiDxAhgz0EtgNdNcb96Z0u+Zyuy2g==} + engines: {node: '>= 18'} + + '@octokit/request@9.2.2': + resolution: {integrity: sha512-dZl0ZHx6gOQGcffgm1/Sf6JfEpmh34v3Af2Uci02vzUYz6qEN6zepoRtmybWXIGXFIK8K9ylE3b+duCWqhArtg==} + engines: {node: '>= 18'} + + '@octokit/types@13.8.0': + resolution: {integrity: sha512-x7DjTIbEpEWXK99DMd01QfWy0hd5h4EN+Q7shkdKds3otGQP+oWE/y0A76i1OvH9fygo4ddvNf7ZvF0t78P98A==} + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -1351,9 +1387,6 @@ packages: '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} - '@selderee/plugin-htmlparser2@0.11.0': - resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==} - '@shikijs/core@3.0.0': resolution: {integrity: sha512-gSm3JQf2J2psiUn5bWokmZwnu5N0jfBtRps4CQ1B+qrFvmZCRAkMVoaxgl9qZgAFK5KisLAS3//XaMFVytYHKw==} @@ -1554,6 +1587,9 @@ packages: '@types/ms@0.7.34': resolution: {integrity: sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==} + '@types/nlcst@2.0.3': + resolution: {integrity: sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA==} + '@types/node@22.13.4': resolution: {integrity: sha512-ywP2X0DYtX3y08eFVx5fNIw7/uIv8hYUKgXoK8oayJlLnKcRfEYCxWMVE1XagUdVtCJlZT1AU4LXEABW+L1Peg==} @@ -1949,6 +1985,9 @@ packages: resolution: {integrity: sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==} engines: {node: '>= 0.4'} + array-iterate@2.0.1: + resolution: {integrity: sha512-I1jXZMjAgCMmxT4qxXfPXa6SthSoE8h6gkSI9BGGNv8mP8G/v0blc+qFnZu6K42vTOiuME596QaLO0TP3Lk0xg==} + array-union@2.1.0: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} @@ -2055,6 +2094,9 @@ packages: bare-events@2.5.0: resolution: {integrity: sha512-/E8dDe9dsbLyh2qrZ64PEPadOQ0F4gbl1sUJOrmph7xOiIxfY8vwab/4bFLh4Y88/Hk/ujKcrQKc+ps0mv873A==} + before-after-hook@3.0.2: + resolution: {integrity: sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==} + bent@7.3.12: resolution: {integrity: sha512-T3yrKnVGB63zRuoco/7Ybl7BwwGZR0lceoVG5XmQyMIH9s19SV5m+a8qam4if0zQuAmOQTyPTPmsQBdAorGK3w==} @@ -2262,6 +2304,11 @@ packages: resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==} engines: {'0': node >= 6.0} + concurrently@9.1.2: + resolution: {integrity: sha512-H9MWcoPsYddwbOGM6difjVwVZHl63nwMEwDJG/L7VGtuaJhb12h2caPG2tVPWs7emuYix252iGfqOyrz1GczTQ==} + engines: {node: '>=18'} + hasBin: true + constant-case@3.0.4: resolution: {integrity: sha512-I2hSBi7Vvs7BEuJDr5dDHfzb/Ruj3FyvFyh7KLilAjNQw3Be+xgqUBA2W6scVEcL0hL1dwPRtIqEPVUCKkSsyQ==} @@ -2421,19 +2468,6 @@ packages: resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} engines: {node: '>=6.0.0'} - dom-serializer@2.0.0: - resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} - - domelementtype@2.3.0: - resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} - - domhandler@5.0.3: - resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} - engines: {node: '>= 4'} - - domutils@3.1.0: - resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==} - dot-case@3.0.4: resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==} @@ -2894,9 +2928,6 @@ packages: estree-util-to-js@2.0.0: resolution: {integrity: sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg==} - estree-util-value-to-estree@3.2.1: - resolution: {integrity: sha512-Vt2UOjyPbNQQgT5eJh+K5aATti0OjCIAGc9SgMdOFYbohuifsWclR74l0iZTJwePMgWYdX1hlVS+dedH9XV8kw==} - estree-util-visit@2.0.0: resolution: {integrity: sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==} @@ -2921,6 +2952,9 @@ packages: extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + fast-content-type-parse@2.0.1: + resolution: {integrity: sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q==} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -2962,6 +2996,9 @@ packages: fastq@1.17.1: resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} + fault@2.0.1: + resolution: {integrity: sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==} + feed@4.2.2: resolution: {integrity: sha512-u5/sxGfiMfZNtJ3OvQpXcvotFpYkL0n9u9mM2vkui2nGo8b4wvDkJ8gAkYqbA8QpGyFCv3RK0Z+Iv+9veCS9bQ==} engines: {node: '>=0.4.0'} @@ -3004,6 +3041,10 @@ packages: resolution: {integrity: sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==} engines: {node: '>= 6'} + format@0.2.2: + resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==} + engines: {node: '>=0.4.x'} + fs-extra@11.2.0: resolution: {integrity: sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==} engines: {node: '>=14.14'} @@ -3199,9 +3240,6 @@ packages: hast-util-parse-selector@4.0.0: resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==} - hast-util-properties-to-mdx-jsx-attributes@1.0.0: - resolution: {integrity: sha512-MZEdAYiXC8wDBfntAc7syyWHbcg/X1h03DQ7IQ6MKagMttpYhnKqOZR/nia0657Dt2v2vuXB8YuKNExw0Fljew==} - hast-util-to-estree@3.1.0: resolution: {integrity: sha512-lfX5g6hqVh9kjS/B9E2gSkvHH4SZNiQFiqWS0x9fENzEl+8W12RqdRxX6d/Cwxi30tPQs3bIO+aolQJNp1bIyw==} @@ -3236,16 +3274,9 @@ packages: resolution: {integrity: sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==} engines: {node: ^16.14.0 || >=18.0.0} - html-to-text@9.0.5: - resolution: {integrity: sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==} - engines: {node: '>=14'} - html-void-elements@3.0.0: resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} - htmlparser2@8.0.2: - resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} - http-status-codes@2.3.0: resolution: {integrity: sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==} @@ -3315,6 +3346,14 @@ packages: resolution: {integrity: sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==} engines: {node: '>=10.13.0'} + ip-regex@4.3.0: + resolution: {integrity: sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==} + engines: {node: '>=8'} + + is-absolute-url@4.0.1: + resolution: {integrity: sha512-/51/TKE88Lmm7Gc4/8btclNXWS+g50wXhYJq8HWIBAGUBnoAdRu1aXeh364t/O7wXDAcTJDP8PNuNKWUDWie+A==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + is-alphabetical@1.0.4: resolution: {integrity: sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==} @@ -3368,6 +3407,10 @@ packages: resolution: {integrity: sha512-l9qO6eFlUETHtuihLcYOaLKByJ1f+N4kthcU9YjHy3N+B3hWv0y/2Nd0mu/7lTFnRQHTrSdXF50HQ3bl5fEnng==} engines: {node: '>= 0.4'} + is-buffer@2.0.5: + resolution: {integrity: sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==} + engines: {node: '>=4'} + is-builtin-module@3.2.1: resolution: {integrity: sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==} engines: {node: '>=6'} @@ -3481,9 +3524,6 @@ packages: resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==} engines: {node: '>=0.10.0'} - is-reference@3.0.3: - resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} - is-regex@1.1.4: resolution: {integrity: sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==} engines: {node: '>= 0.4'} @@ -3696,9 +3736,6 @@ packages: resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==} engines: {node: '>= 0.6.3'} - leac@0.6.0: - resolution: {integrity: sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==} - levn@0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} @@ -3861,6 +3898,9 @@ packages: mdast-util-from-markdown@2.0.2: resolution: {integrity: sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==} + mdast-util-frontmatter@2.0.1: + resolution: {integrity: sha512-LRqI9+wdgC25P0URIJY9vwocIzCcksduHQ9OF2joxQoyTNVduwLAFUzjoopuRJbJAReaKrNQKAZKL3uCMugWJA==} + mdast-util-gfm-autolink-literal@2.0.1: resolution: {integrity: sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==} @@ -3916,6 +3956,9 @@ packages: micromark-core-commonmark@2.0.2: resolution: {integrity: sha512-FKjQKbxd1cibWMM1P9N+H8TwlgGgSkWZMmfuVucLCHaYqeSvJ0hFeHsIa65pA2nYbes0f8LDHPMrd9X7Ujxg9w==} + micromark-extension-frontmatter@2.0.0: + resolution: {integrity: sha512-C4AkuM3dA58cgZha7zVnuVxBhDsbttIMiytjgsM2XbHAB2faRVaHRle40558FBN+DJcrLNCoqG5mlrpdU4cRtg==} + micromark-extension-gfm-autolink-literal@2.1.0: resolution: {integrity: sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==} @@ -4088,6 +4131,13 @@ packages: neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + next-mdx-remote-client@2.1.1: + resolution: {integrity: sha512-FDScfbKe5gNlCw6wdkyZi9Eb3L5MM0laTxB9XeWEoaInOLCagyYIccPQaSnoLZPPhgDRJx5IxusyBPdBCam3QQ==} + engines: {node: '>=18.18.0'} + peerDependencies: + react: '>=19.0.0' + react-dom: '>=19.0.0' + next-secure-headers@2.2.0: resolution: {integrity: sha512-C7OfZ9JdSJyYMz2ZBMI/WwNbt0qNjlFWX9afUp8nEUzbz6ez3JbeopdyxSZJZJAzVLIAfyk6n73rFpd4e22jRg==} engines: {node: '>=10.0.0'} @@ -4120,6 +4170,9 @@ packages: sass: optional: true + nlcst-to-string@4.0.0: + resolution: {integrity: sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA==} + no-case@3.0.4: resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} @@ -4291,18 +4344,15 @@ packages: resolution: {integrity: sha512-SgOTCX/EZXtZxBE5eJ97P4yGM5n37BwRU+YMsH4vNzFqJV/oWFXXCmwFlgWUM4PrakybVOueJJ6pwHqSVhTFDw==} engines: {node: '>=16'} + parse-latin@7.0.0: + resolution: {integrity: sha512-mhHgobPPua5kZ98EF4HWiH167JWBfl4pvAIXXdbaVohtK7a6YBOy56kvhCqduqyo/f3yrHFWmqmiMg/BkBkYYQ==} + parse-numeric-range@1.3.0: resolution: {integrity: sha512-twN+njEipszzlMJd4ONUYgSfZPDxgHhT9Ahed5uTigpQn90FggW4SA/AIPq/6a149fTbE9qBEcSwE3FAEp6wQQ==} - parse-srcset@1.0.2: - resolution: {integrity: sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==} - parse5@7.2.1: resolution: {integrity: sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==} - parseley@0.12.1: - resolution: {integrity: sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==} - pascal-case@3.1.2: resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==} @@ -4340,12 +4390,6 @@ packages: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} - peberminta@0.9.0: - resolution: {integrity: sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==} - - periscopic@3.1.0: - resolution: {integrity: sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==} - picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -4560,9 +4604,6 @@ packages: recma-jsx@1.0.0: resolution: {integrity: sha512-5vwkv65qWwYxg+Atz95acp8DMu1JDSqdGkA2Of1j6rCreyFUE/gp15fC8MnGEuG1W68UKjM6x6+YTWIh7hZM/Q==} - recma-nextjs-static-props@2.0.1: - resolution: {integrity: sha512-XNQmiJYLcIzm130clZM0rzcGnAz9xhO6YX1z7vJIoE6Ve//+vxOtvES++MvwPpnGoIpPVP4V3nGCgt6ROsq2gw==} - recma-parse@1.0.0: resolution: {integrity: sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ==} @@ -4649,8 +4690,8 @@ packages: resolution: {integrity: sha512-Rae5en9z6g7UtPq86+5tOEOGPHAb1vArnFkfNA6Q1oteY7iN29nNoZkJ+ktPk94qC/n5e4sEBRNftkqXcgrfbA==} engines: {node: ^18.0.0 || >=20.0.0} - rehype-mdx-import-media@1.2.0: - resolution: {integrity: sha512-rf+2qnPv3LTqLtCr8GjhHUja2TEbmwWtD1o4jigrmGWbVDggOMxyNeqJhGpC4E3vtH+sY+a+u9WPSEaskEWPFA==} + rehype-external-links@3.0.0: + resolution: {integrity: sha512-yp+e5N9V3C6bwBeAC4n796kc86M4gJCdlVhiMTxIrJG5UHDMh+PJANf9heqORJbt1nrCbDwIlAZKjANIaVBbvw==} rehype-parse@9.0.1: resolution: {integrity: sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag==} @@ -4667,9 +4708,18 @@ packages: rehype-slug@6.0.0: resolution: {integrity: sha512-lWyvf/jwu+oS5+hL5eClVd3hNdmwM1kAC0BUvEGD19pajQMIzcNUd/k9GsfQ+FfECvX+JE+e9/btsKH0EjJT6A==} + remark-flexible-toc@1.1.1: + resolution: {integrity: sha512-aOmKELS8EtHXRnaqr9oNVyL/v+Ki4uKSOZJR5nniFDW/Pamv0wYmPSKuITI8vYmKGlHPV3wL9Sexxs5aD/PP5A==} + + remark-frontmatter@5.0.0: + resolution: {integrity: sha512-XTFYvNASMe5iPN0719nPrdItC9aU0ssC4v14mH1BCi1u0n1gAocqcujWUrByftZTbLhRtiKRyjYTSIOcr69UVQ==} + remark-gfm@4.0.1: resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==} + remark-mdx-remove-esm@1.1.0: + resolution: {integrity: sha512-oN3F9QRuPKSdzZi+wvEodBVjKwya63sl403pWzJvm0+c503iUjCDR+JAnP3Ho/4205IWbQ2NujPQi/B9kU6ZrA==} + remark-mdx@3.1.0: resolution: {integrity: sha512-Ngl/H3YXyBV9RcRNdlYsZujAmhsxwzxpDzpDEhFBVAGthS4GDgnctpDjgFl/ULx5UEDzqtW1cyBSNKqYYrqLBA==} @@ -4679,6 +4729,10 @@ packages: remark-rehype@11.1.1: resolution: {integrity: sha512-g/osARvjkBXb6Wo0XvAeXQohVta8i84ACbenPpoSsxTOQH/Ae0/RGP4WZgnMH5pMLpsj4FG7OHmcIcXxpza8eQ==} + remark-smartypants@3.0.2: + resolution: {integrity: sha512-ILTWeOriIluwEvPjv67v7Blgrcx+LZOkAUVtKI3putuhlZm84FnqDORNXPPm+HY3NdZOMhyDwZ1E+eZB/Df5dA==} + engines: {node: '>=16.0.0'} + remark-stringify@11.0.0: resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} @@ -4726,6 +4780,18 @@ packages: resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} engines: {node: '>=18'} + retext-latin@4.0.0: + resolution: {integrity: sha512-hv9woG7Fy0M9IlRQloq/N6atV82NxLGveq+3H2WOi79dtIYWN8OaxogDm77f8YnVXJL2VD3bbqowu5E3EMhBYA==} + + retext-smartypants@6.2.0: + resolution: {integrity: sha512-kk0jOU7+zGv//kfjXEBjdIryL1Acl4i9XNkHxtM7Tm5lFiCog576fjNC9hjoR7LTKQ0DsPWy09JummSsH1uqfQ==} + + retext-stringify@4.0.0: + resolution: {integrity: sha512-rtfN/0o8kL1e+78+uxPTqu1Klt0yPzKuQ2BfWwwfgIUSayyzxpM1PJzkKt4V8803uB9qSy32MvI7Xep9khTpiA==} + + retext@9.0.0: + resolution: {integrity: sha512-sbMDcpHCNjvlheSgMfEcVrZko3cDzdbe1x/e7G66dFp0Ff7Mldvi2uv6JkJQzdRcvLYE8CA8Oe8siQx8ZOgTcA==} + retry@0.12.0: resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} engines: {node: '>= 4'} @@ -4744,6 +4810,9 @@ packages: resolution: {integrity: sha512-omv1DIv5z1kV+zDAEjaDjWSkx8w5TbFp5NZoPwUipwzYVcor/4So9ZU3bUyQ1c8lxY5Q0Es/ztWW7PGjY7to0Q==} hasBin: true + rxjs@7.8.2: + resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + sade@1.8.1: resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} engines: {node: '>=6'} @@ -4792,9 +4861,6 @@ packages: resolution: {integrity: sha512-3A6sD0WYP7+QrjbfNA2FN3FsOaGGFoekCVgTyypy53gPxhbkCIjtO6YWgdrfM+n/8sI8JeXZOIxsHjMTNxQ4nQ==} engines: {node: ^14.0.0 || >=16.0.0} - selderee@0.11.0: - resolution: {integrity: sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==} - semver@5.7.2: resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} hasBin: true @@ -4811,6 +4877,10 @@ packages: sentence-case@3.0.4: resolution: {integrity: sha512-8LS0JInaQMCRoQ7YUytAo/xUu5W2XnQxV2HI/6uM6U7CITS1RqPElr30V6uIqyMKM9lJGRVFy5/4CuzcixNYSg==} + serialize-error@12.0.0: + resolution: {integrity: sha512-ZYkZLAvKTKQXWuh5XpBw7CdbSzagarX39WyZ2H07CDLC5/KfsRGlIXV8d4+tfqX1M7916mRqR1QfNHSij+c9Pw==} + engines: {node: '>=18'} + serialize-javascript@6.0.2: resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} @@ -4842,6 +4912,10 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + shell-quote@1.8.2: + resolution: {integrity: sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA==} + engines: {node: '>= 0.4'} + shiki@3.0.0: resolution: {integrity: sha512-x6MMdYN9auPGx7kMFtyKbaj65eCdetfrfkvQZwqisZLnGMnAZsZxOpcWD0ElvLPFWHOSMukVyN9Opm7TxQjnZA==} @@ -5027,12 +5101,12 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} + strip-markdown@6.0.0: + resolution: {integrity: sha512-mSa8FtUoX3ExJYDkjPUTC14xaBAn4Ik5GPQD45G5E2egAmeV3kHgVSTfIoSDggbF6Pk9stahVgqsLCNExv6jHw==} + strnum@1.0.5: resolution: {integrity: sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==} - style-to-js@1.1.16: - resolution: {integrity: sha512-/Q6ld50hKYPH3d/r6nr117TZkHR0w0kGGIVfpG9N6D8NymRPM9RqCUv4pRpJ62E5DqOYx2AFpbZMyCPnjQCnOw==} - style-to-object@0.4.4: resolution: {integrity: sha512-HYNoHZa2GorYNyqiCaBgsxvcJIn7OHq6inEga+E6Ke3m5JkoqpQbnFssk4jwe+K7AhGa2fcha4wSOf1Kn01dMg==} @@ -5117,6 +5191,10 @@ packages: text-decoder@1.2.1: resolution: {integrity: sha512-x9v3H/lTKIJKQQe7RPQkLfKAnc9lUTkWDypIQgTzPJAq+5/GCDHonmshfvlsNSj58yyshbIJJDLmU15qNERrXQ==} + tlds@1.255.0: + resolution: {integrity: sha512-tcwMRIioTcF/FcxLev8MJWxCp+GUALRhFEqbDoZrnowmKSGqPrl5pqS+Sut2m8BgJ6S4FExCSSpGffZ0Tks6Aw==} + hasBin: true + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -5124,6 +5202,10 @@ packages: tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + trim-lines@3.0.1: resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} @@ -5194,6 +5276,10 @@ packages: resolution: {integrity: sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==} engines: {node: '>=14.16'} + type-fest@4.37.0: + resolution: {integrity: sha512-S/5/0kFftkq27FPNye0XM1e2NsnoD/3FS+pBmbjmmtLT6I+i344KoOf7pvXreaFsDamWeaJX55nczA1m5PsBDg==} + engines: {node: '>=16'} + typed-array-buffer@1.0.2: resolution: {integrity: sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==} engines: {node: '>= 0.4'} @@ -5282,24 +5368,36 @@ packages: unist-util-is@6.0.0: resolution: {integrity: sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==} + unist-util-modify-children@4.0.0: + resolution: {integrity: sha512-+tdN5fGNddvsQdIzUF3Xx82CU9sMM+fA0dLgR9vOmT0oPT2jH+P1nd5lSqfCfXAw+93NhcXNY2qqvTUtE4cQkw==} + unist-util-position-from-estree@2.0.0: resolution: {integrity: sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ==} unist-util-position@5.0.0: resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + unist-util-remove@4.0.0: + resolution: {integrity: sha512-b4gokeGId57UVRX/eVKej5gXqGlc9+trkORhFJpu9raqZkZhU0zm8Doi05+HaiBsMEIJowL+2WtQ5ItjsngPXg==} + unist-util-stringify-position@2.0.3: resolution: {integrity: sha512-3faScn5I+hy9VleOq/qNbAd6pAx7iH5jYBMS9I1HgQVijz/4mv5Bvw5iw1sC/90CODiKo81G/ps8AJrISn687g==} unist-util-stringify-position@4.0.0: resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + unist-util-visit-children@3.0.0: + resolution: {integrity: sha512-RgmdTfSBOg04sdPcpTSD1jzoNBjt9a80/ZCzp5cI9n1qPzLZWF9YdvWGN2zmTumP1HWhXKdUWexjy/Wy/lJ7tA==} + unist-util-visit-parents@6.0.1: resolution: {integrity: sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==} unist-util-visit@5.0.0: resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} + universal-user-agent@7.0.2: + resolution: {integrity: sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q==} + universalify@2.0.1: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} @@ -5319,6 +5417,10 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + url-regex@5.0.0: + resolution: {integrity: sha512-O08GjTiAFNsSlrUWfqF1jH0H1W3m35ZyadHrGv5krdnmPPoxP27oDTqux/579PtaroiSGm5yma6KT1mHFH6Y/g==} + engines: {node: '>=8'} + util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -5337,6 +5439,12 @@ packages: vfile-location@5.0.3: resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==} + vfile-matter@5.0.0: + resolution: {integrity: sha512-jhPSqlj8hTSkTXOqyxbUeZAFFVq/iwu/jukcApEqc/7DOidaAth6rDc0Zgg0vWpzUnWkwFP7aK28l6nBmxMqdQ==} + + vfile-message@2.0.4: + resolution: {integrity: sha512-DjssxRGkMvifUOJre00juHoP9DPWuzjxKuMDrhNbk2TdaYYBNMStsNhEOt3idrtI12VQYM/1+iM0KOzXi4pxwQ==} + vfile-message@4.0.2: resolution: {integrity: sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==} @@ -5349,6 +5457,9 @@ packages: vfile-statistics@3.0.0: resolution: {integrity: sha512-/qlwqwWBWFOmpXujL/20P+Iuydil0rZZNglR+VNm6J0gpLHwuVM5s7g2TfVoswbXjZ4HuIhLMySEyIw5i7/D8w==} + vfile@4.2.1: + resolution: {integrity: sha512-O6AE4OskCG5S1emQ/4gl8zK586RqA3srz3nfK/Viy0UPToBc5Trp9BVFb1u0CjsKrAWwnpr4ifM/KBXPWwJbCA==} + vfile@6.0.3: resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} @@ -6722,15 +6833,10 @@ snapshots: jsbi: 4.3.0 tslib: 2.8.1 - '@mdx-js/loader@3.1.0(acorn@8.14.0)(webpack@5.96.1)': + '@jsdevtools/rehype-url-inspector@2.0.2': dependencies: - '@mdx-js/mdx': 3.1.0(acorn@8.14.0) - source-map: 0.7.4 - optionalDependencies: - webpack: 5.96.1(webpack-cli@5.1.4) - transitivePeerDependencies: - - acorn - - supports-color + url-regex: 5.0.0 + vfile: 4.2.1 '@mdx-js/mdx@3.1.0(acorn@8.14.0)': dependencies: @@ -6785,13 +6891,6 @@ snapshots: dependencies: fast-glob: 3.3.1 - '@next/mdx@15.1.7(@mdx-js/loader@3.1.0(acorn@8.14.0)(webpack@5.96.1))(@mdx-js/react@3.1.0(@types/react@18.3.12)(react@19.0.0))': - dependencies: - source-map: 0.7.4 - optionalDependencies: - '@mdx-js/loader': 3.1.0(acorn@8.14.0)(webpack@5.96.1) - '@mdx-js/react': 3.1.0(@types/react@18.3.12)(react@19.0.0) - '@next/swc-darwin-arm64@15.1.7': optional: true @@ -6886,6 +6985,47 @@ snapshots: dependencies: which: 4.0.0 + '@octokit/auth-token@5.1.2': {} + + '@octokit/core@6.1.4': + dependencies: + '@octokit/auth-token': 5.1.2 + '@octokit/graphql': 8.2.1 + '@octokit/request': 9.2.2 + '@octokit/request-error': 6.1.7 + '@octokit/types': 13.8.0 + before-after-hook: 3.0.2 + universal-user-agent: 7.0.2 + + '@octokit/endpoint@10.1.3': + dependencies: + '@octokit/types': 13.8.0 + universal-user-agent: 7.0.2 + + '@octokit/graphql@8.2.1': + dependencies: + '@octokit/request': 9.2.2 + '@octokit/types': 13.8.0 + universal-user-agent: 7.0.2 + + '@octokit/openapi-types@23.0.1': {} + + '@octokit/request-error@6.1.7': + dependencies: + '@octokit/types': 13.8.0 + + '@octokit/request@9.2.2': + dependencies: + '@octokit/endpoint': 10.1.3 + '@octokit/request-error': 6.1.7 + '@octokit/types': 13.8.0 + fast-content-type-parse: 2.0.1 + universal-user-agent: 7.0.2 + + '@octokit/types@13.8.0': + dependencies: + '@octokit/openapi-types': 23.0.1 + '@pkgjs/parseargs@0.11.0': optional: true @@ -6933,11 +7073,6 @@ snapshots: '@rtsao/scc@1.1.0': {} - '@selderee/plugin-htmlparser2@0.11.0': - dependencies: - domhandler: 5.0.3 - selderee: 0.11.0 - '@shikijs/core@3.0.0': dependencies: '@shikijs/types': 3.0.0 @@ -7157,6 +7292,10 @@ snapshots: '@types/ms@0.7.34': {} + '@types/nlcst@2.0.3': + dependencies: + '@types/unist': 3.0.3 + '@types/node@22.13.4': dependencies: undici-types: 6.20.0 @@ -7647,6 +7786,8 @@ snapshots: get-intrinsic: 1.2.4 is-string: 1.0.7 + array-iterate@2.0.1: {} + array-union@2.1.0: {} array.prototype.findlast@1.2.5: @@ -7799,6 +7940,8 @@ snapshots: bare-events@2.5.0: optional: true + before-after-hook@3.0.2: {} + bent@7.3.12: dependencies: bytesish: 0.4.4 @@ -8012,6 +8155,16 @@ snapshots: readable-stream: 3.6.2 typedarray: 0.0.6 + concurrently@9.1.2: + dependencies: + chalk: 4.1.2 + lodash: 4.17.21 + rxjs: 7.8.2 + shell-quote: 1.8.2 + supports-color: 8.1.1 + tree-kill: 1.2.2 + yargs: 17.7.2 + constant-case@3.0.4: dependencies: no-case: 3.0.4 @@ -8173,24 +8326,6 @@ snapshots: dependencies: esutils: 2.0.3 - dom-serializer@2.0.0: - dependencies: - domelementtype: 2.3.0 - domhandler: 5.0.3 - entities: 4.5.0 - - domelementtype@2.3.0: {} - - domhandler@5.0.3: - dependencies: - domelementtype: 2.3.0 - - domutils@3.1.0: - dependencies: - dom-serializer: 2.0.0 - domelementtype: 2.3.0 - domhandler: 5.0.3 - dot-case@3.0.4: dependencies: no-case: 3.0.4 @@ -9073,10 +9208,6 @@ snapshots: astring: 1.9.0 source-map: 0.7.4 - estree-util-value-to-estree@3.2.1: - dependencies: - '@types/estree': 1.0.6 - estree-util-visit@2.0.0: dependencies: '@types/estree-jsx': 1.0.5 @@ -9106,6 +9237,8 @@ snapshots: extend@3.0.2: {} + fast-content-type-parse@2.0.1: {} + fast-deep-equal@3.1.3: {} fast-diff@1.3.0: {} @@ -9152,6 +9285,10 @@ snapshots: dependencies: reusify: 1.0.4 + fault@2.0.1: + dependencies: + format: 0.2.2 + feed@4.2.2: dependencies: xml-js: 1.6.11 @@ -9198,6 +9335,8 @@ snapshots: combined-stream: 1.0.8 mime-types: 2.1.35 + format@0.2.2: {} + fs-extra@11.2.0: dependencies: graceful-fs: 4.2.11 @@ -9426,19 +9565,6 @@ snapshots: dependencies: '@types/hast': 3.0.4 - hast-util-properties-to-mdx-jsx-attributes@1.0.0: - dependencies: - '@types/estree': 1.0.6 - '@types/hast': 3.0.4 - comma-separated-tokens: 2.0.3 - estree-util-value-to-estree: 3.2.1 - mdast-util-mdx-jsx: 3.1.3 - property-information: 6.5.0 - space-separated-tokens: 2.0.2 - style-to-js: 1.1.16 - transitivePeerDependencies: - - supports-color - hast-util-to-estree@3.1.0: dependencies: '@types/estree': 1.0.6 @@ -9533,23 +9659,8 @@ snapshots: dependencies: lru-cache: 10.4.3 - html-to-text@9.0.5: - dependencies: - '@selderee/plugin-htmlparser2': 0.11.0 - deepmerge: 4.3.1 - dom-serializer: 2.0.0 - htmlparser2: 8.0.2 - selderee: 0.11.0 - html-void-elements@3.0.0: {} - htmlparser2@8.0.2: - dependencies: - domelementtype: 2.3.0 - domhandler: 5.0.3 - domutils: 3.1.0 - entities: 4.5.0 - http-status-codes@2.3.0: {} human-signals@5.0.0: {} @@ -9603,6 +9714,10 @@ snapshots: interpret@3.1.1: {} + ip-regex@4.3.0: {} + + is-absolute-url@4.0.1: {} + is-alphabetical@1.0.4: {} is-alphabetical@2.0.1: {} @@ -9668,6 +9783,8 @@ snapshots: call-bound: 1.0.3 has-tostringtag: 1.0.2 + is-buffer@2.0.5: {} + is-builtin-module@3.2.1: dependencies: builtin-modules: 3.3.0 @@ -9769,10 +9886,6 @@ snapshots: dependencies: isobject: 3.0.1 - is-reference@3.0.3: - dependencies: - '@types/estree': 1.0.6 - is-regex@1.1.4: dependencies: call-bind: 1.0.7 @@ -9968,8 +10081,6 @@ snapshots: dependencies: readable-stream: 2.3.8 - leac@0.6.0: {} - levn@0.4.1: dependencies: prelude-ls: 1.2.1 @@ -10143,6 +10254,17 @@ snapshots: transitivePeerDependencies: - supports-color + mdast-util-frontmatter@2.0.1: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + escape-string-regexp: 5.0.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + micromark-extension-frontmatter: 2.0.0 + transitivePeerDependencies: + - supports-color + mdast-util-gfm-autolink-literal@2.0.1: dependencies: '@types/mdast': 4.0.4 @@ -10307,6 +10429,13 @@ snapshots: micromark-util-symbol: 2.0.1 micromark-util-types: 2.0.1 + micromark-extension-frontmatter@2.0.0: + dependencies: + fault: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.1 + micromark-extension-gfm-autolink-literal@2.1.0: dependencies: micromark-util-character: 2.1.1 @@ -10542,7 +10671,7 @@ snapshots: micromark@4.0.1: dependencies: '@types/debug': 4.1.12 - debug: 4.3.7 + debug: 4.4.0 decode-named-character-reference: 1.0.2 devlop: 1.1.0 micromark-core-commonmark: 2.0.2 @@ -10610,6 +10739,22 @@ snapshots: neo-async@2.6.2: {} + next-mdx-remote-client@2.1.1(@types/react@18.3.12)(acorn@8.14.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0): + dependencies: + '@babel/code-frame': 7.26.2 + '@mdx-js/mdx': 3.1.0(acorn@8.14.0) + '@mdx-js/react': 3.1.0(@types/react@18.3.12)(react@19.0.0) + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + remark-mdx-remove-esm: 1.1.0 + serialize-error: 12.0.0 + vfile: 6.0.3 + vfile-matter: 5.0.0 + transitivePeerDependencies: + - '@types/react' + - acorn + - supports-color + next-secure-headers@2.2.0: {} next-sitemap@4.2.3(next@15.1.7(@babel/core@7.25.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)): @@ -10645,6 +10790,10 @@ snapshots: - '@babel/core' - babel-plugin-macros + nlcst-to-string@4.0.0: + dependencies: + '@types/nlcst': 2.0.3 + no-case@3.0.4: dependencies: lower-case: 2.0.2 @@ -10871,19 +11020,21 @@ snapshots: lines-and-columns: 2.0.4 type-fest: 3.13.1 - parse-numeric-range@1.3.0: {} + parse-latin@7.0.0: + dependencies: + '@types/nlcst': 2.0.3 + '@types/unist': 3.0.3 + nlcst-to-string: 4.0.0 + unist-util-modify-children: 4.0.0 + unist-util-visit-children: 3.0.0 + vfile: 6.0.3 - parse-srcset@1.0.2: {} + parse-numeric-range@1.3.0: {} parse5@7.2.1: dependencies: entities: 4.5.0 - parseley@0.12.1: - dependencies: - leac: 0.6.0 - peberminta: 0.9.0 - pascal-case@3.1.2: dependencies: no-case: 3.0.4 @@ -10916,14 +11067,6 @@ snapshots: path-type@4.0.0: {} - peberminta@0.9.0: {} - - periscopic@3.1.0: - dependencies: - '@types/estree': 1.0.6 - estree-walker: 3.0.3 - is-reference: 3.0.3 - picocolors@1.1.1: {} picomatch@2.3.1: {} @@ -11091,12 +11234,6 @@ snapshots: transitivePeerDependencies: - acorn - recma-nextjs-static-props@2.0.1: - dependencies: - '@types/estree': 1.0.6 - periscopic: 3.1.0 - unified: 11.0.5 - recma-parse@1.0.0: dependencies: '@types/estree': 1.0.6 @@ -11213,15 +11350,14 @@ snapshots: hastscript: 9.0.0 unist-util-visit: 5.0.0 - rehype-mdx-import-media@1.2.0: + rehype-external-links@3.0.0: dependencies: '@types/hast': 3.0.4 - hast-util-properties-to-mdx-jsx-attributes: 1.0.0 - parse-srcset: 1.0.2 - unified: 11.0.5 + '@ungap/structured-clone': 1.3.0 + hast-util-is-element: 3.0.0 + is-absolute-url: 4.0.1 + space-separated-tokens: 2.0.2 unist-util-visit: 5.0.0 - transitivePeerDependencies: - - supports-color rehype-parse@9.0.1: dependencies: @@ -11255,6 +11391,23 @@ snapshots: hast-util-to-string: 3.0.1 unist-util-visit: 5.0.0 + remark-flexible-toc@1.1.1: + dependencies: + '@types/mdast': 4.0.4 + github-slugger: 2.0.0 + mdast-util-to-string: 4.0.0 + unified: 11.0.5 + unist-util-visit: 5.0.0 + + remark-frontmatter@5.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-frontmatter: 2.0.1 + micromark-extension-frontmatter: 2.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + remark-gfm@4.0.1: dependencies: '@types/mdast': 4.0.4 @@ -11266,6 +11419,14 @@ snapshots: transitivePeerDependencies: - supports-color + remark-mdx-remove-esm@1.1.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-mdxjs-esm: 2.0.1 + unist-util-remove: 4.0.0 + transitivePeerDependencies: + - supports-color + remark-mdx@3.1.0: dependencies: mdast-util-mdx: 3.0.0 @@ -11290,6 +11451,13 @@ snapshots: unified: 11.0.5 vfile: 6.0.3 + remark-smartypants@3.0.2: + dependencies: + retext: 9.0.0 + retext-smartypants: 6.2.0 + unified: 11.0.5 + unist-util-visit: 5.0.0 + remark-stringify@11.0.0: dependencies: '@types/mdast': 4.0.4 @@ -11335,6 +11503,31 @@ snapshots: onetime: 7.0.0 signal-exit: 4.1.0 + retext-latin@4.0.0: + dependencies: + '@types/nlcst': 2.0.3 + parse-latin: 7.0.0 + unified: 11.0.5 + + retext-smartypants@6.2.0: + dependencies: + '@types/nlcst': 2.0.3 + nlcst-to-string: 4.0.0 + unist-util-visit: 5.0.0 + + retext-stringify@4.0.0: + dependencies: + '@types/nlcst': 2.0.3 + nlcst-to-string: 4.0.0 + unified: 11.0.5 + + retext@9.0.0: + dependencies: + '@types/nlcst': 2.0.3 + retext-latin: 4.0.0 + retext-stringify: 4.0.0 + unified: 11.0.5 + retry@0.12.0: {} reusify@1.0.4: {} @@ -11357,6 +11550,10 @@ snapshots: transitivePeerDependencies: - supports-color + rxjs@7.8.2: + dependencies: + tslib: 2.8.1 + sade@1.8.1: dependencies: mri: 1.2.0 @@ -11420,10 +11617,6 @@ snapshots: refa: 0.12.1 regexp-ast-analysis: 0.7.1 - selderee@0.11.0: - dependencies: - parseley: 0.12.1 - semver@5.7.2: {} semver@6.3.1: {} @@ -11436,6 +11629,10 @@ snapshots: tslib: 2.8.1 upper-case-first: 2.0.2 + serialize-error@12.0.0: + dependencies: + type-fest: 4.37.0 + serialize-javascript@6.0.2: dependencies: randombytes: 2.1.0 @@ -11499,6 +11696,8 @@ snapshots: shebang-regex@3.0.0: {} + shell-quote@1.8.2: {} + shiki@3.0.0: dependencies: '@shikijs/core': 3.0.0 @@ -11757,11 +11956,11 @@ snapshots: strip-json-comments@3.1.1: {} - strnum@1.0.5: {} - - style-to-js@1.1.16: + strip-markdown@6.0.0: dependencies: - style-to-object: 1.0.8 + '@types/mdast': 4.0.4 + + strnum@1.0.5: {} style-to-object@0.4.4: dependencies: @@ -11829,12 +12028,16 @@ snapshots: text-decoder@1.2.1: {} + tlds@1.255.0: {} + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 tr46@0.0.3: {} + tree-kill@1.2.2: {} + trim-lines@3.0.1: {} trough@2.2.0: {} @@ -11890,6 +12093,8 @@ snapshots: type-fest@3.13.1: {} + type-fest@4.37.0: {} + typed-array-buffer@1.0.2: dependencies: call-bind: 1.0.7 @@ -12043,6 +12248,11 @@ snapshots: dependencies: '@types/unist': 3.0.3 + unist-util-modify-children@4.0.0: + dependencies: + '@types/unist': 3.0.3 + array-iterate: 2.0.1 + unist-util-position-from-estree@2.0.0: dependencies: '@types/unist': 3.0.3 @@ -12051,6 +12261,12 @@ snapshots: dependencies: '@types/unist': 3.0.3 + unist-util-remove@4.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.0 + unist-util-visit-parents: 6.0.1 + unist-util-stringify-position@2.0.3: dependencies: '@types/unist': 2.0.11 @@ -12059,6 +12275,10 @@ snapshots: dependencies: '@types/unist': 3.0.3 + unist-util-visit-children@3.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-visit-parents@6.0.1: dependencies: '@types/unist': 3.0.3 @@ -12070,6 +12290,8 @@ snapshots: unist-util-is: 6.0.0 unist-util-visit-parents: 6.0.1 + universal-user-agent@7.0.2: {} + universalify@2.0.1: {} update-browserslist-db@1.1.2(browserslist@4.24.4): @@ -12090,6 +12312,11 @@ snapshots: dependencies: punycode: 2.3.1 + url-regex@5.0.0: + dependencies: + ip-regex: 4.3.0 + tlds: 1.255.0 + util-deprecate@1.0.2: {} uvu@0.5.6: @@ -12111,6 +12338,16 @@ snapshots: '@types/unist': 3.0.3 vfile: 6.0.3 + vfile-matter@5.0.0: + dependencies: + vfile: 6.0.3 + yaml: 2.7.0 + + vfile-message@2.0.4: + dependencies: + '@types/unist': 2.0.11 + unist-util-stringify-position: 2.0.3 + vfile-message@4.0.2: dependencies: '@types/unist': 3.0.3 @@ -12137,6 +12374,13 @@ snapshots: vfile: 6.0.3 vfile-message: 4.0.2 + vfile@4.2.1: + dependencies: + '@types/unist': 2.0.11 + is-buffer: 2.0.5 + unist-util-stringify-position: 2.0.3 + vfile-message: 2.0.4 + vfile@6.0.3: dependencies: '@types/unist': 3.0.3 diff --git a/prettier.config.mjs b/prettier.config.mjs index 7a1b3633..3f570e16 100644 --- a/prettier.config.mjs +++ b/prettier.config.mjs @@ -5,7 +5,7 @@ const config = { semi: true, plugins: ["prettier-plugin-tailwindcss"], - tailwindFunctions: ["classnames"], + tailwindFunctions: ["classNames"], }; export default config; diff --git a/scripts/smart-search.mjs b/scripts/smart-search.mjs new file mode 100644 index 00000000..6df5e370 --- /dev/null +++ b/scripts/smart-search.mjs @@ -0,0 +1,217 @@ +import { hash } from "node:crypto"; +import { env, exit } from "node:process"; +import { getTextContentFromMd } from "../src/lib/remark-parsing.mjs"; +import { + getAllDocMeta, + getRawDocContent, + getDocUriFromPath, +} from "../src/lib/remote-mdx-files.mjs"; + +const { + NEXT_PUBLIC_SEARCH_ENDPOINT: endpoint, + NEXT_SEARCH_ACCESS_TOKEN: accessToken, + HEADLESS_METADATA_ENV_BRANCH: branchName, +} = env; + +async function main() { + if (branchName === "main" && (!endpoint || !accessToken)) { + console.error("Search endpoint and accessToken are required for indexing."); + exit(1); + } + + try { + const pages = await collectPages(); + + console.log("Docs Pages collected for indexing:", pages.length); + + if (branchName !== "main") { + console.log("Skipping indexing in non-production mode."); + exit(0); + } + + await deleteOldDocs(); + await sendPagesToEndpoint(pages); + } catch (error) { + console.error("Error in smartSearchPlugin:", error); + } +} + +/** + * Collects all MDX documents in a directory and its subdirectories + * + * @typedef {object} Page + * @property {string} id //The unique identifier of the document. + * @property {object} data //The data to be indexed. + * @property {string} data.title //The title of the document. + * @property {string} data.content //The text content of the document. + * @property {string} data.path //A relative path to the document on the internet. + * @property {string} data.content_type // The type of content. Always "mdx_doc". + * @returns Page[] + */ +async function collectPages() { + const pages = []; + const entries = await getAllDocMeta(); + + for (const entry of entries) { + const entryContent = await getRawDocContent(entry.download_url); + + const parsedContent = await getTextContentFromMd(entryContent); + + const cleanedPath = getDocUriFromPath(entry.path); + + const id = hash("sha-1", `mdx:${cleanedPath}`); + + pages.push({ + id, + data: { + title: parsedContent.data.matter.title, + description: parsedContent.data.matter.description, + content: parsedContent.value, + path: cleanedPath, + content_type: "mdx_doc", + }, + }); + } + + return pages; +} + +async function graphql(body) { + const response = await fetch(endpoint, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${accessToken}`, + }, + body: JSON.stringify(body), + }); + + if (!response.ok) { + throw new Error(`HTTP ${response.status} ${response.statusText}`); + } + + const result = await response.json(); + + if (result.errors) { + throw new Error(result.errors); + } + + return result; +} + +const queryDocuments = ` +query FindIndexedMdxDocs($query: String! $limit: Int! $offset: Int! ) { + find(query: $query limit: $limit offset: $offset) { + total + documents { + id + } + } +} +`; + +const deleteMutation = ` +mutation DeleteDocument($id: ID!) { + delete(id: $id) { + code + message + success + } +} +`; + +async function deleteOldDocs() { + try { + const existingDocs = []; + let total; + let totalCollected = 0; + let firstRun = true; + + while (totalCollected < total || firstRun) { + firstRun = false; + const response = await graphql({ + query: queryDocuments, + variables: { + query: 'content_type:"mdx_doc"', + limit: 10, + offset: totalCollected, + }, + }); + + total = response.data.find.total; + + totalCollected += response.data.find.documents.length; + + existingDocs.push(...response.data.find.documents); + } + + const existingIndexedDocuments = new Set(existingDocs.map((doc) => doc.id)); + + if (existingIndexedDocuments?.size === 0) { + console.log("No documents to delete."); + return; + } + + const deletedDocsCollector = []; + + for (const doc of existingIndexedDocuments.values()) { + const variablesForDelete = { + id: doc, + }; + + deletedDocsCollector.push( + graphql({ + query: deleteMutation, + variables: variablesForDelete, + }), + ); + } + + const results = await Promise.allSettled(deletedDocsCollector); + + for (const result of results) { + if (result.status === "rejected") { + console.error("Error deleting document:", result.reason); + } + } + + console.log(`Deleted ${results.length} documents successfully.`); + } catch (error) { + console.error("Error during deletion process:", error); + } +} + +const bulkIndexMutation = ` + mutation BulkIndex($input: BulkIndexInput!) { + bulkIndex(input: $input) { + code + documents { + id + } + } + } +`; + +async function sendPagesToEndpoint(pages) { + if (pages.length === 0) { + console.warn("No documents found for indexing."); + return; + } + + const documents = pages.map((page) => ({ + id: page.id, + data: page.data, + })); + + const variables = { input: { documents } }; + + try { + await graphql({ query: bulkIndexMutation, variables }); + + console.log(`Indexed ${documents.length} documents successfully.`); + } catch (error) { + console.error("Error during bulk indexing:", error); + } +} + +await main(); diff --git a/src/components/docs-breadcrumbs.jsx b/src/components/docs-breadcrumbs.jsx index 8978fde4..d881e4e8 100644 --- a/src/components/docs-breadcrumbs.jsx +++ b/src/components/docs-breadcrumbs.jsx @@ -31,7 +31,7 @@ export default function DocsBreadcrumbs({ routes }) { }; const router = useRouter(); - const currentPath = router.pathname; + const currentPath = router.asPath; const breadcrumbLinks = generateBreadcrumbs(routes, currentPath); if (breadcrumbLinks.length === 0) { @@ -42,7 +42,7 @@ export default function DocsBreadcrumbs({ routes }) { homeRoute.title = "Docs"; breadcrumbLinks.unshift(homeRoute); - if (currentPath === "/docs") { + if (currentPath === "/docs/") { breadcrumbLinks.shift(0); } diff --git a/src/components/docs-layout.jsx b/src/components/docs-layout.jsx index 155f21ce..48fec74b 100644 --- a/src/components/docs-layout.jsx +++ b/src/components/docs-layout.jsx @@ -11,8 +11,8 @@ import OnThisPageNav from "./on-this-page-nav"; import DocsNav from "@/components/docs-nav"; import Link from "@/components/link"; import Seo from "@/components/seo"; -import routes from "@/pages/docs/nav.json"; import "rehype-callouts/theme/vitepress"; +import { generateGitHubEditUrl } from "@/lib/github"; const flattenRoutes = (routeConfig) => { const flatRoutes = []; @@ -32,15 +32,22 @@ const flattenRoutes = (routeConfig) => { return flatRoutes; }; -export default function DocumentPage({ children, metadata }) { +export default function DocumentPage({ + children, + docsNavData: routes, + source: { frontmatter, scope }, +}) { const flatRoutes = flattenRoutes(routes); - const { asPath } = useRouter(); + const { + asPath, + query: { slug = [] }, + } = useRouter(); return ( <>
- {metadata?.title && ( -

{metadata.title}

+ {frontmatter?.title && ( +

{frontmatter.title}

)} {children} diff --git a/src/components/docs-previous-next-link.jsx b/src/components/docs-previous-next-link.jsx index 7da35ac6..eaf42fce 100644 --- a/src/components/docs-previous-next-link.jsx +++ b/src/components/docs-previous-next-link.jsx @@ -5,7 +5,7 @@ import { normalizeHref } from "@/utils/strings"; export default function DocsPreviousNextLinks({ routes }) { const router = useRouter(); - const currentPath = router.pathname; + const currentPath = router.asPath; const currentIndex = routes.findIndex( (route) => normalizeHref(route.route) === normalizeHref(currentPath), diff --git a/src/components/header.jsx b/src/components/header.jsx index c28a6f11..2d7033ce 100644 --- a/src/components/header.jsx +++ b/src/components/header.jsx @@ -1,7 +1,7 @@ import { SiDiscord, SiGithub } from "@icons-pack/react-simple-icons"; import { useState } from "react"; import FaustLogo from "./faust-logo"; -import PrimaryMenu from "./primary-menu"; +import PrimaryNav from "./primary-nav"; import SearchBar from "./search-bar"; import Link from "@/components/link"; import { classNames } from "@/utils/strings"; @@ -48,7 +48,7 @@ export default function Header() {
- diff --git a/src/components/link.jsx b/src/components/link.jsx index 5e7adbaf..9ea0ee6f 100644 --- a/src/components/link.jsx +++ b/src/components/link.jsx @@ -41,7 +41,7 @@ const CustomLink = forwardRef( {children} {!disableExternalIcon && ( - + )} diff --git a/mdx-components.jsx b/src/components/mdx-components.jsx similarity index 64% rename from mdx-components.jsx rename to src/components/mdx-components.jsx index a51b7bde..5b5f2587 100644 --- a/mdx-components.jsx +++ b/src/components/mdx-components.jsx @@ -1,21 +1,8 @@ -import Image from "next/image"; import Heading from "@/components/heading"; import CustomLink from "@/components/link"; export function useMDXComponents(components) { return { - img: ({ src, alt }, ...props) => ( - {alt} - ), a: (props) => , h1: (props) => , h2: (props) => , diff --git a/src/components/on-this-page-nav.jsx b/src/components/on-this-page-nav.jsx index b804fd2d..56d9a006 100644 --- a/src/components/on-this-page-nav.jsx +++ b/src/components/on-this-page-nav.jsx @@ -2,27 +2,9 @@ import { useEffect, useState } from "react"; import Link from "@/components/link"; import { classNames } from "@/utils/strings"; -export default function OnThisPageNav({ children }) { - const [headings, setHeadings] = useState([]); +export default function OnThisPageNav({ headings }) { const [activeId, setActiveId] = useState(); - useEffect(() => { - const headingsArray = []; - const headingElements = document.querySelectorAll("h2, h3"); - - for (const heading of headingElements) { - if (heading.id) { - headingsArray.push({ - id: heading.id, - text: heading.textContent, - level: Number.parseInt(heading.tagName[1], 10), - }); - } - } - - setHeadings(headingsArray); - }, [children]); - useEffect(() => { const observer = new IntersectionObserver( (entries) => { @@ -52,18 +34,19 @@ export default function OnThisPageNav({ children }) {
    {headings.map((heading) => (
  • - - {heading.text} + + {heading.value}
  • ))} diff --git a/src/components/primary-menu.jsx b/src/components/primary-nav.jsx similarity index 100% rename from src/components/primary-menu.jsx rename to src/components/primary-nav.jsx diff --git a/src/constants/repo.mjs b/src/constants/repo.mjs new file mode 100644 index 00000000..f7becadd --- /dev/null +++ b/src/constants/repo.mjs @@ -0,0 +1,4 @@ +export const DOCS_OWNER = "wpengine"; +export const DOCS_REPO = "faustjs"; +export const DOCS_FOLDER = "docs"; +export const DOCS_BRANCH = "canary"; diff --git a/src/lib/github.mjs b/src/lib/github.mjs new file mode 100644 index 00000000..80a7fb36 --- /dev/null +++ b/src/lib/github.mjs @@ -0,0 +1,16 @@ +import { + DOCS_BRANCH, + DOCS_FOLDER, + DOCS_OWNER, + DOCS_REPO, +} from "@/constants/repo.mjs"; + +/** + * Generates a URL for editing a document on GitHub. + * + * @param {string} slug slug with no beginning or trailing slashes + * @returns + */ +export function generateGitHubEditUrl(slug) { + return `https://github.com/${DOCS_OWNER}/${DOCS_REPO}/edit/${DOCS_BRANCH}/${DOCS_FOLDER}/${slug}/index.md`; +} diff --git a/src/lib/remark-parsing.mjs b/src/lib/remark-parsing.mjs new file mode 100644 index 00000000..b18671c4 --- /dev/null +++ b/src/lib/remark-parsing.mjs @@ -0,0 +1,115 @@ +import rehypeUrlInspector from "@jsdevtools/rehype-url-inspector"; +import { transformerNotationDiff } from "@shikijs/transformers"; +import { serialize } from "next-mdx-remote-client/serialize"; +import rehypeCallouts from "rehype-callouts"; +import rehypeExternalLinks from "rehype-external-links"; +import { rehypePrettyCode } from "rehype-pretty-code"; +import rehypeSlug from "rehype-slug"; +import remarkFlexibleToc from "remark-flexible-toc"; +import remarkFm from "remark-frontmatter"; +import remarkGfm from "remark-gfm"; +import remarkParse from "remark-parse"; +import withSmartQuotes from "remark-smartypants"; +import remarkStringify from "remark-stringify"; +import remarkStrip from "strip-markdown"; +import { unified } from "unified"; +import { visit } from "unist-util-visit"; +import { matter } from "vfile-matter"; +import { getRemoteImgUrl } from "./remote-mdx-files.mjs"; +/** + * Return Text Content of Markdown using unified + * + * @param {string} mdContent + * @returns {Promise} + */ +export async function getTextContentFromMd(mdContent) { + return unified() + .use(remarkParse) + .use(addFrontmatterToVFile) + .use(remarkFm) + .use(remarkStrip) + .use(remarkStringify) + .process(mdContent); +} + +function addFrontmatterToVFile() { + return (_, file) => matter(file); +} + +export function addTocToVFile() { + return (tree, file) => { + const toc = []; + visit(tree, "element", (node) => { + if ( + (node.tagName === "h2" || node.tagName === "h3") && + node.children[0].value + ) { + const title = node.children[0]?.value; + + toc.push({ + id: node.properties.id, + text: title, + level: Number.parseInt(node.tagName[1], 10), + }); + } + }); + + file.data.toc = toc; + }; +} + +/** + * Returns the parsed content of a document from its markdown content. + * + * @param {string} mdContent + * @param {string} pageUrl + * @returns {Promise<{ + * frontmatter: { title: string; description: string; } + * compiledSource: string; + * scope: { toc: { href: string; value: string; depth: number; }[]; } + * }>} + */ +export async function getSerializedContextFromMd(mdContent, pageUrl) { + return serialize({ + source: mdContent, + options: { + parseFrontmatter: true, + vfileDataIntoScope: "toc", + mdxOptions: { + remarkPlugins: [ + [remarkGfm, { singleTilde: false }], + withSmartQuotes, + remarkFlexibleToc, + ], + rehypePlugins: [ + [ + rehypeUrlInspector, + { + selectors: ["img[src]"], + inspectEach: ({ url, node }) => { + node.properties.src = getRemoteImgUrl(url, pageUrl); + }, + }, + ], + // rehypeNextImageMetadata, + [rehypeExternalLinks, { target: "_blank" }], + rehypeSlug, + rehypeCallouts, + [ + rehypePrettyCode, + { + transformers: [ + transformerNotationDiff({ + matchAlgorithm: "v3", + }), + ], + theme: "github-dark-dimmed", + defaultLang: "plaintext", + bypassInlineCode: false, + }, + ], + ], + }, + }, + }); +} diff --git a/src/lib/remote-mdx-files.mjs b/src/lib/remote-mdx-files.mjs new file mode 100644 index 00000000..2faeb209 --- /dev/null +++ b/src/lib/remote-mdx-files.mjs @@ -0,0 +1,160 @@ +import path from "node:path"; +import { env } from "node:process"; +import { Octokit } from "@octokit/core"; +import { + DOCS_OWNER, + DOCS_REPO, + DOCS_BRANCH, + DOCS_FOLDER, +} from "../constants/repo.mjs"; +import { getSerializedContextFromMd } from "./remark-parsing.mjs"; + +if (!env.GITHUB_TOKEN) { + throw new Error("GITHUB_TOKEN is required"); +} + +const octokit = new Octokit({ + auth: env.GITHUB_TOKEN, +}); + +const DOCS_EXT_REG = /(?.*)index.md(?:x?)$/i; +const IMG_PATH_REG = /^(?\.\/)?(?.+)$/i; + +export const DOCS_PATH = `https://raw.githubusercontent.com/${DOCS_OWNER}/${DOCS_REPO}/refs/heads/${DOCS_BRANCH}/${DOCS_FOLDER}`; + +const DOCS_NAV_CONFIG_URL = `${DOCS_PATH}/nav.json`; + +function docUrlFromSlug(slug = []) { + return path.join(DOCS_PATH, ...slug, "index.md"); +} + +/** + * Returns the URL of an image from its path and the URL of the page it's on. + * + * @param {string} imgPath + * @param {string[]} pageUrl + * @returns + */ +function imgUrlFromPath(imgPath, pageUrl) { + if (!Array.isArray(pageUrl)) { + throw new TypeError("pageUrl should be an array"); + } + + return `${DOCS_PATH}/${pageUrl.join("/")}/${imgPath}`; +} + +/** + * Returns the URL of an image from its local path and the URL of the page it's on. + * + * @param {string} localPath + * @param {string[]} pageUrl + * @returns {string} + */ +export function getRemoteImgUrl(localPath, pageUrl) { + return imgUrlFromPath(localPath.match(IMG_PATH_REG).groups.slug, pageUrl); +} + +/** + * Retrieves the metadata for all documents in the docs folder. + * + */ +export async function getAllDocMeta(pathToFolder = DOCS_FOLDER) { + const { status, data } = await octokit.request( + "GET /repos/{owner}/{repo}/contents/{path}", + { + owner: DOCS_OWNER, + repo: DOCS_REPO, + path: pathToFolder, + ref: DOCS_BRANCH, // This makes it so only released features show up in the docs. + }, + ); + + if (status !== 200) { + throw new Error(status); + } + + const items = []; + + const subItems = []; + + for (const item of data) { + if (item.type === "file" && item.name === "index.md") { + items.push(item); + } else if (item.type === "dir" && item.name !== "images") { + subItems.push(getAllDocMeta(item.path)); + } + } + + const subFolderItems = await Promise.all(subItems); + + return [...items, ...subFolderItems.flat()]; +} + +/** + * Retrieves the nav.json file from the docs folder. + * + */ +export async function getDocsNav() { + const resp = await fetch(DOCS_NAV_CONFIG_URL); + + if (!resp.ok) { + throw new Error(resp.statusText); + } + + return resp.json(); +} + +export async function getAllDocUri() { + const data = await getAllDocMeta(); + + if (!Array.isArray(data)) { + console.error(data); + throw new Error("GitHub response should be an array"); + } + + const accumulator = []; + for (const file of data) { + if (DOCS_EXT_REG.test(file.path)) { + accumulator.push(getDocUriFromPath(file.path)); + } + } + + return accumulator; +} + +export function getDocUriFromPath(ghPath) { + return path.join("/", ghPath.match(DOCS_EXT_REG).groups.slug); +} + +/** + * Retrieves the content of a document from its slug. + * + * @param {string} url + * @returns {Promise} + */ +export async function getRawDocContent(url) { + const resp = await fetch(url); + + if (!resp.ok) { + if (resp.status >= 400 && resp.status < 500) { + // eslint-disable-next-line no-throw-literal + throw { notFound: true }; + } + + throw new Error(resp.statusText); + } + + return resp.text(); +} + +/** + * Retrieves the parsed content of a document from its slug. + * + * @param {string} slug + * @returns {ReturnType} + */ +export async function getParsedDoc(slug) { + const content = await getRawDocContent(docUrlFromSlug(slug)); + + return getSerializedContextFromMd(content, slug); +} diff --git a/src/lib/smart-search-plugin.mjs b/src/lib/smart-search-plugin.mjs deleted file mode 100644 index bc4047c6..00000000 --- a/src/lib/smart-search-plugin.mjs +++ /dev/null @@ -1,253 +0,0 @@ -import { hash } from "node:crypto"; -import fs from "node:fs/promises"; -import path from "node:path"; -import { cwd } from "node:process"; -import { htmlToText } from "html-to-text"; - -let isPluginExecuted = false; - -function smartSearchPlugin({ endpoint, accessToken }) { - return { - apply: (compiler) => { - compiler.hooks.done.tapPromise("SmartSearchPlugin", async () => { - if (isPluginExecuted) { - return; - } - - isPluginExecuted = true; - - if (compiler.options.mode !== "production") { - console.log("Skipping indexing in non-production mode."); - return; - } - - try { - const pages = await collectPages(path.join(cwd(), "src/pages/docs")); - - console.log("Docs Pages collected for indexing:", pages.length); - - await deleteOldDocs({ endpoint, accessToken }, pages); - await sendPagesToEndpoint({ endpoint, accessToken }, pages); - } catch (error) { - console.error("Error in smartSearchPlugin:", error); - } - }); - }, - }; -} - -async function collectPages(directory) { - const pages = []; - const entries = await fs.readdir(directory, { withFileTypes: true }); - - for (const entry of entries) { - const entryPath = path.join(directory, entry.name); - - if (entry.isDirectory()) { - const subPages = await collectPages(entryPath); - pages.push(...subPages); - } else if (entry.isFile() && entry.name.endsWith(".mdx")) { - const content = await fs.readFile(entryPath, "utf8"); - - const metadataMatch = content.match( - /export\s+const\s+metadata\s*=\s*(?{[\S\s]*?});/, - ); - - let metadata = {}; - - if (metadataMatch?.groups?.metadata) { - try { - // eslint-disable-next-line no-eval - metadata = eval(`(${metadataMatch.groups.metadata})`); - } catch (error) { - console.error("Error parsing metadata:", error); - continue; - } - } else { - console.warn(`No metadata found in ${entryPath}. Skipping.`); - continue; - } - - const textContent = htmlToText(content); - - const cleanedPath = cleanPath(entryPath); - - const id = hash("sha-1", `mdx:${cleanedPath}`); - - pages.push({ - id, - data: { - title: metadata.title, - content: textContent, - path: cleanedPath, - content_type: "mdx_doc", - }, - }); - } - } - - return pages; -} - -function cleanPath(filePath) { - const relativePath = path.relative(cwd(), filePath); - return ( - "/" + - relativePath - .replace(/^src\/pages\//, "") - .replace(/^pages\//, "") - .replace(/\/index\.mdx$/, "") - .replace(/\.mdx$/, "") - ); -} - -const queryDocuments = ` -query FindIndexedMdxDocs($query: String!) { - find(query: $query) { - documents { - id - } - } -} -`; - -const deleteMutation = ` -mutation DeleteDocument($id: ID!) { - delete(id: $id) { - code - message - success - } -} -`; - -async function deleteOldDocs({ endpoint, accessToken }, pages) { - const currentMdxDocuments = new Set(pages.map((page) => page.id)); - - const variablesForQuery = { - query: 'content_type:"mdx_doc"', - }; - - try { - const response = await fetch(endpoint, { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${accessToken}`, - }, - body: JSON.stringify({ - query: queryDocuments, - variables: variablesForQuery, - }), - }); - - const result = await response.json(); - - if (result.errors) { - console.error("Error fetching existing documents:", result.errors); - return; - } - - const existingIndexedDocuments = new Set( - result.data.find.documents.map((doc) => doc.id), - ); - - const documentsToDelete = - existingIndexedDocuments.difference(currentMdxDocuments); - - if (documentsToDelete?.size === 0) { - console.log("No documents to delete."); - return; - } - - for (const doc of documentsToDelete.values()) { - const variablesForDelete = { - id: doc, - }; - - try { - const deleteResponse = await fetch(endpoint, { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${accessToken}`, - }, - body: JSON.stringify({ - query: deleteMutation, - variables: variablesForDelete, - }), - }); - - const deleteResult = await deleteResponse.json(); - - if (deleteResult.errors) { - console.error( - `Error deleting document ID ${doc}:`, - deleteResult.errors, - ); - } else { - console.log(`Deleted document ID ${doc}:`, deleteResult.data.delete); - } - } catch (error) { - console.error(`Network error deleting document ID ${doc.id}:`, error); - } - } - } catch (error) { - console.error("Error during deletion process:", error); - } -} - -const bulkIndexQuery = ` - mutation BulkIndex($input: BulkIndexInput!) { - bulkIndex(input: $input) { - code - documents { - id - } - } - } -`; - -async function sendPagesToEndpoint({ endpoint, accessToken }, pages) { - if (pages.length === 0) { - console.warn("No documents found for indexing."); - return; - } - - const documents = pages.map((page) => ({ - id: page.id, - data: page.data, - })); - - const variables = { input: { documents } }; - - try { - const response = await fetch(endpoint, { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${accessToken}`, - }, - body: JSON.stringify({ query: bulkIndexQuery, variables }), - }); - - if (!response.ok) { - console.error( - `Error during bulk indexing: ${response.status} ${response.statusText}`, - ); - return; - } - - const result = await response.json(); - - if (result.errors) { - console.error("GraphQL bulk indexing error:", result.errors); - } else { - console.log(`Indexed ${documents.length} documents successfully.`); - } - } catch (error) { - console.error("Error during bulk indexing:", error); - } -} - -export default smartSearchPlugin; diff --git a/src/pages/_app.jsx b/src/pages/_app.jsx index 914facaf..fc3cb0ec 100644 --- a/src/pages/_app.jsx +++ b/src/pages/_app.jsx @@ -30,7 +30,7 @@ export default function MyApp({ Component, pageProps }) { {isDocsRoute ? ( - + ) : ( diff --git a/src/pages/docs/[[...slug]].jsx b/src/pages/docs/[[...slug]].jsx new file mode 100644 index 00000000..21fd2bf7 --- /dev/null +++ b/src/pages/docs/[[...slug]].jsx @@ -0,0 +1,37 @@ +import { MDXClient } from "next-mdx-remote-client"; +import { useMDXComponents } from "@/components/mdx-components"; +import { getParsedDoc, getDocsNav } from "@/lib/remote-mdx-files.mjs"; + +export default function Doc({ source }) { + return ; +} + +export async function getStaticProps({ params }) { + try { + const source = await getParsedDoc(params.slug); + const docsNavData = await getDocsNav(); + + return { + props: { + source, + docsNavData, + }, + revalidate: 1, + }; + } catch (error) { + if (error.notFound) { + console.error(params, error); + return error; + } + + throw error; + } +} + +export async function getStaticPaths() { + const docs_menu_paths = ["/docs/"]; + return { + paths: docs_menu_paths, + fallback: "blocking", + }; +} diff --git a/src/pages/docs/explanation/apollo-client-basics/index.mdx b/src/pages/docs/explanation/apollo-client-basics/index.mdx deleted file mode 100644 index 2a3dcaa0..00000000 --- a/src/pages/docs/explanation/apollo-client-basics/index.mdx +++ /dev/null @@ -1,21 +0,0 @@ -export const metadata = { - title: "Apollo Client Basics", - description: - "Learn about core Apollo Client concepts like queries, fragments, mutations, and caching as they relate to Faust.js and WordPress.", -}; - -Faust.js uses `@apollo/client` under the hood to perform GraphQL operations against your WordPress backend. Having a solid understanding of Apollo Client queries, fragments, and mutations will help you get the most out of Faust.js. - -## Apollo Client GraphQL Concepts - -Below are core concepts for Apollo and GraphQL with links to the docs for your reference. - -- [Queries](https://www.apollographql.com/docs/react/data/queries) – Retrieve data from your WordPress site. -- [Fragments](https://www.apollographql.com/docs/react/data/fragments) – Modularize your queries to make them more maintainable. -- [Mutations](https://www.apollographql.com/docs/graphos/get-started/concepts/graphql#mutations) – Update data in your WordPress backend. -- [Apollo Client Cache](https://www.apollographql.com/docs/react/caching/cache-configuration) – Cache responses to minimize network usage. - -## Faust.js-Specific Notes - -- You don’t need to create or configure the client object yourself; Faust handles that for you. -- You can still customize the underlying Apollo Client via plugin filters if you need advanced configuration. diff --git a/src/pages/docs/explanation/blocks-faq/index.mdx b/src/pages/docs/explanation/blocks-faq/index.mdx deleted file mode 100644 index 9ba12c85..00000000 --- a/src/pages/docs/explanation/blocks-faq/index.mdx +++ /dev/null @@ -1,40 +0,0 @@ -export const metadata = { - title: "Blocks FAQ", - description: "Frequently asked questions about Blocks.", -}; - -## _"What if the block is missing some attributes?"_ - -If the block does not have any attributes or has only a few of them declared in the `block.json` for that block (you can view the `block.json` file for the block at https://github.com/WordPress/gutenberg/blob/trunk/packages/block-library/), you can still try to extend the block API by declaring additional attributes for that block. - -Follow the [filters reference guide](/docs/reference/use-blocks-theme/) to create a block that uses the `additional_block_attributes` property. The attributes will then be available to query from that block. - -```php -class CoreCode extends WPGraphQL\ContentBlocks\Blocks\Block -{ - protected ?array $additional_block_attributes = array( - // Write your attributes here - ); -} -``` - -> [!NOTE] -> If you include those extensions in a custom plugin, your Headless Website application is dependent on the inclusion of this plugin. You need to make sure you bundle them together; otherwise, the queries you perform in the application will fail. - -## _"Can I style the block differently?"_" - -Yes, you can style the block in many ways, choosing to ignore some of the attributes altogether. You can also use an external React Library to style the block, such as Material UI or ChakraUI. - -Bear in mind that this will almost always result in a degraded user editing experience, as the styles in the WordPress block editor view won't match the styles of the rendered headless page. - -## _"What if the block contains custom JavaScript assets?"_ - -Some Core blocks include JavaScript assets that are injected into the WordPress page so they can run in the front view. Many [dynamic blocks](https://developer.wordpress.org/block-editor/how-to-guides/block-tutorial/creating-dynamic-blocks/) use this functionality to include user interactivity. Since React is not bundled as a dependency in the browser, the client-side code that WordPress ships to the frontend in traditional WordPress sites typically consists of plain JavaScript or jQuery. - -To review and handle bundled JavaScript assets, consider these options: - -- **Include them in your code**: This is not recommended, as React does not play well with plain JavaScript and jQuery, which may lead to compatibility issues. -- **Rewrite them as React components**: You can attempt to rewrite the code in React. If the bundled code can be understood and rewritten with low effort, then this could be a viable approach. -- **Use an equivalent React Component from a library**: A simpler alternative is to find a compatible React package and use it instead of replicating the block's interactivity. This can often free the developer from implementing the functionality from scratch. - -Inevitably, this is a common challenge when using Blocks in a Headless Website setup, so it's up to you to weigh the pros and cons of each approach. diff --git a/src/pages/docs/explanation/deploy-your-app/index.mdx b/src/pages/docs/explanation/deploy-your-app/index.mdx deleted file mode 100644 index e389f6cd..00000000 --- a/src/pages/docs/explanation/deploy-your-app/index.mdx +++ /dev/null @@ -1,53 +0,0 @@ -export const metadata = { - title: "Deploying Your App", - description: - "Explains the various options for deploying your Faust.js App to production.", -}; - -The following explains the various options for deploying your Faust.js App to production. - -## Picking The Right Node.js Version - -Faust.js supports supported versions of Node.js v16.0.0 or newer. The [latest LTS version](https://nodejs.org/en/about/previous-releases) of Node.js is recommended and anything [compatible with your Next.js](https://nextjs.org/docs/pages/getting-started/installation) version. Please make sure you are using a Node.js version that is compatible when deploying to avoid unexpected errors. - -Faust.js uses the `package.json` [engines field](https://docs.npmjs.com/cli/v9/configuring-npm/package-json#engines) that will produce warnings when your package is installed as a dependency under a non compatible NPM/Node version. - -## Building Your App - -Since Faust.js is built on top of Next.js, it shares the same build process. For details on how Next.js generates optimized production builds, refer to the [Next.js Build API](https://nextjs.org/docs/pages/building-your-application/deploying#nextjs-build-api) documentation. Additionally, you can explore the Headless Platform [framework guide](https://developers.wpengine.com/docs/atlas/framework-guides/next-js/next/) for Next.js apps. - -## Deploying to WP Engine's Headless Platform - -The Headless Platform is the most effortless way to deploy a Faust.js app. Connect your GitHub repo and Headless Platform will automatically build and deploy your Faust.js project on each push. Some of the benefits you get out of the box are: - -- Automatic installation of required WordPress plugins (Faust, WPGraphQL, etc.) -- Automatic setup of Faust.js environment variables -- Automatic configuration of the Faust WordPress plugin’s settings -- WordPress and your headless app live in the same place -- Automatic rebuilds on code changes -- Support for [custom domains](https://wpengine.com/support/add-domain-in-user-portal/) - -Deploy your Faust.js app and try for the [Headless Platform](https://wpengine.com/headless-wordpress/#form) for free! - -For further reference, please check out the Headless Platform [framework guide](https://developers.wpengine.com/docs/atlas/framework-guides/next-js/next/) docs on Deploying Next.js. - -> [!NOTE] -> If deploying from the WP Engine portal using your own repository (without beginning with a blueprint), you will need to set your environment variables manually. Learn how by taking a look at our [platform docs](https://developers.wpengine.com/docs/atlas/getting-started/deploy-from-existing-repo/#set-environment-variables-optional). - -## Self Hosted - -You can also self host Faust.js apps on any hosting provider that supports Node.js. - -To self host your Faust.js app, you will need to first build the app, then start it. Make sure you have the `faust build` and `faust start` scripts specified in your `package.json` file: - -```json title="package.json" -{ - "scripts": { - "build": "faust build", - "start": "faust start" - } -} -``` - -Then, from the Node.js server run `npm run build` to build the app and `npm run start` to start it. -For reference, please visit the [Next.js self-hosted deployment](https://nextjs.org/docs/pages/building-your-application/deploying#self-hosting) docs. diff --git a/src/pages/docs/explanation/index.mdx b/src/pages/docs/explanation/index.mdx deleted file mode 100644 index 680d3db1..00000000 --- a/src/pages/docs/explanation/index.mdx +++ /dev/null @@ -1,10 +0,0 @@ -export const metadata = { - title: "Explanation", - description: - "High-level conceptual guides that provide background information and understanding of Headless WordPress development.", -}; - -I see you found the root of the Explanatory guides! Explanatory guides are a place where we step away from code and talk high-level concepts and import background information. If you are looking to grow your understanding of Headless WordPress, you are in the right place. - -> [!note] Learn More -> For more info on how we layout our documentation and the the role played by Explanatory Guides, please read about the [_Diátaxis_](https://diataxis.fr/explanation/) approach we use. diff --git a/src/pages/docs/explanation/migrate-from-wp-graphql-gutenberg/index.mdx b/src/pages/docs/explanation/migrate-from-wp-graphql-gutenberg/index.mdx deleted file mode 100644 index 8bd49b81..00000000 --- a/src/pages/docs/explanation/migrate-from-wp-graphql-gutenberg/index.mdx +++ /dev/null @@ -1,146 +0,0 @@ -export const metadata = { - title: "Migrate From WPGraphQL Gutenberg", - description: - "Migrate from wp-graphql-gutenberg to wp-graphql-content-blocks. This guide covers differences between the two plugins, reasons for migration, and step-by-step instructions for updating your GraphQL queries and block structures to ensure compatibility with Faust.js and modern WPGraphQL best practices.", -}; - -With [`wp-graphql-gutenberg`](https://github.com/pristas-peter/wp-graphql-gutenberg) being sunset you may find yourself needing to move to the improved [`wp-graphql-content-blocks`](https://github.com/wpengine/wp-graphql-content-blocks) plugin. - -## What's the difference? - -Both plugins extend WPGraphQL to add support for blocks (aka. Gutenberg) to the WPGraphQL schema. - -However, there are some key differences that you should be aware of: - -- WPGraphQL Gutenberg gets the blocks registry and sends it in a network request to the WordPress PHP application. So it needs to be synced from time to time. - -- It also allows the blocks to be served as JSON using the `blocksJSON` field. If requesting data this way, the data is not typechecked and you may overfetch data as a side effect. - -- Block attributes are using their own types, containing different type for deprecated versions. For example: - -```gql -...on CoreParagraphBlock { - attributes { - ...on CoreParagraphBlockAttributes { - content - } - ...on CoreParagraphBlockDeprecatedV1Attributes { - content - } - } -} -``` - -- It does not allow blocks to be returned as a flat list so you have to use deeply nested queries to get the list of innerBlocks (and this won’t nearly solve the issue 100%). - -- wp-graphql-content-blocks does not save anything in the database (this is actually a good thing) compared to wp-graphql-gutenberg. Therefore it only supports previews when the **“preview”** button is hit in the editor. - -## How do I migrate a block from wp-graphql-gutenberg? - -To answer this question you will have check how you queried the blocks using `wp-graphql-gutenberg`. There are two different cases that you have to consider here: - -### You used the `blocksJSON` property to get the blocks data - -The `wp-graphql-content-blocks` plugin does not expose the `blocksJSON` fields, because it is problematic to do so. Getting the data as plain JSON directly from the database completely overrides the principles of GraphQL and ignores the type safety of the system. If one of the properties is altered, you have no guarantee that the GraphQL server will catch them. Plus, most of the times you will over fetch data leading to slower queries, especially if you have lots of content on the screen. - -So, due to the lack of Introspection, unpredictability, and in order to promote best practices, `wp-graphql-content-blocks` do not expose the block data as plain JSON. Instead, it is recommended to use GraphQL types to retrieve associated block attributes and fields.The effort required to add block fragments is not high. If you follow the recommended approach of co-located fragments, you may add them as properties to each of your block expected attribute list and make sure that you include those into the page query string. - -Take a look at the following example taken from [WebDevStudios nextjs-wordpress-starter](https://github.com/WebDevStudios/nextjs-wordpress-starter/blob/main/components/blocks/core/BlockCode/BlockCode.js). It shows an implementation of the `CoreCode` block using `wp-graphql-gutenberg` and getting the data using `blockJSON`. - -With it, you would have to create a fragment like this: - -```gql -CoreCode.fragments = { - key: `CoreCodeBlockFragment`, - entry: gql` - fragment CoreCodeBlockFragment on CoreCodeBlock { - attributes { - ...on CoreCodeBlockAttributes { - anchor - backgroundColor - content - className - gradient - style - textColor - } - } - } - `, -}; -``` - -Based on the [creating a custom block](/docs/how-to/custom-blocks/) guide from the WordPress Core Blocks you just need to add the following fragment as a new property: - -```gql CoreCode.fragments = { - key: `CoreCodeBlockFragment`, - entry: gql` - fragment CoreCodeBlockFragment on CoreCode { - attributes { - anchor - backgroundColor - content - className - gradient - style - textColor - } - } - `, -}; -``` - -When the `WordPressBlocksViewer` renders the component, it passes the whole block data as a property of that block. If your block is designed to accept different properties for attributes and for `innerBlocks` you have to create a wrapper to forward the properties into the right slots: - -```js title="CoreCode.js" -export default function CoreCode({attributes, children}) { - const BlockCode = dynamic(() => import('@/components/blocks/core/BlockCode')) - return -} -``` - -### You used the block field with GraphQL types and fragments - -If you were using the block field from the `wp-graphql-gutenberg` then most of the component fragment queries should be the same with the following exceptions. - -You should be querying the block attributes without qualifying their type: - -**Before:** - -```gql -...on CoreParagraphBlock { - attributes { - ...on CoreParagraphBlockAttributes { - content - } - } -} -``` - -**After:** - -```gql -...on CoreParagraphBlock { - attributes { - content - } -} -``` - -There are no separate fields `previewBlocks` and `previewBlocksJSON`. If you want to preview posts or pages you should be setting the `asPreview` boolean to `true` for the post/page in your GraphQL request (this is how Faust.js previews work under the hood). - -The base interface for each block contains different fields, so you need to make sure your queries use the valid ones from this list: - -- `renderedHTML`: It’s the HTML of the block as rendered by the render_block function. - -- `name`: The actual name of the block taken from its block.json spec. - -- `__typename`: The type of block transformed from the name field in camel-case notation. - -- `apiVersion`: The apiVersion of the block taken from its block.json spec. - -- `innerBlocks`: The `innerblock` list of that block. - --`isDynamic`: Whether the block is dynamic or not, taken from its block.json spec. - -- `clientId`, `parentClientId`: Unique identifiers for the block and the parent of the block. diff --git a/src/pages/docs/explanation/migrating-to-typescript/index.mdx b/src/pages/docs/explanation/migrating-to-typescript/index.mdx deleted file mode 100644 index 013e034c..00000000 --- a/src/pages/docs/explanation/migrating-to-typescript/index.mdx +++ /dev/null @@ -1,14 +0,0 @@ -export const metadata = { - title: "Migrating to TypeScript", - description: - "Guide for migrating existing Next.js pages to TypeScript in a Faust.js project with resources and references.", -}; - -If you have existing Next.js pages that you want to migrate to TypeScript, you can follow the [official TypeScript Docs](https://www.typescriptlang.org/docs/handbook/migrating-from-javascript.html) for general TypeScript migration tips. - -For Faust, see the [TypeScript Reference Doc](/docs/reference/typescript). - -## Further Reading - -- [TypeScript Reference](/docs/reference/typescript) -- [Typing GraphQL Queries with GraphQL Codegen](/docs/how-to/generate-types-with-graphql-codegen) diff --git a/src/pages/docs/explanation/react-components-to-blocks/images/colorpicker-controls-for-color-attributes.png b/src/pages/docs/explanation/react-components-to-blocks/images/colorpicker-controls-for-color-attributes.png deleted file mode 100644 index ec6729b6..00000000 Binary files a/src/pages/docs/explanation/react-components-to-blocks/images/colorpicker-controls-for-color-attributes.png and /dev/null differ diff --git a/src/pages/docs/explanation/react-components-to-blocks/images/react-component-in-edit-mode.png b/src/pages/docs/explanation/react-components-to-blocks/images/react-component-in-edit-mode.png deleted file mode 100644 index fde8a108..00000000 Binary files a/src/pages/docs/explanation/react-components-to-blocks/images/react-component-in-edit-mode.png and /dev/null differ diff --git a/src/pages/docs/explanation/react-components-to-blocks/images/react-component-in-preview-mode.png b/src/pages/docs/explanation/react-components-to-blocks/images/react-component-in-preview-mode.png deleted file mode 100644 index 721a3ddc..00000000 Binary files a/src/pages/docs/explanation/react-components-to-blocks/images/react-component-in-preview-mode.png and /dev/null differ diff --git a/src/pages/docs/explanation/react-components-to-blocks/index.mdx b/src/pages/docs/explanation/react-components-to-blocks/index.mdx deleted file mode 100644 index cd9b0c92..00000000 --- a/src/pages/docs/explanation/react-components-to-blocks/index.mdx +++ /dev/null @@ -1,208 +0,0 @@ -export const metadata = { - title: "React Components to Blocks", - description: - "Create blocks in your Next.js app that also work in the WordPress Block Editor.", -}; - -The `@faustwp/block-editor-utils` package provides helper functions for converting React components into blocks. This means you can use the same components in both places—your Next.js app and the WordPress Block Editor. - -## Prerequisites - -In order to use this feature, you need to clone down the working boilerplate example [here](https://github.com/wpengine/faustjs/tree/canary/examples/next/block-support) and follow its instructions in the `README.md` file on what plugins you need, packages you need to install and the command that needs to be run to sync the blocks to WordPress. - -> [!WARNING] -> This feature does not work with the latest version of React. It only works with React 18. -> Also, please avoid using this feature in production. It is not fully maintained. - -## Creating and Registering a Block Component - -In the `pages/wp-blocks/block-b/Component.js` file, you will see the following code: - -```js title="pages/wp-blocks/block-b/Component.js" -import { gql } from "@apollo/client"; - -function Component({ style, attributes, children, ...props }) { - const styles = { - ...style, - }; - const cssClassName = "create-block-block-b-message"; - return ( - <> -
    -
    - - ); -} - -Component.fragments = { - key: `CreateBlockBlockBFragment`, - entry: gql` - fragment CreateBlockBlockBFragment on CreateBlockBlockB { - attributes { - message - } - } - `, -}; - -Component.config = { - name: "CreateBlockBlockB", - editorFields: { - message: { - type: "string", - label: "My Message", - location: "editor", - }, - }, -}; -export default Component; -``` - -This defines a React component that renders two `
    ` elements with inline styles and specific CSS class names, inserting `HTML` content from the `attributes` prop using `dangerouslySetInnerHTML`. - -It also specifies a GraphQL fragment to fetch the necessary data for the component, particularly the message attribute, ensuring the data structure is aligned with the expected GraphQL type. Additionally, the component includes a configuration object that defines its name and editor fields, which is used to integrate and manage the block within a WordPress block editor environment. - -Within your `block-b/index.js` file, we see the following code: - -```js title="block-b/index.js" -import { registerFaustBlock } from "@faustwp/block-editor-utils"; -import "./style.scss"; -import BlockB from "./Component.js"; -/** - * Block.json metadata - */ -import metadata from "./block.json"; -/** - * Register React block on the Block Editor - */ -registerFaustBlock(BlockB, { - blockJson: metadata, -}); -``` - -This file imports the necessary styling, component logic, and metadata to register a React block with the WordPress block editor using the Faust.js framework. It leverages the `registerFaustBlock` utility from the `@faustwp/block-editor-utils` package, passing the component and its configuration metadata to properly initialize the block. - -As a result, the block is seamlessly integrated into the editor, with its visual and functional properties defined by the imported SCSS and JSON metadata. - -Now let's take a look at the `block.json` file in the `block-b` folder. Since we declared three configurable attributes for our component, we need to declare them as attributes here. - -Here is the final `block.json` with the assigned attributes object: - -```json -{ - "$schema": "https://schemas.wp.org/trunk/block.json", - "apiVersion": 2, - "name": "create-block/block-b", - "version": "0.1.0", - "title": "Block B", - "category": "widgets", - "icon": "smiley", - "description": "Example static block scaffolded with Create Block tool.", - "supports": { - "html": false - }, - "attributes": { - "message": { - "type": "string", - "default": "Hello World" - }, - "richText": { - "type": "string", - "source": "html", - "selector": ".rich-text", - "default": "Hello World" - } - }, - "textdomain": "block-b", - "editorScript": "file:./index.js", - "editorStyle": "file:./index.css", - "style": "file:./style-index.css" -} -``` - -## Try out the Component in the Block Editor - -Open the WordPress Block Editor and try out the new block. This is what it will look like at first glance in Edit mode: - -![React Component in Edit Mode](./images/react-component-in-edit-mode.png) - -You can interact with the form fields, and then click outside the block contents where you will see the component rendered in Preview mode. - -![React Component in Preview Mode.](./images/react-component-in-preview-mode.png) - -## Configure the Form Controls - -So far we've been able to render the React component in the Block Editor. - -However, a few of the attributes that control the color are using [text field](https://developer.wordpress.org/block-editor/reference-guides/components/text-control/) controls, which may prove problematic since they allow invalid values. What if we wanted to use a proper color picker component? - -Since the `block.json` [attribute types](https://developer.wordpress.org/block-editor/reference-guides/block-api/block-attributes/#type-validation) do not allow `color` as a value, we will have to provide a different configuration to allow that option. - -When registering the React component using `registerFaustBlock`, it allows extra configuration to be used in case you want to declare which kinds of controls to use on each attribute. - -In your `Component.js` file, add the following `editorFields` to your `Component.config` object: - -```js title="block-b/Component.js" -... -Component.config = { - name: "CreateBlockBlockB", - editorFields: {// [!code ++] - bg_color: {// [!code ++] - location: "inspector",// [!code ++] - control: "color",// [!code ++] - },// [!code ++] - text_color: {// [!code ++] - location: "inspector",// [!code ++] - control: "color",// [!code ++] - },// [!code ++] - },// [!code ++] -}; -``` - -`editorFields` represents Block Editor metadata configuration data. This consists of two attributes that we want to specify: the type of control to use and the location within the editor. For example, by using `location: "inspector",` we are telling this control to appear in the [Block Sidebar](https://developer.wordpress.org/block-editor/how-to-guides/block-tutorial/block-controls-toolbar-and-sidebar/) section. By using `control: "color"`, we are indicating that we want to use a [ColorPicker](https://developer.wordpress.org/block-editor/reference-guides/components/color-picker/) component instead of the regular `TextControl`. - -Once you update the component, you can refresh the page and create a new block. Now instead of having two text fields inside the block, we have two `ColorPicker` fields in the sidebar section: - -![Using ColorPicker controls for the color attributes.](./images/colorpicker-controls-for-color-attributes.png) - -## Form Control Reference List - -So far we've seen examples of two controls: The `ColorPicker` handled by the `control: "color"` and the `TextControl`, which is set as default for every `type: "string"` in the `block.json` attributes list. You can experiment with adding more, however. - -The corresponding table represents the mapping logic between the `block.json` attributes and their associated fields: - -| type | field | comment | -| ------- | --------------- | -------------------------------------------------------------------------------------------------------------------------------------- | -| string | TextControl | Renders a [TextControl field](https://developer.wordpress.org/block-editor/reference-guides/components/text-control/) of type `text` | -| boolean | RadioControl | Renders a [RadioControl field](https://developer.wordpress.org/block-editor/reference-guides/components/radio-control/) | -| integer | TextControl | Renders a [TextControl field](https://developer.wordpress.org/block-editor/reference-guides/components/text-control/) of type `number` | -| number | TextControl | Renders a [TextControl field](https://developer.wordpress.org/block-editor/reference-guides/components/text-control/) of type `number` | -| object | TextAreaControl | Renders a [TextAreaControl field](https://developer.wordpress.org/block-editor/reference-guides/components/textarea-control/) | - -The following control types will also be available when using the `editorFields` metadata when specifying a `control` property: - -| control | field | comment | -| -------- | --------------- | -------------------------------------------------------------------------------------------------------------------------------------- | -| color | ColorPicker | Renders a [ColorPicker field](https://developer.wordpress.org/block-editor/reference-guides/components/color-picker/) | -| text | TextControl | Renders a [TextControl field](https://developer.wordpress.org/block-editor/reference-guides/components/text-control/) of type `text` | -| textarea | TextAreaControl | Renders a [TextAreaControl field](https://developer.wordpress.org/block-editor/reference-guides/components/textarea-control/) | -| radio | RadioControl | Renders a [RadioControl field](https://developer.wordpress.org/block-editor/reference-guides/components/radio-control/) | -| select | SelectControl | Renders a [SelectControl field](https://developer.wordpress.org/block-editor/reference-guides/components/select-control/) | -| range | RangeControl | Renders a [RangeControl field](https://developer.wordpress.org/block-editor/reference-guides/components/range-control/) | -| number | TextControl | Renders a [TextControl field](https://developer.wordpress.org/block-editor/reference-guides/components/text-control/) of type `number` | -| checkbox | CheckBoxControl | Renders a [CheckBoxControl field](https://developer.wordpress.org/block-editor/reference-guides/components/checkbox-control/) | - -> [!NOTE] -> The `editorFields` configuration provides necessary hints for the helper to render the specified controls. It always overrides any configuration that is declared in the `block.json` attributes section. If you are not seeing the appropriate control used, check that your `editorFields` contain the correct attribute name and the correct `control` property. - -## Learn More - -This concludes this how-to guide for using the `@faustwp/block-editor-utils` package to convert React components into Block Editor blocks. To learn more, see [this RFC document](https://github.com/wpengine/faustjs/issues/1522) that explains in detail the different options and configurations regarding the usage of the `@faustwp/block-editor-utils`. diff --git a/src/pages/docs/explanation/telemetry/images/telemetry-2.png b/src/pages/docs/explanation/telemetry/images/telemetry-2.png deleted file mode 100644 index b2707b4d..00000000 Binary files a/src/pages/docs/explanation/telemetry/images/telemetry-2.png and /dev/null differ diff --git a/src/pages/docs/explanation/telemetry/images/telemetry-image-1.png b/src/pages/docs/explanation/telemetry/images/telemetry-image-1.png deleted file mode 100644 index b90e6bd3..00000000 Binary files a/src/pages/docs/explanation/telemetry/images/telemetry-image-1.png and /dev/null differ diff --git a/src/pages/docs/explanation/telemetry/index.mdx b/src/pages/docs/explanation/telemetry/index.mdx deleted file mode 100644 index 6d4ce6c6..00000000 --- a/src/pages/docs/explanation/telemetry/index.mdx +++ /dev/null @@ -1,44 +0,0 @@ -export const metadata = { - title: "Telemetry", - description: - "Information about Faust's anonymous telemetry data collection, what data is collected, and how to opt out.", -}; - -Faust collects completely anonymous telemetry data about general usage. Participation in this anonymous program is optional. For more information on how we handle this data, please read our [Privacy Policy](/privacy-policy). - -## What data we collect - -### WordPress Environment - -- WordPress version -- PHP version -- Faust plugin version & settings (_`frontend_uri` is not collected_) -- WordPress Multisite -- If the site is hosted on WP Engine -- Active plugins - -### Node Environment - -- `@faustwp/core` version -- `@faustwp/cli` version -- `@faustwp/blocks` version -- `@faustwp/block-editor-utils` version -- `@faustwp/experimental-app-router` version -- `@apollo/client` version -- Node version -- Next.js version -- If the Node environment is in **dev mode** (i.e., whether `npm run dev` was run) -- The command that was run (e.g., `npm run dev`, `npm run build`) -- The Operating System Platform name - -> **_NOTE:_** We do not collect any personally identifiable information or sensitive information like environment variables, file paths, or any application URLs. - -## Program Participation - -If you'd like to join the program, you can enable telemetry by selecting yes on the prompt: - -![Message displayed in WP Admin](./images/telemetry-image-1.png) - -You can also go to the Faust Settings page and toggle your participation at anytime: - -![Faust settings page showing checkbox for Telemetry](./images/telemetry-2.png) diff --git a/src/pages/docs/how-to/authentication/images/auth-redirect-flow.png b/src/pages/docs/how-to/authentication/images/auth-redirect-flow.png deleted file mode 100644 index c541f5e1..00000000 Binary files a/src/pages/docs/how-to/authentication/images/auth-redirect-flow.png and /dev/null differ diff --git a/src/pages/docs/how-to/authentication/index.mdx b/src/pages/docs/how-to/authentication/index.mdx deleted file mode 100644 index 4436dcc6..00000000 --- a/src/pages/docs/how-to/authentication/index.mdx +++ /dev/null @@ -1,156 +0,0 @@ -export const metadata = { - title: "Auth", - description: - "Guide to implementing authentication in your Next.js app using Faust.js toolkit features.", -}; - -Understanding authentication is crucial for protecting your application's data. This page will guide you through implementing features from the Faust.js toolkit into your Next.js app. - -Before starting, it helps to break down the process into three concepts: - -1. **[Authentication](https://nextjs.org/docs/app/building-your-application/authentication#authentication)**: Verifies if the user is who they say they are. It requires the user to prove their identity with something they have, such as a username and password. - -2. **[Session Management](https://nextjs.org/docs/app/building-your-application/authentication#session-management)**: Tracks the user's auth state across requests. - -3. **[Authorization](https://nextjs.org/docs/app/building-your-application/authentication#authorization)**: Decides what routes and data the user can access. - -This diagram shows the authentication flow using the Faust.js toolkit auth feature: - -![Flow diagram illustrating authentication flow](./images/auth-redirect-flow.png) - -Redirect-based authentication is the default strategy in the Faust.js toolkit. This strategy involves redirecting the user to WordPress to authenticate. Once the user has shown authentication, the user redirects back to the Next.js application with an authorization code, which you can then use to request a refresh and access token, thus completing the login process. - -This strategy is excellent for use cases where your authenticated users are admins/editors/etc. and do not necessarily need a “white label” login/register experience. Typically, you would use the redirect strategy if your primary reason for authentication is previewing posts and pages. - -In this doc, we will walk through how to add authentication support to your headless WordPress project using Faust.js toolkit. - -## Steps - -### 1\. Basic setup - -If you haven't already, follow the [Basic Setup](/docs/how-to/basic-setup/) steps to get Faust.js set up. - -### 2\. Implement auth functionality - -You are now ready to implement the auth functionality. Create a `/gated/index.js` page in the `pages` directory and paste this code block into the file: - -```js title="pages/gated/index.js" -import { useAuth, getApolloAuthClient, useLogout } from "@faustwp/core"; -import { gql, useQuery } from "@apollo/client"; - -function AuthenticatedView() { - const client = getApolloAuthClient(); - const { logout } = useLogout(); - const { data, loading } = useQuery( - gql` - { - viewer { - posts { - nodes { - id - title - } - } - name - } - } - `, - { client }, - ); - - if (loading) { - return <>Loading...; - } - - return ( - <> -

    Welcome {data?.viewer?.name}!

    - -

    My posts

    - -
      - {data?.viewer?.posts?.nodes.map((post) => ( -
    • {post.title}
    • - ))} -
    - - ); -} - -export default function Page() { - const { isAuthenticated, isReady, loginUrl } = useAuth(); - - if (!isReady) { - return <>Loading...; - } - - if (isAuthenticated === true) { - return ; - } - - return ( - <> -

    Welcome!

    - Login - - ); -} -``` - -In this code block, to check if a user is logged in, we use the `useAuth` hook, focusing on its `isReady` and `isAuthenticated` properties. The `isReady` property ensures that the hook is fully set up and ready to determine the user's authentication status. Once `isReady` confirms the setup, it returns a boolean, after which `isAuthenticated` will also return a boolean value indicating whether the user is logged in. - -For authenticated requests, the `getApolloAuthClient()` function is used to obtain an Apollo Client instance with the appropriate access token attached. This client can be utilized for making authenticated requests, either directly via `client.query` or by passing the client into Apollo's `useQuery` hook. - -In the `AuthenticatedView` component, we use the `useQuery` hook along with the authenticated client to fetch data, specifically the user's name and posts. If the data is loading, a loading message is displayed. Once the data is available, the component renders a welcome message, a logout button, and a list of the user's posts. - -The main `Page` component uses `isReady` and `isAuthenticated` to manage the user interface. If the authentication state is not ready, it shows a loading state. If the user is authenticated, it renders the `AuthenticatedView` component. Otherwise, it presents a login link that directs you to log in from the WordPress admin. - -### Step 3. Test auth - -You are now ready to access the auth page you created. In your terminal start up the dev server by running `npm run dev` and visit the [`http://localhost:3000/gated`](http://localhost:3000/gated) route. If the authentication was set up properly, you should be redirected to the WP login screen. Once you login, you should be sent back to the gated page and see the gated content. - -## Next steps - -Here are some things you can do to continue to test and add to your auth strategy with the toolkit: - -- You can verify that authentication is working by adding a `console.log()` with the current user's name to check whether they are being authenticated properly. Using the above code example, it would look like this: - -```js title="pages/gated/index.js" -import { useAuth, getApolloAuthClient, useLogout } from "@faustwp/core"; -import { gql, useQuery } from "@apollo/client"; - -function AuthenticatedView() { - const client = getApolloAuthClient(); - const { logout } = useLogout(); - const { data, loading } = useQuery( - gql` - { - viewer { - posts { - nodes { - id - title - } - } - name - } - } - `, - { client } - ); - - if (loading) { - return <>Loading...; - } - - // Console log the current user's name to verify authentication - console.log("Authenticated user's name:", data?.viewer?.name); // [!code ++] - -// Rest of your component -``` - -- Local Based Authentication is the second strategy available in Faust.js. This strategy involves the user initiating a login request from the Next.js application via the `useLogin` hook. Upon successful login, `useLogin` returns an authorization code used to request a refresh and access token, thus completing the login process. - -This strategy is excellent for use cases where you want to support a more “white label” login/register experience. It routes unauthenticated requests to your specified Next.js login page. In addition, users who wish to log in/register will not have to interact with WordPress or the WordPress backend at all, giving you the flexibility to implement and fine-tune your user flow. If you are interested in using this strategy, please visit this article here on the subject: - -[https://wpengine.com/builders/authentication-in-faust-js-and-headless-wp/#implementing-local-auth](https://wpengine.com/builders/authentication-in-faust-js-and-headless-wp/#implementing-local-auth) diff --git a/src/pages/docs/how-to/basic-setup/images/headless-admin-secret.png b/src/pages/docs/how-to/basic-setup/images/headless-admin-secret.png deleted file mode 100644 index eb6b24ee..00000000 Binary files a/src/pages/docs/how-to/basic-setup/images/headless-admin-secret.png and /dev/null differ diff --git a/src/pages/docs/how-to/basic-setup/index.mdx b/src/pages/docs/how-to/basic-setup/index.mdx deleted file mode 100644 index 75691604..00000000 --- a/src/pages/docs/how-to/basic-setup/index.mdx +++ /dev/null @@ -1,227 +0,0 @@ -export const metadata = { - title: "Basic Setup", - description: - "Step-by-step guide for setting up a WordPress backend and Next.js frontend to use Faust.js toolkit features.", -}; - -In order to leverage any of the tools in the Faust.js toolkit, some preliminary setup needs to be done. Follow the steps below to get started. - -## 0. Prerequisites - -Before you begin, you'll need: - -- A WordPress site (local or remote), if you don't have one yet, we recommend setting up a local WordPress development environment. -- A Next.js project, if you don't have a Next.js project yet, [create one](https://nextjs.org/docs/getting-started/installation) on your local computer. - -> [!INFO]- local WordPress development -> There are lots of options for setting up a local WordPress development environment. Some popular choices include [LocalWP](https://localwp.com/), [`wp-env`](https://developer.wordpress.org/block-editor/getting-started/devenv/get-started-with-wp-env/), or [MAMP](https://www.mamp.info/en/). - -## 1. Set up a WordPress backend and a Next.js frontend - -### A. Install plugins - -Install and activate the [FaustWP](https://wordpress.org/plugins/faustwp/) and [WPGraphQL](https://wordpress.org/plugins/wp-graphql/) plugins on your WordPress backend site. - -### B. Set environment variables - -Create a `.env.local` file in the root of your Next.js project that contains these environment variables: - -```ini title=".env.local" -# Your WordPress site URL -NEXT_PUBLIC_WORDPRESS_URL=https://faustexample.wpengine.com - -# Plugin secret found in WordPress Settings->Faust -FAUST_SECRET_KEY=YOUR_PLUGIN_SECRET -``` - -Replace `https://faustexample.wpengine.com` with the URL for your WordPress backend. - -Replace `YOUR_PLUGIN_SECRET` with the Secret Key found in `Settings → Faust` in your WordPress admin area. - -![Screenshot of the Headless Settings page in the WordPress admin panel, showing the Faust plugin settings. The Secret Key field is highlighted, which is required to enable authentication features like post previews in Faust.js.](./images/headless-admin-secret.png) - -Save your `.env.local` file. - -### C. Install dependencies - -Install the required NPM packages in your Next.js app. - -```bash -npm install @apollo/client @faustwp/core graphql @faustwp/cli -``` - -### D. Create Faust config file - -Create a `faust.config.js` file in the root of your project with this code: - -```js title="faust.config.js" -import { setConfig } from "@faustwp/core"; -import templates from "./src/wp-templates"; -import possibleTypes from "./possibleTypes.json"; - -/** - * @type {import('@faustwp/core').FaustConfig} - **/ -export default setConfig({ - templates, - plugins: [], - possibleTypes, -}); -``` - -### E. Create Faust API route - -Create an API route for Faust.js to use. You can do this by creating a file in `src/pages/api/faust/[[...route]].js`, with the following code: - -```js title="src/pages/api/faust/[[...route]].js" -import "../../faust.config"; // Adjust path as necessary - -export { apiRouter as default } from "@faustwp/core"; -``` - -> [!IMPORTANT] Important -> If you're not using a `src` folder in your project, you can omit "src/" from the file path above. And the same applies for other file paths mentioned throughout this doc. - -### F. Update `_app.js` file - -Once the API router is set up, head to `src/pages/_app.js`. Add the `import` statements, wrap your app in the `FaustProvider`, and pass `key={router.asPath}` to `Component`, as shown below. - -```js title="src/pages/_app.js" -import { useRouter } from "next/router"; -import { FaustProvider } from "@faustwp/core"; -import "../../faust.config"; - -export default function App({ Component, pageProps }) { - const router = useRouter(); - - return ( - - - - ); -} -``` - -## 2. Set up the Template Hierarchy - -### A. Generate types - -In order for Faust.js to run the GraphQL queries it needs to determine the correct template to use, it needs to have a list of all the types available in the GraphQL schema. We'll generate this list of types now. - -In your WordPress admin sidebar, go to `GraphQL` > `Settings`. Check the `Enable Public Introspection` box if it's not checked already and save your changes. Enabling introspection is required for the `npm run generate` command below to work. - -Add the following generate script to your Next.js project's `package.json` file, in the scripts block: - -```json title="package.json" - "scripts": { - "dev": "next dev", - "build": "next build", - "generate": "faust generatePossibleTypes", // [!code ++] - "start": "next start", - "lint": "next lint" - }, -``` - -Run `npm run generate` on the command line. Confirm that a possibleTypes.json has been generated in the root of your project. - -> [!IMPORTANT] Important -> Be sure to enable WPGraphQL introspection before running the `npm run generate` command since it is [disabled by default](https://www.wpgraphql.com/docs/security#introspection-disabled-by-default). - -### B. Add a template - -Create a new `src/wp-templates` folder (or add the `/wp-templates` folder in the root project folder if you don't use a `/src` folder). This is where your template files will be stored. We'll start by adding a template for rendering single blog posts. - -Inside the `wp-templates` folder, create a `single.js` file that contains the following code. - -```js title="wp-templates/single.js" -import { gql } from "@apollo/client"; - -export default function SingleTemplate(props) { - const { title, content } = props.data.post; - - return ( - <> -

    {title}

    -
    - - ); -} - -SingleTemplate.query = gql` - query GetPost($uri: ID!) { - post(id: $uri, idType: URI) { - title - content - } - } -`; - -SingleTemplate.variables = (seedQuery, ctx) => { - return { - uri: seedQuery?.uri, - }; -}; -``` - -Inside of the `SingleTemplate.variables` function, we pass in the URI of the current page so that it's available for GraphQL queries to use. - -Inside the `SingleTemplate.query` tagged template literal, we pass the `uri` for the current page into the GraphQL query, telling WordPress that we want to fetch the `title` and `content` for the post matching that URI. - -In the `SingleTemplate` component, we receive the props, destructure the `title` and `content`, then return some JSX to render the title and content to the page. - -Finally, we have to make Faust.js aware that this template exists. To do that, create an `index.js` file inside the `wp-templates` folder with this code inside: - -```js title="wp-templates/index.js" -import SingleTemplate from "./single"; - -const templates = { - single: SingleTemplate, -}; - -export default templates; -``` - -### C. Create a catch-all route - -Create a `[...wordpressNode].js` file inside your `pages` folder. Add the following code to it. - -```js title="src/pages/[...wordpressNode].js" -import { getWordPressProps, WordPressTemplate } from "@faustwp/core"; - -export default function Page(props) { - return ; -} - -export function getStaticProps(ctx) { - return getWordPressProps({ ctx }); -} - -export async function getStaticPaths() { - return { - paths: [], - fallback: "blocking", - }; -} -``` - -This catch-all route tells Next.js to use the templates to render pages. - -Note that it is still possible to override this with hardcoded pages. For example, if you have a page in your WordPress site with a URI of `/about`, Faust.js will render that page using the relevant template in your project's `wp-templates` folder. However, if you add a `src/pages/about.js` file to your project, Next.js will render the `/about` path in your app using that file instead. - -### D. Test your template - -You should now be able to make use of this template. - -Run `npm run dev` to get your Next.js project running in development mode, then visit the URL for one of the blog posts. The default starter blog path in WordPress is the "hello world" one – `http://localhost:3000/hello-world/`. - -If you've wired everything up correctly, the page should render and display the post's title and content. - -## 3. Next steps - -You are now ready to leverage Faust's other features, including: - -- [Authentication](/docs/how-to/authentication/) - -- [Post Previews](/docs/how-to/post-previews/) - -- [Render Blocks](/docs/how-to/rendering-blocks/) diff --git a/src/pages/docs/how-to/create-a-plugin/index.mdx b/src/pages/docs/how-to/create-a-plugin/index.mdx deleted file mode 100644 index afd75ef3..00000000 --- a/src/pages/docs/how-to/create-a-plugin/index.mdx +++ /dev/null @@ -1,77 +0,0 @@ -export const metadata = { - title: "Create A Plugin", - description: - "Learn how to create Faust.js plugins to extend or alter the framework's behavior using JavaScript/TypeScript actions and filters.", -}; - -Faust plugins are a structured way to extend or alter the framework's behavior, very much like WordPress plugins do in a traditional WordPress environment. Instead of `PHP`, however, Faust's plugins are written in JavaScript/TypeScript, leveraging actions and filters to deliver similar flexibility in a headless architecture. - -## Steps - -### 1. Basic setup - -If you haven't already, follow the [Basic Setup](/docs/how-to/basic-setup/) steps to get Faust.js set up. - -### 2. Create A Plugins Folder and file - -In its very basic form, a Faust Plugin is a JavaScript class with an `apply` method. This apply method has a parameter called `hooks`, which is passed from `@wordpress/hooks`. Create a folder in the root of your Faust project called `plugin`. In the folder, create a file called `SamplePlugin.js`. In this file, add this code block: - -```js title="plugins/SamplePlugin.js" -import { FaustHooks } from "@faustwp/core"; - -export class MyPlugin { - /** - * @param {FaustHooks} hooks - */ - apply(hooks) { - // Plugin logic goes here - } -} -``` - -The `hooks` parameter is an object which contains the functions `addFilter` and `addAction`, amongst others. You can then use these functions to call the respective actions and filters to tap into Faust like so: - -```js {10-16} title="plugins/SamplePlugin.js " -import { FaustHooks } from "@faustwp/core"; - -export class MyPlugin { - /** - * @param {FaustHooks} hooks - */ - apply(hooks) { - const { addAction, addFilter } = hooks; - - addFilter( - "possibleTemplatesList", - "my-namespace", - (possibleTemplates, context) => { - // Filter logic goes here. - }, - ); - } -} -``` - -> [!important] Please Note -> `experimentalPlugins` is being deprecated and replaced with `plugins` in the `faust.config.js` file. -> Please update your configuration accordingly. - -### 3. Add The Plugin to the Faust Config - -You can then implement the plugin by adding it to the plugins property in the App's `faust.config.js` file: - -```js title="faust.config.js" -import { setConfig } from "@faustwp/core"; -import templates from "./wp-templates"; -import possibleTypes from "./possibleTypes.json"; -import { MyPlugin } from "./plugins/MyPlugin.js"; - -/** - * @type {import('@faustwp/core').FaustConfig} - */ -export default setConfig({ - templates, - plugins: [new MyPlugin()], - possibleTypes, -}); -``` diff --git a/src/pages/docs/how-to/custom-blocks/images/block-console.png b/src/pages/docs/how-to/custom-blocks/images/block-console.png deleted file mode 100644 index 5b83efeb..00000000 Binary files a/src/pages/docs/how-to/custom-blocks/images/block-console.png and /dev/null differ diff --git a/src/pages/docs/how-to/custom-blocks/images/core-code-block-decoupled-preview.png b/src/pages/docs/how-to/custom-blocks/images/core-code-block-decoupled-preview.png deleted file mode 100644 index 97d27725..00000000 Binary files a/src/pages/docs/how-to/custom-blocks/images/core-code-block-decoupled-preview.png and /dev/null differ diff --git a/src/pages/docs/how-to/custom-blocks/images/core-code-block-ide-attributes.png b/src/pages/docs/how-to/custom-blocks/images/core-code-block-ide-attributes.png deleted file mode 100644 index e9272ee8..00000000 Binary files a/src/pages/docs/how-to/custom-blocks/images/core-code-block-ide-attributes.png and /dev/null differ diff --git a/src/pages/docs/how-to/custom-blocks/images/core-code-block.png b/src/pages/docs/how-to/custom-blocks/images/core-code-block.png deleted file mode 100644 index e0209069..00000000 Binary files a/src/pages/docs/how-to/custom-blocks/images/core-code-block.png and /dev/null differ diff --git a/src/pages/docs/how-to/custom-blocks/index.mdx b/src/pages/docs/how-to/custom-blocks/index.mdx deleted file mode 100644 index 94df765b..00000000 --- a/src/pages/docs/how-to/custom-blocks/index.mdx +++ /dev/null @@ -1,320 +0,0 @@ -export const metadata = { - title: "Customize Blocks Rendering", - description: - "Learn to add custom implementations of WordPress Blocks in your Faust.js project.", -}; - -Whether you want to customize the look and feel of a [core WordPress block](https://wordpress.org/documentation/article/wordpress-block-editor/), implement a [custom block](https://developer.wordpress.org/block-editor/getting-started/tutorial/), or add a block from a third-party plugin, Faust.js makes it easy to render WP blocks to your liking. This guide will show you how to render a block in your Faust.js project. - -## 0. Prerequisites - -Ensure that you have completed the steps in our [Rendering blocks](/docs/how-to/rendering-blocks/) guide and have WordPress blocks rendering in your app successfully before proceeding with the steps below. - -## 1. Choose a block to override - -For this guide, we are going to override the core WordPress Code Block that renders a formatted block of code.. - -![WordPress Code block in the Block editor](./images/core-code-block.png) - -## 2. Review block features and settings - -Try using the block yourself in the WordPress block editor and get familiar with the features it has. Review the settings it has in the Settings Panel and change a few of them to see what they do. - -## 3. Inspect the block data via the GraphiQL IDE - -In the WordPress admin sidebar, go to `GraphQL` > `GraphiQL IDE` to open the GraphiQL IDE. If you're not familiar, this is a tool we can use for composing and testing GraphQL queries. This can help you understand what data your frontend app will receive when it executes a particular query. - -Paste the following query into the GraphiQL IDE and hit the `▶️` button to execute the query. Replace `/posts/testing` with the path to the blog post that contains the block you want to override. - -```graphql -{ - post(id: "/posts/testing", idType: URI) { - editorBlocks { - renderedHtml - ... on CoreCode { - attributes { - borderColor - backgroundColor - content - style - textColor - fontSize - } - } - } - } -} -``` - -> [!IMPORTANT] Block Type -> Each block is given a unique GraphQL type. In this case, the Core Code block is a type of `CoreCode`. You can find the block type by looking at the `__typename` field. If you need help figuring this out, the following query will list all the blocks on a post with their types: -> -> ```graphql -> { -> post(id: "/posts/testing", idType: URI) { -> editorBlocks { -> __typename -> type -> name -> } -> } -> } -> ``` -> -> The `__typename` or `type` fields will show you the block type for each block on a post. You may find it useful to know this is generated from the `$block_type` argument in the block's [`registerBlockType`](https://developer.wordpress.org/reference/functions/register_block_type/) function; shown here in the `name` field. - -The GraphiQL IDE will then display the data that was returned in the response to that query. It should look something like the example response below. - -```json -{ - "data": { - "post": { - "editorBlocks": [ - { - "renderedHtml": "\n
    // Computes the nth Fibonacci number\nfunction fib(n) {\n    var a = 0, b = 1, c;\n    if (n < 3) {\n        if (n < 0) return fib(-n);\n        if (n === 0) return 0;\n        return 1;\n    }\n    while (--n)\n        c = a + b, a = b, b = c;\n    return c;\n}
    \n", - "attributes": { - "borderColor": null, - "backgroundColor": "tertiary", - "content": "// Computes the nth Fibonacci number\nfunction fib(n) {\n var a = 0, b = 1, c;\n if (n < 3) {\n if (n < 0) return fib(-n);\n if (n === 0) return 0;\n return 1;\n }\n while (--n)\n c = a + b, a = b, b = c;\n return c;\n}", - "style": "{\"color\":{\"text\":\"#333333\"},\"border\":{\"color\":\"#333333\",\"width\":\"2px\"}}", - "textColor": null, - "fontSize": "small" - } - } - ] - } - } -} -``` - -You can try modifying the query and running it again to see how the response changes. To learn about the fields that are available for that block, click the `Docs` link in GraphiQL, search for `CoreCode`, and click on `CoreCodeAttributes`. You will see the following: - -![Core code block attributes](./images/core-code-block-ide-attributes.png) - -> [!INFO]- Styles -> Notice that with some of the attributes like `backgroundColor`, the editor used the [`theme.json`](https://developer.wordpress.org/themes/global-settings-and-styles/introduction-to-theme-json/) palette slug name instead of the actual value, i.e., `tertiary`. It also stored the custom hardcoded styles in the `style` fields as a JSON string. - -## 4. Define the block in your frontend app - -### A. Create a new block component - -Create a new `CoreCode.js` file inside of your `wp-blocks` folder that contains the following code. - -```js title="wp-blocks/CoreCode.js" -export default function CoreCode(props) { - console.log(props.attributes); - return
    CoreCode
    ; -} - -CoreCode.displayName = "CoreCode"; -``` - -For now, we're just rendering a placeholder `div` and logging the data to the console. This will be replaced with actual block code in subsequent steps. - -> [!INFO]- Context -> The `displayName` property is defined here so that when we run the `editorBlocks` query in subsequent steps, the `__typename` field in the query will match this string. This is required so that the `WordPressBlocksViewer` component can resolve and render the block properly. - -### B. Register the new block component - -Open the `index.js` file inside the `wp-blocks` folder and add your new block like this: - -```js title="wp-blocks/index.js" -import { CoreBlocks } from "@faustwp/blocks"; -import CoreCode from "./CoreCode"; - -export default { - ...CoreBlocks, - CoreCode: CoreCode, // [++code] -}; -``` - -> [!NOTE]- Context -> This file will be used to import all of the blocks you're overriding and make them available as the default export, which will be passed into `WordPressBlocksProvider` in a subsequent step. -> -> We also define the name of the block as `CoreCode` (the property name to the left of the ":" character). This key must match the `__typename` value that the blocks has in the GraphQL schema. - -### C. Create the block GraphQL fragment - -Create a fragment that describes the request block fields and attributes. First, `import { gql } from @apollo/client`. Then add the `CoreCode.fragments` code block, as shown below. Include all relevant fields for your implementation. - -```js {1, 08-24} title="wp-blocks/CoreCode.js " -import { gql } from "@apollo/client"; - -export default function CoreCode({ attributes }) { - console.log(attributes); - return
    CoreCode
    ; -} - -CoreCode.fragments = { - key: `CoreCodeBlockFragment`, - entry: gql` - fragment CoreCodeBlockFragment on CoreCode { - attributes { - borderColor - backgroundColor - content - style - textColor - fontSize - fontFamily - cssClassName - } - } - `, -}; -``` - -> [!INFO]- Context -> Attaching the fragment as a property of the function component (`CoreCode.fragments = ...`) is a convention that Faust.js uses to [colocate fragments](https://www.apollographql.com/docs/react/data/fragments#colocating-fragments) with the components that use them. - -### D. Include the block fragment in your page template queries - -Now, you can include the block fragment inside of the GraphQL query for your posts and pages. - -The example below shows a `/wp-templates/single.js` template. - -```js {2, 9, 18} title="wp-templates/single.js" -import { gql } from "@apollo/client"; -import blocks from "../wp-blocks"; - -export default function Page(props) { - // ... -} - -Page.query = gql` - ${blocks.CoreCode.fragments.entry} - query GetPost { - post(id: $databaseId, idType: DATABASE_ID, asPreview: $asPreview) { - editorBlocks { - name - __typename - renderedHtml - id: clientId - parentClientId - ...${blocks.CoreCode.fragments.key} - } - } - } -`; -``` - -- `import blocks from "../wp-blocks";{:js}` imports the blocks we're overriding. -- `${blocks.CoreCode.fragments.entry}{:js}` interpolates the `CoreCode` GraphQL fragment into the page query. -- `...${blocks.CoreCode.fragments.key}{:js}` tells GraphQL to insert the `CoreCode` GraphQL fragment inside of the `editorBlocks { ... }{:gql}` block. - -With this code in place, when the GraphQL query for the single page template is executed, the response will now include data for the `CoreCode` block. You can follow this pattern in your other page templates to query for block data within them, as well. - -### E. Verify the block data in the frontend app - -If you navigate to the page that contains this block, you should be able to inspect the properties in the console and see your block attributes that were logged by `console.log(props.attributes)` in `CoreCode.js`. - -![Browser Console showing logged object](./images/block-console.png) - -## 5. Implement the block - -You now have the block data available in your frontend app and are ready to implement the block's structure, styling, and functionality. - -> [!IMPORTANT] Options -> You've done the hard work of getting the data into your app and integrating with the `@faustwp/blocks` package. How you implement this block is completely up to you. You can use any React component library, CSS-in-JS solution, or custom styles you like. -> -> Keep in mind, If your implementation deviates from the WordPress implementation, your content editors might find this confusing. For this guide, we'll show you a basic implementation of the `CoreCode` block. - -Replace the `CoreCode` component with the following code. This code will render the block in the frontend app with a similar structure to the WordPress block editor. - -```js title="wp-blocks/CoreCode.js" -import { gql } from "@apollo/client"; - -export default function CoreCode({ attributes }) { - return ( -
    -			{`${attributes?.content}`}
    -		
    - ); -} - -CoreCode.fragments = { - key: `CoreCodeBlockFragment`, - entry: gql` - fragment CoreCodeBlockFragment on CoreCode { - attributes { - content - cssClassName - } - } - `, -}; - -CoreCode.displayName = "CoreCode"; -``` - -## 6. Implement the block styles - -Let's add some code highlighting to this component to really make it pop. - -> [!IMPORTANT] Options -> We're going to leave the specific styling up you. You can choose to statically style in here or you could leverage the other available `attributes` to style the block dynamically from the WordPress block editor. Faust does [support styling](/docs/reference/get-styles/) blocks using your WordPress Block theme styles. - -### A. Install the `highlight.js` package by running the following command: - -```bash -npm add highlight.js -``` - -### B. Import the `highlight.js` package in the `_app.js` file: - -```js title="_app.js" -// THIS IS VERY IMPORTANT. -// In Next.js, put this in your _app.js file -import "highlight.js/styles/github.css"; -``` - -### C. Update the Component - -Update `CoreCode.js` with the code bellow to add code highlighting: - -```js title="wp-blocks/CoreCode.js" -import { gql } from "@apollo/client"; -import { useEffect, useRef } from "react"; -import hljs from "highlight.js"; - -export default function CoreCode({ attributes }) { - // Create a ref for the element - const codeRef = useRef(null); - - useEffect(() => { - // Once the component has rendered, apply highlight.js to the code element - if (codeRef.current) { - hljs.highlightElement(codeRef.current); - } - }, [attributes?.content]); - - return ( -
    -			
    -		
    - ); -} - -CoreCode.fragments = { - key: `CoreCodeBlockFragment`, - entry: gql` - fragment CoreCodeBlockFragment on CoreCode { - attributes { - content - cssClassName - } - } - `, -}; - -CoreCode.displayName = "CoreCode"; -``` - -## 7. Wrapping up... - -Navigate to a page that contains the `CoreCode` block and you should see the code block with syntax highlighting applied. - -![](./images/core-code-block-decoupled-preview.png) diff --git a/src/pages/docs/how-to/customize-the-toolbar/images/custom-toolbar-nodes.png b/src/pages/docs/how-to/customize-the-toolbar/images/custom-toolbar-nodes.png deleted file mode 100644 index 14060569..00000000 Binary files a/src/pages/docs/how-to/customize-the-toolbar/images/custom-toolbar-nodes.png and /dev/null differ diff --git a/src/pages/docs/how-to/customize-the-toolbar/index.mdx b/src/pages/docs/how-to/customize-the-toolbar/index.mdx deleted file mode 100644 index 5d85c413..00000000 --- a/src/pages/docs/how-to/customize-the-toolbar/index.mdx +++ /dev/null @@ -1,166 +0,0 @@ -export const metadata = { - title: "Customize The Toolbar", - description: - "Learn how to customize the Faust.js Toolbar using the toolbarNodes filter. We'll walk you through the process of enabling the experimental toolbar feature, adding authentication, and creating a custom plugin to extend the Toolbar with new menu items and submenus.", -}; - -This guide covers how to customize your site's Toolbar utilizing the `toolbarNodes` filter. - -## 0. Prerequisites - -Ensure that you have completed the steps in the following pages before proceeding. - -- [Basic setup](/docs/how-to/basic-setup) - -- [Set up authentication](/docs/how-to/authentication/) - -Implementing authentication is necessary so that Faust.js "knows" whether the current user has the permissions necessary to view and access the toolbar. - -> [!WARNING] -> The Toolbar was an experimental feature introduced in `@faustwp/core@0.2.5`. As of [`@faustwp/core@3.2.0`](https://github.com/wpengine/faustjs/releases/tag/%40faustwp%2Fcore%403.2.0) this feature is deprecated and will no longer be maintained. - -## 1. Add Toolbar - -The Faust Toolbar shares mostly the the same HTML as the WordPress core Toolbar. This enables the use of the same styles that exist in WordPress core, and have been provided for you to import within `@faustwp/core`. - -```js title="pages/_app.js" -import "@faustwp/core/dist/css/toolbar.css"; -``` - -Add `experimentalToolbar: true` to your project's `faust.config.js`. - -```js title="faust.config.js" -import { setConfig } from "@faustwp/core"; -import templates from "./wp-templates"; -import possibleTypes from "./possibleTypes.json"; - -export default setConfig({ - experimentalToolbar: true, // Enable experimental toolbar - templates, - possibleTypes, -}); -``` - -## 2. Login to your WP Admin - -The Toolbar was designed to only load for the authenticated user. - -> [!IMPORTANT] -> A WordPress user will be automatically authenticated with your site when previewing a post. This approach can be used as a quick way to view the Toolbar for development purposes. - -## 3. Create The Plugin - -Create a new file in your Faust project: `plugins/CustomPlugin.tsx`. - -Add the following code to `plugins/CustomPlugin.tsx`: - -```ts title="plugins/CustomPlugin.tsx" - -import React from 'react'; -import { - FaustHooks, - FaustPlugin, - FaustToolbarNodes, - FaustToolbarContext, - ToolbarItem, - ToolbarSubmenu, - ToolbarSubmenuWrapper, -} from '@faustwp/core'; - -/** - * Example Custom Toolbar Plugin. - */ -export class CustomToolbar implements FaustPlugin { - apply(hooks: FaustHooks) { - /** - * This example demonstrates how to filter on the core Toolbar nodes - * in order to add your own custom nodes! - */ - hooks.addFilter( - 'toolbarNodes', - 'faust', - (toolbarNodes: FaustToolbarNodes, context: FaustToolbarContext) => { - const customToolbarNodes: FaustToolbarNodes = [ - { - id: 'custom-node', - location: 'primary', - component: , - }, - { - id: 'custom-node-with-submenu', - location: 'primary', - component: , - }, - ]; - - return [...toolbarNodes, ...customToolbarNodes]; - }, - ); - } -} - -/** - * A simple link. - */ -export function CustomNode() { - return ( - - Custom Node - - ); -} - -/** - * A simple link with a submenu that displays on hover. - */ -export function CustomNodeWithSubmenu() { - return ( - <> - - Custom Node w/ Submenu - - - -
  • - - Link - -
  • -
  • - - Link - -
  • -
  • - - Link - -
  • -
    -
    - - ); -} -``` - -## 4. Register The Plugin - -Import and register your new plugin in `faust.config.js`: - -```js title="faust.config.js" -import { setConfig } from "@faustwp/core"; -import templates from "./wp-templates"; -import possibleTypes from "./possibleTypes.json"; -import { CustomToolbar } from "./plugins/CustomPlugin.tsx"; - -export default setConfig({ - templates, - experimentalToolbar: true, - plugins: [new CustomToolbar()], // Register plugin - possibleTypes, -}); -``` - -You should now be able to see your new Custom Nodes and toolbar at the top of the page if you visit a route as an authenticated user like so: - -![A customized Faust.js Toolbar displaying newly added toolbar nodes, including a custom menu item and a submenu, visible at the top of the page for authenticated users.](./images/custom-toolbar-nodes.png) diff --git a/src/pages/docs/how-to/generate-types-with-graphql-codegen/images/ts-image-one.png b/src/pages/docs/how-to/generate-types-with-graphql-codegen/images/ts-image-one.png deleted file mode 100644 index fd885cc4..00000000 Binary files a/src/pages/docs/how-to/generate-types-with-graphql-codegen/images/ts-image-one.png and /dev/null differ diff --git a/src/pages/docs/how-to/generate-types-with-graphql-codegen/images/ts-image-two.png b/src/pages/docs/how-to/generate-types-with-graphql-codegen/images/ts-image-two.png deleted file mode 100644 index c6cf476c..00000000 Binary files a/src/pages/docs/how-to/generate-types-with-graphql-codegen/images/ts-image-two.png and /dev/null differ diff --git a/src/pages/docs/how-to/generate-types-with-graphql-codegen/index.mdx b/src/pages/docs/how-to/generate-types-with-graphql-codegen/index.mdx deleted file mode 100644 index 95f4290d..00000000 --- a/src/pages/docs/how-to/generate-types-with-graphql-codegen/index.mdx +++ /dev/null @@ -1,146 +0,0 @@ -export const metadata = { - title: "Generate types with GraphQL Codegen", - description: - "Guide to generating TypeScript definitions for your custom GraphQL queries and fragments using GraphQL Code Generator.", -}; - -Faust.js provides built-in TypeScript support, including types for Templates, Blocks, and more. This guide will show you how to generate fully typed definitions for your custom GraphQL queries and fragments using [GraphQL Code Generator](https://the-guild.dev/graphql/codegen). - -> [!TIP] -> If you're looking to generate [Apollo-specific fragment matching](https://www.apollographql.com/docs/react/data/fragments#using-fragments-with-unions-and-interfaces) metadata (i.e. `possibleTypes`), see the `faust generatePossibleTypes` command. However, that command does **not** produce TypeScript definitions for your queries. For typing your queries in Faust, continue reading below. - -## 0. Prerequisites - -If you haven't already, follow the [Basic Setup](/docs/how-to/basic-setup/) steps to get Faust.js configured. Once your project is set up, add `@graphql-codegen/cli` to your project: - -```bash -npm install -D typescript @graphql-codegen/cli -``` - -## 1. Add Codegen Configuration - -In the root of your project, create a configuration file named `codegen.ts` - -```ts title="codegen.ts" -import { CodegenConfig } from "@graphql-codegen/cli"; - -const config: CodegenConfig = { - schema: "https://faustexample.wpengine.com/graphql", - documents: ["src/**/*.{tsx,ts}"], - generates: { - "./src/__generated__/": { - preset: "client", - plugins: [], - presetConfig: { - gqlTagName: "gql", - }, - }, - }, - ignoreNoDocuments: true, -}; - -export default config; -``` - -## 2. Add Codegen Script - -Next, update your package.json to add a script for running the code generator: - -```json title="package.json" -{ - "scripts": { - "generate:types": "graphql-codegen" - } -} -``` - -Now you can run: - -```bash -npm run generate:types -``` - -This command will scan your `src/` folder for any GraphQL queries or fragments, then generate TypeScript types in `src/__generated__/graphql.ts`. - -> [!IMPORTANT] Important -> Be sure to enable WPGraphQL introspection before running the `npm run generate` command since it is [disabled by default](https://www.wpgraphql.com/docs/security#introspection-disabled-by-default). - -## 3. Using Generated Types - -### A. Typing Templates - -After Codegen runs, you'll see auto-generated types in graphql.ts. For example: - -```ts title="src/__generated__/graphql.ts" -export type GetPostQueryVariables = Exact<{ - databaseId: Scalars["ID"]; - asPreview?: InputMaybe; -}>; - -export type GetPostQuery = { - // ... -}; -``` - -You can use these types with the `FaustTemplate` helper in your WordPress templates: - -```tsx title="wp-templates/single.tsx" -import { gql } from "../__generated__"; -import { GetPostQuery } from "../__generated__/graphql"; -import { FaustTemplate } from "@faustwp/core"; - -const Component: FaustTemplate = (props) => { - // `props.data` and other fields are now typed! - return
    {props?.data?.post?.title}
    ; -}; - -export const pageQuery = gql(/* GraphQL */ ` - query GetPost($databaseId: ID!, $asPreview: Boolean) { - ... - } -`); -``` - -Then you can inspect all the types in the `props` parameters as you type: - -![TypeScript auto-complete suggestion in Faust.js, showing the available fields for a GraphQL query's response. The code completion suggests properties like `__SEED_NODE__`, `data`, and `loading`, indicating TypeScript's type inference for the `props` object. -](./images/ts-image-one.png) - -All the data from the query results will be properly typed based on the introspected schema: - -![TypeScript type definition for an author field in a Faust.js GraphQL query. The type structure shows `NodeWithAuthorToUserConnectionEdge`, defining the `User` type with an optional `name` property, demonstrating TypeScript's type safety for GraphQL responses. -](./images/ts-image-two.png) - -### B. Typing Block Components - -If you create blocks with `@faustwp/blocks`, you can use the WordPressBlock type to add strong typing to those components: - -```tsx title="wp-blocks/CoreParagraph.tsx" -import { gql } from "../__generated__"; -import { WordPressBlock } from "@faustwp/blocks"; -import { CoreParagraphFragmentFragment } from "../__generated__/graphql"; - -const CoreParagraph: WordPressBlock = ( - props, -) => { - return

    {props.attributes?.content}

    ; -}; - -export const fragments = { - entry: gql(` - fragment CoreParagraphFragment on CoreParagraph { - attributes { - content - } - } - `), - key: "CoreParagraphFragment", -}; -``` - -By passing in `CoreParagraphFragmentFragment` to WordPressBlock, TypeScript enforces that props only contains fields you've declared in the GraphQL fragment. - -## Further Reading - -- [Migrating to TypeScript](/docs/explanation/migrating-to-typescript) -- [TypeScript Reference](/docs/reference/typescript) diff --git a/src/pages/docs/how-to/index.mdx b/src/pages/docs/how-to/index.mdx deleted file mode 100644 index d5bcb771..00000000 --- a/src/pages/docs/how-to/index.mdx +++ /dev/null @@ -1,10 +0,0 @@ -export const metadata = { - title: "How-to Guides", - description: - "Step-by-step guides for implementing specific features and achieving practical goals with Faust.js.", -}; - -I see you found the root of the How-to guides! How-to guides are a place where we walk you through implementing specific features of Faust.js®. If you are looking to achieve a specific goal with Faust.js®, you are in the right place. - -> [!note] Learn More -> For more info on how we layout our documentation and the the role played by How-to guides, please read about the [_Diátaxis_](https://diataxis.fr/how-to-guides/) approach we use. diff --git a/src/pages/docs/how-to/migrate-from-legacy-faust/images/possible-templates-migration.png b/src/pages/docs/how-to/migrate-from-legacy-faust/images/possible-templates-migration.png deleted file mode 100644 index e68b558f..00000000 Binary files a/src/pages/docs/how-to/migrate-from-legacy-faust/images/possible-templates-migration.png and /dev/null differ diff --git a/src/pages/docs/how-to/migrate-from-legacy-faust/index.mdx b/src/pages/docs/how-to/migrate-from-legacy-faust/index.mdx deleted file mode 100644 index 5b724860..00000000 --- a/src/pages/docs/how-to/migrate-from-legacy-faust/index.mdx +++ /dev/null @@ -1,283 +0,0 @@ -export const metadata = { - title: "Migrate from Legacy Faust", - description: - "Upgrading your headless WordPress setup from GQty-based Faust to the current version. Learn how to reuse presentational components, write GraphQL queries, and adopt the WP Template system for a smooth transition", -}; - -Migration from the previous versions of Faust that use GQty is a manual process. However, there are some conventional techniques and best practices for React Development that will definitely help you with this process. - -To migrate from the legacy version to the current version, follow the guide that most closely represents your model. Each guide recommends an integration path along with example code. - -## Reusing Presentational Components - -The Components folder typically contains components that embody a presentational meaning. - -By presentational, we mean they do not depend on GQty or any hooks or side-effects. They just take props and render the data. Here is an example of a presentational component taken from this blueprint: - -```js title="PostInfo.js" -export default function PostInfo({ className, author, date }) { - if (!date && !author) { - return null; - } - - return ( -
    - {date && ( - - )} - {date && author && <> } - {author && By {author}} -
    - ); -} -``` - -There is no reference to GQty or Apollo and this component can be safely re-used across different applications. - -On the other hand the following component is more difficult to migrate as it depends on the GQty client: - -**Before** - -```js title="NavMenu.js" -export default function NavigationMenu({ className, menuLocation, children }) { - const { useQuery } = client; - const { nodes: menuItems } = useQuery().menuItems({ - where: { - location: menuLocation, - }, - }); -``` - -You will have to either replace the `useQuery()` from GQty with the `useQuery()` of Apollo or perform the query on a higher level and pass on the menu items as props: - -**After** - -```js title="NavMenu.js" -export default function NavigationMenu({ menuItems, className, children }) { - if (!menuItems) { - return null; - } - - return ( -