Skip to content

Commit

Permalink
feat(hashi-head): validate props.image, add twitter:description (#623)
Browse files Browse the repository at this point in the history
* feat(hashi-head): validate that props.image is absolute URL

* feat: throw error in dev

* doc: comment on is-absolute-url

* fix: name not property

* docs: add reference link for absolute URL in open graph

* tests: add test for image error, update descrip test

* docs: add examples to is-aboslute-url

* fix: clean up TODO comments

* chore: update changeset
  • Loading branch information
zchsh committed Jun 22, 2022
1 parent b5fc376 commit 270c679
Show file tree
Hide file tree
Showing 4 changed files with 85 additions and 8 deletions.
5 changes: 5 additions & 0 deletions .changeset/selfish-points-raise.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@hashicorp/react-head': minor
---

Validates that props.image is an absolute URL, and throws an error in development if it is not. As well, adds `twitter:description` when `description` prop is provided.
26 changes: 26 additions & 0 deletions packages/head/helpers/is-absolute-url.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
* Given a string,
* return true if the string is an absolute URL,
* or false otherwise.
*
* Uses regex so may not be 100% accurate.
* Based on https://github.com/sindresorhus/is-absolute-url
*
* @example
* // returns true, this is an absolute URL
* isAbsoluteUrl('https://www.hashicorp.com/foo/bar');
* @example
* // returns false, this is a relative path
* isAbsoluteUrl('./foo/bar');
* @example
* // returns false, this is an absolute path, but not an absolute URL
* isAbsoluteUrl('/foo/bar');
* @param string The URL to test
* @returns true if the URL is absolute, false otherwise
*/
function isAbsoluteUrl(string: string): boolean {
const regex = /^[a-zA-Z][a-zA-Z\d+\-.]*:/
return regex.test(string)
}

export default isAbsoluteUrl
18 changes: 16 additions & 2 deletions packages/head/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ describe('<Head />', () => {
metaHttpEquivHTML,
metaDefaultTagsHTML,
'<meta name="description" property="og:description" content="Page Description">',
'<meta name="twitter:description" content="Page Description">',
'</head>',
].join('')
)
Expand Down Expand Up @@ -85,19 +86,32 @@ describe('<Head />', () => {
})

it('should render and display <meta name="og:image"> tag', () => {
const { container } = renderHead(<Head image="/site-image.jpg" />)
const { container } = renderHead(
<Head image="https://www.hashicorp.com/site-image.jpg" />
)

expect(container.innerHTML).toBe(
[
'<head>',
metaHttpEquivHTML,
metaDefaultTagsHTML,
'<meta property="og:image" content="/site-image.jpg">',
'<meta property="og:image" content="https://www.hashicorp.com/site-image.jpg">',
'</head>',
].join('')
)
})

it('should throw an error in development if image is not an absolute URL', () => {
// Suppress console.error for this test, we expect an error
jest.spyOn(console, 'error')
global.console.error.mockImplementation(() => {})
expect(() => {
renderHead(<Head image="/site-image.jpg" />)
}).toThrowError()
// Restore console.error for further tests
global.console.error.mockRestore()
})

it('should render and display <link rel="preload"> tags', () => {
const { container } = renderHead(
<Head preload={[{ href: '/style.css', as: 'stylesheet' }]} />
Expand Down
44 changes: 38 additions & 6 deletions packages/head/index.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,34 @@
import Head from 'next/head'
import isAbsoluteUrl from './helpers/is-absolute-url'
import { renderMetaTags } from './seo'

export { renderMetaTags }

const IS_DEV = process.env.NODE_ENV !== 'production'

export default function HashiHead(props: HashiHeadProps): React.ReactElement {
/**
* Throw an error if props.image is a relative URL.
* It must be an absolute URL in order to work as expected as og:image.
* Reference: https://ogp.me/#url
*/
if (typeof props.image !== 'undefined' && !isAbsoluteUrl(props.image)) {
const errorMessage = `Error: HashiHead "props.image" must be an absolute URL. Non-absolute URL detected: "${props.image}". Please provide a fully qualified absolute URL or "props.image".`
if (IS_DEV) {
throw new Error(errorMessage)
} else {
/**
* TODO: should we consider alternatives to throwing an error here?
* Eg, perhaps we could log to Datadog or something rather than throw.
* However, Datadog only in hashicorp/dev-portal... maybe if we use it
* on all properties, this type of tracking could be a good fit?
* Related "Removing Sentry" discussion item:
* https://app.asana.com/0/1202347960758186/1202475860181284/f
*/
console.error(errorMessage)
}
}

return (
<Head>
{whenString(props.title, <title>{props.title}</title>)}
Expand All @@ -24,12 +49,19 @@ export default function HashiHead(props: HashiHeadProps): React.ReactElement {
<meta name="theme-color" content="#000" key="themeColor" />
{whenString(
props.description,
<meta
name="description"
property="og:description"
content={props.description}
key="description"
/>
<>
<meta
name="description"
property="og:description"
content={props.description}
key="description"
/>
<meta
name="twitter:description"
content={props.description}
key="twitterDescription"
/>
</>
)}
{whenString(
props.siteName,
Expand Down

1 comment on commit 270c679

@vercel
Copy link

@vercel vercel bot commented on 270c679 Jun 22, 2022

Choose a reason for hiding this comment

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

Please sign in to comment.