Skip to content

Commit f43f683

Browse files
authored
feat(lxlweb): Read site configuration from backend. (#1665)
* feat(lxlweb): Read site configuration from backend * feat(lxlweb): configurable hero image * feat(lxlweb): add another sample hero image * feat(lxlweb): site feature flags * feat(lxlweb): configurable footer * feat(lxlweb): add site feature flag resourceImages * feat(lxlweb): make featuredSearches configurable * feat(lxlweb): make AppMenu configurable * feat(lxlweb): Use siteName in cookie consent dialog * feat(lxlweb): configurable about page * feat: configurable favicon * feat(lxlweb): add another sample hero image
1 parent ea6f8f5 commit f43f683

43 files changed

Lines changed: 734 additions & 389 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

lxl-web/src/app.d.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import type { UserSettings } from '$lib/types/userSettings';
66
import type { DisplayUtil, VocabUtil } from '$lib/utils/xl.server';
77
import type { AdjecentSearchResult, DisplayMapping, QualifierSuggestion2 } from '$lib/types/search';
88
import 'unplugin-icons/types/svelte';
9-
import type { Site } from '$lib/types/site';
9+
import type { AppMenuItem, Features, FooterSection, Site } from '$lib/types/site';
1010

1111
declare global {
1212
namespace App {
@@ -33,6 +33,9 @@ declare global {
3333
}
3434
interface PageData extends LayoutData {
3535
base: string;
36+
features: Features;
37+
footer: FooterSection[];
38+
appMenuItems: AppMenuItem[];
3639
librisSession?: string;
3740
locale: string;
3841
qualifierSuggestions: QualifierSuggestion2[];

lxl-web/src/app.html

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
<html lang="%lang%" data-theme="%theme%">
33
<head>
44
<meta charset="utf-8">
5-
<link rel="icon" href="%sveltekit.assets%/favicon.svg" type="image/svg+xml">
6-
<link rel="icon" href="%sveltekit.assets%/favicon.png" type="image/png">
7-
<link rel="apple-touch-icon" sizes="180x180" href="%sveltekit.assets%/apple-touch-icon.png">
5+
<link rel="icon" href="%sveltekit.assets%/favicon/%favicon%/favicon.svg" type="image/svg+xml">
6+
<link rel="icon" href="%sveltekit.assets%/favicon/%favicon%/favicon.png" type="image/png">
7+
<link rel="apple-touch-icon" sizes="180x180" href="%sveltekit.assets%/favicon/%favicon%/apple-touch-icon.png">
88
<meta name="viewport" content="width=device-width, initial-scale=1">
99
%sveltekit.head%
1010
</head>

lxl-web/src/error.html

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22
<html lang="en">
33
<head>
44
<meta charset="utf-8" />
5-
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
6-
<link rel="icon" href="/favicon.png" type="image/png">
7-
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
5+
<link rel="icon" href="/favicon/libris/favicon.svg" type="image/svg+xml">
6+
<link rel="icon" href="/favicon/libris/favicon.png" type="image/png">
7+
<link rel="apple-touch-icon" sizes="180x180" href="/favicon/libris/apple-touch-icon.png">
88
<link rel="stylesheet" href="/error.css">
99
<meta name="viewport" content="width=device-width" />
1010

lxl-web/src/hooks.server.ts

Lines changed: 56 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,28 @@ import { getLibrary, getOrgMembers, startRefreshLibraries } from '$lib/utils/get
2222
import { getSubsetMapping } from '$lib/utils/subsetCache.server';
2323
import { getQualifierSuggestions } from '$lib/utils/getQualifierSuggestions.server';
2424
import { updateSettings } from '$lib/utils/userSettings.svelte';
25+
import { JsonLd } from '$lib/types/xl';
2526

2627
type QualifierSuggestionsByLocale = Record<keyof typeof Locales, QualifierSuggestion2[]>;
2728
type Util = [VocabUtil, DisplayUtil, QualifierSuggestionsByLocale];
2829
let utilCache: Promise<Util> | undefined;
30+
const subsiteCache: Record<string, Promise<Site>> = {};
2931

3032
// Warm up caches immediately on startup instead of waiting for a request
3133
export const init: ServerInit = async () => {
3234
try {
3335
const [, displayUtil] = await loadUtilCached();
3436
startRefreshLibraries(displayUtil, defaultLocale);
37+
38+
const sites = (env['SUBSITES'] || '')
39+
.split(',')
40+
.map((s) => s.trim())
41+
.filter((s) => s.length > 0)
42+
.map((site) => env[`SUBSITE.${site}.SEARCH_SITE`])
43+
.filter((s) => typeof s === 'string');
44+
for (const s of sites) {
45+
await getSiteConfCached(s);
46+
}
3547
} catch (err) {
3648
// This is OK, handle() will retry
3749
console.error('Startup initialization failed:', err);
@@ -53,7 +65,7 @@ export const handle = async ({ event, resolve }) => {
5365
redirect(302, `?_r=itemHeldByOrg:${clean}`);
5466
}
5567

56-
const site = getSite(event);
68+
const site = await getSite(event);
5769

5870
if (site) {
5971
event.locals.site = site;
@@ -172,7 +184,8 @@ export const handle = async ({ event, resolve }) => {
172184
});
173185

174186
// set data-theme defined in themes.css
175-
const dataTheme = site?.themeName || 'libris';
187+
const dataTheme = site?.configuration?.themeName || 'libris';
188+
const favicon = site?.configuration?.favicon || 'libris';
176189

177190
// get subset mapping
178191
const _r = event.url.searchParams.get('_r');
@@ -188,7 +201,8 @@ export const handle = async ({ event, resolve }) => {
188201
}
189202

190203
return resolve(event, {
191-
transformPageChunk: ({ html }) => html.replace('%lang%', lang).replace('%theme%', dataTheme)
204+
transformPageChunk: ({ html }) =>
205+
html.replace('%lang%', lang).replace('%theme%', dataTheme).replaceAll('%favicon%', favicon)
192206
});
193207
};
194208

@@ -262,23 +276,56 @@ async function loadUtil(): Promise<Util> {
262276
return [vocabUtil, displayUtil, qualifierSuggestionsByLocale];
263277
}
264278

265-
function getSite(event: RequestEvent): Site | null {
279+
function getSite(event: RequestEvent): Promise<Site> | null {
266280
// TODO replace this with proper domain matching
267281
const deepestSubDomain = event.url.hostname.split('.')[0];
268282

269283
if (configuredSubDomains().includes(deepestSubDomain)) {
270284
// TODO fetch from backend?
271285
const site = deepestSubDomain;
272-
return {
273-
name: env[`SUBSITE.${site}.NAME`] || site,
274-
themeName: env[`SUBSITE.${site}.APP_THEME`],
275-
searchSite: env[`SUBSITE.${site}.SEARCH_SITE`]
276-
};
286+
const siteUrl = env[`SUBSITE.${site}.SEARCH_SITE`];
287+
if (!siteUrl) {
288+
return null;
289+
}
290+
return getSiteConfCached(siteUrl);
277291
}
278292

279293
return null;
280294
}
281295

296+
async function getSiteConfCached(siteUrl: string) {
297+
if (!subsiteCache[siteUrl]) {
298+
subsiteCache[siteUrl] = loadSiteConf(siteUrl).catch((err) => {
299+
delete subsiteCache[siteUrl];
300+
throw err;
301+
});
302+
}
303+
return subsiteCache[siteUrl];
304+
}
305+
306+
async function loadSiteConf(siteUrl: string): Promise<Site> {
307+
const fetchUrl = `${env.API_URL}/${siteUrl}?nocache=1`;
308+
console.log(`Fetching ${fetchUrl}`);
309+
const response = await fetch(fetchUrl, {
310+
headers: { Accept: 'application/ld+json' },
311+
signal: AbortSignal.timeout(10_000)
312+
});
313+
if (!response.ok) {
314+
throw new Error(
315+
`Failed to load site conf: ${fetchUrl} ${response.status} ${response.statusText}`
316+
);
317+
}
318+
const conf = (await response.json())[JsonLd.GRAPH][1];
319+
320+
const site = {
321+
name: conf['title'],
322+
searchSite: siteUrl,
323+
configuration: conf['lxlwebConfiguration']
324+
};
325+
console.log(`Loaded site conf: ${JSON.stringify(site, null, 2)}`);
326+
return site;
327+
}
328+
282329
function configuredSubDomains(): string[] {
283330
return env.SUBSITES?.split(',').map((s) => s.trim()) || [];
284331
}
869 KB
Loading
717 KB
Loading
File renamed without changes.

lxl-web/src/lib/components/AppMenuContent.svelte

Lines changed: 25 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -48,43 +48,40 @@
4848
LeadingIcon: IconSearch,
4949
onclick: onclickSearch
5050
},
51-
{
52-
href: page.data.localizeHref('/my-pages'),
53-
label: page.data.t('appMenu.myPages'),
54-
currentPage: page.route.id === '/(app)/[[lang=lang]]/my-pages',
55-
LeadingIcon: IconSaved
56-
},
57-
{
58-
href: page.data.localizeHref('/collections'),
59-
label: page.data.t('appMenu.specialCollections'),
60-
currentPage: page.route.id === '/(app)/[[lang=lang]]/collections',
61-
LeadingIcon: BiListColumnsReversed
62-
},
63-
/*
64-
{
65-
href: page.data.localizeHref('/subsets'),
66-
label: page.data.t('appMenu.subsets'),
67-
currentPage: page.route.id === '/(app)/[[lang=lang]]/subsets'
68-
},
69-
*/
51+
...(page.data.features.myPages
52+
? [
53+
{
54+
href: page.data.localizeHref('/my-pages'),
55+
label: page.data.t('appMenu.myPages'),
56+
currentPage: page.route.id === '/(app)/[[lang=lang]]/my-pages',
57+
LeadingIcon: IconSaved
58+
}
59+
]
60+
: []),
61+
...(page.data.features.specialCollections
62+
? [
63+
{
64+
href: page.data.localizeHref('/collections'),
65+
label: page.data.t('appMenu.specialCollections'),
66+
currentPage: page.route.id === '/(app)/[[lang=lang]]/collections',
67+
LeadingIcon: BiListColumnsReversed
68+
}
69+
]
70+
: []),
7071
{
7172
href: page.data.localizeHref('/help/search'),
7273
label: page.data.t('appMenu.help'),
7374
currentPage: page.route.id === '/(app)/[[lang=lang]]/help/search'
7475
},
75-
{
76-
href: 'https://www.kb.se/om-oss/kontakta-oss#item_125b5507eb18cedffd1a43f869_45b5507eb18cedffd1a4321f3',
77-
label: page.data.t('footer.customerSupport')
78-
},
7976
{
8077
href: page.data.localizeHref('/about'),
81-
label: page.data.t('appMenu.about'),
78+
label: page.data.t('footer.about'),
8279
currentPage: page.route.id === '/(app)/[[lang=lang]]/about'
8380
},
84-
{
85-
href: 'https://www.kb.se/for-bibliotekssektorn/tjanster-och-verktyg/arbeta-med-libris/fragor-och-svar-om-libris-nya-soktjanst.html',
86-
label: page.data.t('appMenu.aboutBeta')
87-
},
81+
...page.data.appMenuItems.map((item) => ({
82+
href: item.href,
83+
label: page.data.t(item.titleKey || '???.???')
84+
})),
8885
{
8986
href: page.data.localizeHref(page.url.pathname + page.url.search + page.url.hash, {
9087
locale: otherLangCode

lxl-web/src/lib/components/CookieConsent.svelte

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,12 @@
1010
1111
const matomoTracker = getMatomoTracker();
1212
13+
const siteName = page.data.siteName ?? 'Libris';
14+
15+
function withSiteName(translations: typeof svTranslations): typeof svTranslations {
16+
return JSON.parse(JSON.stringify(translations).replaceAll('{siteName}', siteName));
17+
}
18+
1319
const config: CookieConsent.CookieConsentConfig = {
1420
guiOptions: {
1521
consentModal: {
@@ -50,8 +56,8 @@
5056
language: {
5157
default: page.data.locale,
5258
translations: {
53-
sv: svTranslations,
54-
en: enTranslations
59+
sv: withSiteName(svTranslations),
60+
en: withSiteName(enTranslations)
5561
}
5662
}
5763
};

lxl-web/src/lib/components/Resource.svelte

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,10 @@
128128
);
129129
130130
const hasHoldingsBtn = $derived(
131-
holdings.byType && Object.keys(holdings.byType).length && instances
131+
holdings.byType &&
132+
Object.keys(holdings.byType).length &&
133+
instances &&
134+
page.data.features.holdings
132135
);
133136
</script>
134137

@@ -177,7 +180,11 @@
177180
</section>
178181
{/if}
179182
<div
180-
class="max-w-10xl wide:max-w-screen mx-auto flex flex-col gap-3 px-3 @sm:gap-6 @sm:px-6 @3xl:grid @3xl:grid-cols-(--two-grid-cols) @3xl:gap-9 @7xl:grid-cols-(--three-grid-cols) @7xl:px-12"
183+
class={[
184+
'max-w-10xl wide:max-w-screen mx-auto flex flex-col gap-3 px-3 @sm:gap-6 @sm:px-6 @3xl:gap-9 @7xl:px-12',
185+
page.data.features.resourceImages &&
186+
'@3xl:grid @3xl:grid-cols-(--two-grid-cols) @7xl:grid-cols-(--three-grid-cols)'
187+
]}
181188
>
182189
{#if tableOfContents.length}
183190
<div class="order-last hidden @7xl:block print:hidden">
@@ -187,17 +194,19 @@
187194
</div>
188195
{/if}
189196
<div>
190-
<div class="sticky mx-auto pt-3 @sm:pt-6 @3xl:max-w-xs @3xl:pb-6">
191-
<ResourceImage
192-
{image}
193-
type={typeForIcon}
194-
alt={page.data.t('general.instanceCover')}
195-
thumbnailTargetWidth={typeForIcon.includes('Bibliography')
196-
? ImageWidth.FULL
197-
: ImageWidth.MEDIUM}
198-
linkToFull
199-
/>
200-
</div>
197+
{#if page.data.features.resourceImages}
198+
<div class="sticky mx-auto pt-3 @sm:pt-6 @3xl:max-w-xs @3xl:pb-6">
199+
<ResourceImage
200+
{image}
201+
type={typeForIcon}
202+
alt={page.data.t('general.instanceCover')}
203+
thumbnailTargetWidth={typeForIcon.includes('Bibliography')
204+
? ImageWidth.FULL
205+
: ImageWidth.MEDIUM}
206+
linkToFull
207+
/>
208+
</div>
209+
{/if}
201210
</div>
202211
<div class="wide:max-w-screen mx-auto flex w-full max-w-4xl flex-col gap-3 py-6 @sm:gap-6">
203212
<section id="{uidPrefix}top">

0 commit comments

Comments
 (0)