From 3746587d5ecf639fb9ed75343aaf8f5b3d71812e Mon Sep 17 00:00:00 2001 From: Mqdd Date: Wed, 3 Dec 2025 14:01:39 +0700 Subject: Initial commit (done showing product based on keywords) --- src/pages/searchkey/[slug].jsx | 89 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 src/pages/searchkey/[slug].jsx diff --git a/src/pages/searchkey/[slug].jsx b/src/pages/searchkey/[slug].jsx new file mode 100644 index 00000000..f09520f4 --- /dev/null +++ b/src/pages/searchkey/[slug].jsx @@ -0,0 +1,89 @@ +import axios from 'axios'; +import { useRouter } from 'next/router'; +import { useEffect, useState } from 'react'; +import Seo from '@/core/components/Seo'; +import dynamic from 'next/dynamic'; +import { capitalizeEachWord } from '../../utils/capializeFIrstWord'; + +const BasicLayout = dynamic(() => + import('@/core/components/layouts/BasicLayout') +); +const ProductSearch = dynamic(() => + import('@/lib/product/components/ProductSearch') +); + +// const BASE_URL = process.env.NEXT_PUBLIC_SELF_HOST; +const BASE_URL = 'https://indoteknik.com'; + +export default function KeywordPage() { + const router = useRouter(); + const [result, setResult] = useState(null); + const [query, setQuery] = useState(null); + + // Ambil slug dari URL dinamis + const keywordSlug = router?.query?.slug || ''; + const keyword = keywordSlug.replace(/-/g, ' ').toLowerCase(); + const url = BASE_URL + router.asPath.split('?')[0]; + const slugTitle = capitalizeEachWord(keyword); + + // Fetch info dari Solr index "url_category_brand" + const getUrls = async (url) => { + try { + const response = await axios( + `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/url-category_brand?url=${url}` + ); + const data = response?.data?.response?.docs[0] || null; + setResult(data); + console.log('[πŸ” result from API]', data); // Tambahin ini + } catch (error) { + console.error('Error fetching data:', error); + } + }; + + // Panggil fetch setelah router siap + useEffect(() => { + if (router.isReady) { + getUrls(url); + } + }, [router.isReady]); + + // Jika Solr index ditemukan, siapkan parameter pencarian + useEffect(() => { + if (result) { + const fq = `category_parent_ids:${result.category_id_i} AND manufacture_id_i:${result.brand_id_i}`; + const q = keyword || '*:*'; // keyword dari URL + console.log('SOLR QUERY:', { q, fq }); + setQuery({ fq, q }); + } + }, [result, keyword]); + + if (!result) { + return ( + + +
+

Produk tidak ditemukan berdasarkan keyword

+
+
+ ); + } + + return ( + + + {query && } + + ); +} -- cgit v1.2.3 From 9ca4e764383ffc3800fbe899dd7e07c297c51e75 Mon Sep 17 00:00:00 2001 From: Mqdd Date: Fri, 5 Dec 2025 10:50:31 +0700 Subject: fix query --- src/pages/api/shop/search.js | 41 ++++++++++++++++++++++++++++++++++ src/pages/searchkey/[slug].jsx | 50 +++++++++++++++++++++++++++++------------- 2 files changed, 76 insertions(+), 15 deletions(-) diff --git a/src/pages/api/shop/search.js b/src/pages/api/shop/search.js index 42d16100..fec75fd8 100644 --- a/src/pages/api/shop/search.js +++ b/src/pages/api/shop/search.js @@ -210,6 +210,47 @@ export default async function handler(req, res) { fq.map((val) => `fq=${encodeURIComponent(val)}`) ); + // Searchkey + if (req.query.from === 'searchkey') { + const phrase = q.replace(/-/g, ' ').trim(); + + // encode q + const encodedQuery = encodeURIComponent(`variants_name_t:"${phrase}"`); + + const strictQuery = [ + `q=${encodedQuery}`, + `defType=edismax`, + `q.op=AND`, + `mm=${encodeURIComponent('100%')}`, + `qf=${encodeURIComponent('name_s description_clean_t')}`, + `rows=${limit}`, + `start=${(page - 1) * limit}`, + ]; + + if (fq) strictQuery.push(`fq=${encodeURIComponent(fq)}`); + + const solrUrl = + process.env.SOLR_HOST + '/solr/product/select?' + strictQuery.join('&'); + + console.log('[STRICT SEARCHKEY QUERY]', solrUrl); + + const result = await axios(solrUrl); + + try { + result.data.response.products = productMappingSolr( + result.data.response.docs, + auth?.pricelist || false + ); + + delete result.data.response.docs; + result.data = camelcaseObjectDeep(result.data); + + return res.status(200).json(result.data); + } catch (e) { + return res.status(400).json({ error: e.message }); + } + } + const solrUrl = process.env.SOLR_HOST + '/solr/product/select?' + parameter.join('&'); diff --git a/src/pages/searchkey/[slug].jsx b/src/pages/searchkey/[slug].jsx index f09520f4..9cf1df05 100644 --- a/src/pages/searchkey/[slug].jsx +++ b/src/pages/searchkey/[slug].jsx @@ -47,26 +47,41 @@ export default function KeywordPage() { } }, [router.isReady]); - // Jika Solr index ditemukan, siapkan parameter pencarian useEffect(() => { if (result) { - const fq = `category_parent_ids:${result.category_id_i} AND manufacture_id_i:${result.brand_id_i}`; - const q = keyword || '*:*'; // keyword dari URL + let fqParts = []; + + if (result.category_id_i) { + fqParts.push(`category_parent_ids:${result.category_id_i}`); + } + + if (result.brand_id_i) { + fqParts.push(`manufacture_id_i:${result.brand_id_i}`); + } + + const fq = fqParts.join(' AND '); + const q = keyword || '*:*'; + console.log('SOLR QUERY:', { q, fq }); - setQuery({ fq, q }); + + setQuery({ + fq, + q, + from: 'searchkey', + }); } }, [result, keyword]); - if (!result) { - return ( - - -
-

Produk tidak ditemukan berdasarkan keyword

-
-
- ); - } + // if (!result) { + // return ( + // + // + //
+ //

Produk tidak ditemukan berdasarkan keyword

+ //
+ //
+ // ); + // } return ( @@ -83,7 +98,12 @@ export default function KeywordPage() { router.asPath.split('?')[0] }`} /> - {query && } + {query && ( + + )} ); } -- cgit v1.2.3 From 95b27ddb0604fbb4fae130f2d80e5ee2aec6d0fc Mon Sep 17 00:00:00 2001 From: Mqdd Date: Thu, 11 Dec 2025 09:00:03 +0700 Subject: fix --- src/pages/api/shop/search.js | 19 +++------ src/pages/api/shop/searchkey.js | 31 ++++++++++++++ src/pages/searchkey/[slug].jsx | 94 +++++++++++++---------------------------- 3 files changed, 68 insertions(+), 76 deletions(-) create mode 100644 src/pages/api/shop/searchkey.js diff --git a/src/pages/api/shop/search.js b/src/pages/api/shop/search.js index fec75fd8..d60b9a46 100644 --- a/src/pages/api/shop/search.js +++ b/src/pages/api/shop/search.js @@ -212,27 +212,22 @@ export default async function handler(req, res) { // Searchkey if (req.query.from === 'searchkey') { - const phrase = q.replace(/-/g, ' ').trim(); + const ids = req.query.ids ? req.query.ids.split(',').filter(Boolean) : []; - // encode q - const encodedQuery = encodeURIComponent(`variants_name_t:"${phrase}"`); + const q = ids.map((id) => `product_id_i:${id}`).join(' OR '); const strictQuery = [ - `q=${encodedQuery}`, - `defType=edismax`, - `q.op=AND`, - `mm=${encodeURIComponent('100%')}`, - `qf=${encodeURIComponent('name_s description_clean_t')}`, + `q=${encodeURIComponent(q)}`, + `fq=-publish_b:false AND price_tier1_v2_f:[1 TO *] AND product_rating_f:[8 TO *]`, + // `qf=variants_code_t variants_name_t`, `rows=${limit}`, - `start=${(page - 1) * limit}`, + `start=${offset}`, ]; - if (fq) strictQuery.push(`fq=${encodeURIComponent(fq)}`); - const solrUrl = process.env.SOLR_HOST + '/solr/product/select?' + strictQuery.join('&'); - console.log('[STRICT SEARCHKEY QUERY]', solrUrl); + console.log('[SEARCHKEY FINAL QUERY]', solrUrl); const result = await axios(solrUrl); diff --git a/src/pages/api/shop/searchkey.js b/src/pages/api/shop/searchkey.js new file mode 100644 index 00000000..e8b535e7 --- /dev/null +++ b/src/pages/api/shop/searchkey.js @@ -0,0 +1,31 @@ +import axios from 'axios'; + +export default async function handler(req, res) { + const { url = '', page = 1, limit = 30 } = req.query; + + let offset = (page - 1) * limit; + + const params = [ + `q.op=AND`, + `q=keywords_s:"${url}"`, + `indent=true`, + `rows=${limit}`, + `start=${offset}`, + ]; + + try { + // let result = await axios( + // process.env.SOLR_HOST + `/solr/searchkey/select?` + params.join('&') + // ); + let result = await axios.post( + process.env.SOLR_HOST + `/solr/searchkey/select`, + params.join('&'), + { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } } + ); + + console.log(result.data); + res.status(200).json(result.data); + } catch (error) { + res.status(500).json({ error: 'Internal Server Error' }); + } +} diff --git a/src/pages/searchkey/[slug].jsx b/src/pages/searchkey/[slug].jsx index 9cf1df05..3ebf6469 100644 --- a/src/pages/searchkey/[slug].jsx +++ b/src/pages/searchkey/[slug].jsx @@ -3,6 +3,7 @@ import { useRouter } from 'next/router'; import { useEffect, useState } from 'react'; import Seo from '@/core/components/Seo'; import dynamic from 'next/dynamic'; +import { getNameFromSlug } from '@/core/utils/slug'; import { capitalizeEachWord } from '../../utils/capializeFIrstWord'; const BasicLayout = dynamic(() => @@ -12,98 +13,63 @@ const ProductSearch = dynamic(() => import('@/lib/product/components/ProductSearch') ); -// const BASE_URL = process.env.NEXT_PUBLIC_SELF_HOST; -const BASE_URL = 'https://indoteknik.com'; - -export default function KeywordPage() { - const router = useRouter(); +export default function FindPage() { + const route = useRouter(); const [result, setResult] = useState(null); const [query, setQuery] = useState(null); - // Ambil slug dari URL dinamis - const keywordSlug = router?.query?.slug || ''; - const keyword = keywordSlug.replace(/-/g, ' ').toLowerCase(); - const url = BASE_URL + router.asPath.split('?')[0]; - const slugTitle = capitalizeEachWord(keyword); + const slugRaw = route.query.slug || null; + console.log(slugRaw); + + // const cleanKey = slugRaw ? getNameFromSlug(slugRaw) : ''; + // console.log(cleanKey); + const readableSlug = capitalizeEachWord(slugRaw); - // Fetch info dari Solr index "url_category_brand" - const getUrls = async (url) => { + const getSearchKeyData = async (clean) => { try { - const response = await axios( - `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/url-category_brand?url=${url}` + const res = await axios( + `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/searchkey?url=${clean}&from=searchkey` ); - const data = response?.data?.response?.docs[0] || null; - setResult(data); - console.log('[πŸ” result from API]', data); // Tambahin ini - } catch (error) { - console.error('Error fetching data:', error); + + setResult(res?.data?.response?.docs?.[0] || null); + } catch (e) { + console.error('Fetching searchkey failed:', e); } }; - // Panggil fetch setelah router siap useEffect(() => { - if (router.isReady) { - getUrls(url); - } - }, [router.isReady]); + if (!route.isReady) return; + if (!slugRaw) return; + + getSearchKeyData(slugRaw); + }, [route.isReady, slugRaw]); useEffect(() => { if (result) { - let fqParts = []; - - if (result.category_id_i) { - fqParts.push(`category_parent_ids:${result.category_id_i}`); - } - - if (result.brand_id_i) { - fqParts.push(`manufacture_id_i:${result.brand_id_i}`); - } - - const fq = fqParts.join(' AND '); - const q = keyword || '*:*'; - - console.log('SOLR QUERY:', { q, fq }); + const ids = result.product_ids_is || []; setQuery({ - fq, - q, + ids: ids.join(','), from: 'searchkey', }); } - }, [result, keyword]); - - // if (!result) { - // return ( - // - // - //
- //

Produk tidak ditemukan berdasarkan keyword

- //
- //
- // ); - // } + }, [result]); return ( - {query && ( - - )} + + {query && } ); } -- cgit v1.2.3 From 7cdb4cef31577818682b63ccbe01b53dd08a9207 Mon Sep 17 00:00:00 2001 From: Mqdd Date: Thu, 11 Dec 2025 09:31:51 +0700 Subject: sitemap done --- src/pages/api/shop/searchkey.js | 9 +++++++- src/pages/sitemap/searchkey.xml.js | 37 ++++++++++++++++++++++++++++++ src/pages/sitemap/searchkey/[page].js | 42 +++++++++++++++++++++++++++++++++++ 3 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 src/pages/sitemap/searchkey.xml.js create mode 100644 src/pages/sitemap/searchkey/[page].js diff --git a/src/pages/api/shop/searchkey.js b/src/pages/api/shop/searchkey.js index e8b535e7..2735e72c 100644 --- a/src/pages/api/shop/searchkey.js +++ b/src/pages/api/shop/searchkey.js @@ -3,11 +3,18 @@ import axios from 'axios'; export default async function handler(req, res) { const { url = '', page = 1, limit = 30 } = req.query; + let q = '*:*'; + + if (!req.query.all) { + const url = (req.query.q || '').trim(); + q = `keywords_s:"${url}"`; + } let offset = (page - 1) * limit; const params = [ `q.op=AND`, - `q=keywords_s:"${url}"`, + // `q=keywords_s:"${url}"`, + `q=${q}`, `indent=true`, `rows=${limit}`, `start=${offset}`, diff --git a/src/pages/sitemap/searchkey.xml.js b/src/pages/sitemap/searchkey.xml.js new file mode 100644 index 00000000..488337d3 --- /dev/null +++ b/src/pages/sitemap/searchkey.xml.js @@ -0,0 +1,37 @@ +import productSearchApi from '@/lib/product/api/productSearchApi'; +import { create } from 'xmlbuilder'; +import _ from 'lodash-contrib'; +import axios from 'axios'; + +export async function getServerSideProps({ res }) { + const baseUrl = process.env.SELF_HOST + '/sitemap/searchkey'; + const limit = 500; + const keywords = await axios( + `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/searchkey?limit=${limit}&all=1` + ); + // console.log(keywords); + const pageCount = Math.ceil(keywords.data.response.numFound / limit); + const pages = Array.from({ length: pageCount }, (_, i) => i + 1); + const sitemapIndex = create('sitemapindex', { encoding: 'UTF-8' }).att( + 'xmlns', + 'http://www.sitemaps.org/schemas/sitemap/0.9' + ); + + const date = new Date(); + // const date = '2025-10-30'; + pages.forEach((page) => { + const sitemap = sitemapIndex.ele('sitemap'); + sitemap.ele('loc', `${baseUrl}/${page}.xml`); + sitemap.ele('lastmod', date.toISOString().slice(0, 10)); + }); + + res.setHeader('Content-Type', 'text/xml'); + res.write(sitemapIndex.end()); + res.end(); + + return { props: {} }; +} + +export default function SitemapProducts() { + return null; +} diff --git a/src/pages/sitemap/searchkey/[page].js b/src/pages/sitemap/searchkey/[page].js new file mode 100644 index 00000000..cee53f5e --- /dev/null +++ b/src/pages/sitemap/searchkey/[page].js @@ -0,0 +1,42 @@ +import productSearchApi from '@/lib/product/api/productSearchApi'; +import { create } from 'xmlbuilder'; +import _ from 'lodash-contrib'; +import { createSlug } from '@/core/utils/slug'; +import axios from 'axios'; + +export async function getServerSideProps({ query, res }) { + // const baseUrl = process.env.SELF_HOST + '/shop/product/'; + const { page } = query; + const limit = 500; + const keywords = await axios( + `${ + process.env.NEXT_PUBLIC_SELF_HOST + }/api/shop/searchkey?limit=${limit}&page=${page.replace('.xml', '')}&all=1` + ); + + const sitemap = create('urlset', { encoding: 'utf-8' }).att( + 'xmlns', + 'http://www.sitemaps.org/schemas/sitemap/0.9' + ); + + const date = new Date(); + // const date = '2025-10-30'; + keywords.data.response.docs.forEach((product) => { + const url = sitemap.ele('url'); + const loc = product.url_s; + url.ele('loc', loc); + url.ele('lastmod', date.toISOString().slice(0, 10)); + url.ele('changefreq', 'daily'); + url.ele('priority', '0.8'); + }); + + res.setHeader('Content-Type', 'text/xml'); + res.write(sitemap.end()); + res.end(); + + return { props: {} }; +} + +export default function SitemapProducts() { + return null; +} -- cgit v1.2.3 From 5808e82529933aee7c63ede955f64028fd1f38ee Mon Sep 17 00:00:00 2001 From: Mqdd Date: Tue, 16 Dec 2025 14:34:47 +0700 Subject: fix bug & add category --- src/pages/api/shop/searchkey.js | 31 +++++++++++++++------------ src/pages/searchkey/[slug].jsx | 47 +++++++++++++++++++++++++++-------------- 2 files changed, 49 insertions(+), 29 deletions(-) diff --git a/src/pages/api/shop/searchkey.js b/src/pages/api/shop/searchkey.js index 2735e72c..f5546a36 100644 --- a/src/pages/api/shop/searchkey.js +++ b/src/pages/api/shop/searchkey.js @@ -1,19 +1,23 @@ import axios from 'axios'; export default async function handler(req, res) { - const { url = '', page = 1, limit = 30 } = req.query; + const { url = '', page = 1, limit = 30, all } = req.query; let q = '*:*'; - if (!req.query.all) { - const url = (req.query.q || '').trim(); - q = `keywords_s:"${url}"`; + // βœ… kalau BUKAN sitemap + if (!all) { + const cleanUrl = url.trim(); + if (!cleanUrl) { + return res.status(400).json({ error: 'Missing url param' }); + } + q = `keywords_s:"${cleanUrl}"`; } - let offset = (page - 1) * limit; + + const offset = (page - 1) * limit; const params = [ `q.op=AND`, - // `q=keywords_s:"${url}"`, `q=${q}`, `indent=true`, `rows=${limit}`, @@ -21,18 +25,19 @@ export default async function handler(req, res) { ]; try { - // let result = await axios( - // process.env.SOLR_HOST + `/solr/searchkey/select?` + params.join('&') - // ); - let result = await axios.post( - process.env.SOLR_HOST + `/solr/searchkey/select`, + const result = await axios.post( + `${process.env.SOLR_HOST}/solr/searchkey/select`, params.join('&'), - { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } } + { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + } ); - console.log(result.data); res.status(200).json(result.data); } catch (error) { + console.error(error?.response?.data || error); res.status(500).json({ error: 'Internal Server Error' }); } } diff --git a/src/pages/searchkey/[slug].jsx b/src/pages/searchkey/[slug].jsx index 3ebf6469..4a6923ff 100644 --- a/src/pages/searchkey/[slug].jsx +++ b/src/pages/searchkey/[slug].jsx @@ -3,9 +3,11 @@ import { useRouter } from 'next/router'; import { useEffect, useState } from 'react'; import Seo from '@/core/components/Seo'; import dynamic from 'next/dynamic'; -import { getNameFromSlug } from '@/core/utils/slug'; import { capitalizeEachWord } from '../../utils/capializeFIrstWord'; +// βœ… Breadcrumb = default export +import Breadcrumb from '@/lib/category/components/Breadcrumb'; + const BasicLayout = dynamic(() => import('@/core/components/layouts/BasicLayout') ); @@ -15,20 +17,19 @@ const ProductSearch = dynamic(() => export default function FindPage() { const route = useRouter(); + const [result, setResult] = useState(null); const [query, setQuery] = useState(null); + const [categoryId, setCategoryId] = useState(null); const slugRaw = route.query.slug || null; - console.log(slugRaw); - - // const cleanKey = slugRaw ? getNameFromSlug(slugRaw) : ''; - // console.log(cleanKey); const readableSlug = capitalizeEachWord(slugRaw); - const getSearchKeyData = async (clean) => { + // πŸ”Ή Fetch searchkey dari Solr + const getSearchKeyData = async (slug) => { try { const res = await axios( - `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/searchkey?url=${clean}&from=searchkey` + `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/searchkey?url=${slug}&from=searchkey` ); setResult(res?.data?.response?.docs?.[0] || null); @@ -37,21 +38,31 @@ export default function FindPage() { } }; + // πŸ”Ή Trigger fetch saat slug siap useEffect(() => { - if (!route.isReady) return; - if (!slugRaw) return; - + if (!route.isReady || !slugRaw) return; getSearchKeyData(slugRaw); }, [route.isReady, slugRaw]); + // πŸ”Ή Ambil product_ids + categoryId dari Solr useEffect(() => { - if (result) { - const ids = result.product_ids_is || []; + if (!result) return; - setQuery({ - ids: ids.join(','), - from: 'searchkey', - }); + // product search + const ids = result.product_ids_is || []; + setQuery({ + ids: ids.join(','), + from: 'searchkey', + }); + + // breadcrumb category + const catId = + result.category_id_i || + result.public_categ_id_i || + (result.category_ids_is && result.category_ids_is[0]); + + if (catId) { + setCategoryId(catId); } }, [result]); @@ -69,6 +80,10 @@ export default function FindPage() { canonical={`${process.env.NEXT_PUBLIC_SELF_HOST}${route.asPath}`} /> + {/* βœ… Breadcrumb (auto fetch via component) */} + {categoryId && } + + {/* βœ… Product result */} {query && } ); -- cgit v1.2.3 From a03ddbe49706870862f692b6c425b4dce6175842 Mon Sep 17 00:00:00 2001 From: Mqdd Date: Fri, 19 Dec 2025 15:12:09 +0700 Subject: add param orderby seearchkey --- src/pages/api/shop/search.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/api/shop/search.js b/src/pages/api/shop/search.js index d60b9a46..5f77b5c6 100644 --- a/src/pages/api/shop/search.js +++ b/src/pages/api/shop/search.js @@ -222,6 +222,7 @@ export default async function handler(req, res) { // `qf=variants_code_t variants_name_t`, `rows=${limit}`, `start=${offset}`, + `sort=${paramOrderBy}`, ]; const solrUrl = -- cgit v1.2.3 From aa31de678b152aabeb5c4ac19e3d63bfe7f785f5 Mon Sep 17 00:00:00 2001 From: Mqdd Date: Fri, 19 Dec 2025 15:12:18 +0700 Subject: increase limit xml --- src/pages/sitemap/searchkey.xml.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/sitemap/searchkey.xml.js b/src/pages/sitemap/searchkey.xml.js index 488337d3..e4ca2434 100644 --- a/src/pages/sitemap/searchkey.xml.js +++ b/src/pages/sitemap/searchkey.xml.js @@ -5,7 +5,7 @@ import axios from 'axios'; export async function getServerSideProps({ res }) { const baseUrl = process.env.SELF_HOST + '/sitemap/searchkey'; - const limit = 500; + const limit = 1000; const keywords = await axios( `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/searchkey?limit=${limit}&all=1` ); -- cgit v1.2.3 From 89978029f6f97bd1bb4390a3e3c2edf073e115c9 Mon Sep 17 00:00:00 2001 From: Mqdd Date: Fri, 19 Dec 2025 15:15:28 +0700 Subject: fix title --- src/pages/searchkey/[slug].jsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/pages/searchkey/[slug].jsx b/src/pages/searchkey/[slug].jsx index 4a6923ff..a23f11b0 100644 --- a/src/pages/searchkey/[slug].jsx +++ b/src/pages/searchkey/[slug].jsx @@ -23,7 +23,11 @@ export default function FindPage() { const [categoryId, setCategoryId] = useState(null); const slugRaw = route.query.slug || null; - const readableSlug = capitalizeEachWord(slugRaw); + const readableSlug = slugRaw + ? decodeURIComponent(slugRaw) + .replace(/-/g, ' ') + .replace(/\b\w/g, (c) => c.toUpperCase()) + : ''; // πŸ”Ή Fetch searchkey dari Solr const getSearchKeyData = async (slug) => { -- cgit v1.2.3 From 4d5dbcda7334c90b54ad25d828c8cf2a6433b682 Mon Sep 17 00:00:00 2001 From: Mqdd Date: Fri, 19 Dec 2025 15:25:10 +0700 Subject: add keyword to breadcrumb --- src/lib/category/components/Breadcrumb.jsx | 72 +++++++++++++++++++----------- src/pages/searchkey/[slug].jsx | 4 +- 2 files changed, 49 insertions(+), 27 deletions(-) diff --git a/src/lib/category/components/Breadcrumb.jsx b/src/lib/category/components/Breadcrumb.jsx index 50557c3e..acd2cbff 100644 --- a/src/lib/category/components/Breadcrumb.jsx +++ b/src/lib/category/components/Breadcrumb.jsx @@ -11,17 +11,19 @@ import React from 'react'; import { useQuery } from 'react-query'; import useDevice from '@/core/hooks/useDevice'; -const Breadcrumb = ({ categoryId }) => { +const Breadcrumb = ({ categoryId, currentLabel }) => { const breadcrumbs = useQuery( ['category-breadcrumbs', categoryId], async () => await odooApi('GET', `/api/v1/category/${categoryId}/category-breadcrumb`) ); - const { isDesktop, isMobile } = useDevice(); + const { isDesktop, isMobile } = useDevice(); const items = breadcrumbs.data ?? []; - const lastIdx = items.length - 1; + /* ========================= + DESKTOP + ========================== */ if (isDesktop) { return (
@@ -54,14 +56,12 @@ const Breadcrumb = ({ categoryId }) => { {/* Categories */} {items.map((category, index) => { - const isLast = index === lastIdx; + const isLastCategory = index === items.length - 1; + const isClickable = currentLabel || !isLastCategory; + return ( - - {isLast ? ( - - {category.name} - - ) : ( + + {isClickable ? ( { > {category.name} + ) : ( + + {category.name} + )} ); })} + + {/* Searchkey / Current Page */} + {currentLabel && ( + + + {currentLabel} + + + )}
); } + /* ========================= + MOBILE + ========================== */ if (isMobile) { - const items = breadcrumbs.data ?? []; const n = items.length; - const lastCat = n >= 1 ? items[n - 1] : null; // terakhir (current) - const secondLast = n >= 2 ? items[n - 2] : null; // sebelum current - const beforeSecond = n >= 3 ? items[n - 3] : null; // sebelum secondLast + const lastCat = n >= 1 ? items[n - 1] : null; + const secondLast = n >= 2 ? items[n - 2] : null; + const beforeSecond = n >= 3 ? items[n - 3] : null; + const hiddenText = n >= 3 ? items @@ -97,19 +113,21 @@ const Breadcrumb = ({ categoryId }) => { .join(' / ') : ''; + const finalLabel = currentLabel || lastCat?.name; + return (
/} // lebih rapat + separator={/} spacing='4px' sx={{ '& ol': { display: 'flex', alignItems: 'center', - overflow: 'hidden', // untuk ellipsis - whiteSpace: 'nowrap', // untuk ellipsis - gap: '0', // no extra gap + overflow: 'hidden', + whiteSpace: 'nowrap', + gap: '0', }, '& li': { display: 'inline-flex', alignItems: 'center' }, '& li:not(:last-of-type)': { @@ -117,7 +135,7 @@ const Breadcrumb = ({ categoryId }) => { whiteSpace: 'nowrap', }, '& li:last-of-type': { - flex: '0 1 auto', // jangan ambil full space biar gak keliatan β€œspace kosong” + flex: '0 1 auto', minWidth: 0, }, }} @@ -130,7 +148,7 @@ const Breadcrumb = ({ categoryId }) => { - {/* Jika ada kategori sebelum secondLast, tampilkan '..' (link ke beforeSecond) */} + {/* Ellipsis */} {beforeSecond && ( { )} - {/* secondLast sebagai link (kalau ada) */} + {/* Second last category */} {secondLast && ( { )} - {/* lastCat (current) dengan truncate & lebar dibatasi */} - {lastCat && ( + {/* Current */} + {finalLabel && ( - {lastCat.name} + {finalLabel} )} @@ -183,6 +201,8 @@ const Breadcrumb = ({ categoryId }) => {
); } + + return null; }; export default Breadcrumb; diff --git a/src/pages/searchkey/[slug].jsx b/src/pages/searchkey/[slug].jsx index a23f11b0..82179b7d 100644 --- a/src/pages/searchkey/[slug].jsx +++ b/src/pages/searchkey/[slug].jsx @@ -85,7 +85,9 @@ export default function FindPage() { /> {/* βœ… Breadcrumb (auto fetch via component) */} - {categoryId && } + {categoryId && ( + + )} {/* βœ… Product result */} {query && } -- cgit v1.2.3 From 8c4d73ff159cb7b5df4f83f1eb76e8a06c7179ce Mon Sep 17 00:00:00 2001 From: Mqdd Date: Fri, 19 Dec 2025 16:08:14 +0700 Subject: fix page name --- src/pages/searchkey/[slug].jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/searchkey/[slug].jsx b/src/pages/searchkey/[slug].jsx index 82179b7d..b0fc9ab8 100644 --- a/src/pages/searchkey/[slug].jsx +++ b/src/pages/searchkey/[slug].jsx @@ -15,7 +15,7 @@ const ProductSearch = dynamic(() => import('@/lib/product/components/ProductSearch') ); -export default function FindPage() { +export default function KeywordPage() { const route = useRouter(); const [result, setResult] = useState(null); -- cgit v1.2.3 From 3e67e48da0ea88f011f1d7d7390758ab2ad40339 Mon Sep 17 00:00:00 2001 From: Mqdd Date: Wed, 18 Feb 2026 15:51:13 +0700 Subject: cr renca --- src/lib/category/components/Breadcrumb.jsx | 2 +- src/pages/sitemap/searchkey.xml.js | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/lib/category/components/Breadcrumb.jsx b/src/lib/category/components/Breadcrumb.jsx index fa2846e4..29bc9c0a 100644 --- a/src/lib/category/components/Breadcrumb.jsx +++ b/src/lib/category/components/Breadcrumb.jsx @@ -11,7 +11,7 @@ import React from 'react'; import { useQuery } from 'react-query'; import useDevice from '@/core/hooks/useDevice'; -const Breadcrumb = ({ categoryId, shortDesc }) => { +const Breadcrumb = ({ categoryId, shortDesc, currentLabel }) => { const breadcrumbs = useQuery( ['category-breadcrumbs', categoryId], async () => diff --git a/src/pages/sitemap/searchkey.xml.js b/src/pages/sitemap/searchkey.xml.js index e4ca2434..b34baa8b 100644 --- a/src/pages/sitemap/searchkey.xml.js +++ b/src/pages/sitemap/searchkey.xml.js @@ -5,16 +5,16 @@ import axios from 'axios'; export async function getServerSideProps({ res }) { const baseUrl = process.env.SELF_HOST + '/sitemap/searchkey'; - const limit = 1000; + const limit = 5000; const keywords = await axios( - `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/searchkey?limit=${limit}&all=1` + `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/searchkey?limit=${limit}&all=1`, ); // console.log(keywords); const pageCount = Math.ceil(keywords.data.response.numFound / limit); const pages = Array.from({ length: pageCount }, (_, i) => i + 1); const sitemapIndex = create('sitemapindex', { encoding: 'UTF-8' }).att( 'xmlns', - 'http://www.sitemaps.org/schemas/sitemap/0.9' + 'http://www.sitemaps.org/schemas/sitemap/0.9', ); const date = new Date(); -- cgit v1.2.3 From 4a85f437cfa7a61bebd341c9a509abccbd64745b Mon Sep 17 00:00:00 2001 From: Mqdd Date: Wed, 18 Feb 2026 17:26:59 +0700 Subject: fix pagination --- src/lib/product/components/ProductSearch.jsx | 74 ++++++++++++++++++++++++---- src/pages/searchkey/[slug].jsx | 16 +++--- 2 files changed, 73 insertions(+), 17 deletions(-) diff --git a/src/lib/product/components/ProductSearch.jsx b/src/lib/product/components/ProductSearch.jsx index c73c7036..3e667966 100644 --- a/src/lib/product/components/ProductSearch.jsx +++ b/src/lib/product/components/ProductSearch.jsx @@ -44,7 +44,7 @@ const ProductSearch = ({ const { page = 1 } = query; const [q, setQ] = useState(query?.q || '*'); const [search, setSearch] = useState(query?.q || '*'); - const [limit, setLimit] = useState(router.query?.limit || 30); + const [limit, setLimit] = useState(parseInt(router.query?.limit) || 30); const [orderBy, setOrderBy] = useState(router.query?.orderBy); const [finalQuery, setFinalQuery] = useState({}); const [queryFinal, setQueryFinal] = useState({}); @@ -86,6 +86,21 @@ const ProductSearch = ({ } }, [router.isReady, router.pathname, router.query?.orderBy, prefixUrl]); + // πŸ”Ή Sync limit state with router.query + useEffect(() => { + if (!router.isReady) return; + const newLimit = parseInt(router.query?.limit) || 30; + setLimit(newLimit); + }, [router.query?.limit, router.isReady]); + + // πŸ”Ή Sync orderBy state with router.query + useEffect(() => { + if (!router.isReady) return; + if (router.query?.orderBy) { + setOrderBy(router.query.orderBy); + } + }, [router.query?.orderBy, router.isReady]); + const dataIdCategories = []; useEffect(() => { if (prefixUrl.includes('category')) { @@ -180,20 +195,55 @@ const ProductSearch = ({ } }; fetchCategoryData(); + } else if (query?.from === 'searchkey' && query?.ids) { + const newQuery = { + ids: query.ids, + from: 'searchkey', + page: router.query.page ? router.query.page : 1, + category: router.query.category ? router.query.category : '', + priceFrom: router.query.priceFrom ? router.query.priceFrom : '', + priceTo: router.query.priceTo ? router.query.priceTo : '', + limit: router.query.limit ? router.query.limit : '', + orderBy: router.query.orderBy ? router.query.orderBy : '', + }; + setFinalQuery(newQuery); } - }, [dataCategoriesProduct, dataLob]); + }, [dataCategoriesProduct, dataLob, query?.from, query?.ids, router.query]); useEffect(() => { if ( prefixUrl.includes('category') || prefixUrl.includes('lob') || + query?.from === 'searchkey' || router.asPath.includes('penawaran') ) { - setQueryFinal({ ...finalQuery, q, limit, orderBy }); + setQueryFinal({ + ...finalQuery, + q, + limit, + orderBy, + page: router.query.page || 1, + }); } else { - setQueryFinal({ ...query, q, limit, orderBy }); + setQueryFinal({ + ...query, + q, + limit, + orderBy, + page: router.query.page || 1, + }); } - }, [prefixUrl, dataCategoriesProduct, query, finalQuery]); + }, [ + prefixUrl, + dataCategoriesProduct, + query, + finalQuery, + router.query, + router.query.page, + limit, + orderBy, + q, + ]); const { productSearch } = useProductSearch({ query: queryFinal, @@ -339,6 +389,7 @@ const ProductSearch = ({ let params = { ...router.query, limit: e.target.value, + page: 1, // Reset to page 1 when limit changes }; params = _.pickBy(params, _.identity); params = toQuery(params); @@ -541,8 +592,10 @@ const ProductSearch = ({ @@ -729,9 +782,10 @@ const ProductSearch = ({ diff --git a/src/pages/searchkey/[slug].jsx b/src/pages/searchkey/[slug].jsx index b0fc9ab8..2fa3cf8d 100644 --- a/src/pages/searchkey/[slug].jsx +++ b/src/pages/searchkey/[slug].jsx @@ -8,11 +8,11 @@ import { capitalizeEachWord } from '../../utils/capializeFIrstWord'; // βœ… Breadcrumb = default export import Breadcrumb from '@/lib/category/components/Breadcrumb'; -const BasicLayout = dynamic(() => - import('@/core/components/layouts/BasicLayout') +const BasicLayout = dynamic( + () => import('@/core/components/layouts/BasicLayout'), ); -const ProductSearch = dynamic(() => - import('@/lib/product/components/ProductSearch') +const ProductSearch = dynamic( + () => import('@/lib/product/components/ProductSearch'), ); export default function KeywordPage() { @@ -33,7 +33,7 @@ export default function KeywordPage() { const getSearchKeyData = async (slug) => { try { const res = await axios( - `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/searchkey?url=${slug}&from=searchkey` + `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/searchkey?url=${slug}&from=searchkey`, ); setResult(res?.data?.response?.docs?.[0] || null); @@ -52,7 +52,7 @@ export default function KeywordPage() { useEffect(() => { if (!result) return; - // product search + // product search - keep ids for API, add from marker for ProductSearch const ids = result.product_ids_is || []; setQuery({ ids: ids.join(','), @@ -90,7 +90,9 @@ export default function KeywordPage() { )} {/* βœ… Product result */} - {query && } + {query && ( + + )} ); } -- cgit v1.2.3 From adb28e824ca2d05244ad939a273067e5b7e38f76 Mon Sep 17 00:00:00 2001 From: Mqdd Date: Thu, 19 Feb 2026 14:25:30 +0700 Subject: done all --- src/lib/product/api/productSearchApi.js | 31 +++++- src/lib/utils/batchSolrQueries.js | 113 +++++++++++++++++++ src/pages/api/shop/search.js | 188 +++++++++++++++++++++++++++----- 3 files changed, 302 insertions(+), 30 deletions(-) create mode 100644 src/lib/utils/batchSolrQueries.js diff --git a/src/lib/product/api/productSearchApi.js b/src/lib/product/api/productSearchApi.js index a84caa3c..1a6ad36a 100644 --- a/src/lib/product/api/productSearchApi.js +++ b/src/lib/product/api/productSearchApi.js @@ -2,8 +2,37 @@ import _ from 'lodash-contrib'; import axios from 'axios'; const productSearchApi = async ({ query, operation = 'OR' }) => { + // Use POST for large product ID arrays to avoid URL length limits + // GET request URL limit is typically 2KB-8KB; switch to POST if query string is large + const QUERY_SIZE_THRESHOLD = 2000; // Switch to POST if query > 2KB + + if (query.length > QUERY_SIZE_THRESHOLD) { + console.log( + `[productSearchApi] Large query (${query.length} chars), using POST`, + ); + + // Parse query string into object for POST body + const params = new URLSearchParams(query); + const bodyData = { + ...Object.fromEntries(params), + operation, + }; + + const dataProductSearch = await axios.post( + `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/search`, + bodyData, + { + headers: { + 'Content-Type': 'application/json', + }, + }, + ); + return dataProductSearch.data; + } + + // Small query, use standard GET request const dataProductSearch = await axios( - `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/search?${query}&operation=${operation}` + `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/search?${query}&operation=${operation}`, ); return dataProductSearch.data; }; diff --git a/src/lib/utils/batchSolrQueries.js b/src/lib/utils/batchSolrQueries.js new file mode 100644 index 00000000..486f1778 --- /dev/null +++ b/src/lib/utils/batchSolrQueries.js @@ -0,0 +1,113 @@ +/** + * Batch utility functions for handling large product ID arrays in Solr queries + * Prevents URL length limit errors when querying with >100 product IDs + */ + +/** + * Split an array into chunks of specified size + * @param {Array} array - Array to split + * @param {number} size - Chunk size (default: 100) + * @returns {Array} Array of chunks + */ +export const chunkArray = (array, size = 100) => { + if (!Array.isArray(array) || array.length === 0) return []; + + const chunks = []; + for (let i = 0; i < array.length; i += size) { + chunks.push(array.slice(i, i + size)); + } + return chunks; +}; + +/** + * Build a product ID OR clause for Solr query + * @param {Array} ids - Array of product IDs + * @returns {string} Formatted OR clause: "product_id_i:123 OR product_id_i:456..." + */ +export const buildProductIdOrClause = (ids) => { + if (!Array.isArray(ids) || ids.length === 0) { + return '*:*'; + } + return ids.map((id) => `product_id_i:${id}`).join(' OR '); +}; + +/** + * Validate query size to prevent exceeding HTTP limits + * @param {string} query - Query string to validate + * @param {number} maxSize - Maximum allowed size in characters (default: 6000) + * @returns {Object} { valid: boolean, message: string, size: number } + */ +export const validateQuerySize = (query, maxSize = 6000) => { + const size = query.length; + return { + valid: size <= maxSize, + message: + size > maxSize + ? `Query size ${size} exceeds limit of ${maxSize}` + : `Query size ${size} is within limit`, + size, + }; +}; + +/** + * Build batched Solr query parameters for large ID arrays + * Chunks IDs into groups and creates separate OR clauses + * @param {Array} ids - Product IDs to query + * @param {number} chunkSize - How many IDs per chunk (default: 100) + * @returns {Array} Array of OR clauses, one per chunk + */ +export const buildBatchedOrClauses = (ids, chunkSize = 100) => { + if (!Array.isArray(ids) || ids.length === 0) { + return ['*:*']; + } + + const chunks = chunkArray(ids, chunkSize); + + if (chunks.length === 1) { + // Single chunk, return standard OR clause + return [buildProductIdOrClause(ids)]; + } + + // Multiple chunks: return OR clauses wrapped with parentheses for combining + return chunks.map((chunk) => `(${buildProductIdOrClause(chunk)})`); +}; + +/** + * Combine multiple OR clauses into a single query (for Solr) + * @param {Array} orClauses - Array of OR clauses + * @returns {string} Combined query with OR between clauses + */ +export const combineOrClauses = (orClauses) => { + if (!Array.isArray(orClauses) || orClauses.length === 0) { + return '*:*'; + } + if (orClauses.length === 1) { + return orClauses[0]; + } + return orClauses.join(' OR '); +}; + +/** + * Merge Solr response documents from multiple queries + * Removes duplicates based on product_id_i + * @param {Array} responseArrays - Array of response.docs arrays + * @returns {Array} Merged and deduplicated docs + */ +export const mergeSolrResults = (responseArrays) => { + const seen = new Set(); + const merged = []; + + responseArrays.forEach((docs) => { + if (Array.isArray(docs)) { + docs.forEach((doc) => { + const id = doc.product_id_i; + if (id && !seen.has(id)) { + seen.add(id); + merged.push(doc); + } + }); + } + }); + + return merged; +}; diff --git a/src/pages/api/shop/search.js b/src/pages/api/shop/search.js index 1f636f28..8954446a 100644 --- a/src/pages/api/shop/search.js +++ b/src/pages/api/shop/search.js @@ -1,6 +1,11 @@ import { productMappingSolr } from '@/utils/solrMapping'; import axios from 'axios'; import camelcaseObjectDeep from 'camelcase-object-deep'; +import { + chunkArray, + buildProductIdOrClause, + mergeSolrResults, +} from '@/lib/utils/batchSolrQueries'; const escapeSolrQuery = (query) => { if (query == '*') return query; @@ -14,7 +19,26 @@ const escapeSolrQuery = (query) => { .join(' '); }; +/** + * Get parameter value from either GET (req.query) or POST (req.body) + * POST takes precedence if available + */ +const getParam = (req, key, defaultValue = undefined) => { + // POST takes precedence + if (req.body && req.body[key] !== undefined) { + return req.body[key]; + } + // Fallback to GET + if (req.query && req.query[key] !== undefined) { + return req.query[key]; + } + return defaultValue; +}; + export default async function handler(req, res) { + // Support both GET and POST requests + const params = req.method === 'POST' ? req.body : req.query; + const { q = '*', page = 1, @@ -28,9 +52,15 @@ export default async function handler(req, res) { limit = 30, source = '', group = 'true', - } = req.query; + } = params; - let { stock = '' } = req.query; + let { stock = '' } = params; + + // Convert string parameters to appropriate types (for POST requests) + const pageNum = parseInt(page, 10) || 1; + const limitNum = parseInt(limit, 10) || 30; + const priceFromNum = parseFloat(priceFrom) || 0; + const priceToNum = parseFloat(priceTo) || 0; // ============================================================ // [PERBAIKAN] 1. LOGIC KHUSUS COMPARE (PAKAI URLSearchParams) @@ -56,7 +86,7 @@ export default async function handler(req, res) { const params = new URLSearchParams(); params.append('q', qCompare); - params.append('rows', limit); + params.append('rows', limitNum); params.append('wt', 'json'); params.append('indent', 'true'); @@ -218,7 +248,7 @@ export default async function handler(req, res) { // ============================================================ if (source === 'sitemap') { try { - const offset = (page - 1) * limit; + const offset = (pageNum - 1) * limitNum; const parameter = [ 'q=*:*', `rows=${limit}`, @@ -311,7 +341,7 @@ export default async function handler(req, res) { keywords = formattedQuery; } - let offset = (page - 1) * limit; + let offset = (pageNum - 1) * limitNum; let parameter = [ 'facet.field={!ex=brand}manufacture_name_s', @@ -334,9 +364,9 @@ export default async function handler(req, res) { parameter.push(`fq=${encodeURIComponent(f)}`); }); - if (priceFrom > 0 || priceTo > 0) { + if (priceFromNum > 0 || priceToNum > 0) { parameter.push( - `fq=price_tier1_v2_f:[${priceFrom || '*'} TO ${priceTo || '*'}]`, + `fq=price_tier1_v2_f:[${priceFromNum || '*'} TO ${priceToNum || '*'}]`, ); } @@ -371,39 +401,139 @@ export default async function handler(req, res) { fq.map((val) => `fq=${encodeURIComponent(val)}`), ); - // Searchkey - if (req.query.from === 'searchkey') { - const ids = req.query.ids ? req.query.ids.split(',').filter(Boolean) : []; + // Searchkey with batching for large ID arrays + if (params.from === 'searchkey') { + try { + // Extract IDs from either query params (GET) or body (POST) + let ids = []; + + if (req.method === 'POST' && req.body && req.body.ids) { + // POST request: IDs in body + ids = Array.isArray(req.body.ids) + ? req.body.ids + : req.body.ids.split(',').filter(Boolean); + } else if (req.query.ids) { + // GET request: IDs in query params + ids = req.query.ids.split(',').filter(Boolean); + } - const q = ids.map((id) => `product_id_i:${id}`).join(' OR '); + if (ids.length === 0) { + return res.status(400).json({ error: 'No product IDs provided' }); + } - const strictQuery = [ - `q=${encodeURIComponent(q)}`, - `fq=-publish_b:false AND price_tier1_v2_f:[1 TO *] AND product_rating_f:[8 TO *]`, - // `qf=variants_code_t variants_name_t`, - `rows=${limit}`, - `start=${offset}`, - `sort=${paramOrderBy}`, - ]; + console.log(`[SEARCHKEY] Processing ${ids.length} product IDs`); - const solrUrl = - process.env.SOLR_HOST + '/solr/product/select?' + strictQuery.join('&'); + // If less than 100 IDs, use single query + if (ids.length <= 100) { + const q = buildProductIdOrClause(ids); - console.log('[SEARCHKEY FINAL QUERY]', solrUrl); + const strictQuery = [ + `q=${encodeURIComponent(q)}`, + `fq=-publish_b:false AND price_tier1_v2_f:[1 TO *] AND product_rating_f:[8 TO *]`, + `rows=${limitNum}`, + `start=${offset}`, + `sort=${paramOrderBy}`, + ]; - const result = await axios(solrUrl); + const solrUrl = + process.env.SOLR_HOST + + '/solr/product/select?' + + strictQuery.join('&'); - try { - result.data.response.products = productMappingSolr( - result.data.response.docs, + console.log('[SEARCHKEY SINGLE QUERY]', solrUrl); + + const result = await axios(solrUrl); + result.data.response.products = productMappingSolr( + result.data.response.docs, + auth?.pricelist || false, + ); + + delete result.data.response.docs; + result.data = camelcaseObjectDeep(result.data); + + return res.status(200).json(result.data); + } + + // Batch large ID arrays into chunks of 100 + const idChunks = chunkArray(ids, 100); + console.log( + `[SEARCHKEY BATCH] Splitting ${ids.length} IDs into ${idChunks.length} chunks`, + ); + + // Execute all chunk queries in parallel + const batchQueries = idChunks.map((chunk) => { + const q = buildProductIdOrClause(chunk); + const queryParams = [ + `q=${encodeURIComponent(q)}`, + `fq=-publish_b:false AND price_tier1_v2_f:[1 TO *] AND product_rating_f:[8 TO *]`, + `rows=100`, // Request maximum 100 rows per chunk (100 IDs per chunk) + `start=0`, + `sort=${paramOrderBy}`, + ]; + + const solrUrl = + process.env.SOLR_HOST + + '/solr/product/select?' + + queryParams.join('&'); + + console.log( + `[SEARCHKEY BATCH QUERY] Chunk: ${chunk.slice(0, 5).join(',')}...`, + ); + + return axios(solrUrl).catch((error) => { + console.error('[SEARCHKEY BATCH ERROR]', error.message); + throw error; + }); + }); + + // Wait for all queries to complete + const batchResults = await Promise.all(batchQueries); + + // Merge all documents from all chunks, removing duplicates + const allDocs = mergeSolrResults( + batchResults.map((r) => r.data.response.docs), + ); + + console.log( + `[SEARCHKEY MERGE] Merged ${allDocs.length} unique documents from ${batchResults.length} chunks`, + ); + + // Apply pagination on merged results + const paginatedDocs = allDocs.slice(offset, offset + limitNum); + + // Use first result's response structure as template + const templateResponse = batchResults[0].data; + + const mergedResponse = { + ...templateResponse, + response: { + ...templateResponse.response, + numFound: allDocs.length, + start: offset, + rows: limitNum, + docs: paginatedDocs, + }, + responseHeader: { + ...templateResponse.responseHeader, + params: { + ...templateResponse.responseHeader.params, + start: offset, + rows: limitNum, + }, + }, + }; + + mergedResponse.response.products = productMappingSolr( + paginatedDocs, auth?.pricelist || false, ); - delete result.data.response.docs; - result.data = camelcaseObjectDeep(result.data); + delete mergedResponse.response.docs; + mergedResponse.data = camelcaseObjectDeep(mergedResponse); - return res.status(200).json(result.data); + return res.status(200).json(mergedResponse.data); } catch (e) { + console.error('[SEARCHKEY ERROR]', e.message); return res.status(400).json({ error: e.message }); } } -- cgit v1.2.3 From 7b79adcbcf0ef7c791e0c679ca946243c9dde7f7 Mon Sep 17 00:00:00 2001 From: Mqdd Date: Thu, 19 Feb 2026 16:50:59 +0700 Subject: fix cannonical tag --- src/pages/searchkey/[slug].jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/searchkey/[slug].jsx b/src/pages/searchkey/[slug].jsx index 2fa3cf8d..fbe72dae 100644 --- a/src/pages/searchkey/[slug].jsx +++ b/src/pages/searchkey/[slug].jsx @@ -81,7 +81,7 @@ export default function KeywordPage() { content: `Beli ${readableSlug}, harga ${readableSlug}, ${readableSlug} murah`, }, ]} - canonical={`${process.env.NEXT_PUBLIC_SELF_HOST}${route.asPath}`} + canonical={`${process.env.NEXT_PUBLIC_SELF_HOST}/searchkey/${slugRaw}`} /> {/* βœ… Breadcrumb (auto fetch via component) */} -- cgit v1.2.3 From 6aecb5c1a2ee384b8ea2847a543142bfaa9c48f2 Mon Sep 17 00:00:00 2001 From: Mqdd Date: Thu, 19 Feb 2026 16:51:50 +0700 Subject: remove console log --- src/pages/api/shop/search.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/pages/api/shop/search.js b/src/pages/api/shop/search.js index 8954446a..f7220568 100644 --- a/src/pages/api/shop/search.js +++ b/src/pages/api/shop/search.js @@ -421,7 +421,7 @@ export default async function handler(req, res) { return res.status(400).json({ error: 'No product IDs provided' }); } - console.log(`[SEARCHKEY] Processing ${ids.length} product IDs`); + // console.log(`[SEARCHKEY] Processing ${ids.length} product IDs`); // If less than 100 IDs, use single query if (ids.length <= 100) { @@ -440,7 +440,7 @@ export default async function handler(req, res) { '/solr/product/select?' + strictQuery.join('&'); - console.log('[SEARCHKEY SINGLE QUERY]', solrUrl); + // console.log('[SEARCHKEY SINGLE QUERY]', solrUrl); const result = await axios(solrUrl); result.data.response.products = productMappingSolr( @@ -456,9 +456,9 @@ export default async function handler(req, res) { // Batch large ID arrays into chunks of 100 const idChunks = chunkArray(ids, 100); - console.log( - `[SEARCHKEY BATCH] Splitting ${ids.length} IDs into ${idChunks.length} chunks`, - ); + // console.log( + // `[SEARCHKEY BATCH] Splitting ${ids.length} IDs into ${idChunks.length} chunks`, + // ); // Execute all chunk queries in parallel const batchQueries = idChunks.map((chunk) => { @@ -494,9 +494,9 @@ export default async function handler(req, res) { batchResults.map((r) => r.data.response.docs), ); - console.log( - `[SEARCHKEY MERGE] Merged ${allDocs.length} unique documents from ${batchResults.length} chunks`, - ); + // console.log( + // `[SEARCHKEY MERGE] Merged ${allDocs.length} unique documents from ${batchResults.length} chunks`, + // ); // Apply pagination on merged results const paginatedDocs = allDocs.slice(offset, offset + limitNum); -- cgit v1.2.3