Stack:
Core:
Node.js (22) with Yeoman Generator framework for scaffolding banner projects
Build Tools:
Gulp task runner with modular Sass compilation, Pug templating, Rollup / Babel for JS bundling, PostCSS for CSS optimization
Frontend:
Vanilla JavaScript, Pug templates, modular SCSS architecture with size-specific partials and responsive banner support
Development:
BrowserSync live reload, ESLint code linting, source maps, Puppeteer for automated screenshots
Scaffolded Output:
Generates HTML5 display ads in multiple sizes with minified assets and automated Node.js ZIP compression for ad networks
The legacy code functioned, but designers would often make changes to campaigns based on size, which required reduntant one-off style changes to each campaign banner.
Scaffolded project src before
├───campaigns
│ ├───campaign-1
│ │ ├───banners
│ │ │ ├───160x600
│ │ │ ├───300x250
│ │ │ ├───300x600
│ │ │ └───728x90
│ │ │ └───styles.scss // Local styles
│ │ ├───js
│ │ ├───pug
│ │ └───sass
│ │ └───campaign.scss // campaign 1 styles
│ └───campaign-2
│ ├───banners
│ │ ├───160x600
│ │ ├───300x250
│ │ ├───300x600
│ │ └───728x90
│ ├───js
│ ├───pug
│ └───sass
│ └───campaign.scss // campaign 2 styles
├───js
├───pug
└───sass
└───_base.scss // universal styles
By adding global size-scoped styles, these changes could be injected into any campain that contained a banner of that size.
Added size-scoped style modules
├───campaigns
│ ├───campaign-1
↑ │ ├───banners
│ ↓ │ ├───160x600
│ │ │ ↓───300x250
↑ │ │ ├───300x600 // size styles applied
│ ↓ │ └───728x90
│ │ ├───js
↑ │ ├───pug
│ ↓ └───sass
│ │ └───_campaign.scss
↑ └───campaign-2
│ ├───banners
│ │ ├───160x600
↑ │ ↓───300x250
│ │ ├───300x600 // size styles applied
│ │ └───728x90
↑ ├───js
│ ├───pug
│ └───sass
↑ └───_campaign.scss
├───js
├───pug
└───sass
├───_base.scss
↑───_global-160x600.scss
├───_global-300x250.scss
├───_global-300x600.scss // size-scoped style module
├───_global-728x90.scss
├───_isi.scss // Modular ISI styles
└───_vars.scss // Modular variables
template\sass\styles.scss
@import '../../sass/_campaign';// Old styles
<%_ } else { _%>
@import '../../sass/_base';// Old styles
<%_ } _%>
<%_ if (size == '728x90' && isi) { _%>
// static dimensions required manual override in each banner stylesheet
#isi-wrapper{
width: 228px;
height: 100%;
}
<%_ } _%>
<%_ const globalSassPath = multipleCampaigns ? "../../../../sass/" : "../../sass/"; _%>
// New dimensions are dynamically assigned via Gulp
@use '<%- globalSassPath %>vars' as * with (
$environment: $gulp-environment,
$adWidth: $gulp-width,
$adHeight: $gulp-height<%_ if (!isiOnAllBanners && isi) { _%>,
$showISI: true<%_ } %>
);
@use '<%- multipleCampaigns ? `${globalSassPath}base` : "../../sass/base" %>';
<%_ if (isi) { _%>
@use '<%- globalSassPath %>isi';
<% } _%>
@use '<%- globalSassPath %>global-<%- size %>';
<%_ if (multipleCampaigns) { _%>
@use '../../sass/campaign';
<% } _%>
Banner height and width are assigned with SCSS variables, which are dynamically assigned for each sleected banner dimension during initialization - this means streamlined styles that can still be manually adjusted at the size, campaign, or individaul banner scope.
template\sass\sizes\_global-300x600.scss
// converted @include to @use rule in accordance with Dart Sass v3.0.0 requirements
@use 'vars' as *;
<%_ if (isiOnAnyBanner) { _%> @use 'sass:math'; <%_ } _%>
<% if (isiOnAnyBanner) { %>
@if $showISI {
$contentHeight: math.floor($adHeight * .66);
$isiHeaderHeight: 2rem;
// Sass variables are assigned recursively with Gulp,
#isi {
width: $adWidth;
height: calc($adHeight - $contentHeight);
}
#isi-header {
height: $isiHeaderHeight;
}
#isi-wrapper {
width: inherit;
height: calc($adHeight - $contentHeight - $isiHeaderHeight);
}
}
<%_ } _%>
template/scss/_base.scss
#banner {
width: $adWidth;
height: $adHeight;
}
Dimensions are assigned to the global style module as Sass variables - Gulp overwrites these variables in each banner based on the target size. The scrolling ISI dimensions are set, and the rest of the content window takes the remainder of available space. Local styles can still override the size-scoped styles, if one-off changes are required.
gulpfile.js
.pipe($.tap((file) => {
const folderName = path.basename(path.dirname(file.path));
const sizes = folderName.split('x');
file.contents = Buffer.from(
(`$gulp-environment: ${
development()
? 'development'
: 'production'
};\n$gulp-width: ${sizes[0]}px;\n$gulp-height: ${sizes[1]}px;\n\n`
).concat(String(file.contents)));
}))
~\sass\sizes\_global-300x600.scss
@use 'vars' as *;
<%_ if (isiOnAnyBanner) { _%> @use 'sass:math'; <%_ } _%>
<% if (isiOnAnyBanner) { %>
@if $showISI {
$contentHeight: math.floor($adHeight * .66);
$isiHeaderHeight: 2rem;
#isi {
width: $adWidth;
height: calc($adHeight - $contentHeight);
}
#isi-header {
height: $isiHeaderHeight;
}
#isi-wrapper {
width: inherit;
height: calc($adHeight - $contentHeight - $isiHeaderHeight);
}
}
<%_ } _%>
@import was deprecated in Dart Sass 1.80.0, and will removed entirely in Dart Sass 3.0.0. To future-proof the generator, I decided to convert to modules, and the @use rule with this refactor.
scaffolded/sass/campaign.scss
@import '../../../sass/_base';
@use '../../../sass/vars' as *;
scaffolded\campaigns\**\styles.scss
@use '../../../../sass/vars' as * with (
$environment: $gulp-environment,
$adWidth: $gulp-width,
$adHeight: $gulp-height
);