Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion src/crates/monolith-server/src/routes/pages.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2837,7 +2837,15 @@ pub async fn translated_static_page(
// been refactored to read their copy from `t.<key>`. For these, keep
// page-<slug>.html as the preferred template even on translated paths
// so the bespoke layout is preserved (with auto-translated strings).
const I18N_AWARE_PAGE_SLUGS: &[&str] = &["about"];
const I18N_AWARE_PAGE_SLUGS: &[&str] = &[
"about",
"accessibility",
"contact",
"cookie-policy",
"corrections",
"privacy",
"terms",
];
let i18n_aware = I18N_AWARE_PAGE_SLUGS.contains(&original_slug.as_str());
let slug_template = format!("page-{}.html", original_slug);
let (first, second): (&str, &str) = if is_translation && !i18n_aware {
Expand Down
205 changes: 204 additions & 1 deletion themes/signaldaily/i18n/en.json

Large diffs are not rendered by default.

74 changes: 38 additions & 36 deletions themes/signaldaily/templates/page-accessibility.html
Original file line number Diff line number Diff line change
@@ -1,78 +1,80 @@
{# Example footer page from signaldaily.co.uk. Replace the hardcoded copy
with your own, or delete this file to fall back to `page.html`. #}
{# Bespoke accessibility page. Copy lives in `themes/signaldaily/i18n/en.json`
under the `accessibility_*` keys so translated paths can render the same
layout (np-broadsheet shell + custom np-audit component) with
auto-translated strings. #}
{% extends "partials/page-shell.html" %}

{% set page_kind = "accessibility" %}
{% set page_section_kicker = "POLICIES · ACCESSIBILITY" %}
{% set page_subtitle = "Our accessibility commitment, in plain English." %}
{% set page_title = "Accessibility." %}
{% set page_lead = "Signal Daily is built to be readable by everyone. That includes keyboard-only visitors, screen-reader users, people with reduced motion settings, and anyone who just likes a bigger font. Here's the state of play and what we're working on." %}
{% set page_meta = "Accessibility statement for Signal Daily — our commitment, known gaps, and how to report an issue." %}
{% set page_section_kicker = t.accessibility_section_kicker | default("POLICIES · ACCESSIBILITY") %}
{% set page_subtitle = t.accessibility_subtitle | default("Our accessibility commitment, in plain English.") %}
{% set page_title = t.accessibility_title | default("Accessibility.") %}
{% set page_lead = t.accessibility_lead | default("Signal Daily is built to be readable by everyone. That includes keyboard-only visitors, screen-reader users, people with reduced motion settings, and anyone who just likes a bigger font. Here's the state of play and what we're working on.") %}
{% set page_meta = t.accessibility_meta | default("Accessibility statement for Signal Daily — our commitment, known gaps, and how to report an issue.") %}

{% block page_body %}

<h2>What we commit to</h2>
<h2>{{ t.accessibility_heading_commit | default("What we commit to") }}</h2>

<p>We aim to meet <a href="https://www.w3.org/WAI/WCAG21/quickref/" rel="noopener">WCAG 2.1 Level AA</a> across every page we publish. That's the bar UK public-sector sites are held to; we hold ourselves to the same bar voluntarily.</p>
<p>{{ (t.accessibility_para_commit | default("We aim to meet <a href=\"https://www.w3.org/WAI/WCAG21/quickref/\" rel=\"noopener\">WCAG 2.1 Level AA</a> across every page we publish. That's the bar UK public-sector sites are held to; we hold ourselves to the same bar voluntarily.")) | safe }}</p>

<p>Specifically:</p>
<p>{{ t.accessibility_para_specifically | default("Specifically:") }}</p>

<ul>
<li>Every page works without a mouse.</li>
<li>Every image carries meaningful alt text (or an explicit <code>alt=""</code> when decorative).</li>
<li>Colour is never the only way information is conveyed.</li>
<li>The site respects <code>prefers-reduced-motion</code> and your browser's zoom settings.</li>
<li>Contrast meets or exceeds 4.5:1 for body text and 3:1 for large text.</li>
<li>{{ t.accessibility_li_keyboard | default("Every page works without a mouse.") }}</li>
<li>{{ (t.accessibility_li_alt | default("Every image carries meaningful alt text (or an explicit <code>alt=\"\"</code> when decorative).")) | safe }}</li>
<li>{{ t.accessibility_li_colour | default("Colour is never the only way information is conveyed.") }}</li>
<li>{{ (t.accessibility_li_motion | default("The site respects <code>prefers-reduced-motion</code> and your browser's zoom settings.")) | safe }}</li>
<li>{{ t.accessibility_li_contrast | default("Contrast meets or exceeds 4.5:1 for body text and 3:1 for large text.") }}</li>
</ul>

<h2>Where we are today</h2>
<h2>{{ t.accessibility_heading_today | default("Where we are today") }}</h2>

<ul class="np-audit">
<li class="np-audit__item">
<span class="np-audit__status np-audit__status--pass" role="img" aria-label="Conforms">✓</span>
<span class="np-audit__status np-audit__status--pass" role="img" aria-label="{{ t.accessibility_audit_label_pass | default("Conforms") }}">✓</span>
<div>
<p class="np-audit__title">Keyboard navigation</p>
<p class="np-audit__detail">Every interactive element is reachable by Tab. Focus rings are visible. A "Skip to content" link appears on Tab from every page.</p>
<p class="np-audit__title">{{ t.accessibility_audit_keyboard_title | default("Keyboard navigation") }}</p>
<p class="np-audit__detail">{{ (t.accessibility_audit_keyboard_detail | default("Every interactive element is reachable by Tab. Focus rings are visible. A \"Skip to content\" link appears on Tab from every page.")) | safe }}</p>
</div>
</li>
<li class="np-audit__item">
<span class="np-audit__status np-audit__status--pass" role="img" aria-label="Conforms">✓</span>
<span class="np-audit__status np-audit__status--pass" role="img" aria-label="{{ t.accessibility_audit_label_pass | default("Conforms") }}">✓</span>
<div>
<p class="np-audit__title">Screen readers</p>
<p class="np-audit__detail">Semantic headings, labelled landmarks, accessible form fields, and ARIA roles where HTML semantics need a hand.</p>
<p class="np-audit__title">{{ t.accessibility_audit_screenreaders_title | default("Screen readers") }}</p>
<p class="np-audit__detail">{{ t.accessibility_audit_screenreaders_detail | default("Semantic headings, labelled landmarks, accessible form fields, and ARIA roles where HTML semantics need a hand.") }}</p>
</div>
</li>
<li class="np-audit__item">
<span class="np-audit__status np-audit__status--pass" role="img" aria-label="Conforms">✓</span>
<span class="np-audit__status np-audit__status--pass" role="img" aria-label="{{ t.accessibility_audit_label_pass | default("Conforms") }}">✓</span>
<div>
<p class="np-audit__title">Reduced motion</p>
<p class="np-audit__detail">All animations respect <code>prefers-reduced-motion: reduce</code> and collapse to instantaneous state changes.</p>
<p class="np-audit__title">{{ t.accessibility_audit_motion_title | default("Reduced motion") }}</p>
<p class="np-audit__detail">{{ (t.accessibility_audit_motion_detail | default("All animations respect <code>prefers-reduced-motion: reduce</code> and collapse to instantaneous state changes.")) | safe }}</p>
</div>
</li>
<li class="np-audit__item">
<span class="np-audit__status np-audit__status--partial" role="img" aria-label="Partially conforms">!</span>
<span class="np-audit__status np-audit__status--partial" role="img" aria-label="{{ t.accessibility_audit_label_partial | default("Partially conforms") }}">!</span>
<div>
<p class="np-audit__title">Video and audio content</p>
<p class="np-audit__detail">Rare on this site, but when we embed third-party video (YouTube, etc.) we rely on the provider's captions, which aren't always available. When we produce our own media, we caption it.</p>
<p class="np-audit__title">{{ t.accessibility_audit_video_title | default("Video and audio content") }}</p>
<p class="np-audit__detail">{{ t.accessibility_audit_video_detail | default("Rare on this site, but when we embed third-party video (YouTube, etc.) we rely on the provider's captions, which aren't always available. When we produce our own media, we caption it.") }}</p>
</div>
</li>
<li class="np-audit__item">
<span class="np-audit__status np-audit__status--partial" role="img" aria-label="Partially conforms">!</span>
<span class="np-audit__status np-audit__status--partial" role="img" aria-label="{{ t.accessibility_audit_label_partial | default("Partially conforms") }}">!</span>
<div>
<p class="np-audit__title">Automated translations</p>
<p class="np-audit__detail">Non-English pages are machine-translated. Phrasing can be awkward; factual meaning is preserved. English is the canonical version — use the UK flag in the dateline to switch back.</p>
<p class="np-audit__title">{{ t.accessibility_audit_translations_title | default("Automated translations") }}</p>
<p class="np-audit__detail">{{ t.accessibility_audit_translations_detail | default("Non-English pages are machine-translated. Phrasing can be awkward; factual meaning is preserved. English is the canonical version — use the UK flag in the dateline to switch back.") }}</p>
</div>
</li>
</ul>

<h2>How to report an issue</h2>
<h2>{{ t.accessibility_heading_report | default("How to report an issue") }}</h2>

<p>If something on this site is blocking you from reading it, tell us and we'll fix it. The fastest route is <a href="mailto:accessibility@signaldaily.co.uk">accessibility@signaldaily.co.uk</a>. Include the page URL and what you tried to do — screenshots welcome.</p>
<p>{{ (t.accessibility_para_report | default("If something on this site is blocking you from reading it, tell us and we'll fix it. The fastest route is <a href=\"mailto:accessibility@signaldaily.co.uk\">accessibility@signaldaily.co.uk</a>. Include the page URL and what you tried to do — screenshots welcome.")) | safe }}</p>

<p>We aim to respond within two working days and to fix confirmed blockers within two weeks. Complex issues get an interim workaround while we build a proper fix.</p>
<p>{{ t.accessibility_para_response | default("We aim to respond within two working days and to fix confirmed blockers within two weeks. Complex issues get an interim workaround while we build a proper fix.") }}</p>

<h2>This statement</h2>
<h2>{{ t.accessibility_heading_statement | default("This statement") }}</h2>

<p>Last reviewed April 2026. We review this statement whenever we make a significant change to the site, or at least once a year.</p>
<p>{{ t.accessibility_para_statement | default("Last reviewed April 2026. We review this statement whenever we make a significant change to the site, or at least once a year.") }}</p>

{% endblock %}
46 changes: 24 additions & 22 deletions themes/signaldaily/templates/page-contact.html
Original file line number Diff line number Diff line change
@@ -1,59 +1,61 @@
{# Example footer page from signaldaily.co.uk. Replace the hardcoded copy
with your own, or delete this file to fall back to `page.html`. #}
{# Bespoke contact page. Copy lives in `themes/signaldaily/i18n/en.json`
under the `contact_*` keys so translated paths can render the same
layout (np-broadsheet shell + custom np-classifieds / np-dispatch-form
components) with auto-translated strings. #}
{% extends "partials/page-shell.html" %}

{% set page_kind = "contact" %}
{% set page_section_kicker = "EDITORIAL · CONTACT" %}
{% set page_subtitle = "Reach the newsroom." %}
{% set page_title = "Get in touch." %}
{% set page_lead = "Pick the right channel and your message will actually be read. Generic PR pitches get a polite ignore; real tips and sharp questions get a real answer." %}
{% set page_meta = "Contact Signal Daily — tips, corrections, and general enquiries." %}
{% set page_section_kicker = t.contact_section_kicker | default("EDITORIAL · CONTACT") %}
{% set page_subtitle = t.contact_subtitle | default("Reach the newsroom.") %}
{% set page_title = t.contact_title | default("Get in touch.") %}
{% set page_lead = t.contact_lead | default("Pick the right channel and your message will actually be read. Generic PR pitches get a polite ignore; real tips and sharp questions get a real answer.") %}
{% set page_meta = t.contact_meta | default("Contact Signal Daily — tips, corrections, and general enquiries.") %}

{% block page_body %}

<h2>Pick your lane</h2>
<h2>{{ t.contact_lanes_heading | default("Pick your lane") }}</h2>

<div class="np-classifieds">
<div class="np-classifieds__ad">
<span class="np-classifieds__ad__label">Tips</span>
<p>Something we should be writing about, ideally with receipts. Anonymity respected.</p>
<span class="np-classifieds__ad__label">{{ t.contact_lane_tips_label | default("Tips") }}</span>
<p>{{ t.contact_lane_tips_desc | default("Something we should be writing about, ideally with receipts. Anonymity respected.") }}</p>
<a href="mailto:tips@signaldaily.co.uk">tips@signaldaily.co.uk</a>
</div>
<div class="np-classifieds__ad">
<span class="np-classifieds__ad__label np-classifieds__ad__label--corrections">Corrections</span>
<p>Something we got wrong. We publish every correction in public, so be specific.</p>
<span class="np-classifieds__ad__label np-classifieds__ad__label--corrections">{{ t.contact_lane_corrections_label | default("Corrections") }}</span>
<p>{{ t.contact_lane_corrections_desc | default("Something we got wrong. We publish every correction in public, so be specific.") }}</p>
<a href="mailto:corrections@signaldaily.co.uk">corrections@signaldaily.co.uk</a>
</div>
<div class="np-classifieds__ad">
<span class="np-classifieds__ad__label">General</span>
<p>Everything else. Hello. Hire us. Argue with us. We read it all.</p>
<span class="np-classifieds__ad__label">{{ t.contact_lane_general_label | default("General") }}</span>
<p>{{ t.contact_lane_general_desc | default("Everything else. Hello. Hire us. Argue with us. We read it all.") }}</p>
<a href="mailto:info@signaldaily.co.uk">info@signaldaily.co.uk</a>
</div>
</div>

<h2>Or send a dispatch</h2>
<h2>{{ t.contact_dispatch_heading | default("Or send a dispatch") }}</h2>

<p>Prefer a form? Use this one. Goes to the same inbox as <a href="mailto:info@signaldaily.co.uk">info@signaldaily.co.uk</a>.</p>
<p>{{ (t.contact_dispatch_intro | default("Prefer a form? Use this one. Goes to the same inbox as <a href=\"mailto:info@signaldaily.co.uk\">info@signaldaily.co.uk</a>.")) | safe }}</p>

<form class="np-dispatch-form" method="post" action="/api/v1/contact">
<div class="np-dispatch-form__field">
<label class="np-dispatch-form__label" for="np-dispatch-name">Your name</label>
<label class="np-dispatch-form__label" for="np-dispatch-name">{{ t.contact_form_name_label | default("Your name") }}</label>
<input class="np-dispatch-form__input" id="np-dispatch-name" name="name" type="text" required>
</div>
<div class="np-dispatch-form__field">
<label class="np-dispatch-form__label" for="np-dispatch-email">Reply-to email</label>
<label class="np-dispatch-form__label" for="np-dispatch-email">{{ t.contact_form_email_label | default("Reply-to email") }}</label>
<input class="np-dispatch-form__input" id="np-dispatch-email" name="email" type="email" required>
</div>
<div class="np-dispatch-form__field">
<label class="np-dispatch-form__label" for="np-dispatch-msg">Message</label>
<label class="np-dispatch-form__label" for="np-dispatch-msg">{{ t.contact_form_message_label | default("Message") }}</label>
<textarea class="np-dispatch-form__textarea" id="np-dispatch-msg" name="message" required></textarea>
</div>
{% set captcha_form = "contact" %}{% include "partials/captcha-widget.html" ignore missing %}
<button type="submit" class="np-dispatch-form__submit">Send dispatch →</button>
<button type="submit" class="np-dispatch-form__submit">{{ t.contact_form_submit | default("Send dispatch →") }}</button>
</form>

<h2>What happens next</h2>
<h2>{{ t.contact_next_heading | default("What happens next") }}</h2>

<p>You'll get a reply from Daniel, usually within a day. Weekends and UK bank holidays might take longer. If you've sent a tip, you may not hear back until we've done the reporting; assume silence means "we're working on it".</p>
<p>{{ t.contact_next_para | default("You'll get a reply from Daniel, usually within a day. Weekends and UK bank holidays might take longer. If you've sent a tip, you may not hear back until we've done the reporting; assume silence means \"we're working on it\".") }}</p>

{% endblock %}
Loading
Loading