diff options
| author | IT Fixcomart <it@fixcomart.co.id> | 2025-11-13 11:18:34 +0000 |
|---|---|---|
| committer | IT Fixcomart <it@fixcomart.co.id> | 2025-11-13 11:18:34 +0000 |
| commit | 74a38d9794e0c4ec937981a45782ab9c80ebf9ef (patch) | |
| tree | 09636bc9399d6ba837893314e778a404bae1b3a4 | |
| parent | 4e24ad6def0e8df890b72f2f7f877424d8ed84cc (diff) | |
| parent | 53e68ba386f4daabd1b6a9e183830132bbf27e99 (diff) | |
Merged in fix_indexing_google (pull request #471)
<MIqdad> fix sitemap
| -rw-r--r-- | src/lib/product/api/productSearchApi.js | 12 | ||||
| -rw-r--r-- | src/pages/api/shop/search.js | 398 | ||||
| -rw-r--r-- | src/pages/sitemap/products.xml.js | 13 | ||||
| -rw-r--r-- | src/pages/sitemap/products/[page].js | 16 |
4 files changed, 175 insertions, 264 deletions
diff --git a/src/lib/product/api/productSearchApi.js b/src/lib/product/api/productSearchApi.js index 8ff8e57d..a84caa3c 100644 --- a/src/lib/product/api/productSearchApi.js +++ b/src/lib/product/api/productSearchApi.js @@ -1,11 +1,11 @@ -import _ from 'lodash-contrib' -import axios from 'axios' +import _ from 'lodash-contrib'; +import axios from 'axios'; const productSearchApi = async ({ query, operation = 'OR' }) => { const dataProductSearch = await axios( `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/search?${query}&operation=${operation}` - ) - return dataProductSearch.data -} + ); + return dataProductSearch.data; +}; -export default productSearchApi +export default productSearchApi; diff --git a/src/pages/api/shop/search.js b/src/pages/api/shop/search.js index 8c9782cb..1b1b6a9c 100644 --- a/src/pages/api/shop/search.js +++ b/src/pages/api/shop/search.js @@ -1,207 +1,11 @@ import { productMappingSolr } from '@/utils/solrMapping'; import axios from 'axios'; import camelcaseObjectDeep from 'camelcase-object-deep'; -import searchSuggestApi from '@/core/api/searchSuggestApi'; -import { ECDH } from 'crypto'; - -export default async function handler(req, res) { - const { - q = '*', - page = 1, - brand = '', - category = '', - priceFrom = 0, - priceTo = 0, - orderBy = '', - operation = 'AND', - fq = '', - limit = 30, - source = '', - } = req.query; - - let { stock = '' } = req.query; - let paramOrderBy = ''; - switch (orderBy) { - case 'flashsale-discount-desc': - paramOrderBy += 'flashsale_discount_f DESC'; - break; - case 'price-asc': - paramOrderBy += 'price_tier1_v2_f ASC'; - break; - case 'price-desc': - paramOrderBy += 'price_tier1_v2_f DESC'; - break; - case 'popular': - paramOrderBy += 'product_rating_f DESC, search_rank_i DESC,'; - break; - case 'popular-weekly': - paramOrderBy += 'search_rank_weekly_i DESC'; - break; - case 'stock': - paramOrderBy += 'product_rating_f DESC, stock_total_f DESC'; - break; - case 'flashsale-price-asc': - paramOrderBy += 'flashsale_price_f ASC'; - break; - default: - paramOrderBy += ''; - break; - } - - // let suggestWord = null; - // let keywords = q; - // let checkQ = null; - - // if (q != '*') { - // checkQ = q.trim().split(/[\s\+\-\!\(\)\{\}\[\]\^"~\*\?:\\\/]+/); - // if (checkQ.length > 1) { - // const dataSearchSuggest = await axios( - // `${process.env.NEXT_PUBLIC_SELF_HOST}/api/shop/suggest?q=${checkQ[1]}` - // ); - // suggestWord = dataSearchSuggest.data.suggestions[0]; - // } - // if (suggestWord && suggestWord?.term.split(' ').length <= 1) { - // keywords = `"${escapeSolrQuery(checkQ[0] + ' ' + suggestWord?.term)}"`; - // } - // } - - // let newQ = keywords; - - let checkQ = q.trim().split(/[\s\+\-\!\(\)\{\}\[\]\^"~\*\?:\\\/]+/); - let newQ = escapeSolrQuery(q); - - const formattedQuery = `(${newQ - .split(' ') - .map((term) => (term.length < 2 ? term : `${term}*`)) - .join(' ')})`; - - const mm = - checkQ.length > 2 - ? checkQ.length > 5 - ? '55%' - : '85%' - : `${checkQ.length}`; - - const filterQueries = [ - '-publish_b:false', - 'product_rating_f:[8 TO *]', - 'price_tier1_v2_f:[1 TO *]', - ]; - - if (orderBy === 'stock') { - filterQueries.push('stock_total_f:[1 TO *]'); - } - - if (fq && source != 'similar' && typeof fq != 'string') { - // filterQueries.push(fq); - fq.push(...filterQueries); - } - const fq_ = filterQueries.join(' AND '); - - let keywords = newQ; - if (source === 'similar' || checkQ.length < 3) { - if (checkQ.length < 2 || checkQ[1].length < 2) { - keywords = newQ; - } else { - keywords = newQ + '*'; - } - } else { - keywords = formattedQuery; - } - - let offset = (page - 1) * limit; - let parameter = [ - // === Facet disjunctive: exclude filter brand/cat saat hitung facet === - 'facet.field={!ex=brand}manufacture_name_s', - 'facet.field={!ex=cat}category_name', - 'facet=true', - 'indent=true', - `facet.query=${escapeSolrQuery(q)}`, - `q.op=OR`, - `q=${keywords}`, - `defType=edismax`, - 'qf=name_s description_clean_t category_name manufacture_name_s variants_code_t variants_name_t category_id_ids default_code_s manufacture_id_i category_id_i ', - `start=${parseInt(offset)}`, - `rows=${limit}`, - `sort=${paramOrderBy}`, - `fq=${encodeURIComponent(fq_)}`, - `mm=${encodeURIComponent(mm)}`, - ]; - - if (priceFrom > 0 || priceTo > 0) { - parameter.push( - `fq=price_tier1_v2_f:[${priceFrom == '' ? '*' : priceFrom} TO ${ - priceTo == '' ? '*' : priceTo - }]` - ); - } - - let { auth } = req.cookies; - if (auth) { - auth = JSON.parse(auth); - if (auth.feature.onlyReadyStock) stock = true; - } - - if (brand) { - // bentuk ekspresi sama seperti versi kamu, tapi dibungkus tag brand - const brandExpr = brand - .split(',') - .map( - (manufacturer) => - `manufacture_name:"${encodeURIComponent(manufacturer)}"` - ) - .join(' OR '); - parameter.push(`fq={!tag=brand}(${brandExpr})`); - } - - if (category) { - // sama: tag kategori - const catExpr = category - .split(',') - .map((cat) => `category_name:"${encodeURIComponent(cat)}"`) - .join(' OR '); - parameter.push(`fq={!tag=cat}(${catExpr})`); - } - - // if (category) parameter.push(`fq=category_name:${capitalizeFirstLetter(category.replace(/,/g, ' OR '))}`) - if (stock) parameter.push(`fq=stock_total_f:(1 TO *)`); - - // Single fq in url params - if (typeof fq === 'string') parameter.push(`fq=${encodeURIComponent(fq)}`); - // Multi fq in url params - if (Array.isArray(fq)) - parameter = parameter.concat( - fq.map((val) => `fq=${encodeURIComponent(val)}`) - ); - - let result = await axios( - process.env.SOLR_HOST + '/solr/product/select?' + parameter.join('&') - ); - - try { - result.data.response.products = productMappingSolr( - result.data.response.docs, - auth?.pricelist || false - ); - result.data.responseHeader.params.start = parseInt( - result.data.responseHeader.params.start - ); - result.data.responseHeader.params.rows = parseInt( - result.data.responseHeader.params.rows - ); - delete result.data.response.docs; - result.data = camelcaseObjectDeep(result.data); - res.status(200).json(result.data); - } catch (error) { - res.status(400).json({ error: error.message }); - } -} +// helper untuk escape karakter spesial const escapeSolrQuery = (query) => { if (query == '*') return query; - query = query.replace(/-/g, ' '); - const specialChars = /([\+\!\(\)\{\}\[\]\^"~\*\?:\\\/])/g; const words = query.split(/\s+/); const escapedWords = words.map((word) => { @@ -210,64 +14,166 @@ const escapeSolrQuery = (query) => { } return word; }); - return escapedWords.join(' '); }; -/*const productResponseMap = (products, pricelist) => { - return products.map((product) => { - let price = product.price_tier1_v2_f || 0 - let priceDiscount = product.price_discount_f || 0 - let discountPercentage = product.discount_f || 0 - - if (pricelist) { - // const pricelistDiscount = product?.[`price_${pricelist}_f`] || false - // const pricelistDiscountPerc = product?.[`discount_${pricelist}_f`] || false - - // if (pricelistDiscount && pricelistDiscount > 0) priceDiscount = pricelistDiscount - // if (pricelistDiscountPerc && pricelistDiscountPerc > 0) - // discountPercentage = pricelistDiscountPerc +export default async function handler(req, res) { + try { + const { + q = '*', + page = 1, + brand = '', + category = '', + priceFrom = 0, + priceTo = 0, + orderBy = '', + operation = 'AND', + fq = '', + limit = 30, + source = '', // <-- penting + } = req.query; + + console.log('🔍 API /shop/search → source:', source); // debug biar tahu kebaca atau enggak + + // --- 1️⃣ MODE SITEMAP: ambil semua produk tanpa filter --- + if (source === 'sitemap') { + const offset = (page - 1) * limit; + const parameter = ['q=*:*', `rows=${limit}`, `start=${offset}`]; + + const solrUrl = + process.env.SOLR_HOST + '/solr/product/select?' + parameter.join('&'); + + console.log('[SOLR QUERY SITEMAP]', solrUrl); + + const result = await axios(solrUrl); + + result.data.response.products = productMappingSolr( + result.data.response.docs, + false + ); + delete result.data.response.docs; + result.data = camelcaseObjectDeep(result.data); + + return res.status(200).json(result.data); + } - price = product?.[`price_${pricelist}_f`] || 0 + // --- 2️⃣ MODE NORMAL (search biasa di website) --- + let checkQ = q.trim().split(/[\s\+\-\!\(\)\{\}\[\]\^"~\*\?:\\\/]+/); + let newQ = escapeSolrQuery(q); + + const formattedQuery = `(${newQ + .split(' ') + .map((term) => (term.length < 2 ? term : `${term}*`)) + .join(' ')})`; + + const mm = + checkQ.length > 2 + ? checkQ.length > 5 + ? '55%' + : '85%' + : `${checkQ.length}`; + + // filter default (mode normal) + const filterQueries = [ + '-publish_b:false', + 'product_rating_f:[8 TO *]', + 'price_tier1_v2_f:[1 TO *]', + ]; + const fq_ = filterQueries.join(' AND '); + + let keywords = newQ; + if (checkQ.length >= 3) keywords = formattedQuery; + + let offset = (page - 1) * limit; + + let paramOrderBy = ''; + switch (orderBy) { + case 'flashsale-discount-desc': + paramOrderBy += 'flashsale_discount_f DESC'; + break; + case 'price-asc': + paramOrderBy += 'price_tier1_v2_f ASC'; + break; + case 'price-desc': + paramOrderBy += 'price_tier1_v2_f DESC'; + break; + case 'popular': + paramOrderBy += 'product_rating_f DESC, search_rank_i DESC,'; + break; + case 'popular-weekly': + paramOrderBy += 'search_rank_weekly_i DESC'; + break; + case 'stock': + paramOrderBy += 'product_rating_f DESC, stock_total_f DESC'; + break; + case 'flashsale-price-asc': + paramOrderBy += 'flashsale_price_f ASC'; + break; + default: + paramOrderBy += ''; + break; } - if (product?.flashsale_id_i > 0) { - price = product?.flashsale_base_price_f || 0 - priceDiscount = product?.flashsale_price_f || 0 - discountPercentage = product?.flashsale_discount_f || 0 + // parameter query normal + let parameter = [ + 'facet.field={!ex=brand}manufacture_name_s', + 'facet.field={!ex=cat}category_name', + 'facet=true', + 'indent=true', + `facet.query=${escapeSolrQuery(q)}`, + `q.op=${operation}`, + `q=${keywords}`, + `defType=edismax`, + 'qf=name_s description_clean_t category_name manufacture_name_s variants_code_t variants_name_t category_id_ids default_code_s manufacture_id_i category_id_i', + `start=${parseInt(offset)}`, + `rows=${limit}`, + `sort=${paramOrderBy}`, + `fq=${encodeURIComponent(fq_)}`, + `mm=${encodeURIComponent(mm)}`, + ]; + + if (priceFrom > 0 || priceTo > 0) { + parameter.push( + `fq=price_tier1_v2_f:[${priceFrom || '*'} TO ${priceTo || '*'}]` + ); } - let productMapped = { - id: product.product_id_i || '', - image: product.image_s || '', - code: product.default_code_s || '', - name: product.name_s || '', - lowestPrice: { price, priceDiscount, discountPercentage }, - variantTotal: product.variant_total_i || 0, - stockTotal: product.stock_total_f || 0, - weight: product.weight_f || 0, - manufacture: {}, - categories: [], - flashSale: { - id: product?.flashsale_id_i, - name: product?.product?.flashsale_name_s, - tag : product?.flashsale_tag_s || 'FLASH SALE' - } + if (brand) { + const brandExpr = brand + .split(',') + .map( + (manufacturer) => + `manufacture_name:"${encodeURIComponent(manufacturer)}"` + ) + .join(' OR '); + parameter.push(`fq={!tag=brand}(${brandExpr})`); } - if (product.manufacture_id_i && product.manufacture_name_s) { - productMapped.manufacture = { - id: product.manufacture_id_i || '', - name: product.manufacture_name_s || '' - } + if (category) { + const catExpr = category + .split(',') + .map((cat) => `category_name:"${encodeURIComponent(cat)}"`) + .join(' OR '); + parameter.push(`fq={!tag=cat}(${catExpr})`); } - productMapped.categories = [ - { - id: product.category_id_i || '', - name: product.category_name_s || '' - } - ] - return productMapped - }) -}*/ + const solrUrl = + process.env.SOLR_HOST + '/solr/product/select?' + parameter.join('&'); + + console.log('[SOLR QUERY NORMAL]', solrUrl); + + const result = await axios(solrUrl); + + result.data.response.products = productMappingSolr( + result.data.response.docs, + false + ); + delete result.data.response.docs; + result.data = camelcaseObjectDeep(result.data); + + res.status(200).json(result.data); + } catch (error) { + console.error('[ERROR /shop/search]', error.message); + res.status(400).json({ error: error.message }); + } +} diff --git a/src/pages/sitemap/products.xml.js b/src/pages/sitemap/products.xml.js index 5ed6b759..b3db3b93 100644 --- a/src/pages/sitemap/products.xml.js +++ b/src/pages/sitemap/products.xml.js @@ -1,12 +1,18 @@ import productSearchApi from '@/lib/product/api/productSearchApi'; import { create } from 'xmlbuilder'; -import _ from 'lodash-contrib'; export async function getServerSideProps({ res }) { const baseUrl = process.env.SELF_HOST + '/sitemap/products'; const limit = 2500; - const query = { limit }; - const products = await productSearchApi({ query: _.toQuery(query) }); + + // ⬇️ Ganti ke URLSearchParams biar key "source" gak hilang + const params = new URLSearchParams({ + limit, + source: 'sitemap', + }).toString(); + + const products = await productSearchApi({ query: params }); + const pageCount = Math.ceil(products.response.numFound / limit); const pages = Array.from({ length: pageCount }, (_, i) => i + 1); const sitemapIndex = create('sitemapindex', { encoding: 'UTF-8' }).att( @@ -15,7 +21,6 @@ export async function getServerSideProps({ res }) { ); const date = new Date(); - // const date = '2025-10-30'; pages.forEach((page) => { const sitemap = sitemapIndex.ele('sitemap'); sitemap.ele('loc', `${baseUrl}/${page}.xml`); diff --git a/src/pages/sitemap/products/[page].js b/src/pages/sitemap/products/[page].js index 3603d64c..846a2df9 100644 --- a/src/pages/sitemap/products/[page].js +++ b/src/pages/sitemap/products/[page].js @@ -1,27 +1,27 @@ import productSearchApi from '@/lib/product/api/productSearchApi'; import { create } from 'xmlbuilder'; -import _ from 'lodash-contrib'; import { createSlug } from '@/core/utils/slug'; export async function getServerSideProps({ query, res }) { const baseUrl = process.env.SELF_HOST + '/shop/product/'; const { page } = query; const limit = 2500; - const queries = { + + // ⬇️ Ganti juga ke URLSearchParams + const params = new URLSearchParams({ limit, page: page.replace('.xml', ''), - // '-publish_b': false, - // product_rating_f: '[8 TO *]', - // price_tier1_v2_f: '[1 TO *]', - }; - const products = await productSearchApi({ query: _.toQuery(queries) }); + source: 'sitemap', + }).toString(); + + const products = await productSearchApi({ query: params }); + 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'; products.response.products.forEach((product) => { const url = sitemap.ele('url'); url.ele('loc', createSlug(baseUrl, product.name, product.id)); |
