{"id":7747,"date":"2025-03-05T02:12:44","date_gmt":"2025-03-05T02:12:44","guid":{"rendered":"https:\/\/www.qedge.ai\/blog\/?p=7747"},"modified":"2025-03-05T02:14:47","modified_gmt":"2025-03-05T02:14:47","slug":"how-to-build-content-search-in-sitecore-jss-using-graphql-and-sxa-tags","status":"publish","type":"post","link":"https:\/\/www.qedge.ai\/blog\/how-to-build-content-search-in-sitecore-jss-using-graphql-and-sxa-tags.html","title":{"rendered":"How to Build Content Search in Sitecore JSS Using GraphQL and SXA Tags"},"content":{"rendered":"\n<div class=\"wp-block-group author\"><div class=\"wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained\">\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"100\" height=\"100\" src=\"https:\/\/www.qedge.ai\/blog\/wp-content\/uploads\/2025\/03\/yaochang.png\" alt=\"\" class=\"wp-image-7749\"><\/figure>\n\n\n\n<p><a href=\"https:\/\/www.linkedin.com\/in\/yaochang-liu\/?lipi=urn%3Ali%3Apage%3Ad_flagship3_pulse_read%3BPsC%2FZorIQyW98hDBpLmXrA%3D%3D\" target=\"_blank\" rel=\"noopener\">Yaochang Liu<\/a><\/p>\n\n\n\n<p>Sitecore Technology MVP 2025 | Sitecore Full Specialization Certified Developer (XM Cloud | XP | CDP | Personalize | Content Hub | Order Cloud)<\/p>\n\n\n\n<style>\n.author {\n    max-width: 600px;\n    margin: 2rem auto;\n    padding: 2rem;\n    background: white;\n    border-radius: 12px;\n    box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);\n    text-align: center;\n}\n\n.author .wp-block-image {\n    margin: 0 auto 1.5rem;\n}\n\n.author img {\n    width: 100px;\n    height: 100px;\n    border-radius: 50%;\n    object-fit: cover;\n    border: 3px solid #f0f0f0;\n    transition: transform 0.3s ease;\n}\n\n.author img:hover {\n    transform: scale(1.05);\n}\n\n.author p:first-of-type {\n    font-size: 1.5rem;\n    font-weight: 600;\n    color: #333;\n    margin: 0 0 0.5rem;\n}\n\n.author p:last-of-type {\n    font-size: 0.9rem;\n    color: #666;\n    line-height: 1.5;\n    margin: 0;\n    max-width: 90%;\n    margin: 0 auto;\n}\n\n.wp-block-group__inner-container {\n    display: flex;\n    flex-direction: column;\n    align-items: center;\n    gap: 0.5rem;\n}\n<\/style>\n\n\n\n<p><\/p>\n<\/div><\/div>\n\n\n\n<p id=\"ember49\">Sitecore JSS, combined with GraphQL and SXA Tags, offers a powerful solution for implementing robust content search functionality in a headless architecture.<\/p>\n\n\n\n<p id=\"ember50\">This article will guide you through the process of building a content search feature in Sitecore JSS, leveraging GraphQL to fetch structured content efficiently and SXA Tags to categorize and filter data.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"ember51\">Step 1: Create categorized tags<\/h2>\n\n\n\n<p id=\"ember52\">Create categorized tags in <em>\/sitecore\/content\/{tenant}\/{site}\/Data\/Tags<\/em><\/p>\n\n\n\n<figure class=\"wp-block-image\"><img decoding=\"async\" src=\"https:\/\/media.licdn.com\/dms\/image\/v2\/D5612AQGUnxKvltFn9A\/article-inline_image-shrink_1500_2232\/article-inline_image-shrink_1500_2232\/0\/1735389022934?e=1746662400&amp;v=beta&amp;t=ZahSlLeYPuxXGUSgYVYmJsJuUua3HeRZlWkBSLaJS5Y\" alt=\"\"><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"ember55\">Step 2: Create the GraphQL Query<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"ember56\">2.1 Create the GraphQL Query for tags<\/h3>\n\n\n\n<p id=\"ember57\">Create the Query as a constant, and define an interface for the data returned from the GraphQL query.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>export const ContentTagsQuery = `\nquery ContentTagsQuery($language: String!) {\n  contentSearchTags: item(path:\"{CA894C0E-946C-412E-ADE9-3B84FBB9A0E5}\",language:$language){\n    children(includeTemplateIDs:\"{25A6B824-672F-4C15-9B98-0B231C80F81C}\"){\n      taxonomies: results{\n        displayName\n        name\n        children(includeTemplateIDs:\"{6B40E84C-8785-49FC-8A10-6BCA862FF7EA}\"){\n          tags: results{\n            name\n            id\n          }\n        }\n      }\n    }\n  }\n}\n`;\ninterface ContentTag {\n  name: string;\n  id: string;\n}\n\nexport interface ContentTaxonomy {\n  displayName: string;\n  name: string;\n  children: {\n    tags: ContentTag&#91;];\n  };\n}\n\nexport interface GraphQLContentTagsResponse {\n  contentSearchTags: {\n    children: {\n      taxonomies: ContentTaxonomy&#91;];\n    };\n  };\n}<\/code><\/pre>\n\n\n\n<p>2.2 Create the GraphQL Query for contents<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import { TextField, ImageField } from '@sitecore-jss\/sitecore-jss-nextjs';\nimport { PageInfo } from '@sitecore-jss\/sitecore-jss\/graphql';\nimport { GraphQLSxaTag } from '..\/types\/sxaTag';\n\nexport const generateContentSearchQuery = (taxonomies: string&#91;]&#91;] = &#91;]) =&gt; {\n  taxonomies = taxonomies.filter((tags) =&gt; tags.length);\n  return `\n    query ContentSearchQuery($keyword: String!, $language: String!, $after: String = null, $first: Int = null) {\n      search(\n        where: {\n          AND: &#91;\n            {\n              name: \"_path\"\n              value: \"{C36E43FB-882B-4848-869F-5045CB508173}\"\n              operator: CONTAINS\n            }\n            { name: \"_language\"  value: $language }\n            { name: \"_hasLayout\" value: \"true\" }\n            {\n              name: \"_templates\"\n              value: \"{F9419E6E-D1A9-4CA0-9D88-A1E5CC7B5731}\"\n              operator: CONTAINS\n            }\n            {\n              OR:&#91;\n                {\n                  name: \"title\"\n                  value: $keyword\n                  operator: CONTAINS\n                }\n                {\n                  name: \"content\"\n                  value: $keyword\n                  operator: CONTAINS\n                }\n              ]\n            }\n            ${\n              !taxonomies.length\n                ? ''\n                : `\n              {\n                AND:&#91;\n                  ${taxonomies.map(\n                    (tags) =&gt; `\n                  {\n                    OR:&#91;\n                      ${tags\n                        .map(\n                          (tag) =&gt; `\n                      {\n                        name: \"sxaTags\"\n                        value: \"${tag}\"\n                        operator: CONTAINS\n                      }\n                      `\n                        )\n                        .join()}\n                    ]\n                  }\n                  `\n                  )}\n                ]\n              }\n              `\n            }\n          ]\n        }\n        after: $after\n        first: $first\n      ){\n        pageInfo{\n          endCursor\n          hasNext\n        }\n        results{\n          ...on PostPage{\n            url{\n              path\n            }\n            sxaTags{\n              ...on MultilistField{\n                targetItems{\n                  ...on Tag{\n                    name\n                    parent{\n                      name\n                    }\n                  }\n                }\n              }\n            }\n            title{\n              value\n            }\n          }\n        }\n        total\n      }\n    }\n`;\n};\n\nexport interface GraphQLContentSearchResult {\n  url: {\n    path: string;\n  };\n  sxaTags: {\n    targetItems: GraphQLSxaTag&#91;];\n  };\n  title: TextField;\n}\n\nexport type GraphQLContentSearchResponse = {\n  search: {\n    pageInfo: PageInfo;\n    results: GraphQLContentSearchResult&#91;];\n    total: number;\n  };\n}; <\/code><\/pre>\n\n\n\n<p>Step 3: Create the Component<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import React, { useCallback, useEffect, useMemo, useState } from 'react';\nimport { Text, Image as JssImage } from '@sitecore-jss\/sitecore-jss-nextjs';\nimport NextLink from 'next\/link';\nimport {\n  generateContentSearchQuery,\n  GraphQLContentSearchResult,\n  GraphQLContentSearchResponse,\n} from '..\/..\/graphql\/graphql-content-search';\nimport {\n  ContentTagsQuery,\n  ContentTaxonomy,\n  GraphQLContentTagsResponse,\n} from '..\/..\/graphql\/graphql-content-tags';\nimport { NextRouter, useRouter } from 'next\/router';\nimport graphqlClientFactory from 'lib\/graphql-client-factory';\nimport { useI18n } from 'next-localization';\n\ntype ContentListProps = {\n  contents: GraphQLContentSearchResult&#91;];\n  ctaText: string;\n};\n\nconst List = (props: ContentListProps): JSX.Element =&gt; (\n  &lt;div&gt;\n    {props.contents.map((item, index) =&gt; (\n      &lt;NextLink key={index} href={item.url.path}&gt;\n        &lt;div&gt;{item.sxaTags.targetItems.map((tag) =&gt; tag.name).join(',')}&lt;\/div&gt;\n        &lt;div&gt;\n          &lt;Text field={item.title} \/&gt;\n        &lt;\/div&gt;\n        &lt;div&gt;{props.ctaText}&lt;\/div&gt;\n      &lt;\/NextLink&gt;\n    ))}\n  &lt;\/div&gt;\n);\n\nexport const SEARCH_PAGE = '\/search';\nexport const KEYWORD_PARAM = 'q';\n\nconst getQueryList = (router: NextRouter, queryKey: string): string&#91;] =&gt;\n  ((router?.query&#91;queryKey] as string) ?? '').split(',').filter((tag) =&gt; tag);\n\nconst ContentSearch = () =&gt; {\n  const router = useRouter();\n  const { t } = useI18n();\n  const &#91;contentTaxonomies, setContentTaxonomies] = useState&lt;ContentTaxonomy&#91;]&gt;(&#91;]);\n  const &#91;contentItems, setContentItems] = useState&lt;GraphQLContentSearchResult&#91;]&gt;(&#91;]);\n  const q = useMemo(() =&gt; (router?.query&#91;KEYWORD_PARAM] as string) ?? '', &#91;router?.query]);\n  const queryTaxonomies = useMemo(\n    () =&gt;\n      contentTaxonomies.map((item) =&gt; ({\n        taxonomy: item.name,\n        tags: getQueryList(router, item.name).map((x) =&gt; ({\n          name: x,\n          id: item.children.tags.find((c) =&gt; c.name == x)?.id ?? '',\n        })),\n      })),\n    &#91;router?.query]\n  );\n  const gqlTaxonomies = useMemo(\n    () =&gt; queryTaxonomies.map((x) =&gt; x.tags.map((tag) =&gt; tag.id)),\n    &#91;router?.query]\n  );\n  const language = useMemo(() =&gt; router.locale ?? 'en', &#91;router?.locale]);\n  const &#91;after, setAfter] = useState('');\n  const &#91;hasMore, setHasMore] = useState(false);\n  const defaultItemsPerQuery = 12;\n\n  const getContentTaxonomies = useCallback(async () =&gt; {\n    const graphQLClient = graphqlClientFactory();\n    const result = await graphQLClient\n      .request&lt;GraphQLContentTagsResponse&gt;(ContentTagsQuery, {\n        language,\n      })\n      .then((res) =&gt; res);\n    if (result.contentSearchTags?.children?.taxonomies) {\n      setContentTaxonomies(result.contentSearchTags.children.taxonomies);\n    }\n  }, &#91;language]);\n\n  const getContents = useCallback(async () =&gt; {\n    const graphQLClient = graphqlClientFactory();\n    const result = await graphQLClient\n      .request&lt;GraphQLContentSearchResponse&gt;(generateContentSearchQuery(gqlTaxonomies), {\n        keyword: q,\n        language,\n        after,\n        first: defaultItemsPerQuery,\n      })\n      .then((res) =&gt; res);\n    setAfter(result.search.pageInfo.endCursor);\n    setHasMore(result.search.pageInfo.hasNext);\n    return result.search.results.filter((item) =&gt; item.url);\n  }, &#91;]);\n\n  const getSearchResult = useCallback(async () =&gt; {\n    const contents = await getContents();\n    setContentItems(contents);\n  }, &#91;getContents]);\n\n  const loadMore = useCallback(async () =&gt; {\n    const contents = await getContents();\n    contentItems.push(...contents);\n    setContentItems(contentItems);\n  }, &#91;getContents]);\n\n  const handleTagChange = useCallback(\n    (taxonomy: string, checked: boolean, tagName: string) =&gt; {\n      let query = `${KEYWORD_PARAM}=${q}`;\n\n      const operateItem = queryTaxonomies.find((x) =&gt; x.taxonomy == taxonomy);\n      const restItems = queryTaxonomies.filter((x) =&gt; x.taxonomy !== taxonomy &amp;&amp; x.tags.length);\n      if (operateItem) {\n        let tags = operateItem.tags.map((x) =&gt; x.name);\n        if (checked) tags.push(tagName);\n        else tags = tags.filter((tag) =&gt; tag !== tagName);\n        tags = tags.filter((tag) =&gt; tag);\n        if (tags.length) query += `&amp;${operateItem.taxonomy}=${tags.join(',')}`;\n      }\n\n      restItems.forEach((x) =&gt; {\n        query += `&amp;${x.taxonomy}=${x.tags.map((x) =&gt; x.name).join(',')}`;\n      });\n\n      router?.push(`${SEARCH_PAGE}?${query}`);\n    },\n    &#91;router]\n  );\n\n  useEffect(() =&gt; {\n    if (!!contentTaxonomies) {\n      getContentTaxonomies();\n    }\n  }, &#91;contentTaxonomies]);\n\n  useEffect(() =&gt; {\n    getSearchResult();\n  }, &#91;getSearchResult]);\n\n  return (\n    &lt;div className=\"component\"&gt;\n      &lt;div className=\"component-content\"&gt;\n        {contentTaxonomies.length &amp;&amp;\n          contentTaxonomies.map(\n            (tags, index) =&gt;\n              tags.children.tags.length &amp;&amp; (\n                &lt;&gt;\n                  &lt;div key={index}&gt;{tags.displayName}&lt;\/div&gt;\n                  &lt;div&gt;\n                    {tags.children.tags.map((tag, tagIndex) =&gt; {\n                      const queryTaxonomy = queryTaxonomies.find((x) =&gt; x.taxonomy == tags.name);\n                      const checked =\n                        queryTaxonomy &amp;&amp; queryTaxonomy.tags.find((x) =&gt; x.id == tag.id);\n                      return (\n                        &lt;&gt;\n                          &lt;label key={tagIndex}&gt;\n                            {checked ? (\n                              &lt;input\n                                type=\"checkbox\"\n                                checked\n                                value={tag.id}\n                                onClick={(e) =&gt;\n                                  handleTagChange(tags.name, e.currentTarget.checked, tag.name)\n                                }\n                              \/&gt;\n                            ) : (\n                              &lt;input\n                                type=\"checkbox\"\n                                value={tag.id}\n                                onClick={(e) =&gt;\n                                  handleTagChange(tags.name, e.currentTarget.checked, tag.name)\n                                }\n                              \/&gt;\n                            )}\n\n                            &lt;span&gt;{tag.name}&lt;\/span&gt;\n                          &lt;\/label&gt;\n                        &lt;\/&gt;\n                      );\n                    })}\n                  &lt;\/div&gt;\n                &lt;\/&gt;\n              )\n          )}\n\n        &lt;div&gt;\n          {queryTaxonomies\n            .filter((item) =&gt; item.tags.length)\n            .map((item, index) =&gt;\n              item.tags.map((tag, tagIndex) =&gt; (\n                &lt;&gt;\n                  &lt;span\n                    key={index + tagIndex}\n                    onClick={() =&gt; handleTagChange(item.taxonomy, false, tag.name)}\n                  &gt;\n                    {tag.name}\n                  &lt;\/span&gt;\n                  &lt;br \/&gt;\n                &lt;\/&gt;\n              ))\n            )}\n        &lt;\/div&gt;\n        &lt;hr \/&gt;\n        {!contentItems || !contentItems.length ? (\n          &lt;p&gt;No result.&lt;\/p&gt;\n        ) : (\n          &lt;&gt;\n            &lt;List contents={contentItems} ctaText={t('Read More') || 'read more'} \/&gt;\n            {hasMore &amp;&amp; &lt;button onClick={loadMore}&gt;{t('Load More') || 'load more'}&lt;\/button&gt;}\n          &lt;\/&gt;\n        )}\n      &lt;\/div&gt;\n    &lt;\/div&gt;\n  );\n};\n\nexport default ContentSearch; <\/code><\/pre>\n\n\n\n<p>Step 4: Integrate the Component into a page<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>import React, { ReactElement } from 'react';\nimport Head from 'next\/head';\n\nimport PageLayout from '..\/..\/components\/NonSitecore\/PageLayout';\nimport ContentSearch from '..\/..\/components\/NonSitecore\/ContentSearch';\n\nconst Search = () =&gt; &lt;ContentSearch \/&gt;;\n\nSearch.getLayout = function getLayout(page: ReactElement) {\n  return (\n    &lt;&gt;\n      &lt;Head&gt;\n        &lt;title&gt;Page - Search&lt;\/title&gt;\n      &lt;\/Head&gt;\n\n      &lt;PageLayout&gt;{page}&lt;\/PageLayout&gt;\n    &lt;\/&gt;\n  );\n};\n\nexport default Search; <\/code><\/pre>\n\n\n\n<p>Happy coding!<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Yaochang Liu Sitecore Technology MVP 2025 | Sitecore Full Specialization Certified Developer (XM Cloud | XP | CDP | Personalize | Content Hub | Order Cloud) Sitecore JSS, combined with GraphQL and SXA Tags, offers a powerful solution for implementing robust content search functionality in a headless architecture. This article will guide you through the [&hellip;]<\/p>\n","protected":false},"author":2,"featured_media":0,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[7],"tags":[],"class_list":["post-7747","post","type-post","status-publish","format-standard","hentry","category-insights"],"views":2425,"_links":{"self":[{"href":"https:\/\/www.qedge.ai\/blog\/wp-json\/wp\/v2\/posts\/7747","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.qedge.ai\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.qedge.ai\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.qedge.ai\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/www.qedge.ai\/blog\/wp-json\/wp\/v2\/comments?post=7747"}],"version-history":[{"count":4,"href":"https:\/\/www.qedge.ai\/blog\/wp-json\/wp\/v2\/posts\/7747\/revisions"}],"predecessor-version":[{"id":7754,"href":"https:\/\/www.qedge.ai\/blog\/wp-json\/wp\/v2\/posts\/7747\/revisions\/7754"}],"wp:attachment":[{"href":"https:\/\/www.qedge.ai\/blog\/wp-json\/wp\/v2\/media?parent=7747"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.qedge.ai\/blog\/wp-json\/wp\/v2\/categories?post=7747"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.qedge.ai\/blog\/wp-json\/wp\/v2\/tags?post=7747"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}