diff --git a/scripts/warm-cache.ts b/scripts/warm-cache.ts index ef8c98f8c8..b50e265903 100644 --- a/scripts/warm-cache.ts +++ b/scripts/warm-cache.ts @@ -13,7 +13,7 @@ import { } from 'lib/learn-client/api/collection' import { splitProductFromFilename } from 'views/tutorial-view/utils' import config from '../config/base.json' -import { activeProductSlugs } from 'lib/products' +import { productSlugs } from 'lib/products' import { ProductSlug } from 'types/products' const DEV_PORTAL_URL = config.dev_dot.canonical_base_url @@ -27,7 +27,7 @@ const fetch = createFetch(null, { timeout: 900 * 1000 }) async function warmDeveloperDocsCache() { const url = new URL('/api/revalidate', DEV_PORTAL_URL) - for (const productSlug of activeProductSlugs) { + for (const productSlug of productSlugs) { const body = JSON.stringify({ product: productSlug }) try { @@ -93,7 +93,7 @@ async function getTutorialUrlsToCache(product: ProductSlug): Promise { try { const tutorialUrls = ( await Promise.all( - activeProductSlugs.map((product: ProductSlug) => + productSlugs.map((product: ProductSlug) => getTutorialUrlsToCache(product) ) ) diff --git a/src/components/command-bar/commands/search/helpers/generate-suggested-pages.tsx b/src/components/command-bar/commands/search/helpers/generate-suggested-pages.tsx index abf1e4c84e..561235637b 100644 --- a/src/components/command-bar/commands/search/helpers/generate-suggested-pages.tsx +++ b/src/components/command-bar/commands/search/helpers/generate-suggested-pages.tsx @@ -84,7 +84,7 @@ const generateBasicSuggestedPages = (productSlug: ProductSlug) => { /** * These are pages listed after the main pages for a product, and just before a - * link to the Ttorials Library. + * link to the Tutorials Library. */ const EXTRA_PAGES: Record< Exclude, diff --git a/src/components/icon-tile/icon-tile.module.css b/src/components/icon-tile/icon-tile.module.css index b6c12ba6c9..950149c457 100644 --- a/src/components/icon-tile/icon-tile.module.css +++ b/src/components/icon-tile/icon-tile.module.css @@ -52,7 +52,9 @@ /** * Colors */ -.color-neutral { +.color-neutral, +.color-sentinel, +.color-hcp { --icon-color: var(--token-color-foreground-faint); --border-color: var(--token-color-border-primary); --background: var(--token-color-surface-faint); diff --git a/src/components/icon-tile/types.ts b/src/components/icon-tile/types.ts index 893c519ab4..39c172c41e 100644 --- a/src/components/icon-tile/types.ts +++ b/src/components/icon-tile/types.ts @@ -5,17 +5,14 @@ import { ProductSlug } from 'types/products' -export type ProductBrandColor = - | 'neutral' - | 'neutral-dark' - | Exclude +export type ProductBrandColor = 'neutral' | 'neutral-dark' | ProductSlug export interface IconTileProps { /** Pass a single child, which should be a Flight icon. For 'small' and 'medium' size, pass the 16px icon size; for other sizes pass the 24px icon size. Note that non-"color" icons will be colored using the "brandColor". */ children: React.ReactNode /** Note: the "extra-large" option is not documented in the design system. It's being used for the IconTileLogo component, as used on the /{product} view pages. */ size?: 'small' | 'medium' | 'large' | 'extra-large' - /** Optional product slug to use for brand color theming. If not provided, defaults to "neutral". Note that "sentinel" and "hcp" are not supported. */ + /** Optional product slug to use for brand color theming. If not provided, defaults to "neutral". Note that "sentinel" and "hcp" currently map to "neutral". */ brandColor?: ProductBrandColor className?: string } diff --git a/src/components/navigation-header/components/product-page-content/utils/get-nav-items.ts b/src/components/navigation-header/components/product-page-content/utils/get-nav-items.ts index 308652561c..ce7a2c27e2 100644 --- a/src/components/navigation-header/components/product-page-content/utils/get-nav-items.ts +++ b/src/components/navigation-header/components/product-page-content/utils/get-nav-items.ts @@ -123,6 +123,9 @@ export function getNavItems(currentProduct: ProductData): NavItem[] { * Tutorials * * Note: we exclude Sentinel, as it does not have tutorials yet. + * Once Sentinel tutorials are published, we can remove this exclusion. + * PR to publish Sentinel tutorials: + * https://github.com/hashicorp/tutorials/pull/2169 */ if (currentProduct.slug !== 'sentinel') { items.push({ diff --git a/src/components/sidebar/helpers/generate-product-landing-nav-items.ts b/src/components/sidebar/helpers/generate-product-landing-nav-items.ts index 620259e713..ffa0178772 100644 --- a/src/components/sidebar/helpers/generate-product-landing-nav-items.ts +++ b/src/components/sidebar/helpers/generate-product-landing-nav-items.ts @@ -72,7 +72,14 @@ export const generateProductLandingSidebarMenuItems = ( menuItems.push(introNavItem) } - // Add a "Tutorials" link for all products except sentinel + /** + * Add a "Tutorials" link for all products. + * + * Note: we exclude Sentinel, as it does not have tutorials yet. + * Once Sentinel tutorials are published, we can remove this exclusion. + * PR to publish Sentinel tutorials: + * https://github.com/hashicorp/tutorials/pull/2169 + */ if (product.slug !== 'sentinel') { menuItems.push({ title: 'Tutorials', diff --git a/src/components/sidebar/helpers/generate-resources-nav-items.ts b/src/components/sidebar/helpers/generate-resources-nav-items.ts index a2c5f4bc44..41c02353b2 100644 --- a/src/components/sidebar/helpers/generate-resources-nav-items.ts +++ b/src/components/sidebar/helpers/generate-resources-nav-items.ts @@ -131,15 +131,11 @@ function generateResourcesNavItems( return [ { heading: 'Resources' }, - ...(productSlug !== 'sentinel' - ? [ - { - // Add a "Tutorials" link for all products except Sentinel - title: 'Tutorial Library', - href: getTutorialLibraryUrl(productSlug), - }, - ] - : []), + { + // Add a "Tutorials" link for all products except Sentinel + title: 'Tutorial Library', + href: getTutorialLibraryUrl(productSlug), + }, ...getCertificationsLink(productSlug), { title: 'Community Forum', diff --git a/src/components/tutorial-card/helpers/build-aria-label.ts b/src/components/tutorial-card/helpers/build-aria-label.ts index 4767bb3d50..119b307b16 100644 --- a/src/components/tutorial-card/helpers/build-aria-label.ts +++ b/src/components/tutorial-card/helpers/build-aria-label.ts @@ -15,6 +15,7 @@ const PRODUCT_LABEL_MAP: Record = { vault: 'Vault', vagrant: 'Vagrant', waypoint: 'Waypoint', + sentinel: 'Sentinel', } export function getSpeakableDuration(duration: TutorialCardProps['duration']) { diff --git a/src/components/tutorial-collection-cards/components/card-badges/index.tsx b/src/components/tutorial-collection-cards/components/card-badges/index.tsx index 788efefb40..0001514ac6 100644 --- a/src/components/tutorial-collection-cards/components/card-badges/index.tsx +++ b/src/components/tutorial-collection-cards/components/card-badges/index.tsx @@ -24,6 +24,7 @@ const PRODUCT_ICON_MAP: Record = { vault: , vagrant: , waypoint: , + sentinel: , } /** * Map all card badge options to icons @@ -46,6 +47,7 @@ const PRODUCT_LABEL_MAP: Record = { vault: 'Vault', vagrant: 'Vagrant', waypoint: 'Waypoint', + sentinel: 'Sentinel', } /** * Map all card badge options to badge labels diff --git a/src/data/sentinel.json b/src/data/sentinel.json index 4d77499d54..e11813de8c 100644 --- a/src/data/sentinel.json +++ b/src/data/sentinel.json @@ -42,11 +42,6 @@ }, "version": "0.18.6", "subnavItems": [ - { - "url": "/sentinel/intro", - "text": "Intro", - "type": "inbound" - }, { "url": "/sentinel", "text": "Docs", diff --git a/src/lib/__tests__/products.test.ts b/src/lib/__tests__/products.test.ts deleted file mode 100644 index f6ea824c2e..0000000000 --- a/src/lib/__tests__/products.test.ts +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: MPL-2.0 - */ - -import { productSlugsToNames } from 'lib/products' - -describe('lib/products.ts', () => { - test('productSlugsToNames is in the correct order', () => { - expect(Object.keys(productSlugsToNames)).toEqual([ - 'sentinel', - 'hcp', - 'terraform', - 'packer', - 'consul', - 'vault', - 'boundary', - 'nomad', - 'waypoint', - 'vagrant', - ]) - }) -}) diff --git a/src/lib/learn-client/api/collection/fetch-product-collections.ts b/src/lib/learn-client/api/collection/fetch-product-collections.ts index 278cc70e6c..ca18b1ae62 100644 --- a/src/lib/learn-client/api/collection/fetch-product-collections.ts +++ b/src/lib/learn-client/api/collection/fetch-product-collections.ts @@ -8,6 +8,7 @@ import { uuid, ProductOption, AllCollectionsProductOptions, + ThemeOption, } from 'lib/learn-client/types' import { get, toError } from '../../index' @@ -27,18 +28,29 @@ export const PRODUCT_COLLECTION_API_ROUTE = ( * includes filtering for theme */ export async function fetchAllCollectionsByProduct( - product: AllCollectionsProductOptions + product: AllCollectionsProductOptions, + /** + * All `ProductOption` values except `sentinel` can be used as "theme" options. + * Theme is mainly used to add a product logo to various UI elements, and + * since Sentinel doesn't have a logo, it's not a valid theme option. + * + * Note: an alternative here might be to implement a `theme` option for + * Sentinel, and for now, set it to render a HashiCorp logo. This might + * be a more future-proof approach. This would require updates to `learn-api`: + * https://github.com/hashicorp/learn-api/blob/main/src/models/collection.ts#L17 + */ + theme?: Exclude | ThemeOption ): Promise { const baseUrl = PRODUCT_COLLECTION_API_ROUTE(product.slug) let route = baseUrl if (product.sidebarSort) { - const params = new URLSearchParams([ - ['topLevelCategorySort', 'true'], - ['theme', product.slug], - ]) - - route = baseUrl + `?${params.toString()}` + const params = [] + params.push(['topLevelCategorySort', 'true']) + if (theme) { + params.push(['theme', theme]) + } + route = baseUrl + `?${new URLSearchParams(params).toString()}` } const getProductCollectionsRes = await get(route) diff --git a/src/lib/learn-client/api/collection/index.ts b/src/lib/learn-client/api/collection/index.ts index aea75e5933..b15e0f7809 100644 --- a/src/lib/learn-client/api/collection/index.ts +++ b/src/lib/learn-client/api/collection/index.ts @@ -80,8 +80,19 @@ export async function getAllCollections( // check if the product option is valid, i.e. not 'cloud' or 'hashicorp' if (options?.product && themeIsProduct(options.product.slug)) { - const allCollections = await fetchAllCollectionsByProduct(options.product) - + /** + * Sentinel cannot use "theme", as the `learn-api` doesn't support a + * `sentinel` theme value. We expect authors to use `theme: hashicorp` + * on Sentinel collections. We could provide "hashicorp" here instead + * of `null`, but that might result in unexpected filtering out if + * authors use a different `theme` for any Sentinel collection. + */ + const theme = + options.product.slug === 'sentinel' ? null : options.product.slug + const allCollections = await fetchAllCollectionsByProduct( + options.product, + theme + ) collections = [...allCollections] } else { const limit = options?.limit?.toString() diff --git a/src/lib/learn-client/types.ts b/src/lib/learn-client/types.ts index 4bd1138e6e..5b08d7e2c6 100644 --- a/src/lib/learn-client/types.ts +++ b/src/lib/learn-client/types.ts @@ -226,6 +226,7 @@ export enum ProductOption { vagrant = 'vagrant', vault = 'vault', waypoint = 'waypoint', + sentinel = 'sentinel', } export enum SectionOption { diff --git a/src/lib/products.ts b/src/lib/products.ts index 2bbedbc2a3..b31f2043c6 100644 --- a/src/lib/products.ts +++ b/src/lib/products.ts @@ -15,10 +15,69 @@ import { /** * A map of product slugs to their proper noun names. * - * 🚨 NOTE: the order of this object matters for the Home page. + * 🚨 NOTE: the order of the keys in this object matters. It determines + * the order in which products are displayed in certain locations. + * The inclusion of product slugs in this map also has many side effects. + * Specifically, we iterate over the `Object.keys()` of this object + * in the following places: + * + * LANDING PAGE RELATED USES (assumes link to `/`) + * - generate-top-level-sub-nav-items.ts (all "mobile" menus) + * - getAllProductsNavItems (home page and old products dropdown) + * - PRODUCT_LINK_CARDS (for 404 pages) + * - (used on the home page) + * + * DOCS-RELATED USES (assumes link to `//docs`) + * - getStaticPaths (for `//docs` landing pages) + * - warm-cache.ts - warmDeveloperDocsCache (relates to //docs) + * - normalizeRemoteLoaderSlug (relates to //docs) + * - rewrite-tutorial-links.test.ts (devDotDocsPath) + * - getProductUrlAdjuster - this was intended for use during migration, after + * which MDX content would be updated to avoid having to rewrite links. The + * MDX content rewrite never happened, i think cause versioned docs made it + * seem impossible to execute in a reasonable way. So, authors still write + * links as if the content exists on dot-io domains. The root issue here is + * something that'll hopefully be much easier to fix once we've migrated + * content to `hashicorp/web-unified-docs`. In the meantime, it'd probably + * still be worth it to use a more specific dot-io-targeted variable. + * - rewrite-docs-url.test.ts (tests getProductUrlAdjuster) + * + * TUTORIAL-RELATED USES (assumes link to `//tutorials`) + * - warm-cache.ts - anonymous function (relates to tutorialUrls) + * - VALID_PRODUCT_SLUGS_FOR_FILTERING (for Tutorials Library sidebar filter) + * - via productSlugsToNames, really only uses slugs + * - getTutorialLandingPaths (for tutorials included in the sitemap) + * - getStaticPaths (for individual [...tutorialSlug] pages) + * - generateProductTutorialHomePaths (for //tutorials landing pages) + * - rewrite-tutorial-links.test.ts (devDotTutorialsPath) + * + * TYPE ASSERTION USES + * - isProductSlug - this feels like it might be used as generic assertion + * across content types. It might be helpful to create more specific type + * guards, eg `isProductSlugWithLogo`, `isProductSlugWithDocs`, + * `isProductSlugWithLandingPage`, `isProductSlugWithTutorials`, + * `isProductSlugWithIntegrations`, etc. + * + * LEGACY DOT-IO MIGRATION USES (assumes a dot-io site existed for the product) + * - getIsRewriteableDocsLink (and related tests) + * - rewrite-tutorial-links tests ("Links to .io home pages are not rewritten") + * + * We already have at least one instance (for HCP Vault secrets) where we've + * avoided adding to this constant because of how it's intertwined with other + * purposes. It probably makes sense for us to refactor some code so that we're + * only ever using this constant as a way to get the product name from a given + * product slug (where "product slug" is any valid product slug across any + * use case). + * + * For all other uses cases, it might feel duplicative, but one approach might + * be to explicitly declare new constants for each use case. Or maybe, since + * much of these use cases rely on data that could be encoded in our existing + * `src/data/.json` files, we'd could gather everything related to + * each product in those files, and derive the maps we need from the already + * exported `PRODUCT_DATA_MAP`. Or maybe there's some other approach that we + * could use to simplify our setup... It feels a bit convoluted right now. */ const productSlugsToNames: { [slug in ProductSlug]: ProductName } = { - sentinel: 'Sentinel', hcp: 'HashiCorp Cloud Platform', terraform: 'Terraform', packer: 'Packer', @@ -28,6 +87,7 @@ const productSlugsToNames: { [slug in ProductSlug]: ProductName } = { nomad: 'Nomad', waypoint: 'Waypoint', vagrant: 'Vagrant', + sentinel: 'Sentinel', } /** @@ -219,11 +279,6 @@ function isProductSlug(string: string): string is ProductSlug { */ const productSlugs = Object.keys(productSlugsToNames) as ProductSlug[] -/** - * An array of product slugs which are "active" on the site. Currently all but sentinel. - */ -const activeProductSlugs = productSlugs.filter((slug) => slug !== 'sentinel') - /** * Generates an array of Product objects from `productSlugs`. */ @@ -233,7 +288,6 @@ const products: Product[] = productSlugs.map((slug: ProductSlug) => { }) export { - activeProductSlugs, isProductSlug, products, productSlugs, diff --git a/src/lib/sitemap/tutorials-content-fields.ts b/src/lib/sitemap/tutorials-content-fields.ts index 146f8bf6eb..1c23ffb82b 100644 --- a/src/lib/sitemap/tutorials-content-fields.ts +++ b/src/lib/sitemap/tutorials-content-fields.ts @@ -5,7 +5,7 @@ import { getAllCollections } from 'lib/learn-client/api/collection' import { SectionOption } from 'lib/learn-client/types' -import { activeProductSlugs } from 'lib/products' +import { productSlugs } from 'lib/products' import tutorialMap from 'data/_tutorial-map.generated.json' import { ProductSlug } from 'types/products' import { getCollectionSlug } from 'views/collection-view/helpers' @@ -13,7 +13,7 @@ import { makeSitemapField } from './helpers' import { Collection as ClientCollection } from 'lib/learn-client/types' function getTutorialLandingPaths(): string[] { - return activeProductSlugs.map( + return productSlugs.map( (productSlug: ProductSlug) => `${productSlug}/tutorials` ) } diff --git a/src/pages/[productSlug]/docs/index.tsx b/src/pages/[productSlug]/docs/index.tsx index 2133afdfeb..b1d4d79b70 100644 --- a/src/pages/[productSlug]/docs/index.tsx +++ b/src/pages/[productSlug]/docs/index.tsx @@ -6,13 +6,13 @@ import { ProductSlug } from 'types/products' import { getStaticProps } from 'views/product-root-docs-path-landing/server' import ProductRootDocsPathLanding from 'views/product-root-docs-path-landing' -import { activeProductSlugs } from 'lib/products' +import { productSlugs } from 'lib/products' /** * Generates the paths for all /:productSlug/docs routes. */ const getStaticPaths = async () => { - const paths = activeProductSlugs.map((productSlug: ProductSlug) => ({ + const paths = productSlugs.map((productSlug: ProductSlug) => ({ params: { productSlug }, })) diff --git a/src/pages/[productSlug]/tutorials/[...tutorialSlug]/index.tsx b/src/pages/[productSlug]/tutorials/[...tutorialSlug]/index.tsx index befc1caa87..772d43df9c 100644 --- a/src/pages/[productSlug]/tutorials/[...tutorialSlug]/index.tsx +++ b/src/pages/[productSlug]/tutorials/[...tutorialSlug]/index.tsx @@ -17,7 +17,7 @@ import { getTutorialPagePaths, getTutorialPageProps, } from 'views/tutorial-view/server' -import { activeProductSlugs } from 'lib/products' +import { productSlugs } from 'lib/products' async function getStaticPaths(): Promise< GetStaticPathsResult @@ -38,7 +38,7 @@ async function getStaticPaths(): Promise< try { paths = ( await Promise.all( - activeProductSlugs.map(async (productSlug) => { + productSlugs.map(async (productSlug) => { // fetch paths from analytics for each product const analyticsPaths = await getStaticPathsFromAnalytics< TutorialPagePaths['params'] diff --git a/src/pages/[productSlug]/tutorials/index.tsx b/src/pages/[productSlug]/tutorials/index.tsx index c27003a235..84e2d0f878 100644 --- a/src/pages/[productSlug]/tutorials/index.tsx +++ b/src/pages/[productSlug]/tutorials/index.tsx @@ -3,7 +3,7 @@ * SPDX-License-Identifier: MPL-2.0 */ -import { GetStaticPropsContext } from 'next' +import { GetStaticPropsContext, GetStaticPropsResult } from 'next' import { LearnProductData, LearnProductSlug, ProductSlug } from 'types/products' import { getCloudTutorialsViewProps, @@ -12,7 +12,7 @@ import { } from 'views/product-tutorials-view/server' import ProductTutorialsView from 'views/product-tutorials-view' import { cachedGetProductData } from 'lib/get-product-data' -import { activeProductSlugs } from 'lib/products' +import { productSlugs } from 'lib/products' /** * Based on the array of beta product slugs, @@ -20,7 +20,7 @@ import { activeProductSlugs } from 'lib/products' * i.e. /vault/tutorials */ function generateProductTutorialHomePaths() { - const paths = activeProductSlugs.map((productSlug: ProductSlug) => ({ + const paths = productSlugs.map((productSlug: ProductSlug) => ({ params: { productSlug }, })) return paths @@ -28,19 +28,28 @@ function generateProductTutorialHomePaths() { export async function getStaticProps({ params, -}: GetStaticPropsContext<{ productSlug: LearnProductSlug }>): Promise<{ - props: ProductTutorialsViewProps -}> { +}: GetStaticPropsContext<{ productSlug: LearnProductSlug }>): Promise< + GetStaticPropsResult +> { const productData = cachedGetProductData(params.productSlug) /** * Note: `hcp` is a "product" in Dev Dot but not in Learn, * so we have to treat it slightly differently. */ - const props = - productData.slug == 'hcp' - ? await getCloudTutorialsViewProps() - : await getProductTutorialsViewProps(productData as LearnProductData) + let props + try { + const props = + productData.slug == 'hcp' + ? await getCloudTutorialsViewProps() + : await getProductTutorialsViewProps(productData as LearnProductData) + } catch (e) { + if (e.toString() === 'Error: 404 Not Found') { + return { notFound: true } + } else { + throw e + } + } return props } diff --git a/src/types/products.ts b/src/types/products.ts index 0246f875bf..b3259e0939 100644 --- a/src/types/products.ts +++ b/src/types/products.ts @@ -26,7 +26,7 @@ interface Product extends ProductMeta { slug: ProductSlug } -type LearnProductSlug = Exclude +type LearnProductSlug = Exclude /** * This is needed so that `LearnProductData` can extend both `ProductData` and @@ -34,10 +34,7 @@ type LearnProductSlug = Exclude * * "Types of property 'name' are incompatible" */ -type LearnProductName = Exclude< - ProductName, - 'HashiCorp Cloud Platform' | 'Sentinel' -> +type LearnProductName = Exclude type HcpProductName = Exclude< ProductName, diff --git a/src/views/collection-view/server.ts b/src/views/collection-view/server.ts index c9ac5a0595..4ce5ee55c7 100644 --- a/src/views/collection-view/server.ts +++ b/src/views/collection-view/server.ts @@ -164,8 +164,8 @@ export async function getCollectionPagePaths(): Promise { collectionProductSlug === 'cloud' ? 'hcp' : collectionProductSlug /** * Only build collections where the `productSlug` is a valid beta product. - * As well, for all non-HCP products, only build collections where - * `theme` matches the `productSlug`. + * As well, for all non-HCP products except Sentinel, only build collections + * where `theme` matches the `productSlug`. * * Once all products are 'onboarded' we can remove this filtering layer * for the beta products. @@ -174,8 +174,9 @@ export async function getCollectionPagePaths(): Promise { * https://app.asana.com/0/1201903760348480/1201932088801131/f */ const isCloud = collectionProductSlug == 'cloud' + const isSentinel = collectionProductSlug === 'sentinel' const themeMatches = collectionProductSlug === collection.theme - const shouldBuildCollectionPath = isCloud || themeMatches + const shouldBuildCollectionPath = isCloud || isSentinel || themeMatches if (shouldBuildCollectionPath) { paths.push({ diff --git a/src/views/tutorial-library/constants.ts b/src/views/tutorial-library/constants.ts index f6ded944e7..8bbabda43a 100644 --- a/src/views/tutorial-library/constants.ts +++ b/src/views/tutorial-library/constants.ts @@ -49,4 +49,4 @@ export const VALID_EDITION_SLUGS_FOR_FILTERING = EDITIONS.map( */ export const VALID_PRODUCT_SLUGS_FOR_FILTERING = Object.keys( productSlugsToNames -).filter((slug) => !['hcp', 'sentinel'].includes(slug)) +).filter((slug) => !['hcp'].includes(slug)) diff --git a/src/views/tutorial-view/server.ts b/src/views/tutorial-view/server.ts index 6782a0e78f..ea662bd1e8 100644 --- a/src/views/tutorial-view/server.ts +++ b/src/views/tutorial-view/server.ts @@ -195,6 +195,10 @@ export async function getTutorialPagePaths(): Promise { * for the beta products. * * @TODO once we implement the `theme` query option, remove the theme filtering + * Addendum 2024-09-13: the `theme` query option is already implemented + * for the learn-api route `/product//collections`... but maybe + * we intend to use the `/collections` learn-api route instead, which + * doesn't yet have the `theme` query option implemented. * https://app.asana.com/0/1201903760348480/1201932088801131/f */ const isCloud = productSlugFromCollection == 'cloud' diff --git a/src/views/tutorials-landing/constants.tsx b/src/views/tutorials-landing/constants.tsx index 34765b2dc9..42c48675ee 100644 --- a/src/views/tutorials-landing/constants.tsx +++ b/src/views/tutorials-landing/constants.tsx @@ -22,10 +22,7 @@ const PAGE_SUBTITLE = * ProductSection constants */ -const PRODUCT_SECTIONS_ORDER_BY_SLUG: Exclude< - ProductSlug, - 'hcp' | 'sentinel' ->[] = [ +const PRODUCT_SECTIONS_ORDER_BY_SLUG: Exclude[] = [ 'terraform', 'vault', 'consul', @@ -34,6 +31,7 @@ const PRODUCT_SECTIONS_ORDER_BY_SLUG: Exclude< 'boundary', 'vagrant', 'waypoint', + 'sentinel', ] const PRODUCT_DESCRIPTIONS = { @@ -53,6 +51,7 @@ const PRODUCT_DESCRIPTIONS = { 'Build a developer platform with a PaaS-like interface for infrastructure self-service', vagrant: 'Build, manage, and share virtual machine environments with a single workflow', + sentinel: 'Enforce policy as code for HashiCorp products', } /** diff --git a/src/views/tutorials-landing/index.tsx b/src/views/tutorials-landing/index.tsx index ff81b07ca3..bff6b3130b 100644 --- a/src/views/tutorials-landing/index.tsx +++ b/src/views/tutorials-landing/index.tsx @@ -59,34 +59,44 @@ const ProductSectionBackgroundSvg = ({ productSlug, side }) => { } const renderProductSections = (productSlugs, pageContent) => { - return productSlugs.map((productSlug: ProductSlug, index: number) => { - const productName = productSlugsToNames[productSlug] - const { certification, featuredUseCases, featuredCollections } = - pageContent[productSlug] + return productSlugs + .filter((slug) => { + /** + * Some products may not have page content defined. For example, + * after we enable Sentinel tutorials, but before content is added, + * we expect that Sentinel will not have page content. + */ + const hasPageContent = slug in pageContent + return hasPageContent + }) + .map((productSlug: ProductSlug, index: number) => { + const productName = productSlugsToNames[productSlug] + const { certification, featuredUseCases, featuredCollections } = + pageContent[productSlug] - return ( -
- - -
- ) - }) + return ( +
+ + +
+ ) + }) } const TutorialsLandingView = ({ pageContent }: $TSFixMe) => {