diff --git a/eslint.config.mjs b/eslint.config.mjs index fb3677e2..bb8ea130 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -43,6 +43,7 @@ const config = [ ...unicorn.configs["flat/recommended"].rules, // neon disables a lot of unicorn rules so this reenables defaults "react/no-danger": "warn", "react/jsx-sort-props": "off", + "@stylistic/jsx/jsx-sort-props": "off", "unicorn/prevent-abbreviations": [ "error", { diff --git a/package.json b/package.json index 0ceca381..3a8373d8 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "downshift": "^9.0.8", "graphql": "^16.8.1", "html-to-text": "^9.0.5", + "http-status-codes": "^2.3.0", "lodash.debounce": "^4.0.8", "next": "^15.0.1", "react": "^18.3.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c767a070..94b8211a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -58,6 +58,9 @@ importers: html-to-text: specifier: ^9.0.5 version: 9.0.5 + http-status-codes: + specifier: ^2.3.0 + version: 2.3.0 lodash.debounce: specifier: ^4.0.8 version: 4.0.8 @@ -4874,6 +4877,12 @@ packages: integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==, } + http-status-codes@2.3.0: + resolution: + { + integrity: sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==, + } + human-signals@5.0.0: resolution: { @@ -12139,6 +12148,8 @@ snapshots: domutils: 3.1.0 entities: 4.5.0 + http-status-codes@2.3.0: {} + human-signals@5.0.0: {} husky@9.1.7: {} diff --git a/src/lib/gtag.js b/src/lib/gtag.js index 4c92896e..4f66ef29 100644 --- a/src/lib/gtag.js +++ b/src/lib/gtag.js @@ -1,9 +1,10 @@ -// eslint-disable-next-line no-restricted-globals, n/prefer-global/process +import process from "process"; // eslint-disable-line unicorn/prefer-node-protocol + export const GA_TRACKING_ID = process.env.NEXT_PUBLIC_GOOGLE_ANALYTICS_KEY; // https://developers.google.com/analytics/devguides/collection/gtagjs/pages export const logPageview = (url) => { - window.gtag("config", GA_TRACKING_ID, { + globalThis.gtag("config", GA_TRACKING_ID, { page_path: url, }); }; diff --git a/src/lib/smart-search-plugin.mjs b/src/lib/smart-search-plugin.mjs index 6e7943aa..bc4047c6 100644 --- a/src/lib/smart-search-plugin.mjs +++ b/src/lib/smart-search-plugin.mjs @@ -1,11 +1,12 @@ +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"; -function smartSearchPlugin({ endpoint, accessToken }) { - let isPluginExecuted = false; +let isPluginExecuted = false; +function smartSearchPlugin({ endpoint, accessToken }) { return { apply: (compiler) => { compiler.hooks.done.tapPromise("SmartSearchPlugin", async () => { @@ -25,8 +26,8 @@ function smartSearchPlugin({ endpoint, accessToken }) { console.log("Docs Pages collected for indexing:", pages.length); - await deleteExistingDocs(endpoint, accessToken); - await sendPagesToEndpoint(pages, endpoint, accessToken); + await deleteOldDocs({ endpoint, accessToken }, pages); + await sendPagesToEndpoint({ endpoint, accessToken }, pages); } catch (error) { console.error("Error in smartSearchPlugin:", error); } @@ -54,12 +55,9 @@ async function collectPages(directory) { let metadata = {}; - if ( - metadataMatch && - metadataMatch.groups && - metadataMatch.groups.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); @@ -74,9 +72,7 @@ async function collectPages(directory) { const cleanedPath = cleanPath(entryPath); - const id = `mdx:${cleanedPath}`; - - console.log(`Indexing document with ID: ${id}, path: ${cleanedPath}`); + const id = hash("sha-1", `mdx:${cleanedPath}`); pages.push({ id, @@ -105,16 +101,28 @@ function cleanPath(filePath) { ); } -async function deleteExistingDocs(endpoint, accessToken) { - const queryDocuments = ` - query FindDocumentsToDelete($query: String!) { - find(query: $query) { - documents { - id - } +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"', @@ -136,30 +144,25 @@ async function deleteExistingDocs(endpoint, accessToken) { const result = await response.json(); if (result.errors) { - console.error("Error fetching documents to delete:", result.errors); + console.error("Error fetching existing documents:", result.errors); return; } - const documentsToDelete = result.data.find.documents; + const existingIndexedDocuments = new Set( + result.data.find.documents.map((doc) => doc.id), + ); + + const documentsToDelete = + existingIndexedDocuments.difference(currentMdxDocuments); - if (!documentsToDelete || documentsToDelete.length === 0) { + if (documentsToDelete?.size === 0) { console.log("No documents to delete."); return; } - for (const doc of documentsToDelete) { - const deleteMutation = ` - mutation DeleteDocument($id: ID!) { - delete(id: $id) { - code - message - success - } - } - `; - + for (const doc of documentsToDelete.values()) { const variablesForDelete = { - id: doc.id, + id: doc, }; try { @@ -179,14 +182,11 @@ async function deleteExistingDocs(endpoint, accessToken) { if (deleteResult.errors) { console.error( - `Error deleting document ID ${doc.id}:`, + `Error deleting document ID ${doc}:`, deleteResult.errors, ); } else { - console.log( - `Deleted document ID ${doc.id}:`, - deleteResult.data.delete, - ); + console.log(`Deleted document ID ${doc}:`, deleteResult.data.delete); } } catch (error) { console.error(`Network error deleting document ID ${doc.id}:`, error); @@ -198,18 +198,17 @@ async function deleteExistingDocs(endpoint, accessToken) { } const bulkIndexQuery = ` - mutation BulkIndex($documents: [DocumentInput!]!) { - bulkIndex(input: { documents: $documents }) { + mutation BulkIndex($input: BulkIndexInput!) { + bulkIndex(input: $input) { code documents { id - data } } } `; -async function sendPagesToEndpoint(pages, endpoint, accessToken) { +async function sendPagesToEndpoint({ endpoint, accessToken }, pages) { if (pages.length === 0) { console.warn("No documents found for indexing."); return; @@ -220,7 +219,7 @@ async function sendPagesToEndpoint(pages, endpoint, accessToken) { data: page.data, })); - const variables = { documents }; + const variables = { input: { documents } }; try { const response = await fetch(endpoint, { diff --git a/src/pages/_app.jsx b/src/pages/_app.jsx index 8b27e39c..0ee2f89b 100644 --- a/src/pages/_app.jsx +++ b/src/pages/_app.jsx @@ -2,19 +2,19 @@ import { WordPressBlocksProvider } from "@faustwp/blocks"; import { FaustProvider } from "@faustwp/core"; import { useRouter } from "next/router"; import { useEffect } from "react"; +import Layout from "@/components/layout"; +import { logPageview } from "@/lib/gtag.js"; +import blocks from "@/wp-blocks"; import "../../faust.config"; import "./global.css"; -import Layout from "@/components/layout"; import "@faustwp/core/dist/css/toolbar.css"; -import * as gtag from "@/lib/gtag"; -import blocks from "@/wp-blocks"; export default function MyApp({ Component, pageProps }) { const router = useRouter(); // Record a Google Analytics pageview on route change useEffect(() => { - const handleRouteChange = (url) => gtag.logPageview(url); + const handleRouteChange = (url) => logPageview(url); router.events.on("routeChangeComplete", handleRouteChange); return () => { diff --git a/src/pages/api/search.js b/src/pages/api/search.js index 854289d8..fdf11122 100644 --- a/src/pages/api/search.js +++ b/src/pages/api/search.js @@ -1,4 +1,5 @@ import process from "node:process"; +import { ReasonPhrases, StatusCodes } from "http-status-codes"; function cleanPath(filePath) { return ( @@ -15,8 +16,16 @@ export default async function handler(req, res) { const accessToken = process.env.NEXT_SEARCH_ACCESS_TOKEN; const { query } = req.query; + if (req.method !== "GET") { + return res + .status(StatusCodes.METHOD_NOT_ALLOWED) + .json({ error: ReasonPhrases.METHOD_NOT_ALLOWED }); + } + if (!query) { - return res.status(400).json({ error: "Search query is required." }); + return res + .status(StatusCodes.BAD_REQUEST) + .json({ error: "Search query is required." }); } const graphqlQuery = ` @@ -44,56 +53,65 @@ export default async function handler(req, res) { }), }); + 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(500).json({ errors: result.errors }); + return res + .status(StatusCodes.INTERNAL_SERVER_ERROR) + .json({ errors: result.errors }); } - const formattedResults = result.data.find.documents - .map((content) => { - const contentType = - content.data.content_type || content.data.post_type || "mdx_doc"; - - if (contentType === "mdx_doc" && content.data.title) { - const path = content.data.path ? cleanPath(content.data.path) : "/"; - return { - 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 - ) { - return { - id: content.id, - title: content.data.post_title, - path: `/blog/${content.data.post_name}`, - type: "post", - }; - } - - return null; - }) - .filter((item) => item !== null); - const seenIds = new Set(); - const uniqueResults = formattedResults.filter((item) => { + 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)) { - return false; + continue; } + seenIds.add(item.id); - return true; - }); + formattedResults.push(item); + } - return res.status(200).json(uniqueResults); + return res.status(StatusCodes.OK).json(formattedResults); } catch (error) { console.error("Error fetching search data:", error); - return res.status(500).json({ error: "Internal server error" }); + return res + .status(StatusCodes.Inter) + .json({ error: ReasonPhrases.INTERNAL_SERVER_ERROR }); } } diff --git a/src/wp-templates/index-template.jsx b/src/wp-templates/index-template.jsx index 8a7fa187..a529ad4c 100644 --- a/src/wp-templates/index-template.jsx +++ b/src/wp-templates/index-template.jsx @@ -46,11 +46,6 @@ export default function IndexTemplate() { childrenKey: "innerBlocks", }); - console.log({ - editorBlocks, - blockList, - }); - return ( <> diff --git a/src/wp-templates/page.jsx b/src/wp-templates/page.jsx index 2ec04005..04e2be87 100644 --- a/src/wp-templates/page.jsx +++ b/src/wp-templates/page.jsx @@ -8,6 +8,7 @@ export default function Component(props) {