Skip to content

Commit 0a3d721

Browse files
feat(eu): migrate legacy website services and links (#4074)
* fix(genouest): replace legacy website links with Hub routes * feat(subsites): add GenOuest native routes * fix(redirects): map legacy EU routes to Hub pages * fix(pasteur): replace legacy navbar links with Hub routes * fix(ifb): replace legacy people links with Hub routes * docs(freiburg): add Galaxy services page * docs(freiburg): add Galaxy projects page * fix(freiburg): replace legacy site links with Hub routes * docs(eu): add embeddable widgets page * docs(eu): simplify about page structure and internal link * feat(eu): add TIaaS service page * fix(erasmusmc): use local people and citations pages - Point navigation and footer links to migrated Hub routes. - Remove the obsolete team teaser content. * fix(elixir-it): link publications to local citation page - Remove obsolete disabled team content. * fix(belgium): link TIaaS to migrated Hub page * fix(eu): rename microgalaxy route to microbiology * feat(eu): automate VGP genome count updates - Mark the assembly page count for targeted replacement. - Add a script that fetches the current count from the VGP/ERGA sheet. - Support check-only mode for CI validation. * ci(eu): schedule VGP genome count updates * test(eu): cover genome count update script behavior * fix(eu): restore calendar feed and file redirects * ci(eu): update genome count workflow actions * fix(eu): replace active legacy website links * content(eu): make legacy news and events Hub-owned * fix(use): replace legacy EU website links * fix(fair): replace legacy EU website links * fix(events): replace legacy EU website links * fix(examples): use canonical EU navigation links * fix(updates): replace 2018 legacy EU website links in galaxy-updates * fix(news): replace 2017-2018 legacy EU website links * fix(news): replace H1 2019 legacy EU website links * fix(news): replace H2 2019 legacy EU website links * fix(news): replace 2020-2022 legacy EU website links * fix(news): replace 2023-2024 legacy EU website links * fix(news): replace 2025 legacy EU website links * fix(eu/bare): constrain narrow viewport content * fix(eu/bare): centralize compatibility redirects * fix(eu): address migration PR QA findings * fix(news): restore migrated EU media references * feat(embed): support Flickr cover images * fix(contributors): preserve local avatar paths * chore(data): refresh grants registry Normalize YAML formatting and add missing migration-related grant records. * chore(data): refresh organisations registry Normalize YAML formatting and add missing Galaxy Europe support organisations. * docs(eu): expand TIaaS service page * docs(events): migrate 2019 Freiburg workshop page * docs(events): update Freiburg HTS workshop metadata * docs(events): add microscopy course metadata * docs(events): redirect postponed machine learning workshop * docs(events): localize machine learning workshop page * docs(events): localize single cell workshop page * docs(events): fix tool wrapping workshop links * docs(fair): fix Galaxy subdomain links * docs(freiburg): redirect legacy services include * docs(freiburg): redirect legacy acknowledgements include * docs(freiburg): expand projects page * docs(freiburg): expand services page * docs(freiburg): add services tools image * docs(freiburg): add first training photo * docs(freiburg): add second training photo * docs(freiburg): add third training photo * docs(usegalaxy): expand FTP upload guidance * docs(updates): normalize January 2018 news * docs(news): normalize December 2018 update * docs(news): normalize January 2019 update * docs(news): normalize February 2019 update * docs(news): update TIaaS feedback metadata * docs(news): redirect duplicate Melbourne workshop post * docs(news): fix UseGalaxy.eu stats links * docs(news): redirect duplicate UseGalaxy.eu stats post * docs(news): normalize May 2020 update * docs(news): normalize Galaxify tool article * docs(news): fix 20000 users team link * docs(news): normalize subdomains article * docs(news): fix 30000 users team link * docs(news): normalize sustainable storage metadata * docs(news): update 50000 users article * docs(news): update Galaxy OpenAPI article metadata * docs(news): normalize EuroScienceGateway article * docs(news): normalize NFDI4Bioimage article * docs(news): format PathoGFAIR preprint article * docs(news): fix Traefik article links * docs(news): fix Data Commons team link * docs(news): fix wet lab TIaaS story team link * docs(news): fix Morard TIaaS story team link * docs(news): fix TMR TIaaS story team link * fix(redirects): add legacy EU content routes * docs(use): link Erasmus MC support team * Optimised images with calibre/image-actions --------- Co-authored-by: galaxy-hub-bot[bot] <274848829+galaxy-hub-bot[bot]@users.noreply.github.com>
1 parent df81f20 commit 0a3d721

161 files changed

Lines changed: 3152 additions & 2123 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
name: Update EU Genome Count
2+
on:
3+
workflow_dispatch:
4+
schedule:
5+
# Every Sunday at 23:00 UTC (same cadence as update-eu-tools)
6+
- cron: "00 23 * * 0"
7+
8+
permissions:
9+
contents: write
10+
pull-requests: write
11+
12+
jobs:
13+
update-genome-count:
14+
runs-on: ubuntu-latest
15+
if: github.repository_owner == 'galaxyproject'
16+
steps:
17+
- name: Checkout
18+
uses: actions/checkout@v6
19+
20+
- name: Generate GitHub App token
21+
id: app-token
22+
uses: actions/create-github-app-token@v3
23+
with:
24+
client-id: ${{ secrets.HUB_BOT_APP_CLIENT_ID }}
25+
private-key: ${{ secrets.HUB_BOT_APP_PRIVATE_KEY }}
26+
27+
- name: Setup Python
28+
uses: actions/setup-python@v6
29+
with:
30+
python-version: "3.12"
31+
32+
- name: Install dependencies
33+
run: pip install pandas
34+
35+
- name: Run update script
36+
run: python scripts/update-eu-genome-count.py
37+
38+
- name: Create Pull Request
39+
uses: peter-evans/create-pull-request@v8
40+
with:
41+
token: ${{ steps.app-token.outputs.token }}
42+
commit-message: "chore(eu/assembly): update VGP/ERGA genome count"
43+
title: "Update VGP/ERGA Genome Count"
44+
body: |
45+
Auto-generated PR with the updated genome count from the
46+
[VGP/ERGA Google Sheet](https://docs.google.com/spreadsheets/d/1Kya38LUDom4u7osTTY3uYyNor7AMpyLYxz5MaulciZc/)
47+
for the [Galaxy Assembly](https://assembly.usegalaxy.eu) bare page.
48+
branch: update-eu-genome-count
49+
delete-branch: true

astro/src/build/generate-redirects.mjs

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,16 @@ function legacyNormalize(slug) {
5454
.join('/');
5555
}
5656

57+
function hasFileExtension(urlPath) {
58+
const pathname = urlPath.split(/[?#]/, 1)[0];
59+
return /\.[^/.]+$/.test(pathname);
60+
}
61+
62+
function normalizeRedirectTarget(toPath) {
63+
if (toPath.includes('://') || toPath.endsWith('/') || hasFileExtension(toPath)) return toPath;
64+
return `${toPath}/`;
65+
}
66+
5767
/**
5868
* Extract a frontmatter field value, handling single-line and multi-line YAML
5969
*/
@@ -110,8 +120,7 @@ async function getContentData() {
110120
if (redirect && !redirect.includes('://')) {
111121
const fromPath = `/${slug}/`.replace(/\/+/g, '/');
112122
const toPath = redirect.startsWith('/') ? redirect : `/${redirect}`;
113-
const normalizedTo = toPath.endsWith('/') ? toPath : `${toPath}/`;
114-
contentRedirects[fromPath] = normalizedTo;
123+
contentRedirects[fromPath] = normalizeRedirectTarget(toPath);
115124
}
116125
}
117126
}
@@ -225,8 +234,7 @@ async function generateRedirects() {
225234

226235
for (const [from, to] of Object.entries(yamlData.redirects)) {
227236
const fromPath = from.endsWith('/') ? from : `${from}/`;
228-
const isExternal = to.includes('://');
229-
const toPath = isExternal || to.endsWith('/') ? to : `${to}/`;
237+
const toPath = normalizeRedirectTarget(to);
230238
redirects[fromPath] = toPath;
231239
}
232240

@@ -275,4 +283,4 @@ if (import.meta.url === `file://${process.argv[1]}`) {
275283
});
276284
}
277285

278-
export { generateRedirects, legacyNormalize, legacyNormalizeSegment };
286+
export { generateRedirects, legacyNormalize, legacyNormalizeSegment, normalizeRedirectTarget, hasFileExtension };
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { normalizeRedirectTarget, hasFileExtension } from './generate-redirects.mjs';
3+
4+
describe('redirect normalization helpers', () => {
5+
describe('hasFileExtension', () => {
6+
it('returns true for paths with file extensions', () => {
7+
expect(hasFileExtension('/eu/events/calendar.ics')).toBe(true);
8+
expect(hasFileExtension('/events-ical.html')).toBe(true);
9+
expect(hasFileExtension('/some/path/file.txt')).toBe(true);
10+
});
11+
12+
it('returns false for paths without file extensions', () => {
13+
expect(hasFileExtension('/eu/events/')).toBe(false);
14+
expect(hasFileExtension('/eu/events')).toBe(false);
15+
});
16+
});
17+
18+
describe('normalizeRedirectTarget', () => {
19+
it('appends a trailing slash to ordinary internal page targets', () => {
20+
expect(normalizeRedirectTarget('/eu/events')).toBe('/eu/events/');
21+
expect(normalizeRedirectTarget('/news')).toBe('/news/');
22+
});
23+
24+
it('preserves external URLs', () => {
25+
expect(normalizeRedirectTarget('https://usegalaxy.org/bushman')).toBe('https://usegalaxy.org/bushman');
26+
expect(normalizeRedirectTarget('http://gcc2015.tsl.ac.uk/')).toBe('http://gcc2015.tsl.ac.uk/');
27+
});
28+
29+
it('preserves trailing-slash targets', () => {
30+
expect(normalizeRedirectTarget('/eu/events/')).toBe('/eu/events/');
31+
expect(normalizeRedirectTarget('/')).toBe('/');
32+
});
33+
34+
it('preserves file-extension targets', () => {
35+
expect(normalizeRedirectTarget('/eu/events/calendar.ics')).toBe('/eu/events/calendar.ics');
36+
expect(normalizeRedirectTarget('/events-ical.html')).toBe('/events-ical.html');
37+
});
38+
});
39+
});

astro/src/components/embed/Flickr.vue

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,16 @@ const props = defineProps<{
66
albumId?: string;
77
userId?: string;
88
url?: string;
9+
link?: string;
10+
title?: string;
11+
cover?: string;
12+
coverWidth?: string | number;
13+
coverHeight?: string | number;
914
}>();
1015
1116
const embedUrl = computed(() => {
1217
if (props.url) return props.url;
18+
if (props.link) return props.link;
1319
1420
if (props.photoId) {
1521
return `https://www.flickr.com/photos/${props.userId || 'galaxyproject'}/${props.photoId}/`;
@@ -21,6 +27,9 @@ const embedUrl = computed(() => {
2127
2228
return '';
2329
});
30+
31+
const imageWidth = computed(() => (props.coverWidth ? String(props.coverWidth) : undefined));
32+
const imageHeight = computed(() => (props.coverHeight ? String(props.coverHeight) : undefined));
2433
</script>
2534

2635
<template>
@@ -32,6 +41,15 @@ const embedUrl = computed(() => {
3241
rel="noopener noreferrer"
3342
class="inline-block p-4 bg-ebony-clay-50 rounded-lg hover:bg-ebony-clay-100 transition-colors"
3443
>
44+
<img
45+
v-if="cover"
46+
:src="cover"
47+
:alt="title || 'Flickr photo'"
48+
:width="imageWidth"
49+
:height="imageHeight"
50+
class="mx-auto mb-3 rounded-lg"
51+
loading="lazy"
52+
/>
3553
<div class="flex items-center gap-2 text-chicago-700">
3654
<svg class="w-6 h-6" viewBox="0 0 24 24" fill="currentColor">
3755
<circle cx="7" cy="12" r="5" fill="#0063DC" />

astro/src/components/mdx/EuSiteFooter.astro

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,11 @@ const links = [
2020
<p class="mx-auto text-chicago-700">
2121
UseGalaxy.eu is maintained largely by the <a
2222
class="text-galaxy-primary underline underline-offset-2 hover:text-galaxy-gold"
23-
href="https://usegalaxy-eu.github.io/freiburg/">Freiburg Galaxy Team</a
23+
href="/freiburg/">Freiburg Galaxy Team</a
2424
> and collectively by groups and individuals from across Europe. All member sites in this repository contribute to the
2525
European Galaxy Project. For acknowledgement, please refer to the <a
2626
class="text-galaxy-primary underline underline-offset-2 hover:text-galaxy-gold"
27-
href="https://usegalaxy-eu.github.io/about">About section</a
27+
href="/eu/about/">About section</a
2828
>. All content on this site is available under <a
2929
class="text-galaxy-primary underline underline-offset-2 hover:text-galaxy-gold"
3030
href="https://creativecommons.org/share-your-work/public-domain/cc0/">CC0-1.0</a

astro/src/layouts/BareArticleLayout.astro

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,21 @@ const { title, description } = Astro.props;
4747
--tw-prose-headings: #2c3143;
4848
}
4949

50+
.prose-galaxy table {
51+
width: 100%;
52+
max-width: 100%;
53+
border-collapse: collapse;
54+
display: block;
55+
overflow-x: auto;
56+
-webkit-overflow-scrolling: touch;
57+
}
58+
5059
.prose-galaxy a {
5160
color: var(--tw-prose-links);
5261
text-decoration: underline;
5362
text-decoration-color: color-mix(in srgb, var(--tw-prose-links) 40%, transparent);
5463
text-underline-offset: 2px;
64+
overflow-wrap: anywhere;
5565
}
5666

5767
.prose-galaxy a:hover {
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import { getCollection } from 'astro:content';
2+
import type { APIContext } from 'astro';
3+
4+
// Format date for iCalendar (YYYYMMDDTHHMMSSZ)
5+
function formatICSDate(date: Date): string {
6+
return date
7+
.toISOString()
8+
.replace(/[-:]/g, '')
9+
.replace(/\.\d{3}/, '');
10+
}
11+
12+
// Format date for all-day events (YYYYMMDD)
13+
function formatICSDateOnly(date: Date): string {
14+
return date.toISOString().split('T')[0].replace(/-/g, '');
15+
}
16+
17+
// Escape special characters for iCalendar
18+
function escapeICS(text: string): string {
19+
return text.replace(/\\/g, '\\\\').replace(/;/g, '\\;').replace(/,/g, '\\,').replace(/\n/g, '\\n');
20+
}
21+
22+
// Fold long lines (iCal spec: max 75 octets per line)
23+
function foldLine(line: string): string {
24+
const maxLength = 75;
25+
let result = '';
26+
while (line.length > maxLength) {
27+
result += line.substring(0, maxLength) + '\r\n ';
28+
line = line.substring(maxLength);
29+
}
30+
result += line;
31+
return result;
32+
}
33+
34+
export async function GET(context: APIContext) {
35+
const events = await getCollection('events');
36+
37+
const now = new Date();
38+
const oneYearAgo = new Date(now.getFullYear() - 1, now.getMonth(), now.getDate());
39+
40+
// Get events from the past year and all future events, filtered to subsite 'eu'
41+
const relevantEvents = events
42+
.filter((event) => {
43+
const subsites = event.data.subsites;
44+
const subsiteList = Array.isArray(subsites) ? subsites : subsites ? [subsites] : [];
45+
if (!subsiteList.includes('eu')) return false;
46+
47+
const eventDate = event.data.date instanceof Date ? event.data.date : new Date(event.data.date || '');
48+
return eventDate >= oneYearAgo;
49+
})
50+
.sort((a, b) => {
51+
const dateA = a.data.date instanceof Date ? a.data.date : new Date(a.data.date || 0);
52+
const dateB = b.data.date instanceof Date ? b.data.date : new Date(b.data.date || 0);
53+
return dateB.getTime() - dateA.getTime(); // Future/recent events first
54+
});
55+
56+
const siteUrl = (context.site?.toString() || 'https://galaxyproject.org').replace(/\/$/, '');
57+
58+
const icsContent = [
59+
'BEGIN:VCALENDAR',
60+
'VERSION:2.0',
61+
'PRODID:-//Galaxy Project//Galaxy Europe Events Calendar//EN',
62+
'CALSCALE:GREGORIAN',
63+
'METHOD:PUBLISH',
64+
'X-WR-CALNAME:Galaxy Europe Events',
65+
'X-WR-CALDESC:Events from the Galaxy Europe community',
66+
];
67+
68+
for (const event of relevantEvents) {
69+
const startDate = event.data.date instanceof Date ? event.data.date : new Date(event.data.date || 0);
70+
const endDate = event.data.end instanceof Date ? event.data.end : event.data.end ? new Date(event.data.end) : null;
71+
72+
// Use end date or default to same day
73+
const effectiveEnd = endDate || new Date(startDate.getTime() + 24 * 60 * 60 * 1000);
74+
75+
let location = '';
76+
if (event.data.location) {
77+
if (typeof event.data.location === 'string') {
78+
location = event.data.location;
79+
} else if (event.data.location.name) {
80+
location = event.data.location.name;
81+
}
82+
}
83+
84+
const uid = `eu-${event.data.slug}@galaxyproject.org`;
85+
const slug = event.data.slug || event.id;
86+
const pathSlug = slug.startsWith('events/') ? slug : `events/${slug}`;
87+
const url = `${siteUrl}/${pathSlug}/`;
88+
const title = event.data.title || 'Untitled Event';
89+
const description = event.data.tease || '';
90+
91+
icsContent.push('BEGIN:VEVENT');
92+
icsContent.push(foldLine(`UID:${uid}`));
93+
icsContent.push(foldLine(`DTSTAMP:${formatICSDate(now)}`));
94+
icsContent.push(foldLine(`DTSTART;VALUE=DATE:${formatICSDateOnly(startDate)}`));
95+
icsContent.push(foldLine(`DTEND;VALUE=DATE:${formatICSDateOnly(effectiveEnd)}`));
96+
icsContent.push(foldLine(`SUMMARY:${escapeICS(title)}`));
97+
if (description) {
98+
icsContent.push(foldLine(`DESCRIPTION:${escapeICS(description)}`));
99+
}
100+
if (location) {
101+
icsContent.push(foldLine(`LOCATION:${escapeICS(location)}`));
102+
}
103+
icsContent.push(foldLine(`URL:${url}`));
104+
icsContent.push('END:VEVENT');
105+
}
106+
107+
icsContent.push('END:VCALENDAR');
108+
109+
return new Response(icsContent.join('\r\n'), {
110+
headers: {
111+
'Content-Type': 'text/calendar; charset=utf-8',
112+
'Content-Disposition': 'attachment; filename="galaxy-europe-events.ics"',
113+
},
114+
});
115+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { getSubsiteStaticPaths, subsites } from './subsiteStore';
3+
4+
describe('European member subsites', () => {
5+
it('registers GenOuest for native subsite routes', () => {
6+
expect(subsites).toContainEqual({
7+
id: 'genouest',
8+
name: 'GenOuest',
9+
path: '/genouest/',
10+
});
11+
expect(getSubsiteStaticPaths().map(({ params }) => params.subsite)).toContain('genouest');
12+
});
13+
});

astro/src/stores/subsiteStore.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export const subsites = [
1212
{ id: 'pasteur', name: 'Pasteur', path: '/pasteur/' },
1313
{ id: 'elixir-it', name: 'ELIXIR-IT', path: '/elixir-it/' },
1414
{ id: 'ifb', name: 'ELIXIR-FR/IFB', path: '/ifb/' },
15+
{ id: 'genouest', name: 'GenOuest', path: '/genouest/' },
1516
] as const;
1617

1718
export type SubsiteId = (typeof subsites)[number]['id'];
@@ -94,6 +95,10 @@ export const subsiteLabels: Record<string, { title: string; description: string
9495
title: 'Galaxy ELIXIR-FR/IFB',
9596
description: 'Galaxy resources and news from ELIXIR France / IFB',
9697
},
98+
genouest: {
99+
title: 'Galaxy GenOuest',
100+
description: 'Galaxy resources and news from the GenOuest bioinformatics platform',
101+
},
97102
};
98103

99104
// Static paths for subsite pages (reused by normal and embed routes)

astro/src/utils/contributors.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ function parseHallOfFameFlag(flag: unknown, defaultValue = true): boolean {
7878
function normalizeAvatar(avatar?: string): string | undefined {
7979
if (!avatar) return undefined;
8080
if (/^https?:\/\//i.test(avatar)) return avatar;
81+
if (/^\/(?:images|assets)\//.test(avatar)) return avatar;
8182
const trimmed = avatar.replace(/^\/+/, '');
8283
return `${AVATAR_BASE}/${trimmed}`;
8384
}

0 commit comments

Comments
 (0)