Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
540d36a
Add monthly newsletter page
claude Mar 23, 2026
e3ded22
[automated] Fix code linting
nf-core-bot Mar 23, 2026
20c609b
Improve newsletter page: reorder sections, add logo, fix dark mode
claude Mar 23, 2026
c877a3f
Add dark mode support with logo swap and CSS classes
claude Mar 23, 2026
38d4259
Replace newsletter logos with correct versions from oldsite
ewels Mar 23, 2026
855475b
Fix dark mode to use site's data-bs-theme instead of prefers-color-sc…
ewels Mar 23, 2026
4a8da60
Fix dark mode: add !important and :global() for scoped class overrides
ewels Mar 23, 2026
3469787
Deduplicate pipeline releases by grouping per pipeline
ewels Mar 23, 2026
efd2584
Improve proposals section: categorize, show status, strip prefixes
ewels Mar 23, 2026
816d975
Widen newsletter layout, add 2-column grid, blog images, require head…
ewels Mar 23, 2026
f9aed6b
Reorder newsletter sections, fix missing headerImage in 3 blog posts
ewels Mar 23, 2026
02eebcc
Add optional headerImage to events, show in newsletter
ewels Mar 23, 2026
0f8003b
Only show event image in newsletter when headerImage is set
ewels Mar 23, 2026
f14882b
Fix local asset images in newsletter by resolving through Astro glob
ewels Mar 23, 2026
2d88c25
Fix asset glob path depth for newsletter page location
ewels Mar 23, 2026
3beb562
Remove baseUrl fallback for local asset images
ewels Mar 23, 2026
5722685
Fix async image resolution: pre-resolve URLs in frontmatter
ewels Mar 23, 2026
70c2fb5
Rename "Events This Month" to clarify they are past events
ewels Mar 23, 2026
2cf1420
Tone down dark mode green: make links light, keep green only where in…
ewels Mar 23, 2026
bb25bfa
Fix section headings, use Image component, 2-col upcoming events
ewels Mar 23, 2026
8ef0b0f
Fix image glob: use Vite root-relative /src/assets/** path
ewels Mar 23, 2026
c5d24f6
Only show proposals closed as 'completed', not 'not_planned'
ewels Mar 23, 2026
d73661f
Increase newsletter image resolution for retina displays
ewels Mar 23, 2026
8f4d3f5
Remove max-height from upcoming event images
ewels Mar 23, 2026
281cc0a
Move blog/event images to right column instead of full-width banner
ewels Mar 24, 2026
ea130ee
Sentence case headings and increase heading font sizes
ewels Mar 24, 2026
9246d10
Add small thumbnail images to 'In case you missed it' blog posts
ewels Mar 24, 2026
181ca5b
Add prev/next navigation below newsletter content
ewels Mar 24, 2026
5940a34
Enforce landscape aspect ratio on all newsletter blog/event images
ewels Mar 24, 2026
2cc703e
Add email preview text and 'view in browser' link
ewels Mar 24, 2026
b3cdfbd
Style preview text: centre-aligned, very muted, simpler link
ewels Mar 24, 2026
8c051d8
Restructure newsletter as retrospective: content from previous month
ewels Mar 24, 2026
8ff4c15
Fix newsletter wording for retrospective tone, show '1st Month Year' …
ewels Mar 24, 2026
8a64bfc
Show event subtitle in upcoming events section of newsletter
ewels Mar 24, 2026
9cdf329
Add month select dropdown to bottom navigation of newsletter
ewels Mar 24, 2026
fc2295a
Keep natural aspect ratio for event images, only force landscape on b…
ewels Mar 24, 2026
b7701b0
Rearrange upcoming events layout: tag + title with date right-aligned…
ewels Mar 24, 2026
80b0e16
Move event image below subtitle, keep date right-aligned across full …
ewels Mar 24, 2026
3757fc7
Fix dark mode dividers for upcoming events and footer separator
ewels Mar 24, 2026
d00655e
Fix footer divider: use border-bottom on td for dark mode compatibility
ewels Mar 24, 2026
6d75b4f
Add horizontal spacing between 2-column items in newsletter
ewels Mar 24, 2026
65d54b1
Add newsletter features: advisories, RSS feed, email HTML, nav link
ewels Mar 24, 2026
6bdcee1
Fix RSS feed imports: use path aliases instead of relative paths
ewels Mar 24, 2026
8f74934
Fix: move pipelines const inside getStaticPaths for build compatibility
ewels Mar 24, 2026
88611a1
Add older advisories to 'In case you missed it', remove subheadings
ewels Mar 24, 2026
0bad5d9
Improve email HTML compatibility
ewels Mar 24, 2026
d88ce5b
Use fluid hybrid layout for Gmail-compatible responsive 2-column
ewels Mar 24, 2026
4ee31c8
Replace <Image> with plain <img> for consistent absolute URLs
ewels Mar 24, 2026
4b16f3b
Fix esbuild parse error: pre-compute advisory styles and URLs
ewels Mar 24, 2026
debfd7e
Use div-based fluid hybrid layout for responsive 2-column
ewels Mar 24, 2026
8fa118a
Fix dark mode dividers for 2-column div-based layout
ewels Mar 24, 2026
ca7a589
Fix image link accessibility and missing height attributes
ewels Mar 24, 2026
e13cdd0
Add gap between 2-column items
ewels Mar 24, 2026
1b0e3b7
Fix responsive stacking on mobile: use max-width + display block
ewels Mar 24, 2026
bc8c78a
Fix 2-column gap: use box-sizing border-box with padding-right
ewels Mar 24, 2026
b452175
Make blog post layout responsive: stack image below text on mobile
ewels Mar 24, 2026
a34c3d9
Fix blog post responsive layout: stack image below text on mobile
ewels Mar 24, 2026
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
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions sites/main-site/src/components/BaseHead.astro
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ description = description?.replaceAll(/<[^>]*>?/g, "");
<link rel="alternate" type="application/rss+xml" title="nf-core Blog" href="/blog/rss.xml" />
)
}
{
Astro.url.pathname.includes("newsletter") && (
<link rel="alternate" type="application/rss+xml" title="nf-core Newsletter" href="/newsletter/rss.xml" />
)
}
<meta name="generator" content={Astro.generator} />
<link rel="sitemap" href="/sitemap-index.xml" />

Expand Down
6 changes: 6 additions & 0 deletions sites/main-site/src/components/navbar/Navbar.astro
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,12 @@ if (onGoingEvents?.length > 0) {
<BlogPostLabel timeSpan={blogPostTimeSpan} /></a
>
</li>
<li class="">
<a
class="dropdown-item d-flex align-items-center justify-content-between"
href="/newsletter/">Newsletter</a
>
</li>
<li><hr class="dropdown-divider" /></li>
<li>
<span class="dropdown-header">
Expand Down
788 changes: 788 additions & 0 deletions sites/main-site/src/components/newsletter/NewsletterContent.astro

Large diffs are not rendered by default.

14 changes: 8 additions & 6 deletions sites/main-site/src/content.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,13 @@ const events = defineCollection({
hackathonProjectListModals: z.string().optional(),
youtubeEmbed: z.array(z.string().url()).optional().or(z.string().url()).optional(),
hideExportButton: z.boolean().optional(),
headerImage: z.string().url().or(z.string().startsWith("/assets/images/events/")).optional(),
headerImageAlt: z.string().optional(),
})
.refine(
(data) => !data.headerImage || data.headerImageAlt,
{ message: "Please provide alt text for your `headerImage` in `headerImageAlt`." },
)
.transform((data) => {
// Create start and end date objects
try {
Expand Down Expand Up @@ -293,8 +299,8 @@ const blog = defineCollection({
title: z.string(),
subtitle: z.string(),
shortTitle: z.string().optional(),
headerImage: z.string().url().optional().or(z.string().startsWith("/assets/images/blog/")).optional(),
headerImageAlt: z.string().optional(),
headerImage: z.string().url().or(z.string().startsWith("/assets/images/blog/")),
headerImageAlt: z.string(),
headerImageDim: z.array(z.number(), z.number()).optional(),
label: z.array(z.string()),
pubDate: z.date(),
Expand All @@ -311,10 +317,6 @@ const blog = defineCollection({
maxHeadingDepth: z.number().optional(),
})
.refine((data) => {
// Check if headerImage is present but headerImageAlt is not
if (data.headerImage && !data.headerImageAlt) {
throw new Error("Please provide alt text for your `headerImage` in `headerImageAlt`.");
}
// Check if headerImageDim is present but headerImage is not present or does not start with /assets/
if (data.headerImageDim && (!data.headerImage || !data.headerImage.startsWith("/assets/"))) {
throw new Error(
Expand Down
2 changes: 2 additions & 0 deletions sites/main-site/src/content/blog/2020/data_management.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
---
title: Data management
subtitle: How to plan your project, estimate resources, and share your results.
headerImage: https://images.unsplash.com/photo-1558494949-ef010cbdcc31
headerImageAlt: Server room with rows of data storage hardware
pubDate: 2020-04-14T12:00:00+01:00
authors:
- "elinkronander"
Expand Down
2 changes: 2 additions & 0 deletions sites/main-site/src/content/blog/2025/paper-v2.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
---
title: "Empowering bioinformatics communities with Nextflow and nf-core"
subtitle: "Next nf-core scientific article now published in Genome Biology!"
headerImage: https://images.unsplash.com/photo-1532094349884-543bc11b234d
headerImageAlt: Scientific journal article with data visualizations
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the alt text is not describing what is in the photo...

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll let you know when I've reviewed my own code 😅

I haven't read it at all yet - just trying to get the end result to roughly the right place first before going back, simplifying, reviewing and getting ready for review/merge.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, sorry, I know, I will stop looking at this draft 🙂. I just got confused while looking at the deploy preview 😀

pubDate: 2025-08-06T12:00:00+01:00
authors:
- "maxulysse"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
---
title: Making nf-core/configs strict syntax compliant
subtitle: Announcement and recommendations for updating nf-core/configs Nextflow strict syntax compliant
headerImage: https://images.unsplash.com/photo-1555949963-ff9fe0c870eb
headerImageAlt: Code on a dark screen showing configuration syntax
pubDate: 2026-03-05T09:00:00+01:00
headerImage: https://images.unsplash.com/photo-1760548425425-e42e77fa38f1
headerImageAlt: Image of code on a screen
Expand Down
2 changes: 2 additions & 0 deletions sites/main-site/src/content/blog/2026/statement-on-ai.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
---
title: "nf-core and AI"
subtitle: "Core team statement on AI within nf-core"
headerImage: "/assets/images/blog/statement-on-ai/maxime-pigeon.png"
headerImageAlt: "Badly photoshopped image of a pigeon developing code on a laptop"
pubDate: 2026-01-14T10:00:00+01:00
headerImage: https://images.unsplash.com/photo-1718241905696-cb34c2c07bed
headerImageAlt: Image of a pigeon developing code on a laptop, and Maxime Garcia giving a thumbs up from the laptop screen
Expand Down
2 changes: 2 additions & 0 deletions sites/main-site/src/content/events/2026/hackathon-boston.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
title: Hackathon - April 2026 (Boston)
subtitle: An in-person hackathon held in Boston
type: hackathon
headerImage: /assets/images/events/2026/hackathon-boston/summit-2026-boston.png
headerImageAlt: Nextflow Summit 2026 Boston promotional card
announcement:
start: 2026-03-20T09:00:00+01:00
startDate: "2026-04-28"
Expand Down
153 changes: 153 additions & 0 deletions sites/main-site/src/pages/newsletter/[year]/[month].astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
---
import PageLayout from "@layouts/PageLayout.astro";
import { getCollection } from "astro:content";
import pipelines_json from "@public/pipelines.json";
import { getNewsletterStaticPathsData, getNewsletterContentData, getMonthName } from "@utils/newsletter";
import type { NewsletterMonth } from "@utils/newsletter";
import NewsletterContent from "@components/newsletter/NewsletterContent.astro";

const images = import.meta.glob("/src/assets/**");
export async function getStaticPaths() {
const pipelines = pipelines_json.remote_workflows;
const { months, allProposals } = await getNewsletterStaticPathsData(getCollection, pipelines);

return months.map(({ year, month }, index) => ({
params: {
year: String(year),
month: String(month).padStart(2, "0"),
},
props: {
year,
month,
allMonths: months,
allProposals,
prevMonth: months[index + 1] || null,
nextMonth: months[index - 1] || null,
},
}));
}

const { year, month, allMonths, allProposals, prevMonth, nextMonth } = Astro.props as {
year: number;
month: number;
allMonths: NewsletterMonth[];
allProposals: any[];
prevMonth: NewsletterMonth | null;
nextMonth: NewsletterMonth | null;
};

const pipelines = pipelines_json.remote_workflows;
const contentData = await getNewsletterContentData(getCollection, pipelines, year, month, allProposals, images);

function monthUrl(m: NewsletterMonth): string {
return `/newsletter/${m.year}/${String(m.month).padStart(2, "0")}`;
}
---

<PageLayout
title={`Newsletter - ${contentData.monthName} ${year}`}
subtitle={`nf-core community newsletter for ${contentData.monthName} ${year}`}
mainpage_container={true}
subfooter={false}
>
{/* ===== NAVIGATION (excluded from email) ===== */}
<nav class="mb-4 newsletter-nav">
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2">
{
prevMonth ? (
<a href={monthUrl(prevMonth)} class="btn btn-outline-secondary btn-sm">
&larr; {getMonthName(prevMonth.month)} {prevMonth.year}
</a>
) : (
<span />
)
}

<div class="d-flex align-items-center gap-2">
<a href="/newsletter/rss.xml" class="btn btn-outline-secondary btn-sm" title="RSS Feed">
<i class="fa-solid fa-rss"></i>
</a>
<a
href={`${monthUrl({ year, month })}/email`}
class="btn btn-outline-secondary btn-sm"
title="Email HTML version"
>
<i class="fa-solid fa-envelope"></i>
</a>
<select class="newsletter-month-select form-select form-select-sm w-auto">
{
allMonths.map((m) => (
<option value={monthUrl(m)} selected={m.year === year && m.month === month}>
{getMonthName(m.month)} {m.year}
</option>
))
}
</select>
</div>

{
nextMonth ? (
<a href={monthUrl(nextMonth)} class="btn btn-outline-secondary btn-sm">
{getMonthName(nextMonth.month)} {nextMonth.year} &rarr;
</a>
) : (
<span />
)
}
</div>
</nav>

{/* ===== NEWSLETTER CONTENT ===== */}
<NewsletterContent data={contentData} />

{/* ===== BOTTOM NAVIGATION (excluded from email) ===== */}
<hr class="my-4" />
<nav class="mb-4">
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2">
{
prevMonth ? (
<a href={monthUrl(prevMonth)} class="btn btn-outline-secondary btn-sm">
&larr; {getMonthName(prevMonth.month)} {prevMonth.year}
</a>
) : (
<span />
)
}

<select class="newsletter-month-select form-select form-select-sm w-auto">
{
allMonths.map((m) => (
<option value={monthUrl(m)} selected={m.year === year && m.month === month}>
{getMonthName(m.month)} {m.year}
</option>
))
}
</select>

{
nextMonth ? (
<a href={monthUrl(nextMonth)} class="btn btn-outline-secondary btn-sm">
{getMonthName(nextMonth.month)} {nextMonth.year} &rarr;
</a>
) : (
<span />
)
}
</div>
</nav>
</PageLayout>

<script>
document.querySelectorAll<HTMLSelectElement>(".newsletter-month-select").forEach((select) => {
select.addEventListener("change", () => {
window.location.href = select.value;
});
});
</script>

<style>
.newsletter-nav {
border-bottom: 1px solid var(--bs-border-color);
padding-bottom: 1rem;
}
</style>
68 changes: 68 additions & 0 deletions sites/main-site/src/pages/newsletter/[year]/[month]/email.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
---
import { getCollection } from "astro:content";
import pipelines_json from "@public/pipelines.json";
import { getNewsletterStaticPathsData, getNewsletterContentData, getMonthName } from "@utils/newsletter";
import NewsletterContent from "@components/newsletter/NewsletterContent.astro";

const images = import.meta.glob("/src/assets/**");

export async function getStaticPaths() {
const pipelines = pipelines_json.remote_workflows;
const { months, allProposals } = await getNewsletterStaticPathsData(getCollection, pipelines);

return months.map(({ year, month }) => ({
params: {
year: String(year),
month: String(month).padStart(2, "0"),
},
props: {
year,
month,
allProposals,
},
}));
}

const { year, month, allProposals } = Astro.props as {
year: number;
month: number;
allProposals: any[];
};

const pipelines = pipelines_json.remote_workflows;
const monthName = getMonthName(month);
const contentData = await getNewsletterContentData(getCollection, pipelines, year, month, allProposals, images);
---

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>nf-core Newsletter - {monthName} {year}</title>
<style>
body {
margin: 0;
padding: 20px;
background-color: #f5f5f5;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
@media (prefers-color-scheme: dark) {
body {
background-color: #121212;
}
}
@media screen and (max-width: 600px) {
#newsletter-content .nl-col,
#newsletter-content .nl-blog-text,
#newsletter-content .nl-blog-img {
max-width: 100% !important;
display: block !important;
}
}
</style>
</head>
<body style="margin: 0; padding: 0;">
<NewsletterContent data={contentData} />
</body>
</html>
64 changes: 64 additions & 0 deletions sites/main-site/src/pages/newsletter/index.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
---
import PageLayout from "@layouts/PageLayout.astro";
import { getCollection } from "astro:content";
import pipelines_json from "@public/pipelines.json";
import { getNewsletterMonths, getMonthName } from "@utils/newsletter";

let blogPosts = await getCollection("blog");
blogPosts = blogPosts.filter((post) => new Date(post.data.pubDate) < new Date());

let events = await getCollection("events");
events = events.filter((e) => e.id.split("/").length === 2);

const pipelines = pipelines_json.remote_workflows;
const advisories = await getCollection("advisories");
const months = getNewsletterMonths(blogPosts, events, pipelines, advisories);

// Group by year
const byYear = new Map<number, { year: number; month: number }[]>();
for (const m of months) {
if (!byYear.has(m.year)) byYear.set(m.year, []);
byYear.get(m.year)!.push(m);
}
const years = [...byYear.keys()].sort((a, b) => b - a);

// Latest newsletter for redirect
const latest = months[0];
---

<PageLayout
title="nf-core Newsletter"
subtitle="Monthly community newsletters from nf-core"
mainpage_container={true}
subfooter={false}
>
{
latest && (
<div class="alert alert-success mb-4">
<strong>Latest newsletter:</strong>
<a href={`/newsletter/${latest.year}/${String(latest.month).padStart(2, "0")}`}>
{getMonthName(latest.month)} {latest.year}
</a>
</div>
)
}

{
years.map((year) => (
<div class="mb-4">
<h2>{year}</h2>
<div class="list-group">
{byYear.get(year)!.map((m) => (
<a
href={`/newsletter/${m.year}/${String(m.month).padStart(2, "0")}`}
class="list-group-item list-group-item-action d-flex justify-content-between align-items-center"
>
{getMonthName(m.month)} {m.year}
<span class="badge bg-success rounded-pill">&rarr;</span>
</a>
))}
</div>
</div>
))
}
</PageLayout>
Loading
Loading