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}
-
-
-
-
- ),
- )}
-
-
+
+ {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 (
-
- {previousPage && (
-
- Previous
-
-
-
- {previousPage.title}
-
- )}
- {nextPage && (
-
- Next
- {nextPage.title}
-
-
-
-
- )}
-
- );
-}
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 ? (
) : (
@@ -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 (
);
@@ -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 */}
))}