Skip to content

Commit ab044fe

Browse files
authored
feat: Banner pathname regex (#11761)
* feat: add regex patterns and support for multiple banners * add support for expiration date * fix banner link text wrapping * fix mobile issues * simplify ternary * improve doc comments * add contributing page * fix banner color in dark mode * remove demo banners
1 parent df405f2 commit ab044fe

File tree

5 files changed

+191
-84
lines changed

5 files changed

+191
-84
lines changed
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
---
2+
title: Banners
3+
noindex: true
4+
sidebar_order: 80
5+
---
6+
7+
You can add arbitrary banners to the top of a page by adding adding an entry to the `BANNERS` array on
8+
the `banner/index.tsx` file. The `BANNERS` array is an array of objects with the following properties:
9+
10+
```typescript {filename:banner/index.tsx}
11+
type BannerType = {
12+
/** This is an array of strings or RegExps to feed into new RegExp() */
13+
appearsOn: (string | RegExp)[];
14+
/** The label for the call to action button */
15+
linkText: string;
16+
/** The destination url of the call to action button */
17+
linkURL: string;
18+
/** The main text of the banner */
19+
text: string;
20+
/** Optional ISO Date string that will hide the banner after this date without the need for a rebuild */
21+
expiresOn?: string;
22+
};
23+
```
24+
25+
You can add as many banners as you like. If you need to disable all banners, simply delete them from the array.
26+
27+
Each banner is evaluated in order, and the first one that matches will be shown.
28+
29+
Examples:
30+
31+
```typescript {filename:banner/index.tsx}
32+
// ...
33+
// appearsOn = []; // This is disabled
34+
// appearsOn = ['^/$']; // This is enabled on the home page
35+
// appearsOn = ['^/welcome/']; // This is enabled on the "/welcome" page
36+
// ...
37+
38+
const BANNERS = [
39+
// This one will take precedence over the last banner in the array
40+
// (which matches all /platforms pages), because it matches first.
41+
{
42+
appearsOn: ['^/platforms/javascript/guides/astro/'],
43+
text: 'This banner appears on the Astro guide',
44+
linkURL: 'https://sentry.io/thought-leadership',
45+
linkText: 'Get webinarly',
46+
},
47+
48+
// This one will match the /welcome page and all /for pages
49+
{
50+
appearsOn: ['^/$', '^/platforms/'],
51+
text: 'This banner appears on the home page and all /platforms pages',
52+
linkURL: 'https://sentry.io/thought-leadership',
53+
linkText: 'Get webinarly',
54+
},
55+
];
56+
57+
```
58+
59+
Optionally, you can add an `expiresOn` property to a banner to hide it after a certain date without requiring a rebuild or manual removeal.
60+
the ISO Date string should be in the format `YYYY-MM-DDTHH:MM:SSZ` to be parsed correctly and account for timezones.
61+
62+
```typescript {filename:banner/index.tsx}
63+
const BANNERS = [
64+
{
65+
appearsOn: ['^/$'],
66+
text: 'This home page banner will disappear after 2024-12-06',
67+
linkURL: 'https://sentry.io/party',
68+
linkText: 'RSVP',
69+
expiresOn: '2024-12-06T00:00:00Z',
70+
},
71+
];
72+
```
Lines changed: 10 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,18 @@
11
.promo-banner {
2+
font-size: 15px;
3+
color: #21201c;
24
background: var(--accent-yellow);
3-
padding: 0.5rem;
5+
padding: 0.5rem 1rem;
46
display: flex;
57
justify-content: center;
68
position: relative;
79
width: 100%;
810
z-index: 2;
9-
margin-top: var(--header-height);
1011
animation: slide-down 0.08s ease-out;
1112

1213
a {
1314
color: inherit;
1415
}
15-
16-
&.banner-module {
17-
border-radius: 5px;
18-
margin-bottom: 1rem;
19-
}
20-
21-
&+ :global(.hero) {
22-
margin-top: -60px;
23-
}
2416
}
2517

2618
@keyframes slide-down {
@@ -40,24 +32,18 @@
4032
align-items: center;
4133
text-align: left;
4234

43-
>img {
44-
max-height: 3rem;
45-
margin-right: 0.5rem;
46-
flex-shrink: 0;
47-
}
48-
49-
>span a {
35+
> span a {
5036
text-decoration: underline;
5137
margin-left: 0.5rem;
5238
}
5339
}
5440

5541
.promo-banner-dismiss {
5642
background: var(--flame6);
57-
height: 3rem;
58-
width: 3rem;
43+
height: 1.5rem;
44+
width: 1.5rem;
45+
font-size: 1rem;
5946
line-height: 100%;
60-
font-size: 2.5rem;
6147
border-radius: 3rem;
6248
text-align: center;
6349
position: absolute;
@@ -70,9 +56,8 @@
7056
text-decoration: none;
7157
}
7258

73-
@media (min-width: 576px) {
74-
height: 1.5rem;
75-
width: 1.5rem;
76-
font-size: 1rem;
59+
@media (max-width: 576px) {
60+
top: 1.5rem;
61+
right: 1rem;
7762
}
7863
}

src/components/banner/index.tsx

Lines changed: 102 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,64 @@
11
'use client';
22

33
import {useEffect, useState} from 'react';
4-
import Image from 'next/image';
54

65
import styles from './banner.module.scss';
76

8-
//
9-
// BANNER CONFIGURATION
10-
// This is a lazy way of doing things but will work until
11-
// we put a more robust solution in place.
12-
//
13-
const SHOW_BANNER = false;
14-
const BANNER_TEXT =
15-
'Behind the Code: A Conversation With Backend Experts featuring CEOs of Laravel, Prisma, and Supabase.';
16-
const BANNER_LINK_URL =
17-
'https://sentry.io/resources/behind-the-code-a-discussion-with-backend-experts/';
18-
const BANNER_LINK_TEXT = 'RSVP';
19-
const OPTIONAL_BANNER_IMAGE = null;
7+
type BannerType = {
8+
/** This is an array of strings or RegExps to feed into new RegExp() */
9+
appearsOn: (string | RegExp)[];
10+
/** The label for the call to action button */
11+
linkText: string;
12+
/** The destination url of the call to action button */
13+
linkURL: string;
14+
/** The main text of the banner */
15+
text: string;
16+
/** Optional ISO Date string that will hide the banner after this date without the need for a rebuild */
17+
expiresOn?: string;
18+
};
2019

20+
// BANNERS is an array of banner objects. You can add as many as you like. If
21+
// you need to disable all banners, simply delete them from the array. Each banner
22+
// is evaluated in order, and the first one that matches will be shown.
23+
//
24+
// Examples:
25+
// appearsOn = []; // This is disabled
26+
// appearsOn = ['^/$']; // This is enabled on the home page
27+
// appearsOn = ['^/welcome/']; // This is enabled on the "/welcome" page
28+
// const BANNERS = [
2129
//
22-
// BANNER CODE
23-
// Don't edit unless you need to change how the banner works.
30+
// This one will take precedence over the last banner in the array
31+
// (which matches all /platforms pages), because it matches first.
32+
// {
33+
// appearsOn: ['^/platforms/javascript/guides/astro/'],
34+
// text: 'This banner appears on the Astro guide',
35+
// linkURL: 'https://sentry.io/thought-leadership',
36+
// linkText: 'Get webinarly',
37+
// },
2438
//
39+
// // This one will match the /welcome page and all /for pages
40+
// {
41+
// appearsOn: ['^/$', '^/platforms/'],
42+
// text: 'This banner appears on the home page and all /platforms pages',
43+
// linkURL: 'https://sentry.io/thought-leadership',
44+
// linkText: 'Get webinarly',
45+
// },
46+
// ];
47+
48+
const BANNERS: BannerType[] = [
49+
/// ⚠️ KEEP THIS LAST BANNER ACTIVE FOR DOCUMENTATION
50+
// check it out on `/contributing/pages/banners/`
51+
{
52+
appearsOn: ['^/contributing/pages/banners/'],
53+
text: 'Edit this banner on `/src/components/banner/index.tsx`',
54+
linkURL: 'https://docs.sentry.io/contributing/pages/banners/',
55+
linkText: 'CTA',
56+
},
57+
];
2558

2659
const LOCALSTORAGE_NAMESPACE = 'banner-manifest';
2760

61+
// https://stackoverflow.com/questions/6122571/simple-non-secure-hash-function-for-javascript
2862
const fastHash = (input: string) => {
2963
let hash = 0;
3064
if (input.length === 0) {
@@ -52,53 +86,63 @@ const readOrResetLocalStorage = () => {
5286
}
5387
};
5488

55-
export function Banner({isModule = false}) {
56-
const [isVisible, setIsVisible] = useState(false);
57-
const hash = fastHash(`${BANNER_TEXT}:${BANNER_LINK_URL}`).toString();
58-
59-
const enablebanner = () => {
60-
setIsVisible(true);
61-
};
89+
export function Banner() {
90+
type BannerWithHash = BannerType & {hash: string};
91+
const [banner, setBanner] = useState<BannerWithHash | null>(null);
6292

6393
useEffect(() => {
64-
const manifest = readOrResetLocalStorage();
65-
if (!manifest) {
66-
enablebanner();
94+
const matchingBanner = BANNERS.find(b => {
95+
return b.appearsOn.some(matcher =>
96+
new RegExp(matcher).test(window.location.pathname)
97+
);
98+
});
99+
100+
// Bail if no banner matches this page or if the banner has expired
101+
if (
102+
!matchingBanner ||
103+
(matchingBanner.expiresOn &&
104+
new Date() > new Date(matchingBanner.expiresOn ?? null))
105+
) {
67106
return;
68107
}
69108

70-
if (manifest.indexOf(hash) === -1) {
71-
enablebanner();
109+
const manifest = readOrResetLocalStorage();
110+
const hash = fastHash(matchingBanner.text + matchingBanner.linkURL).toString();
111+
112+
// Bail if this banner has already been seen
113+
if (manifest && manifest.indexOf(hash) >= 0) {
114+
return;
72115
}
73-
});
74-
75-
return SHOW_BANNER
76-
? isVisible && (
77-
<div
78-
className={[styles['promo-banner'], isModule && styles['banner-module']]
79-
.filter(Boolean)
80-
.join(' ')}
81-
>
82-
<div className={styles['promo-banner-message']}>
83-
{OPTIONAL_BANNER_IMAGE ? <Image src={OPTIONAL_BANNER_IMAGE} alt="" /> : ''}
84-
<span>
85-
{BANNER_TEXT}
86-
<a href={BANNER_LINK_URL}>{BANNER_LINK_TEXT}</a>
87-
</span>
88-
</div>
89-
<button
90-
className={styles['promo-banner-dismiss']}
91-
role="button"
92-
onClick={() => {
93-
const manifest = readOrResetLocalStorage() || [];
94-
const payload = JSON.stringify([...manifest, hash]);
95-
localStorage.setItem(LOCALSTORAGE_NAMESPACE, payload);
96-
setIsVisible(false);
97-
}}
98-
>
99-
×
100-
</button>
101-
</div>
102-
)
103-
: null;
116+
117+
// Enable the banner
118+
setBanner({...matchingBanner, hash});
119+
}, []);
120+
121+
if (!banner) {
122+
return null;
123+
}
124+
return (
125+
<div className={[styles['promo-banner']].filter(Boolean).join(' ')}>
126+
<div className={styles['promo-banner-message']}>
127+
<span className="flex flex-col md:flex-row gap-4">
128+
{banner.text}
129+
<a href={banner.linkURL} className="min-w-max">
130+
{banner.linkText}
131+
</a>
132+
</span>
133+
</div>
134+
<button
135+
className={styles['promo-banner-dismiss']}
136+
role="button"
137+
onClick={() => {
138+
const manifest = readOrResetLocalStorage() || [];
139+
const payload = JSON.stringify([...manifest, banner.hash]);
140+
localStorage.setItem(LOCALSTORAGE_NAMESPACE, payload);
141+
setBanner(null);
142+
}}
143+
>
144+
×
145+
</button>
146+
</div>
147+
);
104148
}

src/components/docPage/index.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {getUnversionedPath} from 'sentry-docs/versioning';
99

1010
import './type.scss';
1111

12+
import {Banner} from '../banner';
1213
import {Breadcrumbs} from '../breadcrumbs';
1314
import {CodeContextProvider} from '../codeContext';
1415
import {GitHubCTA} from '../githubCTA';
@@ -75,6 +76,9 @@ export function DocPage({
7576
fullWidth ? 'max-w-none w-full' : 'w-[75ch] xl:max-w-[calc(100%-250px)]',
7677
].join(' ')}
7778
>
79+
<div className="mb-4">
80+
<Banner />
81+
</div>
7882
{leafNode && <Breadcrumbs leafNode={leafNode} />}
7983
<div>
8084
<hgroup>

src/components/home.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,9 @@ export async function Home() {
3333
return (
3434
<div className="tw-app">
3535
<Header pathname="/" searchPlatforms={[]} />
36-
<Banner />
36+
<div className="mt-[var(--header-height)]">
37+
<Banner />
38+
</div>
3739
<div className="hero max-w-screen-xl mx-auto px-6 lg:px-8 py-2">
3840
<div className="flex flex-col md:flex-row gap-4 mx-auto justify-between pt-20">
3941
<div className="flex flex-col justify-center items-start">

0 commit comments

Comments
 (0)