Skip to content

Commit

Permalink
Merge pull request #34 from alphagov/global-banner
Browse files Browse the repository at this point in the history
Add global banner handling
  • Loading branch information
KludgeKML authored Feb 4, 2025
2 parents 24cad77 + fb3b433 commit 7efa287
Show file tree
Hide file tree
Showing 30 changed files with 726 additions and 119 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ Include a link to your pull request.
When adding a new banner to gov.uk page, release a minor version.
For typos, release a patch version.

## Unreleased
## UNRELEASED

* Add global banner support ([PR #34](https://github.com/alphagov/govuk_web_banners/pull/34))
* Remove configuration for "HMRC banner 03/01/2025" ([PR #53](https://github.com/alphagov/govuk_web_banners/pull/53))
* Gem updates ([PR #50](https://github.com/alphagov/govuk_web_banners/pull/50))

Expand Down
2 changes: 1 addition & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ GEM
govuk_personalisation (1.1.0)
plek (>= 1.9.0)
rails (>= 6, < 9)
govuk_publishing_components (50.0.1)
govuk_publishing_components (51.0.0)
govuk_app_config
govuk_personalisation (>= 0.7.0)
kramdown
Expand Down
129 changes: 116 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ Proof of Concept for centralising handling of Recruitment, Global, and Emergency
banners (currently spread across apps)

## Usage
Currently supports the emergency banner and recruitment banners.

## Adding the gem to your application
Add this line to your application's Gemfile:
Expand All @@ -30,7 +29,13 @@ Add the JS dependencies to your existing asset dependencies file:

## Adding emergency banners

Emergency banners are passed to the [Layout for Public](https://components.publishing.service.gov.uk/component-guide/layout_for_public) component, which is currently applied to each frontend app by the slimmer/static wrapping code - so you will only need to handle emergency banners in your app when Slimmer is removed from it. Once Slimmer is removed and you are calling the layout_for_public component directly in your app, add the emergency banner partial to the component's `emergency_banner:` key:
Emergency banners are passed to the [Layout for
Public](https://components.publishing.service.gov.uk/component-guide/layout_for_public)
component, which is currently applied to each frontend app by the slimmer/static
wrapping code - so you will only need to handle emergency banners in your app
when Slimmer is removed from it. Once Slimmer is removed and you are calling the
layout_for_public component directly in your app, add the emergency banner
partial to the component's `emergency_banner:` key:

```
<%= render "govuk_publishing_components/components/layout_for_public", {
Expand All @@ -40,7 +45,8 @@ Emergency banners are passed to the [Layout for Public](https://components.publi
...etc
```

if you want the homepage variant of the banner, you can add `homepage: true` to the render call:
if you want the homepage variant of the banner, you can add `homepage: true` to
the render call:

```
<%= render "govuk_publishing_components/components/layout_for_public", {
Expand All @@ -50,9 +56,16 @@ if you want the homepage variant of the banner, you can add `homepage: true` to
...etc
```

Your app will also need access to the whitehall shared redis cluster (which is used to signal the emergency banner is up), via the `EMERGENCY_BANNER_REDIS_URL` environment variable (here is an example of [setting this in govuk-helm-charts](https://github.com/alphagov/govuk-helm-charts/blob/7818eaa22fc194d21548f316bcc5a46c2023dcb6/charts/app-config/values-staging.yaml#L3337-L3338)). You'll need to allow this in all three environments.
Your app will also need access to the whitehall shared redis cluster (which is
used to signal the emergency banner is up), via the `EMERGENCY_BANNER_REDIS_URL`
environment variable (here is an example of [setting this in
govuk-helm-charts](https://github.com/alphagov/govuk-helm-charts/blob/7818eaa22fc194d21548f316bcc5a46c2023dcb6/charts/app-config/values-staging.yaml#L3337-L3338)).
You'll need to allow this in all three environments.

Finally, you'll need to configure a connection to the redis cluster, available at `Rails.application.config.emergency_banner_redis_client`. The suggested way of doing this is creating an initializer at `/config/initializers/govuk_web_banners.rb` with the content:
Finally, you'll need to configure a connection to the redis cluster, available
at `Rails.application.config.emergency_banner_redis_client`. The suggested way
of doing this is creating an initializer at
`/config/initializers/govuk_web_banners.rb` with the content:

```
Rails.application.config.emergency_banner_redis_client = Redis.new(
Expand All @@ -61,11 +74,100 @@ Rails.application.config.emergency_banner_redis_client = Redis.new(
)
```

## Adding global banners

Global banners are passed to the [Layout for
Public](https://components.publishing.service.gov.uk/component-guide/layout_for_public)
component, which is currently applied to each frontend app by the slimmer/static
wrapping code - so you will only need to handle global banners in your app when
Slimmer is removed from it. Once Slimmer is removed and you are calling the
layout_for_public component directly in your app, add the global banner partial
to the component's `global_banner:` key:

```
<%= render "govuk_publishing_components/components/layout_for_public", {
draft_watermark: draft_environment,
global_banner: render("govuk_web_banners/global_banner"), # <-- Add this line
full_width: false,
...etc
```

## Updating banner information in the gem

Data for the global banners can be found at
`config/govuk_web_banners/global_banners.yml`. To add a banner to the config,
add an entry under the banners: array. Note that this array must always be
valid, so if there are no banners in the file, it must contain at least
`global_banners: []`

### Example banner entry

```
global_banners:
- name: Banner 1
title: "Register to Vote"
title_href: /register-to-vote
text: "You must register to vote before the election"
permanent: false
exclude_paths:
- /find-your-local-electoral-office
start_date: 2024/10/21
end_date: 2024/11/18
```
Each banner must include a `title`, `title_href`, `text`, and a valid `start_date`.
`title_href` can be either a valid URL or a path on gov.uk.

> [!NOTE]
>
> `start_date` is **mandatory** here (unlike in recruitment banners) because it's
> needed to create a banner_version to pass to the underlying component. This lets
> the component reset the cookie that records how many times a banner has been seen
> (by default banners are shown only three times, see the `permanent` option below.)
Optional keys are:
- `name` (an identifying name for this banner, not rendered
anywhere)
- `permanent` (defaults to false. If false, banner is hidden if the user
has consented to cookies and has seen this banner more than 3 times)
- `exclude_paths` an array of paths on which the banner should not be shown.
Note that the banner is never shown on the path it points to, this
list is to include any additional pages.
- `end_date` (the banner stops being active at the *start* of the day
specified as `end_date`). Start and end dates must be in the YYYY/MM/DD
format parsable as a YAML -> Date.

### Validations on the global banners config file

The config file will be checked during CI, so an invalid file can't be released
as a gem and we are nudged to make sure it's kept tidy. These checks include:

* the global_banners array must be a valid YAML array
* all banners have a `title`, `title_href`, and `info_text`.
* all banners must have a valid `start_date`.

It will also display warnings (but not fail CI)

* if there are banners that have expired - you are encouraged to remove
obsolete config, but it will not prevent you merging changes.
* if `title_href` of any banner points to a page that are not currently live on
GOV.UK - this may be intentional (if the banner points to a page that isn't
yet published), or it may indicate a typo in the path.
* if any `exclude_paths` value points to a page that is not currently live on
GOV.UK - this may be intentional, or it may indicate a typo in the path.
* if two global banners will be active on the same day (this is only a warning
because ultimately we may need this, but the current iteration of the
component does not support it)

Note that some of this validation code is in the
`/lib/govuk_web_banners/validators/global_banner.rb` file, which should be
tested to ensure the checking is valid, but will not be bundled into the
released gem.

## Adding recruitment banners

Add a call to the partial in the layout or view that you want banners to appear
in (typically recruitment banners should be in the layout, below the breadcrumbs
and just above the `main` element):
in (typically recruitment banners should be in the layout, below the
breadcrumbs and just above the `main` element):

```
<%= render "govuk_web_banners/recruitment_banner" %>
Expand Down Expand Up @@ -102,8 +204,8 @@ banners:
page_paths:
- /
- /foreign-travel-advice
start_date: 21/10/2024
end_date: 18/11/2024
start_date: 2024/10/21
end_date: 2024/11/18
```

The required keys are `suggestion_text`, `suggestion_link_text`, and
Expand All @@ -113,10 +215,10 @@ paths on which the banner should be shown).
Optional keys are `name` (an identifying name for this banner, not rendered
anywhere), and `start_date` / `end_date` (the banner becomes active at the start
of the day specified as `start_date`, and stops at the *start* of the day
specified as `end_date`). Start and end dates must be in the DD/MM/YYYY format
specified as `end_date`). Start and end dates must be in the YYYY/MM/DD format
parsable as a YAML -> Date.

### Keeping the config file valid and tidy
### Validations on the recruitment banners config file

The config file will be checked during CI, so an invalid file can't be released
as a gem and we are forced to make sure it's kept tidy. These checks include:
Expand All @@ -137,8 +239,9 @@ It will also display warnings (but not fail CI)
may indicate a typo in the path.

Note that some of this validation code is in the
lib/govuk_web_banners/validators path, which should be tested to ensure the
checking is valid, but will not be bundled into the released gem.
`lib/govuk_web_banners/validator/recruitment_banner.rb` file, which should be
tested to ensure the checking is valid, but will not be bundled into the
released gem.

## License
The gem is available as open source under the terms of the [MIT
Expand Down
32 changes: 23 additions & 9 deletions Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -12,35 +12,49 @@ require "bundler/gem_tasks"
RuboCop::RakeTask.new
RSpec::Core::RakeTask.new

require "govuk_web_banners/validators/global_banner"
require "govuk_web_banners/validators/recruitment_banner"
require "rainbow"

desc "show errors in the live config"
task :check_config do
validator = GovukWebBanners::Validators::RecruitmentBanner.new(GovukWebBanners::RecruitmentBanner.all_banners)

def output_validator_info(validator, name)
if !validator.valid?
puts Rainbow("\nLive config contains errors!").red
puts Rainbow("\nLive #{name} config contains errors!").red
validator.errors.each_key do |key|
puts(key)
validator.errors[key].each { |error| puts(" - #{error}") }
end
puts
exit(1)
elsif validator.warnings?
puts Rainbow("\nLive config is valid, but with warnings").yellow
puts Rainbow("\nLive #{name} config is valid, but with warnings").yellow
validator.warnings.each_key do |key|
puts(key)
validator.warnings[key].each { |warnings| puts(" - #{warnings}") }
end
puts
else
puts Rainbow("\nLive config is valid!\n").green
puts Rainbow("\nLive #{name} config is valid!\n").green
end
end

desc "show errors in the live global banner config"
task :check_global_config do
validator = GovukWebBanners::Validators::GlobalBanner.new(GovukWebBanners::GlobalBanner.all_banners)
output_validator_info(validator, "global banner")
rescue StandardError => e
puts(e)
puts Rainbow("Live global banner config could not be read (if there are no banners, check global_banner key is marked as an empty array - global_banners: [])").red
exit(1)
end

desc "show errors in the live recruitment banner config"
task :check_recruitment_config do
validator = GovukWebBanners::Validators::RecruitmentBanner.new(GovukWebBanners::RecruitmentBanner.all_banners)
output_validator_info(validator, "recruitment banner")
rescue StandardError => e
puts(e)
puts("Live config could not be read (if there are no banners, check banner key is marked as an empty array - banners: [])")
puts Rainbow("Live recruitment banner config could not be read (if there are no banners, check banner key is marked as an empty array - banners: [])").red
exit(1)
end

task default: %i[check_config rubocop spec]
task default: %i[check_global_config check_recruitment_config rubocop spec]
10 changes: 10 additions & 0 deletions app/views/govuk_web_banners/_global_banner.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<% global_banners = GovukWebBanners::GlobalBanner.for_path(request.path) %>
<% if global_banners.any? %>
<%= render "govuk_publishing_components/components/global_banner", {
title: global_banners.first.title,
title_path: global_banners.first.title_href,
text: global_banners.first.text,
permanent: global_banners.first.permanent,
banner_version: global_banners.first.version,
} %>
<% end %>
2 changes: 2 additions & 0 deletions config/govuk_web_banners/global_banners.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Check README.md for how to format this file
global_banners: []
24 changes: 4 additions & 20 deletions config/govuk_web_banners/recruitment_banners.yml
Original file line number Diff line number Diff line change
@@ -1,22 +1,6 @@
# Example usage of adding a banner to the banners list

# banners:
# - name: Banner 1
# suggestion_text: "Help improve GOV.UK"
# suggestion_link_text: "Sign up to take part in user research (opens in a new tab)"
# survey_url: https://google.com
# page_paths:
# - /
# - /foreign-travel-advice
# start_date: 21/10/2024
# end_date: 18/11/2024

# start_date and end_date are optional, everything else is mandatory.
#
# Note that this file must contain a valid banners array, so if there are no banners
# currently included, the file should at least contain banners: []
# Check README.md for how to format this file
banners:
- name: UKVI banner 30/12/2025
- name: UKVI banner 2025/12/30
suggestion_text: "Help improve GOV.UK"
suggestion_link_text: "Take part in user research (opens in a new tab)"
survey_url: https://surveys.publishing.service.gov.uk/s/XYVRGN/
Expand All @@ -26,5 +10,5 @@ banners:
# collections
- /browse/visas-immigration
- /government/organisations/uk-visas-and-immigration
start_date: 30/12/2024
end_date: 24/02/2025
start_date: 2024/12/30
end_date: 2025/02/24
2 changes: 2 additions & 0 deletions lib/govuk_web_banners.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
require "govuk_publishing_components"

require "govuk_web_banners/global_banner"
require "govuk_web_banners/emergency_banner"
require "govuk_web_banners/engine"
require "govuk_web_banners/recruitment_banner"
require "govuk_web_banners/version"

module GovukWebBanners
TIME_ZONE = "London".freeze
end
46 changes: 46 additions & 0 deletions lib/govuk_web_banners/global_banner.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
module GovukWebBanners
class GlobalBanner
BANNER_CONFIG_FILE = "../../config/govuk_web_banners/global_banners.yml".freeze

def self.for_path(path)
active_banners.reject { |b| b.exclude_paths.include?(path) }
end

def self.active_banners
all_banners.select(&:active?)
end

def self.all_banners
global_banners_file_path = Rails.root.join(__dir__, BANNER_CONFIG_FILE)
global_banners_data = YAML.load_file(global_banners_file_path)
global_banners_data["global_banners"].map { |attributes| GlobalBanner.new(attributes:) }
end

attr_reader :name, :title, :title_href, :text, :start_date, :end_date, :show_arrows, :permanent, :exclude_paths

def initialize(attributes:)
@name = attributes["name"]

@title = attributes["title"]
@title_href = attributes["title_href"]
@text = attributes["text"]

@start_date = attributes["start_date"] ? ActiveSupport::TimeZone[GovukWebBanners::TIME_ZONE].parse(attributes["start_date"]) : nil
@end_date = attributes["end_date"] ? ActiveSupport::TimeZone[GovukWebBanners::TIME_ZONE].parse(attributes["end_date"]) : Time.now + 10.years
@show_arrows = attributes["show_arrows"] == "true"
@permanent = attributes["permanent"] == "true"
@exclude_paths = attributes["exclude_paths"] || []
@exclude_paths << title_href if title_href&.start_with?("/")
end

# NB: .between? is inclusive. To make it exclude the end date, we set the end range as
# 1 second earlier.
def active?
Time.zone.now.between?(start_date, end_date - 1.second)
end

def version
start_date.getutc.to_i
end
end
end
4 changes: 2 additions & 2 deletions lib/govuk_web_banners/recruitment_banner.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ def initialize(attributes:)
@suggestion_link_text = attributes["suggestion_link_text"]
@survey_url = attributes["survey_url"]
@page_paths = attributes["page_paths"]
@start_date = attributes["start_date"] ? Time.parse(attributes["start_date"]) : Time.at(0)
@end_date = attributes["end_date"] ? Time.parse(attributes["end_date"]) : Time.now + 10.years
@start_date = attributes["start_date"] ? ActiveSupport::TimeZone[GovukWebBanners::TIME_ZONE].parse(attributes["start_date"]) : Time.at(0)
@end_date = attributes["end_date"] ? ActiveSupport::TimeZone[GovukWebBanners::TIME_ZONE].parse(attributes["end_date"]) : Time.now + 10.years
end

# NB: .between? is inclusive. To make it exclude the end date, we set the end range as
Expand Down
Loading

0 comments on commit 7efa287

Please sign in to comment.