Skip to content

feat: zendesk search integration #2722

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 23 commits into from
Apr 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
d1dccd9
WIP: zendesk integration
LeahMarieBush Mar 20, 2025
90961a5
change name to knowledgebase
LeahMarieBush Mar 20, 2025
57b3986
Merge branch 'main' into leah/feat/zendesk
LeahMarieBush Mar 20, 2025
62e017c
update tab name and icon
LeahMarieBush Mar 21, 2025
02ed54c
add zendesk hits to global
LeahMarieBush Mar 21, 2025
f092e16
use algolia snippet compoenent
LeahMarieBush Mar 24, 2025
0d98ebf
update global hits
LeahMarieBush Mar 24, 2025
d20da69
use dev zendesk site for dev algolia index
LeahMarieBush Mar 25, 2025
f49e7f7
add external link indicator
LeahMarieBush Mar 26, 2025
cd805a1
Merge branch 'main' into leah/feat/zendesk
LeahMarieBush Mar 26, 2025
f2ec232
change to using prod zendesk url
LeahMarieBush Mar 27, 2025
d2f038f
revert
LeahMarieBush Mar 27, 2025
f70dd41
Merge branch 'main' into leah/feat/zendesk
LeahMarieBush Mar 31, 2025
005c143
make all tab only have 20 results
LeahMarieBush Mar 31, 2025
bf4b645
clean up types
LeahMarieBush Mar 31, 2025
b4753e4
more words in snippet
LeahMarieBush Mar 31, 2025
01734c1
add filters
LeahMarieBush Apr 1, 2025
9ca9261
Merge branch 'main' into leah/feat/zendesk
LeahMarieBush Apr 1, 2025
19a669a
change index id to use enum
LeahMarieBush Apr 2, 2025
0816331
Merge branch 'main' into leah/feat/zendesk
LeahMarieBush Apr 2, 2025
28ee5d9
Merge branch 'main' into leah/feat/zendesk
LeahMarieBush Apr 2, 2025
7fe62d3
update attributesToHighlight length to avoid hitting the limit
LeahMarieBush Apr 2, 2025
caf25a1
only add knowledgebase hits to global once
LeahMarieBush Apr 2, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion config/base.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
"description": "Explore {product} product documentation, tutorials, and examples."
},
"algolia": {
"unifiedIndexName": "prod_DEVDOT_omni"
"unifiedIndexName": "prod_DEVDOT_omni",
"zendeskIndexName": "prod_ZENDESK"
},
"analytics": {
"included_domains": "developer.hashi-mktg.com developer.hashicorp.com"
Expand Down
3 changes: 2 additions & 1 deletion config/preview.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
},
"dev_dot": {
"algolia": {
"unifiedIndexName": "staging_DEVDOT_omni"
"unifiedIndexName": "staging_DEVDOT_omni",
"zendeskIndexName": "dev_ZENDESK"
}
},
"flags": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@
*/

import { ReactNode, createContext, useContext, useState } from 'react'
import type { SearchContentTypes } from '../unified-search/types'

const SearchHitsContext = createContext([])

type HitCounts = Record<'docs' | 'tutorials' | 'integrations', number>
type HitCounts = Record<
Exclude<SearchContentTypes, SearchContentTypes.GLOBAL>,
number
>

/**
* Intended to provides search hit counts across content types.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*/

import { ProductSlug } from 'types/products'
import { UnifiedSearchableContentType } from '../../../types'
import { SearchContentTypes } from '../../../types'

/**
* Given an optional product slug,
Expand All @@ -18,7 +18,7 @@ import { UnifiedSearchableContentType } from '../../../types'
*/
export function getAlgoliaFilters(
productSlug?: ProductSlug,
resultType?: UnifiedSearchableContentType
resultType?: SearchContentTypes
): string {
/**
* Product filter
Expand All @@ -43,7 +43,7 @@ export function getAlgoliaFilters(
* Type filter
*/
let typeFilter = ''
if (resultType && resultType !== 'global') {
if (resultType && resultType !== SearchContentTypes.GLOBAL) {
typeFilter = `type:${resultType}`
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,12 @@ import {
import type { Hit } from 'instantsearch.js'
import type { ProductSlug } from 'types/products'
import type { SuggestedPageProps } from '../suggested-pages/types'
import type {
UnifiedSearchResults,
UnifiedSearchableContentType,
} from '../../types'
import { type UnifiedSearchResults, SearchContentTypes } from '../../types'
// Styles
import s from './dialog-body.module.css'

const ALGOLIA_INDEX_NAME = __config.dev_dot.algolia.unifiedIndexName
const ZENDESK_ALGOLIA_INDEX_NAME = __config.dev_dot.algolia.zendeskIndexName

// Initialize the algolia search client
const appId = process.env.NEXT_PUBLIC_ALGOLIA_APP_ID
Expand Down Expand Up @@ -112,11 +110,12 @@ function SearchResults({
docs: { hits: [] },
integration: { hits: [] },
tutorial: { hits: [] },
knowledgebase: { hits: [] },
})
/**
* `setHitData` allows easy updating of hits for a specific content type
*/
function setHitData(type: UnifiedSearchableContentType, hits: Hit[]) {
function setHitData(type: SearchContentTypes, hits: Hit[]) {
setUnifiedSearchResults((previous) => ({ ...previous, [type]: { hits } }))
}

Expand All @@ -136,8 +135,15 @@ function SearchResults({
{/* <InstantSearch /> updates algoliaData, and renders nothing.
Maybe helpful to think of this as "the part that fetches results". */}
<InstantSearch indexName={ALGOLIA_INDEX_NAME} searchClient={searchClient}>
{['global', 'docs', 'integration', 'tutorial'].map(
(type: UnifiedSearchableContentType) => {
{[
SearchContentTypes.GLOBAL,
SearchContentTypes.DOCS,
SearchContentTypes.INTEGRATION,
SearchContentTypes.TUTORIAL,
].map(
Comment on lines +138 to +143
Copy link
Contributor

Choose a reason for hiding this comment

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

What do you think about just using Object.values here?

Suggested change
{[
SearchContentTypes.GLOBAL,
SearchContentTypes.DOCS,
SearchContentTypes.INTEGRATION,
SearchContentTypes.TUTORIAL,
].map(
{Object.values(SearchContentTypes).map(

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I like this idea but since we are not mapping over every value (SearchContentTypes.KNOWLEDGEBASE is not here), I'm going to keep this the same for now. When we switch over to using the omni index after launch I'll update this to use Object.values tho!

Copy link
Contributor

@rmainwork rmainwork Apr 2, 2025

Choose a reason for hiding this comment

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

Oof - you're absolutely right! I thought I'd checked for this, but my eyes glanced over the missing SearchContentTypes.KNOWLEDGEBASE. Good catch!

(
type: Exclude<SearchContentTypes, SearchContentTypes.KNOWLEDGEBASE>
) => {
const filters = getAlgoliaFilters(currentProductSlug, type)
return (
<Index key={type} indexName={ALGOLIA_INDEX_NAME} indexId={type}>
Expand All @@ -147,6 +153,20 @@ function SearchResults({
)
}
)}

<Index indexName={ZENDESK_ALGOLIA_INDEX_NAME} indexId={SearchContentTypes.KNOWLEDGEBASE}>
<Configure
query={currentInputValue}
attributesToSnippet={['description:25']}
attributesToHighlight={['page_title', 'description:25']}
filters={getAlgoliaFilters(currentProductSlug, SearchContentTypes.KNOWLEDGEBASE)}
/>
<HitsReporter
setHits={(hits) =>
setHitData(SearchContentTypes.KNOWLEDGEBASE, hits)
}
/>
</Index>
</InstantSearch>
{/* UnifiedHitsContainer renders search results in a tabbed interface. */}
<UnifiedHitsContainer
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@ import Badge from 'components/badge'
import Text from 'components/text'
import { useCommandBar } from 'components/command-bar'
import s from './no-results-message.module.css'
import type { SearchContentTypes } from '../../types'

interface NoResultsMessageProps {
tabsWithResults: {
type: 'global' | 'docs' | 'tutorials' | 'integrations'
type: SearchContentTypes
heading: string
icon: ReactElement<React.JSX.IntrinsicElements['svg']>
}[]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,20 @@ import { getTutorialSlug } from 'views/collection-view/helpers'
import { getIntegrationUrl } from 'lib/integrations'
// Types
import type { Hit } from 'instantsearch.js'
import { SearchContentTypes } from '../../../types'

/**
* Builds a URL path to an arbitrary hit from
* our unified `<env>_DEVDOT_omni` Algolia indices.
*/
export function buildUrlPath(searchHit: Hit): string {
if (searchHit.type === 'docs') {
if (searchHit.type === SearchContentTypes.DOCS) {
const objectIdWithoutType = searchHit.objectID.replace('docs_', '')
return `/${objectIdWithoutType}`.replace(/\/index$/, '')
} else if (searchHit.type === 'tutorial') {
} else if (searchHit.type === SearchContentTypes.TUTORIAL) {
const { slug, defaultContext } = searchHit
return getTutorialSlug(slug, defaultContext.slug)
} else if (searchHit.type === 'integration') {
} else if (searchHit.type === SearchContentTypes.INTEGRATION) {
const {
external_only,
external_url,
Expand All @@ -39,6 +40,8 @@ export function buildUrlPath(searchHit: Hit): string {
organization: { slug: organization_slug } as $TSFixMe,
slug,
} as $TSFixMe)
} else if (searchHit.type === SearchContentTypes.KNOWLEDGEBASE) {
return `https://support.hashicorp.com/hc/${searchHit.slug}`
} else {
/**
* Something's gone wrong, this should never happen in our indexing.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,18 @@ import { productSlugsToNames } from 'lib/products'
// Types
import type { Hit, HitAttributeHighlightResult } from 'instantsearch.js'
import type { UnifiedHitProps } from '../types'
import type { SearchContentTypes } from '../../../types'

/**
* Given a search object Hit,
* Return props needed to render a UnifiedHit list item
*/
export function getUnifiedHitProps(hit: Hit): UnifiedHitProps {
// Content type, for icon
const type = hit.type as 'docs' | 'tutorial' | 'integration'
const type = hit.type as Exclude<
SearchContentTypes,
SearchContentTypes.GLOBAL
>

// Link attributes, href is also used for "breadcrumb"
const ariaLabel = `${hit.page_title}. ${hit.description}`
Expand All @@ -43,5 +47,6 @@ export function getUnifiedHitProps(hit: Hit): UnifiedHitProps {
productSlug,
titleHtml,
type,
hit,
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { isProductSlug } from 'lib/products'
// Types
import type { Hit } from 'instantsearch.js'
import type { ProductSlug } from 'types/products'
import { SearchContentTypes } from '../../../types'

/**
* Determine the "default product slug" for a provided search object.
Expand All @@ -24,7 +25,7 @@ import type { ProductSlug } from 'types/products'
*/
export function parseDefaultProductSlug(hit: Hit): ProductSlug | null {
let defaultProductSlug: ProductSlug
if (hit.type === 'tutorial') {
if (hit.type === SearchContentTypes.TUTORIAL) {
const normalizedSlug = normalizeSlugForDevDot(hit.defaultContext.section)
defaultProductSlug = isProductSlug(normalizedSlug) ? normalizedSlug : null
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,20 @@

// Icons
import { IconDot16 } from '@hashicorp/flight-icons/svg-react/dot-16'
import { IconExternalLink16 } from '@hashicorp/flight-icons/svg-react/external-link-16'
// Components
import Text from 'components/text'
import IconTile from 'components/icon-tile'
import ProductIcon from 'components/product-icon'
import LinkRegion from 'components/link-region'
import { Snippet } from 'react-instantsearch'
// Content (icons by content type)
import { tabContentByType } from '../../content'
// Types
import { UnifiedHitProps } from './types'
// Styles
import s from './unified-hit.module.css'
import { SearchContentTypes } from '../../types'

/**
* Render a card-like link item to a search hit.
Expand All @@ -28,9 +31,15 @@ export function UnifiedHit({
descriptionHtml,
productSlug,
productName,
hit,
}: UnifiedHitProps) {
return (
<LinkRegion className={s.root} href={href} ariaLabel={ariaLabel}>
<LinkRegion
className={s.root}
href={href}
ariaLabel={ariaLabel}
opensInNewTab={type === SearchContentTypes.KNOWLEDGEBASE}
>
<IconTile className={s.icon} size="small">
{tabContentByType[type].icon}
</IconTile>
Expand All @@ -42,12 +51,21 @@ export function UnifiedHit({
size={300}
weight="medium"
/>
<Text
dangerouslySetInnerHTML={{ __html: descriptionHtml }}
asElement="span"
className={s.description}
size={200}
/>
{type === SearchContentTypes.KNOWLEDGEBASE ? (
<Snippet
hit={hit}
attribute="description"
className={s.description}
/>
) : (
<Text
dangerouslySetInnerHTML={{ __html: descriptionHtml }}
asElement="span"
className={s.description}
size={200}
/>
)}

<div className={s.meta}>
{productSlug ? (
<>
Expand All @@ -62,6 +80,9 @@ export function UnifiedHit({
</>
) : null}
<div className={s.breadcrumb}>{href}</div>
{type === SearchContentTypes.KNOWLEDGEBASE && (
<IconExternalLink16 className={s.externalLink} />
)}
</div>
</div>
</LinkRegion>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,16 @@
*/

import type { ProductSlug } from 'types/products'
import type { UnifiedSearchableContentType } from '../../types'
import type { Hit } from 'instantsearch.js'
import type { SearchContentTypes } from '../../types'

export interface UnifiedHitProps {
type: Exclude<UnifiedSearchableContentType, 'global'>
type: Exclude<SearchContentTypes, SearchContentTypes.GLOBAL>
href: string
ariaLabel: string
titleHtml: string
descriptionHtml: string
productSlug: ProductSlug
productName: string
hit: Hit
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@

.description {
composes: styleNestedMarkElements;
composes: hds-typography-body-200 from global;
color: var(--token-color-foreground-primary);
display: block;
margin-top: 2px;
Expand Down Expand Up @@ -104,3 +105,9 @@
text-overflow: ellipsis;
white-space: nowrap;
}

.externalLink {
color: var(--token-color-foreground-faint);
width: 14px;
height: 14px;
}
Loading
Loading