« get me outta code hell

hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--src/content/dependencies/generateGroupGalleryPage.js169
-rw-r--r--src/content/dependencies/generateGroupInfoPage.js170
-rw-r--r--src/content/dependencies/generateGroupNavLinks.js142
-rw-r--r--src/content/dependencies/generateGroupSidebar.js35
-rw-r--r--src/content/dependencies/generateGroupSidebarCategoryDetails.js77
-rw-r--r--src/content/dependencies/linkGroupExtra.js34
-rw-r--r--src/page/group.js315
-rw-r--r--src/page/index.js2
8 files changed, 646 insertions, 298 deletions
diff --git a/src/content/dependencies/generateGroupGalleryPage.js b/src/content/dependencies/generateGroupGalleryPage.js
new file mode 100644
index 0000000..ab8a06a
--- /dev/null
+++ b/src/content/dependencies/generateGroupGalleryPage.js
@@ -0,0 +1,169 @@
+import {
+  getTotalDuration,
+  sortChronologically,
+} from '../../util/wiki-data.js';
+
+export default {
+  contentDependencies: [
+    'generateColorStyleRules',
+    'generateCoverGrid',
+    'generateGroupNavLinks',
+    'generateGroupSidebar',
+    'generatePageLayout',
+    'image',
+    'linkAlbum',
+    'linkListing',
+  ],
+
+  extraDependencies: ['html', 'language', 'wikiData'],
+
+  sprawl({listingSpec, wikiInfo}) {
+    const sprawl = {};
+    sprawl.enableGroupUI = wikiInfo.enableGroupUI;
+
+    if (wikiInfo.enableListings && wikiInfo.enableGroupUI) {
+      sprawl.groupsByCategoryListing =
+        listingSpec
+          .find(l => l.directory === 'groups/by-category');
+    }
+
+    return sprawl;
+  },
+
+  relations(relation, sprawl, group) {
+    const relations = {};
+
+    const albums =
+      sortChronologically(group.albums.slice(), {latestFirst: true});
+
+    relations.layout =
+      relation('generatePageLayout');
+
+    relations.navLinks =
+      relation('generateGroupNavLinks', group);
+
+    if (sprawl.enableGroupUI) {
+      relations.sidebar =
+        relation('generateGroupSidebar', group);
+    }
+
+    relations.colorStyleRules =
+      relation('generateColorStyleRules', group.color);
+
+    if (sprawl.groupsByCategoryListing) {
+      relations.groupListingLink =
+        relation('linkListing', sprawl.groupsByCategoryListing);
+    }
+
+    relations.coverGrid =
+      relation('generateCoverGrid');
+
+    relations.links =
+      albums
+        .map(album => relation('linkAlbum', album));
+
+    relations.images =
+      albums.map(album =>
+        (album.hasCoverArt
+          ? relation('image', album.artTags)
+          : relation('iamge')));
+
+    return relations;
+  },
+
+  data(sprawl, group) {
+    const albums =
+      sortChronologically(group.albums.slice(), {latestFirst: true});
+
+    const tracks = albums.flatMap((album) => album.tracks);
+    const totalDuration = getTotalDuration(tracks, {originalReleasesOnly: true});
+
+    return {
+      name: group.name,
+
+      numAlbums: albums.length,
+      numTracks: tracks.length,
+      totalDuration,
+
+      names: albums.map(album => album.name),
+      paths: albums.map(album =>
+        (album.hasCoverArt
+          ? ['media.albumCover', album.directory, album.coverArtFileExtension]
+          : null)),
+    };
+  },
+
+  generate(data, relations, {html, language}) {
+    return relations.layout
+      .slots({
+        title: language.$('groupGalleryPage.title', {group: data.name}),
+        headingMode: 'static',
+
+        colorStyleRules: [relations.colorStyleRules],
+
+        mainClasses: ['top-index'],
+        mainContent: [
+          /*
+          getCarouselHTML({
+            items: group.featuredAlbums.slice(0, 12 + 1),
+            srcFn: getAlbumCover,
+            linkFn: link.album,
+          }),
+          */
+
+          html.tag('p',
+            {class: 'quick-info'},
+            language.$('groupGalleryPage.infoLine', {
+              tracks: html.tag('b',
+                language.countTracks(data.numTracks, {
+                  unit: true,
+                })),
+              albums: html.tag('b',
+                language.countAlbums(data.numAlbums, {
+                  unit: true,
+                })),
+              time: html.tag('b',
+                language.formatDuration(data.totalDuration, {
+                  unit: true,
+                })),
+            })),
+
+          relations.groupListingLink &&
+            html.tag('p',
+              {class: 'quick-info'},
+              language.$('groupGalleryPage.anotherGroupLine', {
+                link:
+                  relations.groupListingLink
+                    .slot('content', language.$('groupGalleryPage.anotherGroupLine.link')),
+              })),
+
+          relations.coverGrid
+            .slots({
+              links: relations.links,
+              names: data.names,
+              images:
+                relations.images.map((image, i) =>
+                  image.slots({
+                    path: data.paths[i],
+                    missingSourceContent:
+                      language.$('misc.albumGrid.noCoverArt', {
+                        album: data.names[i],
+                      }),
+                  })),
+            }),
+        ],
+
+        ...(
+          relations.sidebar
+            ?.slot('currentExtra', 'gallery')
+            ?.content
+          ?? {}),
+
+        navLinkStyle: 'hierarchical',
+        navLinks:
+          relations.navLinks
+            .slot('currentExtra', 'gallery')
+            .content,
+      });
+  },
+};
diff --git a/src/content/dependencies/generateGroupInfoPage.js b/src/content/dependencies/generateGroupInfoPage.js
new file mode 100644
index 0000000..3cffb74
--- /dev/null
+++ b/src/content/dependencies/generateGroupInfoPage.js
@@ -0,0 +1,170 @@
+import {empty} from '../../util/sugar.js';
+
+export default {
+  contentDependencies: [
+    'generateColorStyleRules',
+    'generateContentHeading',
+    'generateGroupNavLinks',
+    'generateGroupSidebar',
+    'generatePageLayout',
+    'linkAlbum',
+    'linkExternal',
+    'linkGroupGallery',
+    'linkGroup',
+    'transformContent',
+  ],
+
+  extraDependencies: ['html', 'language', 'wikiData'],
+
+  sprawl({wikiInfo}) {
+    return {
+      enableGroupUI: wikiInfo.enableGroupUI,
+    };
+  },
+
+  relations(relation, sprawl, group) {
+    const relations = {};
+    const sec = relations.sections = {};
+
+    relations.layout =
+      relation('generatePageLayout');
+
+    relations.navLinks =
+      relation('generateGroupNavLinks', group);
+
+    if (sprawl.enableGroupUI) {
+      relations.sidebar =
+        relation('generateGroupSidebar', group);
+    }
+
+    relations.colorStyleRules =
+      relation('generateColorStyleRules', group.color);
+
+    sec.info = {};
+
+    if (!empty(group.urls)) {
+      sec.info.visitLinks =
+        group.urls
+          .map(url => relation('linkExternal', url));
+    }
+
+    if (group.description) {
+      sec.info.description =
+        relation('transformContent', group.description);
+    }
+
+    if (!empty(group.albums)) {
+      sec.albums = {};
+
+      sec.albums.heading =
+        relation('generateContentHeading');
+
+      sec.albums.galleryLink =
+        relation('linkGroupGallery', group);
+
+      sec.albums.entries =
+        group.albums.map(album => {
+          const links = {};
+          links.albumLink = relation('linkAlbum', album);
+
+          const otherGroup = album.groups.find(g => g !== group);
+          if (otherGroup) {
+            links.groupLink = relation('linkGroup', otherGroup);
+          }
+
+          return links;
+        });
+    }
+
+    return relations;
+  },
+
+  data(sprawl, group) {
+    const data = {};
+
+    data.name = group.name;
+
+    if (!empty(group.albums)) {
+      data.albumYears =
+        group.albums
+          .map(album => album.date?.getFullYear());
+    }
+
+    return data;
+  },
+
+  generate(data, relations, {html, language}) {
+    const {sections: sec} = relations;
+
+    return relations.layout
+      .slots({
+        title: language.$('groupInfoPage.title', {group: data.name}),
+        headingMode: 'sticky',
+
+        colorStyleRules: [relations.colorStyleRules],
+
+        mainContent: [
+          sec.info.visitLinks &&
+            html.tag('p',
+              language.$('releaseInfo.visitOn', {
+                links: language.formatDisjunctionList(sec.info.visitLinks),
+              })),
+
+          html.tag('blockquote',
+            {[html.onlyIfContent]: true},
+            sec.info.description
+              ?.slot('mode', 'multiline')),
+
+          sec.albums && [
+            sec.albums.heading
+              .slots({
+                tag: 'h2',
+                title: language.$('groupInfoPage.albumList.title'),
+              }),
+
+            html.tag('p',
+              language.$('groupInfoPage.viewAlbumGallery', {
+                link:
+                  sec.albums.galleryLink
+                    .slot('content', language.$('groupInfoPage.viewAlbumGallery.link')),
+              })),
+
+            html.tag('ul',
+              sec.albums.entries.map(({albumLink, groupLink}, index) => {
+                // All these strings are really jank, and should probably
+                // be implemented with the same 'const parts = [], opts = {}'
+                // form used elsewhere...
+                const year = data.albumYears[index];
+                const item =
+                  (year
+                    ? language.$('groupInfoPage.albumList.item', {
+                        year,
+                        album: albumLink,
+                      })
+                    : language.$('groupInfoPage.albumList.item.withoutYear', {
+                        album: albumLink,
+                      }));
+
+                return html.tag('li',
+                  (groupLink
+                    ? language.$('groupInfoPage.albumList.item.withAccent', {
+                        item,
+                        accent:
+                          html.tag('span', {class: 'other-group-accent'},
+                            language.$('groupInfoPage.albumList.item.otherGroupAccent', {
+                              group:
+                                groupLink.slot('color', false),
+                            })),
+                      })
+                    : item));
+              })),
+          ],
+        ],
+
+        ...relations.sidebar?.content ?? {},
+
+        navLinkStyle: 'hierarchical',
+        navLinks: relations.navLinks.content,
+      });
+  },
+};
diff --git a/src/content/dependencies/generateGroupNavLinks.js b/src/content/dependencies/generateGroupNavLinks.js
new file mode 100644
index 0000000..0b52536
--- /dev/null
+++ b/src/content/dependencies/generateGroupNavLinks.js
@@ -0,0 +1,142 @@
+import {empty} from '../../util/sugar.js';
+
+export default {
+  contentDependencies: [
+    'generatePreviousNextLinks',
+    'linkGroup',
+    'linkGroupGallery',
+    'linkGroupExtra',
+  ],
+
+  extraDependencies: ['html', 'language', 'wikiData'],
+
+  sprawl({groupCategoryData, wikiInfo}) {
+    return {
+      groupCategoryData,
+      enableGroupUI: wikiInfo.enableGroupUI,
+      enableListings: wikiInfo.enableListings,
+    };
+  },
+
+  relations(relation, sprawl, group) {
+    if (!sprawl.enableGroupUI) {
+      return {};
+    }
+
+    const relations = {};
+
+    relations.mainLink =
+      relation('linkGroup', group);
+
+    relations.previousNextLinks =
+      relation('generatePreviousNextLinks');
+
+    const groups = sprawl.groupCategoryData
+      .flatMap(category => category.groups);
+
+    const index = groups.indexOf(group);
+
+    if (index > 0) {
+      relations.previousLink =
+        relation('linkGroupExtra', groups[index - 1]);
+    }
+
+    if (index < groups.length - 1) {
+      relations.nextLink =
+        relation('linkGroupExtra', groups[index + 1]);
+    }
+
+    relations.infoLink =
+      relation('linkGroup', group);
+
+    if (!empty(group.albums)) {
+      relations.galleryLink =
+        relation('linkGroupGallery', group);
+    }
+
+    return relations;
+  },
+
+  data(sprawl) {
+    return {
+      enableGroupUI: sprawl.enableGroupUI,
+      enableListings: sprawl.enableListings,
+    };
+  },
+
+  slots: {
+    showExtraLinks: {type: 'boolean', default: false},
+
+    currentExtra: {
+      validate: v => v.is('gallery'),
+    },
+  },
+
+  generate(data, relations, slots, {language}) {
+    if (!data.enableGroupUI) {
+      return [
+        {auto: 'home'},
+        {auto: 'current'},
+      ];
+    }
+
+    const previousNextLinks =
+      (relations.previousLink || relations.nextLink) &&
+        relations.previousNextLinks.slots({
+          previousLink:
+            relations.previousLink
+              ?.slot('extra', slots.currentExtra)
+              ?.content
+            ?? null,
+          nextLink:
+            relations.nextLink
+              ?.slot('extra', slots.currentExtra)
+              ?.content
+            ?? null,
+        });
+
+    const previousNextPart =
+      previousNextLinks &&
+        language.formatUnitList(
+          previousNextLinks.content.filter(Boolean));
+
+    const infoLink =
+      relations.infoLink.slots({
+        attributes: {class: slots.currentExtra === null && 'current'},
+        content: language.$('misc.nav.info'),
+      });
+
+    const extraLinks = [
+      relations.galleryLink?.slots({
+        attributes: {class: slots.currentExtra === 'gallery' && 'current'},
+        content: language.$('misc.nav.gallery'),
+      }),
+    ];
+
+    const extrasPart =
+      (empty(extraLinks)
+        ? ''
+        : language.formatUnitList([infoLink, ...extraLinks]));
+
+    const accent =
+      `(${[extrasPart, previousNextPart].filter(Boolean).join('; ')})`;
+
+    return [
+      {auto: 'home'},
+
+      data.enableListings &&
+        {
+          path: ['localized.listingIndex'],
+          title: language.$('listingIndex.title'),
+        },
+
+      {
+        accent,
+        html:
+          language.$('groupPage.nav.group', {
+            group: relations.mainLink,
+          }),
+      },
+    ].filter(Boolean);
+  },
+};
diff --git a/src/content/dependencies/generateGroupSidebar.js b/src/content/dependencies/generateGroupSidebar.js
new file mode 100644
index 0000000..6baf37f
--- /dev/null
+++ b/src/content/dependencies/generateGroupSidebar.js
@@ -0,0 +1,35 @@
+export default {
+  contentDependencies: ['generateGroupSidebarCategoryDetails'],
+  extraDependencies: ['html', 'language', 'wikiData'],
+
+  sprawl({groupCategoryData}) {
+    return {groupCategoryData};
+  },
+
+  relations(relation, sprawl, group) {
+    return {
+      categoryDetails:
+        sprawl.groupCategoryData.map(category =>
+          relation('generateGroupSidebarCategoryDetails', category, group)),
+    };
+  },
+
+  slots: {
+    currentExtra: {
+      validate: v => v.is('gallery'),
+    },
+  },
+
+  generate(relations, slots, {html, language}) {
+    return {
+      leftSidebarContent: [
+        html.tag('h1',
+          language.$('groupSidebar.title')),
+
+        relations.categoryDetails
+          .map(details =>
+            details.slot('currentExtra', slots.currentExtra)),
+      ],
+    };
+  },
+};
diff --git a/src/content/dependencies/generateGroupSidebarCategoryDetails.js b/src/content/dependencies/generateGroupSidebarCategoryDetails.js
new file mode 100644
index 0000000..ec707e3
--- /dev/null
+++ b/src/content/dependencies/generateGroupSidebarCategoryDetails.js
@@ -0,0 +1,77 @@
+import {empty} from '../../util/sugar.js';
+
+export default {
+  contentDependencies: [
+    'generateColorStyleVariables',
+    'linkGroup',
+    'linkGroupGallery',
+  ],
+
+  extraDependencies: ['html', 'language'],
+
+  relations(relation, category) {
+    return {
+      colorVariables: relation('generateColorStyleVariables', category.color),
+
+      // Which of these is used depends on the currentExtra slot, so all
+      // available links are included here.
+      groupLinks: category.groups.map(group => {
+        const links = {};
+        links.info = relation('linkGroup', group);
+
+        if (!empty(group.albums)) {
+          links.gallery = relation('linkGroupGallery', group);
+        }
+
+        return links;
+      }),
+    };
+  },
+
+  data(category, group) {
+    const data = {};
+
+    data.name = category.name;
+    data.isCurrentCategory = category === group.category;
+
+    if (data.isCurrentCategory) {
+      data.currentGroupIndex = category.groups.indexOf(group);
+    }
+
+    return data;
+  },
+
+  slots: {
+    currentExtra: {
+      validate: v => v.is('gallery'),
+    },
+  },
+
+  generate(data, relations, slots, {html, language}) {
+    return html.tag('details',
+      {
+        open: data.isCurrentCategory,
+        class: data.isCurrentCategory && 'current',
+      },
+      [
+        html.tag('summary',
+          {style: relations.colorVariables},
+          html.tag('span',
+            language.$('groupSidebar.groupList.category', {
+              category:
+                html.tag('span', {class: 'group-name'},
+                  data.name),
+            }))),
+
+        html.tag('ul',
+          relations.groupLinks.map((links, index) =>
+            html.tag('li',
+              {class: index === data.currentGroupIndex && 'current'},
+              language.$('groupSidebar.groupList.item', {
+                group:
+                  links[slots.currentExtra ?? 'info'] ??
+                  links.info,
+              })))),
+      ]);
+  },
+};
diff --git a/src/content/dependencies/linkGroupExtra.js b/src/content/dependencies/linkGroupExtra.js
new file mode 100644
index 0000000..ee6a3b1
--- /dev/null
+++ b/src/content/dependencies/linkGroupExtra.js
@@ -0,0 +1,34 @@
+import {empty} from '../../util/sugar.js';
+
+export default {
+  contentDependencies: [
+    'linkGroup',
+    'linkGroupGallery',
+  ],
+
+  extraDependencies: ['html'],
+
+  relations(relation, group) {
+    const relations = {};
+
+    relations.info =
+      relation('linkGroup', group);
+
+    if (!empty(group.albums)) {
+      relations.gallery =
+        relation('linkGroupGallery', group);
+    }
+
+    return relations;
+  },
+
+  slots: {
+    extra: {
+      validate: v => v.is('gallery'),
+    },
+  },
+
+  generate(relations, slots) {
+    return relations[slots.extra ?? 'info'] ?? relations.info;
+  },
+};
diff --git a/src/page/group.js b/src/page/group.js
index 81e1728..4d5f91c 100644
--- a/src/page/group.js
+++ b/src/page/group.js
@@ -15,307 +15,28 @@ export function targets({wikiData}) {
   return wikiData.groupData;
 }
 
-export function write(group, {wikiData}) {
-  const {listingSpec, wikiInfo} = wikiData;
+export function pathsForTarget(group) {
+  const hasGalleryPage = !empty(group.albums);
 
-  const tracks = group.albums.flatMap((album) => album.tracks);
-  const totalDuration = getTotalDuration(tracks, {originalReleasesOnly: true});
+  return [
+    {
+      type: 'page',
+      path: ['groupInfo', group.directory],
 
-  const albumLines = group.albums.map((album) => ({
-    album,
-    otherGroup: album.groups.find((g) => g !== group),
-  }));
-
-  const infoPage = {
-    type: 'page',
-    path: ['groupInfo', group.directory],
-    page: ({
-      fancifyURL,
-      generateInfoGalleryLinks,
-      generateNavigationLinks,
-      getLinkThemeString,
-      getThemeString,
-      html,
-      language,
-      link,
-      transformMultiline,
-    }) => ({
-      title: language.$('groupInfoPage.title', {group: group.name}),
-
-      themeColor: group.color,
-      theme: getThemeString(group.color),
-
-      main: {
-        headingMode: 'sticky',
-
-        content: [
-          !empty(group.urls) &&
-            html.tag('p',
-              language.$('releaseInfo.visitOn', {
-                links: language.formatDisjunctionList(
-                  group.urls.map(url => fancifyURL(url, {language}))),
-              })),
-
-          group.description &&
-            html.tag('blockquote',
-              transformMultiline(group.description)),
-
-          ...html.fragment(
-            !empty(group.albums) && [
-              html.tag('h2',
-                {class: ['content-heading']},
-                language.$('groupInfoPage.albumList.title')),
-
-              html.tag('p',
-                language.$('groupInfoPage.viewAlbumGallery', {
-                  link: link.groupGallery(group, {
-                    text: language.$('groupInfoPage.viewAlbumGallery.link'),
-                  }),
-                })),
-
-              html.tag('ul',
-                albumLines.map(({album, otherGroup}) => {
-                  const item = album.date
-                    ? language.$('groupInfoPage.albumList.item', {
-                        year: album.date.getFullYear(),
-                        album: link.album(album),
-                      })
-                    : language.$('groupInfoPage.albumList.item.withoutYear', {
-                        album: link.album(album),
-                      });
-                  return html.tag('li',
-                    otherGroup
-                      ? language.$('groupInfoPage.albumList.item.withAccent', {
-                          item,
-                          accent: html.tag('span',
-                            {class: 'other-group-accent'},
-                            language.$('groupInfoPage.albumList.item.otherGroupAccent', {
-                              group: link.groupInfo(otherGroup, {
-                                color: false,
-                              }),
-                            })),
-                        })
-                      : item);
-                })),
-            ]),
-        ],
-      },
-
-      sidebarLeft: generateGroupSidebar(group, false, {
-        getLinkThemeString,
-        html,
-        language,
-        link,
-        wikiData,
-      }),
-
-      nav: generateGroupNav(group, false, {
-        generateInfoGalleryLinks,
-        generateNavigationLinks,
-        language,
-        link,
-        wikiData,
-      }),
-    }),
-  };
-
-  const galleryPage = !empty(group.albums) && {
-    type: 'page',
-    path: ['groupGallery', group.directory],
-    page: ({
-      generateInfoGalleryLinks,
-      generateNavigationLinks,
-      getAlbumCover,
-      getAlbumGridHTML,
-      getCarouselHTML,
-      getLinkThemeString,
-      getThemeString,
-      html,
-      language,
-      link,
-    }) => ({
-      title: language.$('groupGalleryPage.title', {group: group.name}),
-
-      themeColor: group.color,
-      theme: getThemeString(group.color),
-
-      main: {
-        classes: ['top-index'],
-        headingMode: 'static',
-
-        content: [
-          getCarouselHTML({
-            items: group.featuredAlbums.slice(0, 12 + 1),
-            srcFn: getAlbumCover,
-            linkFn: link.album,
-          }),
-
-          html.tag('p',
-            {class: 'quick-info'},
-            language.$('groupGalleryPage.infoLine', {
-              tracks: html.tag('b',
-                language.countTracks(tracks.length, {
-                  unit: true,
-                })),
-              albums: html.tag('b',
-                language.countAlbums(group.albums.length, {
-                  unit: true,
-                })),
-              time: html.tag('b',
-                language.formatDuration(totalDuration, {
-                  unit: true,
-                })),
-            })),
-
-          wikiInfo.enableGroupUI &&
-          wikiInfo.enableListings &&
-            html.tag('p',
-              {class: 'quick-info'},
-              language.$('groupGalleryPage.anotherGroupLine', {
-                link: link.listing(
-                  listingSpec.find(l => l.directory === 'groups/by-category'),
-                  {
-                    text: language.$('groupGalleryPage.anotherGroupLine.link'),
-                  }),
-              })),
-
-          html.tag('div',
-            {class: 'grid-listing'},
-            getAlbumGridHTML({
-              entries: sortChronologically(
-                group.albums
-                  .filter(album => album.isListedInGalleries)
-                  .map(album => ({
-                    item: album,
-                    directory: album.directory,
-                    name: album.name,
-                    date: album.date,
-                  }))
-              ).reverse(),
-              details: true,
-            })),
-        ],
+      contentFunction: {
+        name: 'generateGroupInfoPage',
+        args: [group],
       },
+    },
 
-      sidebarLeft: generateGroupSidebar(group, true, {
-        getLinkThemeString,
-        html,
-        language,
-        link,
-        wikiData,
-      }),
+    hasGalleryPage && {
+      type: 'page',
+      path: ['groupGallery', group.directory],
 
-      nav: generateGroupNav(group, true, {
-        generateInfoGalleryLinks,
-        generateNavigationLinks,
-        language,
-        link,
-        wikiData,
-      }),
-    }),
-  };
-
-  return [infoPage, galleryPage].filter(Boolean);
-}
-
-// Utility functions
-
-function generateGroupSidebar(currentGroup, isGallery, {
-  getLinkThemeString,
-  html,
-  language,
-  link,
-  wikiData,
-}) {
-  const {groupCategoryData, wikiInfo} = wikiData;
-
-  if (!wikiInfo.enableGroupUI) {
-    return null;
-  }
-
-  return {
-    content: [
-      html.tag('h1',
-        language.$('groupSidebar.title')),
-
-      ...groupCategoryData.map((category) =>
-        html.tag('details',
-          {
-            open: category === currentGroup.category,
-            class: category === currentGroup.category && 'current',
-          },
-          [
-            html.tag('summary',
-              {style: getLinkThemeString(category.color)},
-              html.tag('span',
-                language.$('groupSidebar.groupList.category', {
-                  category: `<span class="group-name">${category.name}</span>`,
-                }))),
-            html.tag('ul',
-              category.groups.map((group) => {
-                const linkKey = (
-                  isGallery && !empty(group.albums)
-                    ? 'groupGallery'
-                    : 'groupInfo');
-
-                return html.tag('li',
-                  {
-                    class: group === currentGroup && 'current',
-                    style: getLinkThemeString(group.color),
-                  },
-                  language.$('groupSidebar.groupList.item', {
-                    group: link[linkKey](group),
-                  }));
-              })),
-          ])),
-    ],
-  };
-}
-
-function generateGroupNav(currentGroup, isGallery, {
-  generateInfoGalleryLinks,
-  generateNavigationLinks,
-  link,
-  language,
-  wikiData,
-}) {
-  const {groupData, wikiInfo} = wikiData;
-
-  if (!wikiInfo.enableGroupUI) {
-    return {simple: true};
-  }
-
-  const linkKey = isGallery ? 'groupGallery' : 'groupInfo';
-
-  const infoGalleryLinks = generateInfoGalleryLinks(currentGroup, isGallery, {
-    linkKeyGallery: 'groupGallery',
-    linkKeyInfo: 'groupInfo',
-  });
-
-  const previousNextLinks = generateNavigationLinks(currentGroup, {
-    data: groupData,
-    linkKey,
-  });
-
-  return {
-    linkContainerClasses: ['nav-links-hierarchy'],
-    links: [
-      {toHome: true},
-      wikiInfo.enableListings && {
-        path: ['localized.listingIndex'],
-        title: language.$('listingIndex.title'),
-      },
-      {
-        html: language.$('groupPage.nav.group', {
-          group: link[linkKey](currentGroup, {class: 'current'}),
-        }),
-      },
-      {
-        divider: false,
-        html: previousNextLinks
-          ? `(${infoGalleryLinks}; ${previousNextLinks})`
-          : `(${previousNextLinks})`,
+      contentFunction: {
+        name: 'generateGroupGalleryPage',
+        args: [group],
       },
-    ],
-  };
+    },
+  ];
 }
diff --git a/src/page/index.js b/src/page/index.js
index 311fcd6..5a62213 100644
--- a/src/page/index.js
+++ b/src/page/index.js
@@ -11,7 +11,7 @@ export * as album from './album.js';
 export * as artist from './artist.js';
 export * as artistAlias from './artist-alias.js';
 // export * as flash from './flash.js';
-// export * as group from './group.js';
+export * as group from './group.js';
 // export * as homepage from './homepage.js';
 // export * as listing from './listing.js';
 // export * as news from './news.js';