Skip to content
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

fix(404): add button for searches #869

Merged
merged 11 commits into from
Nov 18, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ const createInfobarStyles = tv({
},
})

const compoundStyles = createInfobarStyles()
export const compoundStyles = createInfobarStyles()

const Infobar = ({
title,
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export { default } from "./Infobar"
export { default, compoundStyles } from "./Infobar"
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { type NotFoundPageSchemaType } from "~/engine"
import { renderComponent } from "../../render"
import { getTailwindVariantLayout } from "~/utils"
import { compoundStyles } from "../../components/complex/Infobar/Infobar"
import { LinkButton } from "../../components/internal/LinkButton"
import { Skeleton } from "../Skeleton"
import { NotFoundSearchButton } from "./NotFoundSearchButton"

const NotFoundLayout = ({
site,
Expand All @@ -9,7 +12,13 @@ const NotFoundLayout = ({
LinkComponent,
ScriptComponent,
}: NotFoundPageSchemaType) => {
const simplifiedLayout = getTailwindVariantLayout(layout)

return (
// NOTE: This is taken from Infobar in components.
// However, we duplicated it here so that we can set the
// search button as a client component and avoid streaming over a
// huge payload to our end user
<Skeleton
site={site}
page={page}
Expand All @@ -22,18 +31,57 @@ const NotFoundLayout = ({
// but cannot be used here as tailwind does not support dynamic class names
className={`[&_.component-content]:mx-auto [&_.component-content]:max-w-screen-xl [&_.component-content]:px-6 [&_.component-content]:md:px-10`}
>
{renderComponent({
component: {
type: "infobar",
title: "404: Page not found",
description: "Sorry, the page you were looking for cannot be found",
buttonLabel: "Go to homepage",
buttonUrl: "/",
},
layout,
site,
LinkComponent,
})}
<section>
<div
className={compoundStyles.outerContainer({
layout: simplifiedLayout,
})}
>
<div
className={compoundStyles.innerContainer({
layout: simplifiedLayout,
})}
>
<div
className={compoundStyles.headingContainer({
layout: simplifiedLayout,
})}
>
<h2
className={compoundStyles.title({ layout: simplifiedLayout })}
>
Page not found
</h2>

<p
className={compoundStyles.description({
layout: simplifiedLayout,
})}
>
This page might have been moved or deleted. Try searching for
this page instead.
</p>
</div>

<div
className={compoundStyles.buttonContainer({
layout: simplifiedLayout,
})}
>
<NotFoundSearchButton LinkComponent={LinkComponent} />
<LinkButton
href="/"
size="lg"
variant="outline"
LinkComponent={LinkComponent}
isWithFocusVisibleHighlight
>
Go to homepage
</LinkButton>
</div>
</div>
</div>
</section>
</div>
</Skeleton>
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"use client"

import { useEffect, useState } from "react"

import type { NotFoundPageSchemaType } from "~/engine"
import { getWordsFromPermalink } from "~/utils"
import { LinkButton } from "../../components/internal/LinkButton"

type NotFoundSearchButtonProps = Pick<NotFoundPageSchemaType, "LinkComponent">
export const NotFoundSearchButton = ({
LinkComponent,
}: NotFoundSearchButtonProps) => {
const [permalink, setPermalink] = useState("")

useEffect(() => {
// The check for typeof window and navigator ensures this only runs in browser environments, not during server-side rendering
if (
typeof window !== "undefined" &&
typeof window.location !== "undefined"
) {
setPermalink(window.location.pathname)
}
}, [])

const missingPath = getWordsFromPermalink(permalink)

return (
<LinkButton
href={`/search?q=${missingPath}`}
size="lg"
LinkComponent={LinkComponent}
isWithFocusVisibleHighlight
>
Search for this page
</LinkButton>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { describe, expect, it } from "vitest"

import { getWordsFromPermalink } from "~/utils"

describe("getWordsFromPermalink", () => {
it("should trim out all symbols and return the permalink as a space separated sentence for single level permalinks", () => {
// Arrange
const singleLevelPermalink = "/this-._single=level|"
const expected = "this+single+level"

// Act
const actual = getWordsFromPermalink(singleLevelPermalink)

// Assert
expect(actual).toBe(expected)
})

it("should trim out all symbols and return the last section as a space separated sentence for nested permalinks", () => {
// Arrange
const nestedPermalink = "/nested/deeply/this-._nest'fff=level|"
const expected = "this+nest+fff+level"

// Act
const actual = getWordsFromPermalink(nestedPermalink)

// Assert
expect(actual).toBe(expected)
})

it("should handle uri-encoded strings correctly", () => {
// Arrange
const singleLevelPermalink = "/this-._single=level|"
const encodedPermalink = encodeURIComponent(singleLevelPermalink)
const expected = "this+single+level"

// Act
const actual = getWordsFromPermalink(encodedPermalink)

// Assert
expect(actual).toBe(expected)
})

it("should work with a trailing /", () => {
// Arrange
const singleLevelPermalink = "/this-._single=level|/"
const expected = "this+single+level"

// Act
const actual = getWordsFromPermalink(singleLevelPermalink)

// Assert
expect(actual).toBe(expected)
})
})
15 changes: 15 additions & 0 deletions packages/components/src/utils/getWordsFromPermalink.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export const getWordsFromPermalink = (permalink: string): string => {
Copy link
Contributor

Choose a reason for hiding this comment

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

checking understanding:

so https://example.com/some-page_name+ will yield some+page+name right

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yeap excatly

const trimmedPermalink = permalink.endsWith("/")
? permalink.slice(0, -1)
: permalink
const lastUrlSegment = trimmedPermalink.split("/").at(-1) ?? ""
// NOTE: Replace all non-alphanumeric characters with spaces
// then remove all spaces and join by `+`.
// This is because we might have run-on spaces from sequences of symbols
// like: `+=`, which would lead to 2 spaces
return decodeURIComponent(lastUrlSegment)
.replaceAll(/[\W_]/gi, " ")
.split(" ")
.filter((v) => !!v)
.join("+")
}
1 change: 1 addition & 0 deletions packages/components/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ export { isExternalUrl } from "./isExternalUrl"
export { orderedListSchemaBuilder } from "./orderedListSchemaBuilder"
export * from "./tailwind"
export { unorderedListSchemaBuilder } from "./unorderedListSchemaBuilder"
export { getWordsFromPermalink } from "./getWordsFromPermalink"
Loading