Website: Add webinar article template page. (#43627)

Changes:
- Added support for a new article category: `webinar`.
- Added a template page for webinar articles.
- Added an additional route for webinar articles that users are taken to
to watch the webinar recording.
- Added `deliver-webinar-access-request`, an action that updates CRM
records when users fill out the form on the webinar template page.
- Updated the accepted `intentSignal` values in the
create-historical-event helper.
- Added an article for the "Beyond the hype, practical AI for device
management" webinar.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Public webinar pages (/webinars/:slug and /watch) with optional
embedded video and a new page template, script, and styles.
* Sidebar signup form (first name, last name, work email) with prefill
for signed-in users and improved scroll behavior.
* POST API to request webinar access: validates email domain, records a
webinar-request event, triggers background CRM sync, and returns a watch
view on success.
* Static-site build now recognizes webinar articles and enforces
embedded-video URL validation.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Eric 2026-04-16 09:14:28 -05:00 committed by GitHub
parent 0cf1ea7ca8
commit be14f7c10d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 1139 additions and 3 deletions

View file

@ -0,0 +1,26 @@
## Moving from manual engineering workflows to AI
AI promises to transform IT—but what does that actually look like for teams managing devices at scale? This session brings together practitioners from Fleet and Foursquare to share hard-won lessons from modernizing their device management practices: consolidating tools for radical visibility across platforms, adopting GitOps to bring stability and accountability to fleet operations, and using AI to accelerate engineering output without adding headcount.
If you're leading an IT or corporate engineering team and wondering how to move from fragmented, manual device management toward a more programmable, resilient, efficient, and auditable approach—this is the session for you.
### What you will learn
- Why consolidating onto a single, cross-platform device management solution changes how your team sees and operates its entire environment.
- How GitOps reduces risk and improves change management—and why it's a prerequisite for using AI safely.
- How AI tools empower your team to do more—without replacing the judgment and expertise they bring.
- How these practices give smaller teams the ability to scale and operate like larger ones.
As device management complexity grows, IT and platform teams are rethinking how they work, not just what tools they use. Join Allen Houchins (Fleet), Harley Williams (Senior Systems Engineer), and Mike Meyer (Senior Manager of Corporate Engineering) from Foursquare for a practical look at how consolidating tooling, adopting GitOps, and integrating AI have changed the way their teams operate. You'll walk away with real-world lessons on what worked, what didn't, and how these shifts are reshaping the future of device management engineering.
<meta name="articleTitle" value="Beyond the hype, practical AI for device management">
<meta name="authorFullName" value="n/a">
<meta name="authorGitHubUsername" value="fleet-release">
<meta name="category" value="webinar">
<meta name="publishedOn" value="2026-04-15">
<meta name="description" value="Watch Fleet and Foursquare practitioners share lessons on consolidating device management tools, adopting GitOps, and using AI to scale IT operations.">
<meta name="webinarEmbeddedVideoUrl" value="https://player.mediadelivery.net/play/637410/cb9dd085-6316-48cb-b9d0-9a0257673ff9">

View file

@ -59,6 +59,7 @@ We use `<meta>` tags in Markdown articles to set metadata information about the
- `comparison` - Articles that present a comparison between Fleet and a competing product. Articles in this category are only visible in the list of all articles at [fleetdm.com/blog](/blog)
- `articles` - A catch-all category for articles and blog posts that do not fit into other categories. Articles in this category are only visible in the list of all articles at [fleetdm.com/blog](/blog)
- `whitepaper` - Articles that offer a downloadable PDF that is gated behind the user filling out a form. Articles in this category are available at [fleetdm.com/whitepapers](/whitepapers)
- `webinar` - Articles that offer a recording of a webinar hosted by Fleet that is gated behind the user filling out a form.
- `publishedOn`: An ISO 8601 formatted date (YYYY-MM-DD) of the articles publish date. If the article is a guide, this value should be updated whenever a change to the guide is made.
- Optional meta tags:
- `articleImageUrl`: A relative link to a cover image for the article used for social share previews. If provided, the image needs to live in the /website/assets/images/articles folder.
@ -179,6 +180,12 @@ Whitepaper articles use a separate article template that requires additional `<m
```
### Webinar article meta tags
Webinar articles use a separate article template and require one additional meta tag:
- `webinarEmbeddedVideoUrl` : The URL of the webinar video. This will be embedded on a page that users are taken to after filling out the form to watch a webinar.
## Linking to a location on GitHub
When adding a link to any text in the docs, handbook, or website always be sure to use the canonical form of the URL (e.g. _"https//www.fleetdm.com/

View file

@ -0,0 +1,66 @@
module.exports = {
friendlyName: 'View basic webinar',
description: 'Display "Basic webinar" page.',
inputs: {
slug: {
type: 'string',
description: 'The slug of the webinar that will be displayed to the user',
required: true,
}
},
exits: {
success: {
viewTemplatePath: 'pages/articles/basic-webinar'
},
badConfig: {
responseType: 'badConfig'
},
notFound: {
responseType: 'notFound'
},
},
fn: async function ({slug}) {
if (!_.isObject(sails.config.builtStaticContent) || !_.isArray(sails.config.builtStaticContent.markdownPages) || !sails.config.builtStaticContent.markdownPages) {
throw {badConfig: 'builtStaticContent.markdownPages'};
}
let thisPage = _.find(sails.config.builtStaticContent.markdownPages, { url: '/webinars/'+encodeURIComponent(slug) });
if (!thisPage) {
throw 'notFound';
}
let pageTitleForMeta;
let pageDescriptionForMeta;
if(thisPage.meta.articleTitle) {
pageTitleForMeta = thisPage.meta.articleTitle;
}
if(thisPage.meta.description) {
pageDescriptionForMeta = thisPage.meta.description;
}
// Respond with view.
return {
pageTitleForMeta,
pageDescriptionForMeta,
path: require('path'),
thisPage: thisPage,
};
}
};

View file

@ -0,0 +1,77 @@
module.exports = {
friendlyName: 'Deliver webinar access request',
description: '',
inputs: {
firstName: {type: 'string', required: true },
lastName: {type: 'string', required: true },
emailAddress: {type: 'string', required: true },
webinarName: {type: 'string', required: true },
},
exits: {
success: {description: 'A users webinar access request was successfully submitted.'},
invalidEmailDomain: {
description: 'This email address is on a denylist of domains and was not delivered.',
responseType: 'badRequest'
},
},
fn: async function ({firstName, lastName, emailAddress, webinarName}) {
let emailDomain = emailAddress.split('@')[1];
if(_.includes(sails.config.custom.bannedEmailDomainsForWebsiteSubmissions, emailDomain.toLowerCase())){
throw 'invalidEmailDomain';
}
// If the submitter has a marketing attribution cookie, send the details when creating/updating a contact/account/historical record.
let attributionCookieOrUndefined = this.req.cookies.marketingAttribution;
sails.helpers.flow.build(async ()=>{
let recordIds = await sails.helpers.salesforce.updateOrCreateContactAndAccount.with({
emailAddress: emailAddress,
firstName: firstName,
lastName: lastName,
contactSource: 'Website - Contact forms',
description: `Submitted a form to watch the ${webinarName} webinar.`,
marketingAttributionCookie: attributionCookieOrUndefined
}).intercept((err)=>{
return new Error(`Could not create/update a contact or account. Full error: ${require('util').inspect(err)}`);
});
// If the Contact record returned by the updateOrCreateContactAndAccount does not have a parent Account record, throw an error to stop the build helper.
if(!recordIds.salesforceAccountId) {
throw new Error(`Could not create historical event. The contact record (ID: ${recordIds.salesforceContactId}) returned by the updateOrCreateContactAndAccount helper is missing a parent account record.`);
}
// Create the new Fleet website page view record.
await sails.helpers.salesforce.createHistoricalEvent.with({
salesforceAccountId: recordIds.salesforceAccountId,
salesforceContactId: recordIds.salesforceContactId,
eventType: 'Intent signal',
intentSignal: 'Requested webinar recording',
eventContent: webinarName,
}).intercept((err)=>{
return new Error(`Could not create an historical event. Full error: ${require('util').inspect(err)}`);
});
}).exec((err)=>{
if(err){
sails.log.warn(`Background task failed: When a user (email: ${emailAddress} submitted a form to watch the ${webinarName} webinar, a Contact/Account/website activity record could not be created/updated in the CRM.`, require('util').inspect(err));
}
return;
});//_∏_
// All done.
return;
}
};

View file

@ -66,6 +66,7 @@ module.exports = {
'Signed up for a fleetdm.com account',
'Requested whitepaper download',
'Created a quote for a self-service Fleet Premium license',
'Requested webinar recording',
]
},
eventContent: {

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,76 @@
parasails.registerPage('basic-webinar', {
// ╦╔╗╔╦╔╦╗╦╔═╗╦ ╔═╗╔╦╗╔═╗╔╦╗╔═╗
// ║║║║║ ║ ║╠═╣║ ╚═╗ ║ ╠═╣ ║ ║╣
// ╩╝╚╝╩ ╩ ╩╩ ╩╩═╝ ╚═╝ ╩ ╩ ╩ ╩ ╚═╝
data: {
formData: {
emailAddress: undefined,
firstName: undefined,
lastName: undefined,
},
formRules: {
emailAddress: {isEmail: true, required: true},
firstName: {required: true},
lastName: {required: true},
},
formDataToPrefillForLoggedInUsers: {},
formErrors: {},
syncing: false,
cloudError: '',
cloudSuccess: '',
scrollDistance: undefined,
},
// ╦ ╦╔═╗╔═╗╔═╗╦ ╦╔═╗╦ ╔═╗
// ║ ║╠╣ ║╣ ║ ╚╦╝║ ║ ║╣
// ╩═╝╩╚ ╚═╝╚═╝ ╩ ╚═╝╩═╝╚═╝
beforeMount: function() {
if(this.me){// prefill from database
this.formDataToPrefillForLoggedInUsers.emailAddress = this.me.emailAddress;
this.formDataToPrefillForLoggedInUsers.firstName = this.me.firstName;
this.formDataToPrefillForLoggedInUsers.lastName = this.me.lastName;
this.formData = _.clone(this.formDataToPrefillForLoggedInUsers);
}
},
mounted: async function() {
this.formData.webinarName = this.thisPage.meta.articleTitle;
// Add an event listener to add a class to the right sidebar when the header is hidden.
window.addEventListener('scroll', this.handleScrollingInArticle);
},
// ╦╔╗╔╔╦╗╔═╗╦═╗╔═╗╔═╗╔╦╗╦╔═╗╔╗╔╔═╗
// ║║║║ ║ ║╣ ╠╦╝╠═╣║ ║ ║║ ║║║║╚═╗
// ╩╝╚╝ ╩ ╚═╝╩╚═╩ ╩╚═╝ ╩ ╩╚═╝╝╚╝╚═╝
methods: {
submittedDownloadForm: async function () {
if(typeof qualified !== 'undefined') {
qualified('saveFormData',
{
email: this.formData.emailAddress,
name: this.formData.firstName+' '+this.formData.lastName,
});
qualified('showFormExperience', 'experience-1772126772950');
}
this.syncing = true;
this.goto(this.thisPage.url+'/watch');
},
handleScrollingInArticle: function () {
let rightNavBar = document.querySelector('div[purpose="right-sidebar"]');
let scrollTop = window.pageYOffset;
let windowHeight = window.innerHeight;
// Add/remove the 'header-hidden' class to the right sidebar to scroll it upwards with the website's header.
if (rightNavBar) {
if (scrollTop > this.scrollDistance && scrollTop > windowHeight * 1.5) {
rightNavBar.classList.add('header-hidden');
this.lastScrollTop = scrollTop;
} else if(scrollTop < this.lastScrollTop - 60) {
rightNavBar.classList.remove('header-hidden');
this.lastScrollTop = scrollTop;
}
}
this.scrollDistance = scrollTop;
},
}
});

View file

@ -108,3 +108,4 @@
@import 'pages/articles/basic-whitepaper.less';
@import 'pages/landing-pages/replace-jamf.less';
@import 'pages/partners.less';
@import 'pages/articles/basic-webinar.less';

View file

@ -0,0 +1,749 @@
#basic-webinar {
h1 {
font-size: 36px;
}
hr {
margin-top: 40px;
margin-bottom: 40px;
border-top: 2px solid @core-vibrant-blue-15;
width: 100%;
}
.markdown-heading:hover {
.markdown-link {
height: 16px;
vertical-align: middle;
margin-left: 8px;
content: url('/images/icon-link-green-16x16@2x.png');
}
}
@keyframes copiedText {
0% {opacity: 0;}
20% {opacity: 100;}
30% {opacity: 80;}
50% {opacity: 60;}
70% {opacity: 40;}
80% {opacity: 20;}
100% {opacity: 0;}
}
[purpose='page-container'] {
max-width: 1200px;
padding: 64px;
}
[purpose='embedded-video'] {
position: relative;
margin-bottom: 40px;
width: 100%;
padding-bottom: 56%;
border-radius: 12px;
iframe {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border: none;
// border-radius: 12px;
}
[purpose='video-note'] {
position: absolute;
bottom: 0;
max-width: 480px;
margin-left: auto;
margin-right: auto;
padding: 16px;
border-radius: 8px;
border: 1px solid #E6E3D0;
background: #FFFEF9;
p {
margin-bottom: 0px;
}
}
}
[purpose='right-sidebar'] {
position: sticky;
top: 144px;
width: 392px;
font-size: 14px;
transition-property: transform;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 500ms;
}
[purpose='form-container'] {
border-radius: 16px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
padding: 32px;
background: #F8F8F8;
h4 {
color: #192147;
/* Heading/H4 */
font-family: Inter;
font-size: 20px;
font-style: normal;
font-weight: 700;
line-height: 120%;
margin-bottom: 0px;
}
}
[purpose='webinar-recording-form'] {
display: flex;
scroll-margin-top: 130px;
flex-direction: column;
justify-content: center;
align-items: flex-start;
gap: 32px;
label {
color: #192147;
/* Body SM (bold) */
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 700;
line-height: 150%;
}
input {
display: flex;
height: 36px;
padding: 0 12px;
align-items: center;
gap: 8px;
align-self: stretch;
border-radius: 6px;
border: 1px solid #C5C7D1;
background: #FFF;
}
}
[purpose='watch-button'] {
display: flex;
height: 36px;
padding: 16px 12px;
justify-content: center;
align-items: center;
gap: 4px;
flex: 1 0 0;
color: #FFF;
text-align: center;
/* Body SM (bold) */
font-family: Inter;
font-size: 14px;
font-style: normal;
font-weight: 700;
line-height: 150%;
width: 100%;
}
[purpose='watch-cta'] {
display: flex;
flex-direction: column;
align-items: center;
[purpose='watch-button'] {
max-width: 255px;
}
}
.header-hidden { // For scrolling the sidebar with the sticky header
transform: translateY(-120px);
}
[purpose='article-container'] {
max-width: 800px;
padding-right: 64px;
display: flex;
flex-direction: column;
min-width: 0px;
}
[purpose='article-title-and-image'] {
display: flex;
flex-direction: row;
gap: 64px;
align-items: center;
}
[purpose='article-image'] {
width: calc(~'50% - 32px');
img {
height: auto;
width: 100%;
}
}
[purpose='article-title-and-introduction'] {
width: 50%;
padding-top: 64px;
margin-bottom: 64px;
display: flex;
flex-direction: column;
gap: 32px;
h1 {
color: #192147;
/* Heading/H1 */
font-family: Inter;
font-size: 48px;
font-style: normal;
font-weight: 800;
line-height: 120%;
margin-bottom: 0px;
}
h2 {
font-size: 24px;
font-weight: 400;
line-height: 32px;
}
h4 {
color: #515774;
font-feature-settings: 'salt' on, 'ss01' on, 'ss02' on;
font-family: 'Roboto Mono';
font-size: 16px;
font-style: normal;
font-weight: 400;
line-height: 150%;
text-transform: uppercase;
margin-bottom: 0px;
margin-top: 0px;
}
[purpose='article-introduction'] {
display: flex;
flex-direction: column;
gap: 32px;
p {
margin-bottom: 0px;
}
}
}
[purpose='article-content'] {
word-wrap: break-word;
h1:first-of-type {
display: none;
}
h1:first-of-type + h2:first-of-type {
display: none;
}
img {
max-width: 100%;
width: auto;
height: auto;
max-height: 100%;
margin-left: auto;
margin-right: auto;
padding: 24px 0px 16px 0px;
}
h2 {
color: #192147;
/* Heading/H2 */
font-family: Inter;
font-size: 32px;
font-style: normal;
font-weight: 800;
line-height: 120%;
margin-bottom: 32px;
}
h3 {
color: #192147;
/* Heading/H3 */
font-family: Inter;
font-size: 24px;
font-style: normal;
font-weight: 800;
line-height: 120%;
margin-top: 40px;
margin-bottom: 32px;
}
h4 {
font-size: 16px;
margin-top: 24px;
margin-bottom: 24px;
}
h2 > code:not(.bash):not(.nohighlight):not(.mermaid) {
padding: 0px 4px;
font-weight: 400;
font-size: 22px;
line-height: inherit;
}
h3 > code:not(.bash):not(.nohighlight):not(.mermaid) {
padding: 0px 4px;
font-weight: 400;
font-size: 18px;
line-height: inherit;
}
p {
font-size: 16px;
line-height: 150%;
color: #515774;
margin-bottom: 32px;
}
img + em { // Image captions
position: relative;
top: -12px;
display: flex;
align-items: center;
flex-direction: column;
margin-bottom: 16px;
}
ul, ol {
padding-left: 16px;
}
li {
// padding-bottom: 16px;
}
li::marker {
color: #515774;
}
.markdown-heading {
scroll-margin-top: 140px;
}
[purpose='checklist-item'] {
display: flex;
input[type='checkbox'] {
margin-left: -18px;
min-width: 16px;
min-height: 16px;
align-self: center;
}
input:checked, input:active {
accent-color: @core-vibrant-blue;
}
[purpose='task'] {
margin-left: 16px;
input {
display: none;
}
p {
margin-bottom: 0px;
}
}
}
[purpose='checklist-item']::marker {
display: none;
}
ul {
list-style-type: disc;
padding-left: 16px;
margin-bottom: 32px;
}
li {
line-height: 24px;
// padding-bottom: 16px;
p { // Making sure our list items stay a consistent size if the contents get wrapped in a <p> tag coverted from Markdown.
margin-bottom: 0;
}
ol, ul { // adding padding to nested lists
padding-top: 16px;
li:last-child { // removing the padding from the last item of nested lists
padding-bottom: 0px;
}
}
}
ol {
counter-reset: custom-counter;
list-style-type: none;
padding-inline-start: 0px;
padding: 0;
margin-top: 16px;
margin-bottom: 20px;
ul > li {
text-indent: 0px;
margin-left: 0px;
}
> li {
counter-increment: custom-counter;
margin-left: 36px;
text-indent: -36px;
padding-left: 0px;
margin-bottom: 16px;
code:not(.nohighlight):not(.mermaid) {
display: inline;
}
p {
display: inline;
margin-bottom: 0px;
}
blockquote {
text-indent: 0px;
}
}
> li::before {
content: counter(custom-counter);
background-color: #E2E4EA;
width: 24px;
font-size: 13px;
display: inline-block;
border-radius: 50%;
margin-right: 10px;
padding: 2px 4px;
text-align: center;
line-height: 20px;
text-indent: 0px;
}
}
a > code:not(.hljs):not(.nohighlight):not(.mermaid) {
color: inherit;
text-decoration: inherit;
}
code:not(.bash):not(.hljs):not(.nohighlight):not(.mermaid) {
background: #F1F0FF;
padding: 4px 8px;
font-family: @code-font;
font-size: 13px;
line-height: 16px;
color: @core-fleet-black;
}
pre {
code {
background: none;
padding: 0px;
font-family: @code-font;
font-size: 13px;
line-height: 16px;
color: @core-fleet-black;
}
padding: 24px;
border: 1px solid #E2E4EA;
border-radius: 6px;
margin: 0px 0px 40px;
font-family: @code-font;
background: #F9FAFC;
white-space: break-spaces;
}
code.mermaid:not([data-processed='true']) {
opacity: 0;
}
code.mermaid {
color: @core-fleet-black-75;
margin-bottom: 16px;
font-family: @code-font;
display: inline-block;
margin-left: auto;
margin-right: auto;
background: #fff;
svg {
max-height: 600px;
}
}
blockquote {
display: block;
position: relative;
}
[purpose='quote'] {
margin: 72px 0 32px 0px;
font-style: italic;
line-height: 150%;
font-size: 20px;
max-width: 640px;
p {
margin-top: 8px;
}
}
[purpose='large-quote'] {
margin: 72px 0 32px 0px;
font-style: italic;
line-height: 150%;
margin-bottom: 24px;
font-size: 16px;
p:last-of-type {
margin-bottom: 0px;
}
strong {
color: #515774;
font-family: Inter;
font-size: 12px;
font-style: normal;
font-weight: 700;
line-height: 18px;
}
}
[purpose='large-quote']::before, [purpose='quote']::before {
content: ' ';
background-image: url('/images/icon-quote-21x17@2x.png');
background-repeat: no-repeat;
background-size: 21px 17px;
display: block;
position: relative;
top: -16px;
left: 0px;
width: 21px;
height: 17px;
}
[purpose='attribution-quote'], [purpose='large-quote'] {
margin: 72px 0 32px 0px;
p {
font-style: italic;
line-height: 150%;
margin-bottom: 24px;
font-size: 20px;
}
p:last-of-type {
margin-bottom: 0px;
}
strong {
color: #515774;
font-family: Inter;
font-size: 12px;
font-style: normal;
font-weight: 700;
line-height: 18px;
}
}
[purpose='attribution-quote']::before {
content: ' ';
background-image: url('/images/icon-quote-21x17@2x.png');
background-repeat: no-repeat;
background-size: 21px 17px;
display: block;
position: relative;
top: -16px;
left: 0px;
width: 21px;
height: 17px;
}
[purpose='checklist'] {
margin-bottom: 32px;
p {
font-size: 16px;
line-height: 150%;
font-style: normal;
font-weight: 400;
padding-left: 40px;
text-indent: -40px;
margin-bottom: 16px;
padding-top: 12px;
padding-bottom: 12px;
&:last-of-type {
margin-bottom: 0px;
}
}
p::before {
content: ' ';
background-image: url('/images/icon-checkbox-checked-24x24@2x.png');
background-size: 24px 24px;
display: inline-block;
position: relative;
top: 7px;
margin-right: 16px;
width: 24px;
height: 24px;
}
}
[purpose='tip'] {
margin: 16px 0 32px;
background: #F7F7FC;
border: 1px solid @core-vibrant-blue-50;
padding: 16px;
border-radius: 8px;
display: flex;
img {
display: flex;
margin: 4px 12px 0 0;
height: 16px;
width: 16px;
padding: 0px;
}
p {
display: block;
margin-bottom: 16px;
line-height: 24px;
font-size: 16px;
}
p:only-child, p:last-child {
margin-bottom: 0px;
}
ul:last-child {
margin-bottom: 0px;
}
li:last-child {
padding-bottom: 0px;
}
:first-child {// Remove top padding from the first element inside purpose="tip" blockquotes
padding-top: 0;
}
}
[purpose='embedded-content'] {
position: relative;
margin-bottom: 40px;
width: 100%;
padding-bottom: 57%;
iframe {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
}
[purpose='embedded-document'] {
margin-bottom: 40px;
width: 100%;
height: 800px;
}
table {
table-layout: auto;
border: 1px solid @border-lt-gray;
border-collapse: collapse;
font-size: 16px;
line-height: 28px;
margin-bottom: 16px;
max-width: 100%;
th {
font-weight: @bold;
border: 1px solid @border-lt-gray;
padding: 8px 7px 7px 8px;
}
td {
border: 1px solid @border-lt-gray;
padding: 8px 7px 7px 8px;
}
}
}
[purpose='social-share-and-edit-buttons'] {
padding-top: 24px;
margin-top: 64px;
border-top: 1px solid #E2E4EA;
[purpose='social-share-buttons'] {
gap: 16px;
img {
width: 20px;
height: 20px;
padding: 0;
}
}
[purpose='edit-link'] {
a {
font-size: 14px;
line-height: 20px;
display: flex;
flex-direction: row;
align-items: center;
gap: 12px;
text-decoration: none;
img {
width: 16px;
padding: 0;
height: 16px;
}
}
}
}
@media (max-width: 991px) {
[purpose='page-container'] {
max-width: 1200px;
padding: 48px;
}
[purpose='right-sidebar'] {
margin-top: 64px;
margin-bottom: 48px;
width: 100%;
}
.header-hidden {
transform: translateY(0);
}
[purpose='article-container'] {
max-width: 100%;
padding-right: 0px;
display: flex;
flex-direction: column;
}
[purpose='article-content'] {
padding-bottom: 0px;
}
[purpose='webinar-recording-form'] {
max-width: 324px;
}
[purpose='article-title-and-introduction'] {
margin-bottom: 0px;
padding-top: 0px;
width: 100%;
}
[purpose='article-title-and-image'] {
display: flex;
flex-direction: column;
gap: 48px;
justify-content: center;
align-items: center;
}
[purpose='article-image'] {
width: 100%;
img {
height: unset;
width: 100%;
}
}
}
@media (max-width: 769px) {
[purpose='article-title'] {
padding-top: 40px;
margin-bottom: 8px;
h1 {
font-size: 28px;
line-height: 38px;
}
}
[purpose='article-content'] {
img {
padding-bottom: 0px;
}
img + em { // Image captions
top: 4px;
margin-bottom: 16px;
}
}
}
@media (max-width: 575px) {
[purpose='page-container'] {
padding: 32px 24px;
}
[purpose='article-title-and-introduction'] {
h1 {
color: #192147;
/* Heading/H1 */
font-family: Inter;
font-size: 32px;
font-style: normal;
font-weight: 800;
line-height: 120%;
}
}
[purpose='article-content'] {
h2 {
color: #192147;
/* Heading/H2 */
font-family: Inter;
font-size: 24px;
font-style: normal;
font-weight: 800;
line-height: 120%;
}
h3 {
color: #192147;
/* Heading/H3 */
font-family: Inter;
font-size: 20px;
font-style: normal;
font-weight: 800;
line-height: 120%;
}
}
}
}

View file

@ -77,4 +77,5 @@ module.exports.policies = {
'deliver-whitepaper-download-request': true,
'deliver-partner-registration-submission': true,
'view-partners': true,
'deliver-webinar-access-request': true,
};

View file

@ -626,6 +626,23 @@ module.exports.routes = {
}
},
'GET /webinars/:slug': {
action: 'articles/view-basic-webinar',
locals: {
displayVideo: false,
}
},
'GET /webinars/:slug/watch': {
action: 'articles/view-basic-webinar',
locals: {
displayVideo: true,
disableChatbotAndIndexing: true,
hideFooterLinks: true,
}
},
// ╦ ╔═╗╔═╗╔═╗╔═╗╦ ╦ ╦═╗╔═╗╔╦╗╦╦═╗╔═╗╔═╗╔╦╗╔═╗
// ║ ║╣ ║ ╦╠═╣║ ╚╦╝ ╠╦╝║╣ ║║║╠╦╝║╣ ║ ║ ╚═╗
// ╩═╝╚═╝╚═╝╩ ╩╚═╝ ╩ ╩╚═╚═╝═╩╝╩╩╚═╚═╝╚═╝ ╩ ╚═╝
@ -1351,6 +1368,7 @@ module.exports.routes = {
'POST /api/v1/deliver-gitops-request': { action: 'deliver-gitops-workshop-request' },
'POST /api/v1/admin/reset-one-fleet-premium-local-trial': { action: 'admin/reset-one-fleet-premium-local-trial' },
'POST /api/v1/deliver-whitepaper-download-request': { action: 'deliver-whitepaper-download-request' },
'POST /api/v1/deliver-webinar-access-request': { action: 'deliver-webinar-access-request' },
'POST /api/v1/deliver-partner-registration-submission': { action: 'deliver-partner-registration-submission' },
// ╔╦╗╦╔═╗╦═╗╔═╗╔═╗╔═╗╔═╗╔╦╗ ╔═╗╦═╗╔═╗═╗ ╦╦ ╦ ╔═╗╔╗╔╔╦╗╔═╗╔═╗╦╔╗╔╔╦╗╔═╗

View file

@ -666,7 +666,7 @@ module.exports = {
throw new Error(`Failed compiling markdown content: An article page is missing a category meta tag (<meta name="category" value="guides">) at "${path.join(topLvlRepoPath, pageSourcePath)}". To resolve, add a meta tag with the category of the article`);
} else {
// Throwing an error if the article has an invalid category.
let validArticleCategories = ['deploy', 'articles', 'security', 'engineering', 'success stories', 'announcements', 'guides', 'releases', 'podcasts', 'report', 'case study', 'comparison', 'whitepaper' ];
let validArticleCategories = ['deploy', 'articles', 'security', 'engineering', 'success stories', 'announcements', 'guides', 'releases', 'podcasts', 'report', 'case study', 'comparison', 'whitepaper', 'webinar' ];
if(!validArticleCategories.includes(embeddedMetadata.category)) {
throw new Error(`Failed compiling markdown content: An article page has an invalid category meta tag (<meta name="category" value="${embeddedMetadata.category}">) at "${path.join(topLvlRepoPath, pageSourcePath)}". To resolve, change the meta tag to a valid category, one of: ${validArticleCategories}`);
}
@ -764,6 +764,22 @@ module.exports = {
}
}
}
// If this is a webinar article, we'll check to make sure it has a webinarEmbeddedVideoUrl meta tag.
if(embeddedMetadata.category === 'webinar') {
if(!embeddedMetadata.webinarEmbeddedVideoUrl){
throw new Error(`Failed compiling markdown content: A webinar article is missing a 'webinarEmbeddedVideoUrl' meta tag at ${path.join(topLvlRepoPath, pageSourcePath)}. To resolve, add a webinarEmbeddedVideoUrl meta tag with a value that is the URL of the webinar this article is presenting, and try running this script again.`);
} else {
let parsedVideoUrl;
try {
parsedVideoUrl = URL.parse(embeddedMetadata.webinarEmbeddedVideoUrl);
} catch(err) {
throw new Error(`Failed compiling markdown content: A webinar article has an invalid "webinarEmbeddedVideoUrl" value (${embeddedMetadata.webinarEmbeddedVideoUrl}) at ${path.join(topLvlRepoPath, pageSourcePath)}. Please change this value to be a valid URL of the webinar recording with no query strings.`, err);
}
if(parsedVideoUrl.search) {
throw new Error(`Failed compiling markdown content: A webinar article has a "webinarEmbeddedVideoUrl" value that contains query strings (${parsedVideoUrl.search}) at ${path.join(topLvlRepoPath, pageSourcePath)}. To resolve, remove the query strings from this value and try running this script again. `);
}
}
}
// If this is a comparison article, we will require a different set of meta tags and will determine the URL of the page using the articleSlugInCategory meta tag.
if(embeddedMetadata.category === 'comparison') {
if(!embeddedMetadata.articleSlugInCategory){
@ -780,7 +796,7 @@ module.exports = {
// If the article is categorized as 'product' we'll replace the category with 'use-cases', or if it is categorized as 'success story' we'll replace it with 'device-management'
rootRelativeUrlPath = (
'/' +
(encodeURIComponent(embeddedMetadata.category === 'success stories' ? 'success-stories' : embeddedMetadata.category === 'security' ? 'securing' : embeddedMetadata.category === 'whitepaper' ? 'whitepapers' : embeddedMetadata.category === 'case study' ? 'case-study' : embeddedMetadata.category)) + '/' +
(encodeURIComponent(embeddedMetadata.category === 'success stories' ? 'success-stories' : embeddedMetadata.category === 'security' ? 'securing' : embeddedMetadata.category === 'whitepaper' ? 'whitepapers' : embeddedMetadata.category === 'webinar' ? 'webinars' : embeddedMetadata.category === 'case study' ? 'case-study' : embeddedMetadata.category)) + '/' +
(pageUnextensionedUnwhitespacedLowercasedRelPath.split(/\//).map((fileOrFolderName) => encodeURIComponent(fileOrFolderName.replace(/^[0-9]+[\-]+/,'').replace(/\./g, '-'))).join('/'))
);
}

View file

@ -889,6 +889,7 @@
<script src="/js/pages/admin/sandbox-waitlist.page.js"></script>
<script src="/js/pages/articles/articles.page.js"></script>
<script src="/js/pages/articles/basic-article.page.js"></script>
<script src="/js/pages/articles/basic-webinar.page.js"></script>
<script src="/js/pages/articles/basic-whitepaper.page.js"></script>
<script src="/js/pages/articles/case-study.page.js"></script>
<script src="/js/pages/configuration-builder.page.js"></script>

View file

@ -0,0 +1,97 @@
<div id="basic-webinar" v-cloak>
<%if(!displayVideo){%>
<div purpose="page-container" class="container-fluid">
<div purpose="article-title-and-image">
<div purpose="article-title-and-introduction">
<div>
<h4>Webinar</h4>
<h1><%=thisPage.meta.articleTitle %></h1>
</div>
<div purpose="article-introduction">
<%if(thisPage.meta.introductionTextBlockOne){%><p><%- thisPage.meta.introductionTextBlockOne %></p><%}%>
<%if(thisPage.meta.introductionTextBlockTwo){%><p><%- thisPage.meta.introductionTextBlockTwo %></p><%}%>
</div>
</div>
<%if(thisPage.meta.articleImageUrl){%>
<div purpose="article-image">
<img alt="A preview image of the webinar" src="<%= thisPage.meta.articleImageUrl %>">
</div>
<% } %>
</div>
<div purpose="article-and-sidebar" class="d-flex flex-lg-row-reverse flex-column justify-content-lg-between">
<div purpose="sidebar-container">
<div purpose="right-sidebar" class="d-flex flex-column">
<div purpose="form-container">
<div id="webinar-recording-form" purpose="webinar-recording-form">
<h4>Sign up to watch the webinar</h4>
<ajax-form class="w-100" action="deliverWebinarAccessRequest" :form-errors.sync="formErrors" :form-data="formData" :form-rules="formRules" :syncing.sync="syncing" :cloud-error.sync="cloudError" @submitted="submittedDownloadForm()">
<div class="form-group">
<label for="first-name">First name *</label>
<input tabindex="1" class="form-control" id="first-name" type="text" :class="[formErrors.firstName ? 'is-invalid' : '']" v-model.trim="formData.firstName" autocomplete="first-name">
<div class="invalid-feedback" v-if="formErrors.firstName">Please enter your first name.</div>
</div>
<div class="form-group">
<label for="last-name">Last name *</label>
<input tabindex="2" class="form-control" id="last-name" type="text" :class="[formErrors.lastName ? 'is-invalid' : '']" v-model.trim="formData.lastName" autocomplete="last-name">
<div class="invalid-feedback" v-if="formErrors.lastName">Please enter your last name.</div>
</div>
<div class="form-group">
<label for="email-address">Work email *</label>
<input tabindex="3" class="form-control" id="email-address" type="email" autocomplete="email" :class="[formErrors.emailAddress ? 'is-invalid' : '']" v-model.trim="formData.emailAddress">
<div class="invalid-feedback" v-if="formErrors.emailAddress" focus-first>This doesnt appear to be a valid email address</div>
</div>
<ajax-button tabindex="4" purpose="watch-button" spinner="true" type="submit" :syncing="syncing" class="btn btn-primary">Watch the webinar</ajax-button>
<cloud-error v-if="cloudError === 'invalidEmailDomain'">
<p>
Please enter your work or school email address.
</p>
</cloud-error>
<cloud-error v-else-if="cloudError">
</cloud-error>
</ajax-form>
</div>
</div>
</div>
</div>
<div purpose="article-container">
<div purpose="article-content" parasails-has-no-page-script>
<%- partial(path.relative(path.dirname(__filename), path.resolve( sails.config.appPath, path.join(sails.config.builtStaticContent.compiledPagePartialsAppPath, thisPage.htmlId)))) %>
<div purpose="watch-cta" class="d-lg-none d-flex">
<a purpose="watch-button" class="btn btn-primary" href="#webinar-recording-form">Watch the recording</a>
</div>
<div purpose="about-fleet-section">
<h3>About Fleet</h3>
<p>Fleet is the single endpoint management platform for macOS, iOS, Android, Windows, Linux, ChromeOS, and cloud infrastructure. Trusted by over 1,300 organizations, Fleet empowers IT and security teams to accelerate productivity, build verifiable trust, and optimize costs.</p>
<p>By bringing infrastructure-as-code (IaC) practices to device management, Fleet ensures endpoints remain secure and operational, freeing engineering teams to focus on strategic initiatives.</p>
<p>Fleet offers total deployment flexibility: on-premises, air-gapped, container-native (Docker and Kubernetes), or cloud-agnostic (AWS, Azure, GCP, DigitalOcean). Organizations can also choose fully managed SaaS via Fleet Cloud, ensuring complete control over data residency and legal jurisdiction.</p>
</div>
<div purpose="social-share-and-edit-buttons" class="d-flex flex-row justify-content-between align-items-center">
<div purpose="social-share-buttons" class="d-flex flex-row">
<a :href="`https://news.ycombinator.com/submitlink?u=https://fleetdm.com${encodeURIComponent(thisPage.url)}&t=${encodeURIComponent(thisPage.meta.articleTitle)}`"><img src="/images/social-share-icon-hacker-news-20x20@2x.png" alt="Share this article on Hacker News"></a>
<a :href="`https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent('https://fleetdm.com'+thisPage.url)}`"><img src="/images/social-share-icon-linkedin-20x20@2x.png" alt="Share this article on LinkedIn"></a>
<a :href="`https://twitter.com/intent/tweet?url=${encodeURIComponent('https://fleetdm.com'+thisPage.url)}`"><img src="/images/social-share-icon-twitter-20x20@2x.png" alt="Share this article on Twitter"></a>
</div>
<div purpose="edit-link">
<a :href="'https://github.com/fleetdm/fleet/edit/main/articles/'+thisPage.sectionRelativeRepoPath"> <img src="/images/icon-edit-16x16@2x.png" alt="Suggest an edit">Suggest an edit</a>
</div>
</div>
</div>
</div>
</div>
</div>
<%} else {%>
<div purpose="page-container" class="mx-auto">
<div purpose="success-message">
<h1 class="pb-3"><%= thisPage.meta.articleTitle %></h1>
<div purpose="embedded-video">
<iframe title="Webinar recording: <%= thisPage.meta.articleTitle %>" src="<%= thisPage.meta.webinarEmbeddedVideoUrl %>?loop=false&muted=false&preload=true&responsive=false" allow="accelerometer;gyroscope;autoplay;encrypted-media;picture-in-picture;" allowfullscreen="true"></iframe>
</div>
</div>
</div>
<%}%>
</div>
<%- /* Expose server-rendered data as window.SAILS_LOCALS :: */ exposeLocalsToBrowser() %>