diff --git a/package.json b/package.json index b11dd5b5..a2b592c6 100644 --- a/package.json +++ b/package.json @@ -25,8 +25,6 @@ "@faustwp/cli": "^3.2.3", "@faustwp/core": "^3.2.3", "@headlessui/react": "^2.2.4", - "@heroicons/react": "^2.2.0", - "@icons-pack/react-simple-icons": "^13.1.0", "@jsdevtools/rehype-url-inspector": "^2.0.2", "@next/third-parties": "^15.3.4", "@octokit/core": "^7.0.2", @@ -44,6 +42,8 @@ "next-sitemap": "^4.2.3", "react": "^19.1.0", "react-dom": "^19.1.0", + "react-icons": "^5.5.0", + "react-intersection-observer": "^9.16.0", "rehype-callouts": "^2.1.0", "rehype-external-links": "^3.0.0", "rehype-pretty-code": "^0.14.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f37bf7a2..cc6329e3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,12 +32,6 @@ importers: '@headlessui/react': specifier: ^2.2.4 version: 2.2.4(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@heroicons/react': - specifier: ^2.2.0 - version: 2.2.0(react@19.1.0) - '@icons-pack/react-simple-icons': - specifier: ^13.1.0 - version: 13.1.0(react@19.1.0) '@jsdevtools/rehype-url-inspector': specifier: ^2.0.2 version: 2.0.2 @@ -89,6 +83,12 @@ importers: react-dom: specifier: ^19.1.0 version: 19.1.0(react@19.1.0) + react-icons: + specifier: ^5.5.0 + version: 5.5.0(react@19.1.0) + react-intersection-observer: + specifier: ^9.16.0 + version: 9.16.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0) rehype-callouts: specifier: ^2.1.0 version: 2.1.0 @@ -423,11 +423,6 @@ packages: react: ^18 || ^19 || ^19.0.0-rc react-dom: ^18 || ^19 || ^19.0.0-rc - '@heroicons/react@2.2.0': - resolution: {integrity: sha512-LMcepvRaS9LYHJGsF0zzmgKCUim/X3N/DQKc4jepAXJ7l8QxJ1PmxJzqplF2Z3FE4PqBAIGyJAQ/w4B5dsqbtQ==} - peerDependencies: - react: '>= 16 || ^19.0.0-rc' - '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -448,11 +443,6 @@ packages: resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} - '@icons-pack/react-simple-icons@13.1.0': - resolution: {integrity: sha512-BrBHWxWM1X2pCdFmQfYnwkfwwm9Ta5NMdvSNe/ns2e9rhVK863rRAF6AhDjsvUIDKXSkRG1HfXZiSX/Uij0kxg==} - peerDependencies: - react: ^16.13 || ^17 || ^18 || ^19 - '@img/sharp-darwin-arm64@0.34.2': resolution: {integrity: sha512-OfXHZPppddivUJnqyKoi5YVeHRkkNE2zUFT2gbpKxp/JZCFYEYubnMg+gOp6lWfasPrTS+KPosKqdI+ELYVDtg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -3674,6 +3664,20 @@ packages: peerDependencies: react: ^19.1.0 + react-icons@5.5.0: + resolution: {integrity: sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==} + peerDependencies: + react: '*' + + react-intersection-observer@9.16.0: + resolution: {integrity: sha512-w9nJSEp+DrW9KmQmeWHQyfaP6b03v+TdXynaoA964Wxt7mdR3An11z4NNCQgL4gKSK7y1ver2Fq+JKH6CWEzUA==} + peerDependencies: + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + react-dom: + optional: true + react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} @@ -4917,10 +4921,6 @@ snapshots: react-dom: 19.1.0(react@19.1.0) use-sync-external-store: 1.5.0(react@19.1.0) - '@heroicons/react@2.2.0(react@19.1.0)': - dependencies: - react: 19.1.0 - '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.6': @@ -4934,10 +4934,6 @@ snapshots: '@humanwhocodes/retry@0.4.3': {} - '@icons-pack/react-simple-icons@13.1.0(react@19.1.0)': - dependencies: - react: 19.1.0 - '@img/sharp-darwin-arm64@0.34.2': optionalDependencies: '@img/sharp-libvips-darwin-arm64': 1.1.0 @@ -8826,6 +8822,16 @@ snapshots: react: 19.1.0 scheduler: 0.26.0 + react-icons@5.5.0(react@19.1.0): + dependencies: + react: 19.1.0 + + react-intersection-observer@9.16.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + dependencies: + react: 19.1.0 + optionalDependencies: + react-dom: 19.1.0(react@19.1.0) + react-is@16.13.1: {} react@19.1.0: {} diff --git a/scripts/smart-search.mjs b/scripts/smart-search.mjs index 7f2ce2e2..80698ecc 100644 --- a/scripts/smart-search.mjs +++ b/scripts/smart-search.mjs @@ -1,11 +1,12 @@ -import { hash } from "node:crypto"; import { env, exit } from "node:process"; import { getTextContentFromMd } from "../src/lib/remark-parsing.mjs"; import { getAllDocMeta, getRawDocContent, getDocUriFromPath, + generateDocIdFromUri, } from "../src/lib/remote-mdx-files.mjs"; +import { smartSearchConfig } from "../src/lib/smart-search.mjs"; const { NEXT_PUBLIC_SEARCH_ENDPOINT: endpoint, @@ -24,6 +25,9 @@ async function main() { console.log("Docs Pages collected for indexing:", pages.length); await deleteOldDocs(); + + await setSearchConfig(); + await sendPagesToEndpoint(pages); } catch (error) { console.error("Error in smartSearchPlugin:", error); @@ -36,10 +40,10 @@ async function main() { * @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". + * @property {string} data.post_title //The title of the document. + * @property {string} data.post_content //The text content of the document. + * @property {string} data.post_url //A relative path to the document on the internet. + * @property {string} data.post_type // The type of content. Always "mdx_doc". * @returns Page[] */ async function collectPages() { @@ -53,16 +57,16 @@ async function collectPages() { const cleanedPath = getDocUriFromPath(entry.path); - const id = hash("sha-1", `mdx:${cleanedPath}`); + const id = generateDocIdFromUri(cleanedPath); pages.push({ id, data: { - title: parsedContent.data.matter.title, + post_title: parsedContent.data.matter.title, description: parsedContent.data.matter.description, - content: parsedContent.value, - path: cleanedPath, - content_type: "mdx_doc", + post_content: parsedContent.value, + post_url: cleanedPath, + post_type: "mdx_doc", }, }); } @@ -126,7 +130,7 @@ async function deleteOldDocs() { const response = await graphql({ query: queryDocuments, variables: { - query: 'content_type:"mdx_doc"', + query: 'post_type:"mdx_doc"', limit: 10, offset: totalCollected, }, @@ -208,4 +212,34 @@ async function sendPagesToEndpoint(pages) { } } +const searchConfigMutation = ` +mutation setSemanticSearchConfiguration($fields: [String!]!, $chunking: ChunkingConfig!) { + config { + semanticSearch(fields: $fields, chunking: $chunking) { + type + fields + chunking { + enabled + } + } + } +}`; + +async function setSearchConfig() { + const variables = { + fields: smartSearchConfig.fields, + chunking: smartSearchConfig.chunking, + }; + + try { + const response = await graphql({ query: searchConfigMutation, variables }); + console.log( + "Search configuration updated successfully.", + JSON.stringify(response.data.config.semanticSearch, undefined, 2), + ); + } catch (error) { + console.error("Error updating search configuration:", error); + } +} + await main(); diff --git a/src/components/blog-breadcrumbs.jsx b/src/components/blog-breadcrumbs.jsx index 989827a7..b1b6a6c2 100644 --- a/src/components/blog-breadcrumbs.jsx +++ b/src/components/blog-breadcrumbs.jsx @@ -1,4 +1,4 @@ -import { ChevronRightIcon } from "@heroicons/react/24/outline"; +import { HiOutlineChevronRight } from "react-icons/hi2"; import Link from "@/components/link"; export default function BlogBreadcrumbs({ currentPostTitle }) { @@ -13,7 +13,7 @@ export default function BlogBreadcrumbs({ currentPostTitle }) { > Blog - + diff --git a/src/components/doc-type-tag.jsx b/src/components/doc-type-tag.jsx new file mode 100644 index 00000000..f1d4aef8 --- /dev/null +++ b/src/components/doc-type-tag.jsx @@ -0,0 +1,33 @@ +import { classNames } from "@/utils/strings"; + +export default function DocTypeTag(type) { + if (!type || !type.type) { + return; + } + + const theType = type.type || type; + + const config = { + name: "Ext", + className: "bg-gray-500", + }; + + if (theType === "mdx_doc") { + config.name = "Doc"; + config.className = "bg-teal-800"; + } else if (theType === "post" || theType === "page") { + config.name = "Blog"; + config.className = "bg-purple-600"; + } + + return ( + + {config.name} + + ); +} diff --git a/src/components/docs-breadcrumbs.jsx b/src/components/docs-breadcrumbs.jsx index 1da32435..4069ee02 100644 --- a/src/components/docs-breadcrumbs.jsx +++ b/src/components/docs-breadcrumbs.jsx @@ -1,10 +1,10 @@ -import { ChevronRightIcon } from "@heroicons/react/24/outline"; import { useRouter } from "next/router"; +import { HiOutlineChevronRight } from "react-icons/hi2"; import Link from "@/components/link"; import { sendSelectItemEvent } from "@/lib/analytics.mjs"; -import { normalizeHref } from "@/utils/strings"; +import { classNames, normalizeHref } from "@/utils/strings"; -export default function DocsBreadcrumbs({ routes }) { +export default function DocsBreadcrumbs({ routes, className }) { const generateBreadcrumbs = (breadcrumbRoutes, currentRoute) => { const breadcrumbs = []; @@ -48,37 +48,40 @@ export default function DocsBreadcrumbs({ routes }) { } return ( -
-
- {breadcrumbLinks.map((breadcrumb, index) => - normalizeHref(breadcrumb.route) === normalizeHref(currentPath) ? ( - {breadcrumb.title} - ) : ( - { - sendSelectItemEvent({ - list: { id: "docs_breadcrumbs", name: "Docs Breadcrumbs" }, - item: { - item_id: breadcrumb.route, - item_name: breadcrumb.title, - item_category: "mdx_doc", - }, - }); - }} - > - {breadcrumb.title} - - - - - ), - )} -
-
+ ); } diff --git a/src/components/docs-layout.jsx b/src/components/docs-layout.jsx index bd55d506..9a852ec4 100644 --- a/src/components/docs-layout.jsx +++ b/src/components/docs-layout.jsx @@ -3,12 +3,12 @@ import { DisclosureButton, DisclosurePanel, } from "@headlessui/react"; -import { ChevronRightIcon, ChevronDownIcon } from "@heroicons/react/24/outline"; import { useRouter } from "next/router"; +import { HiOutlineChevronRight, HiOutlineChevronDown } from "react-icons/hi2"; import DocsBreadcrumbs from "./docs-breadcrumbs"; -import DocsPreviousNextLinks from "./docs-previous-next-link"; import OnThisPageNav from "./on-this-page-nav"; import DocsNav from "@/components/docs-nav"; +import Recommendations from "@/components/docs-recommendations"; import Link from "@/components/link"; import Seo from "@/components/seo"; import "rehype-callouts/theme/vitepress"; @@ -36,6 +36,7 @@ export default function DocumentPage({ children, docsNavData: routes, source: { frontmatter, scope }, + id, }) { const flatRoutes = flattenRoutes(routes); const { @@ -57,8 +58,8 @@ export default function DocumentPage({ className="sticky top-[84px] z-5 border-b-[1px] border-gray-800 bg-gray-900/80 backdrop-blur-xs md:hidden" > - - + + Menu @@ -93,17 +94,32 @@ export default function DocumentPage({ Edit this doc on GitHub -
- - {frontmatter?.title && ( -

{frontmatter.title}

- )} - {children} - -
+
+ +
+ {frontmatter?.title && ( +

{frontmatter.title}

+ )} + {children} +
+ + { + // This only puts recommendations on content pages and not on the main docs index or category pages + slug.length > 1 && ( + <> +
+ + + ) + } +
); diff --git a/src/components/docs-previous-next-link.jsx b/src/components/docs-previous-next-link.jsx deleted file mode 100644 index eaf42fce..00000000 --- a/src/components/docs-previous-next-link.jsx +++ /dev/null @@ -1,56 +0,0 @@ -import { ChevronLeftIcon, ChevronRightIcon } from "@heroicons/react/24/outline"; -import { useRouter } from "next/router"; -import Link from "@/components/link"; -import { normalizeHref } from "@/utils/strings"; - -export default function DocsPreviousNextLinks({ routes }) { - const router = useRouter(); - const currentPath = router.asPath; - - const currentIndex = routes.findIndex( - (route) => normalizeHref(route.route) === normalizeHref(currentPath), - ); - - if (currentIndex === -1) { - return; - } - - const previousPage = routes[currentIndex - 1]; - const nextPage = routes[currentIndex + 1]; - - return ( - - ); -} diff --git a/src/components/docs-recommendations.jsx b/src/components/docs-recommendations.jsx new file mode 100644 index 00000000..061520eb --- /dev/null +++ b/src/components/docs-recommendations.jsx @@ -0,0 +1,81 @@ +import { useCallback, useEffect, useState } from "react"; +import { HiOutlineArrowPath } from "react-icons/hi2"; +import { useInView } from "react-intersection-observer"; +import DocTypeTag from "./doc-type-tag"; +import Link from "./link"; +import { sendSelectItemEvent } from "@/lib/analytics.mjs"; + +export default function DocsRecommended({ docID, count = 5 }) { + const [recommendations, setRecommendations] = useState([]); + const [loading, setLoading] = useState(true); + + const { ref, inView } = useInView({ + /* Optional options */ + threshold: 0.1, + triggerOnce: true, // Fetch only once when the component comes into view + }); + const fetchRecommendations = useCallback(async () => { + try { + const response = await fetch( + `/api/recommend/?docID=${docID}&count=${count}`, + ); + if (!response.ok) { + console.error("Failed to fetch recommendations:", response.statusText); + return; + } + + const data = await response.json(); + setRecommendations(data); + } catch (error) { + console.error("Error fetching recommendations:", error); + } finally { + setLoading(false); + } + }, [docID, count]); + + useEffect(() => { + if (!inView) return; + fetchRecommendations(); + }, [inView, fetchRecommendations]); + + return ( + + ); +} diff --git a/src/components/header.jsx b/src/components/header.jsx index 1dca442b..dbf5854a 100644 --- a/src/components/header.jsx +++ b/src/components/header.jsx @@ -1,4 +1,4 @@ -import { SiDiscord, SiGithub } from "@icons-pack/react-simple-icons"; +import { SiDiscord, SiGithub } from "react-icons/si"; import FaustLogo from "./faust-logo"; import PrimaryNav from "./primary-nav"; import Search from "./search/search"; diff --git a/src/components/heading.jsx b/src/components/heading.jsx index 852d9df8..ff9fb094 100644 --- a/src/components/heading.jsx +++ b/src/components/heading.jsx @@ -1,4 +1,4 @@ -import { LinkIcon } from "@heroicons/react/24/outline"; +import { HiOutlineLink } from "react-icons/hi2"; import Link from "./link"; // Custom heading component with clickable anchor links @@ -16,7 +16,7 @@ export default function Heading({ level, children, id, ...props }) { noDefaultStyles > {children} - + ); diff --git a/src/components/link.jsx b/src/components/link.jsx index 9ea0ee6f..60201f4c 100644 --- a/src/components/link.jsx +++ b/src/components/link.jsx @@ -1,7 +1,7 @@ -import { ArrowTopRightOnSquareIcon } from "@heroicons/react/24/outline"; import Link from "next/link"; import { useRouter } from "next/router"; import { forwardRef } from "react"; +import { HiOutlineArrowTopRightOnSquare } from "react-icons/hi2"; import { classNames } from "@/utils/strings"; const CustomLink = forwardRef( @@ -41,7 +41,7 @@ const CustomLink = forwardRef( {children} {!disableExternalIcon && ( - + )} diff --git a/src/components/primary-nav.jsx b/src/components/primary-nav.jsx index 0eac614b..faba1ae2 100644 --- a/src/components/primary-nav.jsx +++ b/src/components/primary-nav.jsx @@ -4,8 +4,8 @@ import { PopoverPanel, CloseButton, } from "@headlessui/react"; -import { Bars3Icon, XMarkIcon } from "@heroicons/react/24/outline"; import { forwardRef } from "react"; +import { HiOutlineBars3, HiOutlineXMark } from "react-icons/hi2"; import Link from "@/components/link"; import { sendSelectItemEvent } from "@/lib/analytics.mjs"; import { classNames } from "@/utils/strings"; @@ -92,9 +92,9 @@ export default function PrimaryMenu({ className }) { Open main nav - + Open main nav - + - + {inputValue.length > 0 && ( @@ -129,7 +129,7 @@ export default function SearchBar() { > {isLoading ? (
- + Searching
) : ( @@ -143,7 +143,7 @@ export default function SearchBar() { name: "Search Results", }, item: { - item_id: item.path, + item_id: item.href, item_name: item.title, item_category: item.type, }, diff --git a/src/components/search/search-results-list.jsx b/src/components/search/search-results-list.jsx index f16fb4cc..84cf3d2d 100644 --- a/src/components/search/search-results-list.jsx +++ b/src/components/search/search-results-list.jsx @@ -1,10 +1,11 @@ +import DocTypeTag from "@/components/doc-type-tag"; import Link from "@/components/link"; export default function SearchResults({ items, onSelectItem }) { return (
    {items.map((item) => { - if (!item?.id || !item?.title || !item?.path) { + if (!item?.id || !item?.title || !item?.href) { console.warn("Invalid item in search results:", item); return; } @@ -12,19 +13,17 @@ export default function SearchResults({ items, onSelectItem }) { return (
  • { onSelectItem(item); }} className="flex w-full cursor-pointer items-center justify-between" > {item.title} - - {item.type === "mdx_doc" ? "Doc" : "Blog"} - +
  • ); diff --git a/src/components/search/search.jsx b/src/components/search/search.jsx index 4d99d010..bcab3678 100644 --- a/src/components/search/search.jsx +++ b/src/components/search/search.jsx @@ -1,4 +1,4 @@ -import { MagnifyingGlassIcon } from "@heroicons/react/24/outline"; +import { HiOutlineMagnifyingGlass } from "react-icons/hi2"; import { useSearch } from "./state"; import { isBrowser } from "@/utils/booleans"; @@ -16,7 +16,7 @@ export default function Search() { onClick={() => dialog.current?.showModal()} > Open search - + Search docs... diff --git a/src/lib/remote-mdx-files.mjs b/src/lib/remote-mdx-files.mjs index e7c00106..6e86a100 100644 --- a/src/lib/remote-mdx-files.mjs +++ b/src/lib/remote-mdx-files.mjs @@ -1,3 +1,4 @@ +import { hash } from "node:crypto"; import path from "node:path"; import { env } from "node:process"; import { Octokit } from "@octokit/core"; @@ -158,3 +159,13 @@ export async function getParsedDoc(slug) { return getSerializedContextFromMd(content, slug); } + +/** + * Generates Document ID from a URI + * + * @param {string} uri + * @returns {string} + */ +export function generateDocIdFromUri(uri) { + return `mdx:${hash("sha-1", uri)}`; +} diff --git a/src/lib/smart-search.mjs b/src/lib/smart-search.mjs new file mode 100644 index 00000000..7439492a --- /dev/null +++ b/src/lib/smart-search.mjs @@ -0,0 +1,55 @@ +import { URL } from "node:url"; + +function cleanPath(filePath) { + return ( + filePath + .replace(/^\/?src\/pages/, "") + .replace(/^\/?pages/, "") + .replace(/\/index\.mdx$/, "") + .replace(/\.mdx$/, "") || "/" + ); +} + +export function normalizeSmartSearchResponse(results) { + if (!results || !Array.isArray(results)) { + throw new TypeError("An array of results was expected"); + } + + return results.map((result) => { + const { id, data } = result; + // console.log("Data:", result); + switch (data.post_type) { + case "mdx_doc": { + const path = data.post_url ? cleanPath(data.post_url) : "/"; + + return { + id, + title: data.post_title, + href: path, + type: data.post_type, + }; + } + + case "post": + case "page": { + return { + id, + title: data.post_title, + href: new URL(data.post_url).pathname, + type: data.post_type, + }; + } + + default: { + throw new TypeError(`Unknown content type: ${data.post_type}`); + } + } + }); +} + +export const smartSearchConfig = { + fields: ["post_title", "post_content"], + chunking: { + enabled: true, + }, +}; diff --git a/src/pages/api/recommend.js b/src/pages/api/recommend.js new file mode 100644 index 00000000..f963a8d9 --- /dev/null +++ b/src/pages/api/recommend.js @@ -0,0 +1,71 @@ +import process from "node:process"; +import { ReasonPhrases, StatusCodes } from "http-status-codes"; +import { normalizeSmartSearchResponse } from "@/lib/smart-search.mjs"; + +export default async function handler(req, res) { + const endpoint = process.env.NEXT_PUBLIC_SEARCH_ENDPOINT; + const accessToken = process.env.NEXT_SEARCH_ACCESS_TOKEN; + const { docID, count } = req.query; + + if (req.method !== "GET") { + return res + .status(StatusCodes.METHOD_NOT_ALLOWED) + .json({ error: ReasonPhrases.METHOD_NOT_ALLOWED }); + } + + if (!docID) { + return res + .status(StatusCodes.BAD_REQUEST) + .json({ error: "Document ID (docID) is required." }); + } + + const graphqlQuery = ` + query RelatedDocuments($docID: String!, $count: Int = 3) { + recommendations(count: $count) { + documents: relatedDocuments(docID: $docID, minScore: 0.7) { + id: docID + data: source + score + } + } + }`; + + try { + const response = await fetch(endpoint, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${accessToken}`, + }, + body: JSON.stringify({ + query: graphqlQuery, + variables: { docID, count }, + }), + }); + + if (!response.ok) { + return res + .status(StatusCodes.SERVICE_UNAVAILABLE) + .json({ error: ReasonPhrases.SERVICE_UNAVAILABLE }); + } + + const result = await response.json(); + + if (result.errors) { + return res + .status(StatusCodes.INTERNAL_SERVER_ERROR) + .json({ errors: result.errors }); + } + + return res + .status(StatusCodes.OK) + .json( + normalizeSmartSearchResponse(result.data.recommendations.documents), + ); + } catch (error) { + console.error("Error fetching search data:", error); + return res + .status(StatusCodes.Inter) + .json({ error: ReasonPhrases.INTERNAL_SERVER_ERROR }); + } +} diff --git a/src/pages/api/search.js b/src/pages/api/search.js index fdf11122..598aa58f 100644 --- a/src/pages/api/search.js +++ b/src/pages/api/search.js @@ -1,15 +1,6 @@ import process from "node:process"; import { ReasonPhrases, StatusCodes } from "http-status-codes"; - -function cleanPath(filePath) { - return ( - filePath - .replace(/^\/?src\/pages/, "") - .replace(/^\/?pages/, "") - .replace(/\/index\.mdx$/, "") - .replace(/\.mdx$/, "") || "/" - ); -} +import { normalizeSmartSearchResponse } from "@/lib/smart-search.mjs"; export default async function handler(req, res) { const endpoint = process.env.NEXT_PUBLIC_SEARCH_ENDPOINT; @@ -30,7 +21,13 @@ export default async function handler(req, res) { const graphqlQuery = ` query FindDocuments($query: String!) { - find(query: $query) { + find( + query: $query + semanticSearch: { + searchBias: 5, + fields: ["post_title", "post_content"] + } + ) { total documents { id @@ -67,51 +64,13 @@ export default async function handler(req, res) { .json({ errors: result.errors }); } - const seenIds = new Set(); - const formattedResults = []; - - for (const content of result.data.find.documents) { - const contentType = - content.data.content_type || content.data.post_type || "mdx_doc"; - - let item = {}; - - if (contentType === "mdx_doc" && content.data.title) { - const path = content.data.path ? cleanPath(content.data.path) : "/"; - item = { - id: content.id, - title: content.data.title, - path, - type: "mdx_doc", - }; - } - - if ( - (contentType === "wp_post" || contentType === "post") && - content.data.post_title && - content.data.post_name - ) { - item = { - id: content.id, - title: content.data.post_title, - path: `/blog/${content.data.post_name}`, - type: "post", - }; - } - - if (seenIds.has(item.id)) { - continue; - } - - seenIds.add(item.id); - formattedResults.push(item); - } - - return res.status(StatusCodes.OK).json(formattedResults); + return res + .status(StatusCodes.OK) + .json(normalizeSmartSearchResponse(result.data.find.documents)); } catch (error) { console.error("Error fetching search data:", error); return res - .status(StatusCodes.Inter) + .status(StatusCodes.INTERNAL_SERVER_ERROR) .json({ error: ReasonPhrases.INTERNAL_SERVER_ERROR }); } } diff --git a/src/pages/blog/index.jsx b/src/pages/blog/index.jsx index 9a7a49e7..62b0ec20 100644 --- a/src/pages/blog/index.jsx +++ b/src/pages/blog/index.jsx @@ -1,8 +1,8 @@ import { gql, useQuery } from "@apollo/client"; import { getNextStaticProps } from "@faustwp/core"; -import { ArrowPathIcon, ChevronDownIcon } from "@heroicons/react/24/outline"; import Link from "next/link"; import { useState } from "react"; +import { HiOutlineArrowPath, HiOutlineChevronDown } from "react-icons/hi2"; import Card from "@/components/card"; import Date from "@/components/date"; import Seo from "@/components/seo"; @@ -45,7 +45,7 @@ export default function BlogIndex() { if (loading && !data) return (
    - Loading + Loading
    ); @@ -129,11 +129,11 @@ const LoadMoreButton = ({ onClick }) => { > {loading ? ( <> - Loading + Loading ) : ( <> - Load more + Load more )} diff --git a/src/pages/docs/[[...slug]].jsx b/src/pages/docs/[[...slug]].jsx index 97cea921..2a654ddf 100644 --- a/src/pages/docs/[[...slug]].jsx +++ b/src/pages/docs/[[...slug]].jsx @@ -1,6 +1,11 @@ +import path from "node:path"; import { MDXClient } from "next-mdx-remote-client"; import { useMDXComponents } from "@/components/mdx-components"; -import { getParsedDoc, getDocsNav } from "@/lib/remote-mdx-files.mjs"; +import { + getParsedDoc, + getDocsNav, + generateDocIdFromUri, +} from "@/lib/remote-mdx-files.mjs"; export default function Doc({ source }) { return ; @@ -13,6 +18,11 @@ export async function getStaticProps({ params }) { return { props: { + id: generateDocIdFromUri( + params.slug?.length > 1 + ? path.join("/docs", ...params.slug, "/") + : "/docs/", + ), source, docsNavData, }, diff --git a/src/pages/index.jsx b/src/pages/index.jsx index e603c53d..8ff678dd 100644 --- a/src/pages/index.jsx +++ b/src/pages/index.jsx @@ -1,11 +1,11 @@ import { - ArrowTopRightOnSquareIcon, - ChevronRightIcon, - CodeBracketIcon, - CursorArrowRaysIcon, - KeyIcon, - RectangleGroupIcon, -} from "@heroicons/react/24/outline"; + HiOutlineArrowTopRightOnSquare, + HiOutlineChevronRight, + HiOutlineCodeBracket, + HiOutlineCursorArrowRays, + HiOutlineKey, + HiOutlineRectangleGroup, +} from "react-icons/hi2"; import Card from "@/components/card"; import Link from "@/components/link"; import Seo from "@/components/seo"; @@ -35,7 +35,7 @@ export default function Index() { noDefaultStyles > Read the Docs - @@ -47,7 +47,7 @@ export default function Index() { noDefaultStyles > Join the Discord - @@ -67,7 +67,7 @@ export default function Index() { className="bg-blue-1100/20 col-span-full flex flex-col overflow-hidden rounded-2xl p-4 ring-1 ring-blue-500/10 md:col-span-6 md:p-6 lg:col-span-7 lg:p-8" >
    - +

    Authentication

    @@ -80,7 +80,7 @@ export default function Index() { className="bg-blue-1100/20 col-span-full flex flex-col overflow-hidden rounded-2xl p-4 ring-1 ring-blue-500/10 md:col-span-6 md:p-6 lg:col-span-5 lg:p-8" >

    - +

    Post previews

    @@ -94,7 +94,7 @@ export default function Index() { className="bg-blue-1100/20 col-span-full flex flex-col overflow-hidden rounded-2xl p-4 ring-1 ring-blue-500/10 md:col-span-6 md:p-6 lg:col-span-5 lg:p-8" >

    - +

    Template hierarchy

    @@ -115,7 +115,7 @@ export default function Index() { className="bg-blue-1100/20 col-span-full flex flex-col overflow-hidden rounded-2xl p-4 ring-1 ring-blue-500/10 md:col-span-6 md:p-6 lg:col-span-7 lg:p-8" >

    - +

    Block editor support

    @@ -135,7 +135,7 @@ export default function Index() { noDefaultStyles > Get Started - diff --git a/src/pages/showcase/index.jsx b/src/pages/showcase/index.jsx index 5627a1a6..581bf770 100644 --- a/src/pages/showcase/index.jsx +++ b/src/pages/showcase/index.jsx @@ -1,5 +1,5 @@ -import { ArrowTopRightOnSquareIcon } from "@heroicons/react/24/outline"; import Image from "next/image"; +import { HiOutlineArrowTopRightOnSquare } from "react-icons/hi2"; import Link from "@/components/link"; import Seo from "@/components/seo"; @@ -63,7 +63,7 @@ export default function Showcase() { {/* Applying solid blue background with opacity and making it fit the card width */}

    {showcase.title} - +
    ))}