Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
"cSpell.words": [
"faustwp",
"Kevinbatdorf",
"pagefind",
"postbuild",
"sindresorhus",
"stylesheet",
"tailwindcss"
Expand Down
99 changes: 62 additions & 37 deletions components/SearchBar.js
Original file line number Diff line number Diff line change
@@ -1,62 +1,73 @@
import React, { useState, useEffect } from "react";
import { gql, useQuery } from "@apollo/client";
import React, { useState, useEffect, useRef } from "react";
import { MagnifyingGlassIcon } from "@heroicons/react/24/solid";
import { MDXProvider } from "@mdx-js/react"; // For rendering MDX

const DOC_SEARCH_QUERY = gql`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We will still need out search to cover blog posts, yeah? Should we setup search to do both?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm, we could or should we setup Smart Search for the WP side of things?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can decide this after our meeting tomorrow.

query DOC_SEARCH_QUERY($searchTerm: String!) {
contentNodes(where: { search: $searchTerm }) {
nodes {
... on Doc {
title
id
uri
}
}
// Function to call your Lunr-based search API
async function performSearch(query) {
try {
const res = await fetch(`/api/search?query=${encodeURIComponent(query)}`);

if (!res.ok) {
throw new Error(`Error: ${res.status} - ${res.statusText}`);
}

const data = await res.json();
return data;
} catch (error) {
console.error("An error occurred while performing the search:", error);
return [];
}
`;
}

export default function SearchBar() {
const [isOpen, setIsOpen] = useState(false);
const [query, setQuery] = useState("");
const [results, setResults] = useState([]);

const { loading, error, data } = useQuery(DOC_SEARCH_QUERY, {
variables: { searchTerm: query },
skip: !query,
});

useEffect(() => {
if (data && data.contentNodes) {
setResults(data.contentNodes.nodes);
}
}, [data]);
const modalRef = useRef(null);

const openModal = () => {
setIsOpen(true);
};

const closeModal = () => {
setIsOpen(false);
setQuery("");
setResults([]);
};

const handleKeyDown = (event) => {
if (event.metaKey && event.key === "k") {
event.preventDefault();
openModal();
}
if (event.key === "Escape") {
closeModal();
}
};

const handleClickOutside = (event) => {
if (modalRef.current && !modalRef.current.contains(event.target)) {
closeModal();
}
};

useEffect(() => {
document.addEventListener("keydown", handleKeyDown);
document.addEventListener("mousedown", handleClickOutside);

return () => {
document.removeEventListener("keydown", handleKeyDown);
document.removeEventListener("mousedown", handleClickOutside);
};
}, []);

async function handleSearch() {
if (query) {
const searchResults = await performSearch(query);
setResults(searchResults);
}
}

useEffect(() => {
handleSearch();
}, [query]);

return (
<>
<button
Expand All @@ -74,8 +85,11 @@ export default function SearchBar() {
</button>

{isOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50">
<div className="relative w-full max-w-3xl rounded-lg bg-gray-900 p-6 shadow-lg">
<div className="bg-black fixed inset-0 z-50 flex items-center justify-center bg-opacity-50">
<div
className="relative w-full max-w-3xl rounded-lg bg-gray-900 p-6 shadow-lg"
ref={modalRef}
>
<button
className="absolute right-4 top-4 rounded-md bg-gray-800 px-2 py-1 text-xs text-gray-400 hover:bg-gray-700"
onClick={closeModal}
Expand All @@ -94,14 +108,9 @@ export default function SearchBar() {
<MagnifyingGlassIcon className="absolute left-3 top-3 h-5 w-5 text-gray-400" />
</div>
<div id="searchResults" className="mt-4 max-h-96 overflow-y-auto">
{loading && <p>Loading...</p>}
{error && <p>Error: {error.message}</p>}
{results.length === 0 && query && <p>No results found.</p>}
{results.map((result) => (
<div key={result.id} className="border-b border-gray-700 p-2">
<a href={result.uri} className="text-white hover:underline">
{result.title}
</a>
</div>
<Result key={result.ref} result={result} />
))}
</div>
</div>
Expand All @@ -110,3 +119,19 @@ export default function SearchBar() {
</>
);
}

// Helper component to display search results with MDX content
function Result({ result }) {
return (
<a
href={`${result.path}`} // Adjust this to match your desired URL structure
className="block text-white hover:underline"
>
<h3>{result.title}</h3>
{/* Render the excerpt as MDX */}
<MDXProvider>
<div>{result.excerpt}</div>
</MDXProvider>
</a>
);
}
1 change: 1 addition & 0 deletions globalStylesheet.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

92 changes: 92 additions & 0 deletions lib/search.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
const lunr = require("lunr");
const fs = require("node:fs");
const path = require("node:path");

// Helper function to recursively get all markdown files
function getAllMarkdownFiles(dirPath, arrayOfFiles) {
const files = fs.readdirSync(dirPath);
arrayOfFiles = arrayOfFiles || [];

files.forEach(function (file) {
const filePath = path.join(dirPath, file);
const stats = fs.statSync(filePath);

if (stats.isDirectory()) {
// If it's a directory, recurse into it
arrayOfFiles = getAllMarkdownFiles(filePath, arrayOfFiles);
} else if (filePath.endsWith(".mdx") || filePath.endsWith(".md")) {
// If it's a markdown file, add it to the array
arrayOfFiles.push(filePath);
}
});

return arrayOfFiles;
}

// Custom function to extract metadata
function extractMetadata(fileContent) {
const metadataRegex = /export const metadata = ({[\s\S]*?});/;
const match = fileContent.match(metadataRegex);

if (match) {
try {
const metadata = eval("(" + match[1] + ")");
return metadata;
} catch (err) {
console.error("Error parsing metadata:", err);
}
}

return {}; // Return empty object if no metadata is found
}

function createIndex() {
const docsDirectory = path.join(process.cwd(), "pages", "docs");

// Get all markdown files, including those in subdirectories
const markdownFiles = getAllMarkdownFiles(docsDirectory);

// Store documents separately
const documents = [];

const idx = lunr(function () {
this.ref("id");
this.field("title");
this.field("body");

markdownFiles.forEach((filePath, id) => {
const fileContent = fs.readFileSync(filePath, "utf-8");
const metadata = extractMetadata(fileContent);
const content = fileContent
.replace(/export const metadata = ({[\s\S]*?});/, "")
.trim(); // Remove metadata section

// Remove process root and "pages" from the file path
let relativePath = filePath.replace(process.cwd(), "");

// Ensure we only replace the first occurrence of "/pages/docs"
relativePath = relativePath.replace("/pages/docs", "/docs");

// Remove the file extension
relativePath = relativePath.replace(/\.mdx?$/, "");

// Remove trailing '/index' if present for clean URLs
relativePath = relativePath.replace(/\/index$/, "");

// Construct the document object
const doc = {
id: id.toString(),
title: metadata.title || path.basename(filePath),
body: content,
path: relativePath, // Corrected path for Next.js
};

documents.push(doc); // Add document to the store
this.add(doc); // Add document to the index
});
});

return { idx, documents };
}

module.exports = { createIndex };
7 changes: 6 additions & 1 deletion next.config.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
const { withFaust, getWpHostname } = require("@faustwp/core");
const { createSecureHeaders } = require("next-secure-headers");
const withMDX = require("@next/mdx")();
const withMDX = require("@next/mdx")({
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm guessing this is needed. But can you articulate why? I'm guessing its unrelated to the specifics of the PR. Maybe we should separate this out.

extension: /\.mdx?$/,
options: {
providerImportSource: "@mdx-js/react", // Enables MDX context components
},
});

/**
* @type {import('next').NextConfig}
Expand Down
Loading