feat: certificates, landing pages, ai tutor, website update, compliance
This commit is contained in:
@@ -1,16 +1,42 @@
|
||||
<script lang="ts">
|
||||
import { PageRestricted } from '$features/ui';
|
||||
import { currentOrg } from '$lib/utils/store/org';
|
||||
import { onMount } from 'svelte';
|
||||
import { afterNavigate } from '$app/navigation';
|
||||
import { env } from '$env/dynamic/public';
|
||||
import { analytics } from '@cio/analytics/client';
|
||||
import type { Snippet } from 'svelte';
|
||||
import type { AccountOrg } from '$features/app/types';
|
||||
|
||||
interface Props {
|
||||
children?: import('svelte').Snippet;
|
||||
children?: Snippet;
|
||||
data: {
|
||||
isOrgSite: boolean;
|
||||
org: import('$features/app/types').AccountOrg | null;
|
||||
org: AccountOrg | null;
|
||||
};
|
||||
}
|
||||
|
||||
let { children, data }: Props = $props();
|
||||
|
||||
onMount(() => {
|
||||
if (!env.PUBLIC_SERVER_URL) return;
|
||||
|
||||
analytics.init({
|
||||
endpoint: `${env.PUBLIC_SERVER_URL.replace(/\/$/, '')}/dash/track`,
|
||||
orgId: data.org?.id ?? undefined
|
||||
});
|
||||
analytics.pageView({ kind: 'landing' });
|
||||
});
|
||||
|
||||
afterNavigate(({ to }) => {
|
||||
if (!to) return;
|
||||
|
||||
const isCoursePage = /\/course\//.test(to.url.pathname);
|
||||
analytics.pageView({
|
||||
kind: isCoursePage ? 'course' : 'landing',
|
||||
path: to.url.pathname
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if data.org?.isRestricted || $currentOrg.isRestricted}
|
||||
|
||||
@@ -11,7 +11,7 @@ type GetOrganizationSuccess = Extract<InferResponseType<GetOrganizationRequest>,
|
||||
type GetPublicCourseRequest = (typeof classroomio)['org-site']['course'][':courseSlug']['$get'];
|
||||
type GetPublicCourseSuccess = Extract<InferResponseType<GetPublicCourseRequest>, { success: true }>;
|
||||
|
||||
export const load = async ({ params = { slug: '' }, parent }) => {
|
||||
export const load = async ({ params = { slug: '' }, parent, url }) => {
|
||||
const { org: parentOrg } = await parent();
|
||||
|
||||
// PUBLIC courses are served by the anonymous org-site endpoint and bypass the
|
||||
@@ -67,18 +67,23 @@ export const load = async ({ params = { slug: '' }, parent }) => {
|
||||
}
|
||||
}
|
||||
|
||||
const canonicalUrl = new URL(url.pathname, url.origin).href;
|
||||
|
||||
const pageMetaTags = Object.freeze({
|
||||
title: course.title,
|
||||
description: course.description,
|
||||
canonical: canonicalUrl,
|
||||
openGraph: {
|
||||
type: 'website',
|
||||
url: canonicalUrl,
|
||||
title: course.title,
|
||||
description: course.description,
|
||||
images: [
|
||||
{
|
||||
url: course.logo || '',
|
||||
alt: course.title,
|
||||
width: 280,
|
||||
height: 200,
|
||||
width: 1200,
|
||||
height: 630,
|
||||
secureUrl: course.logo,
|
||||
type: 'image/jpeg'
|
||||
}
|
||||
@@ -91,7 +96,7 @@ export const load = async ({ params = { slug: '' }, parent }) => {
|
||||
title: course.title,
|
||||
description: course.description,
|
||||
image: course.logo,
|
||||
imageAlt: 'ClassroomIO OG Image'
|
||||
imageAlt: course.title
|
||||
}
|
||||
}) satisfies MetaTagsProps;
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { resolve } from '$app/paths';
|
||||
import { page } from '$app/state';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
import { basePath } from '$lib/utils/store/app';
|
||||
@@ -57,6 +58,25 @@
|
||||
const orgName = $derived(data.org?.name ?? data.course?.org?.name ?? 'ClassroomIO');
|
||||
const logoUrl = $derived(data.org?.avatarUrl ?? undefined);
|
||||
|
||||
const courseJsonLd = $derived.by(() => {
|
||||
if (!data.course) return null;
|
||||
|
||||
const schema = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'Course',
|
||||
name: data.course.title,
|
||||
description: data.course.description || '',
|
||||
provider: {
|
||||
'@type': 'Organization',
|
||||
name: orgName
|
||||
},
|
||||
url: page.url.href,
|
||||
...(data.course.logo ? { image: data.course.logo } : {})
|
||||
};
|
||||
|
||||
return JSON.stringify(schema).replace(/</g, '\\u003c');
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
let NavComponent = $state<Component<any> | null>(null);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
@@ -83,6 +103,12 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
{#if courseJsonLd}
|
||||
{@html `<script type="application/ld+json">${courseJsonLd}</script>`}
|
||||
{/if}
|
||||
</svelte:head>
|
||||
|
||||
{#if data.course}
|
||||
<PoweredBy />
|
||||
|
||||
@@ -102,12 +128,6 @@
|
||||
|
||||
<CourseLandingPage courseData={data.course} />
|
||||
|
||||
<OrgLandingPageFooter
|
||||
{orgName}
|
||||
{logoUrl}
|
||||
footerLinks={landingSettings.footerLinks}
|
||||
footerText={landingSettings.footerText}
|
||||
variant={landingSettings.theme}
|
||||
/>
|
||||
<OrgLandingPageFooter {orgName} {logoUrl} footer={landingSettings.footer} variant={landingSettings.theme} />
|
||||
</main>
|
||||
{/if}
|
||||
|
||||
@@ -120,6 +120,7 @@
|
||||
|
||||
<svelte:head>
|
||||
<title>Join {data.course?.title ?? 'Course'} on ClassroomIO</title>
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
</svelte:head>
|
||||
|
||||
<AuthUI isLogin={false} {handleSubmit} isLoading={loading || !$profile.id} showOnlyContent={true} showLogo={true}>
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { classroomio, type InferResponseType } from '$lib/utils/services/api';
|
||||
import { getApiKeyHeaders, safeServerApi } from '$lib/utils/services/api/server';
|
||||
import { error } from '@sveltejs/kit';
|
||||
|
||||
type GetPublicCourseRequest = (typeof classroomio)['org-site']['course'][':courseSlug']['$get'];
|
||||
type GetPublicCourseSuccess = Extract<InferResponseType<GetPublicCourseRequest>, { success: true }>;
|
||||
|
||||
export const load = async ({ params = { slug: '' } }) => {
|
||||
const treeResult = await safeServerApi<GetPublicCourseSuccess>(() =>
|
||||
classroomio['org-site'].course[':courseSlug'].$get({ param: { courseSlug: params.slug } }, getApiKeyHeaders())
|
||||
);
|
||||
|
||||
if (!treeResult.ok) {
|
||||
throw error(404, 'Public course not found');
|
||||
}
|
||||
|
||||
return {
|
||||
tree: treeResult.body.data
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,62 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/state';
|
||||
import { PublicCourse } from '@cio/ui';
|
||||
import { toPublicSidebarSections } from '$features/course/utils/public-course-mappers';
|
||||
import type { PublicCourseSidebarItem, PublicCourseSidebarSection } from '@cio/ui/custom/public-course';
|
||||
import { t } from '$lib/utils/functions/translations';
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
let { data, children }: { data: { tree: any }; children?: Snippet } = $props();
|
||||
|
||||
const sections: PublicCourseSidebarSection[] = $derived(toPublicSidebarSections(data.tree));
|
||||
const flatItems = $derived(sections.flatMap((section) => section.items));
|
||||
|
||||
const itemSlug = $derived(page.params.itemSlug ?? '');
|
||||
const activeIndex = $derived(flatItems.findIndex((entry) => entry.slug === itemSlug));
|
||||
const activeItem = $derived<PublicCourseSidebarItem | null>(flatItems[activeIndex] ?? null);
|
||||
const prevItem = $derived(activeIndex > 0 ? flatItems[activeIndex - 1] : null);
|
||||
const nextItem = $derived(activeIndex >= 0 && activeIndex < flatItems.length - 1 ? flatItems[activeIndex + 1] : null);
|
||||
|
||||
const courseTitle = $derived(data.tree.course.title);
|
||||
const org = $derived(data.tree.course.org ?? null);
|
||||
|
||||
const hrefFor = (item: PublicCourseSidebarItem) => `/course/${data.tree.course.slug}/lesson/${item.slug}`;
|
||||
|
||||
async function navigateTo(item: PublicCourseSidebarItem | null) {
|
||||
if (!item) return;
|
||||
|
||||
await goto(hrefFor(item));
|
||||
}
|
||||
</script>
|
||||
|
||||
{#key data.slug}
|
||||
<PublicCourse.PublicCourseShell
|
||||
{sections}
|
||||
{courseTitle}
|
||||
{org}
|
||||
activeSlug={itemSlug}
|
||||
{activeItem}
|
||||
activeFlatIndex={activeIndex >= 0 ? activeIndex : null}
|
||||
totalItems={flatItems.length}
|
||||
hasPrev={!!prevItem}
|
||||
hasNext={!!nextItem}
|
||||
{prevItem}
|
||||
{nextItem}
|
||||
{hrefFor}
|
||||
exploreHref="/courses"
|
||||
signInHref="/login"
|
||||
exploreLabel={$t('public_course.header.explore_courses')}
|
||||
signInLabel={$t('public_course.header.sign_in')}
|
||||
footerPrevLabel={$t('public_course.footer_nav.previous')}
|
||||
footerNextLabel={$t('public_course.footer_nav.next')}
|
||||
courseSlug={data.tree.course.slug}
|
||||
poweredByLabel={$t('public_course.powered_by.label')}
|
||||
poweredByBrand={$t('public_course.powered_by.brand')}
|
||||
onItemClick={navigateTo}
|
||||
onPrev={() => navigateTo(prevItem)}
|
||||
onNext={() => navigateTo(nextItem)}
|
||||
>
|
||||
{@render children?.()}
|
||||
</PublicCourse.PublicCourseShell>
|
||||
{/key}
|
||||
+33
-24
@@ -4,31 +4,24 @@ import { classroomio, type InferResponseType } from '$lib/utils/services/api';
|
||||
import { getApiKeyHeaders, safeServerApi } from '$lib/utils/services/api/server';
|
||||
import { error } from '@sveltejs/kit';
|
||||
|
||||
type GetPublicCourseRequest = (typeof classroomio)['org-site']['course'][':courseSlug']['$get'];
|
||||
type GetPublicCourseItemRequest =
|
||||
(typeof classroomio)['org-site']['course'][':courseSlug']['item'][':itemSlug']['$get'];
|
||||
|
||||
type GetPublicCourseSuccess = Extract<InferResponseType<GetPublicCourseRequest>, { success: true }>;
|
||||
type GetPublicCourseItemSuccess = Extract<InferResponseType<GetPublicCourseItemRequest>, { success: true }>;
|
||||
|
||||
export const load = async ({ params = { slug: '', itemSlug: '' } }) => {
|
||||
const [treeResult, itemResult] = await Promise.all([
|
||||
safeServerApi<GetPublicCourseSuccess>(() =>
|
||||
classroomio['org-site'].course[':courseSlug'].$get({ param: { courseSlug: params.slug } }, getApiKeyHeaders())
|
||||
),
|
||||
safeServerApi<GetPublicCourseItemSuccess>(() =>
|
||||
classroomio['org-site'].course[':courseSlug'].item[':itemSlug'].$get(
|
||||
{ param: { courseSlug: params.slug, itemSlug: params.itemSlug } },
|
||||
getApiKeyHeaders()
|
||||
)
|
||||
)
|
||||
]);
|
||||
export const load = async ({ params = { slug: '', itemSlug: '' }, url, parent }) => {
|
||||
const { tree } = await parent();
|
||||
|
||||
if (!treeResult.ok || !itemResult.ok) {
|
||||
const itemResult = await safeServerApi<GetPublicCourseItemSuccess>(() =>
|
||||
classroomio['org-site'].course[':courseSlug'].item[':itemSlug'].$get(
|
||||
{ param: { courseSlug: params.slug, itemSlug: params.itemSlug } },
|
||||
getApiKeyHeaders()
|
||||
)
|
||||
);
|
||||
|
||||
if (!itemResult.ok) {
|
||||
throw error(404, 'Public course or lesson not found');
|
||||
}
|
||||
|
||||
const tree = treeResult.body.data;
|
||||
const item = itemResult.body.data;
|
||||
|
||||
const itemTitle = 'title' in item ? item.title : tree.course.title;
|
||||
@@ -37,29 +30,45 @@ export const load = async ({ params = { slug: '', itemSlug: '' } }) => {
|
||||
? item.body.replace(/<[^>]+>/g, '').slice(0, 155)
|
||||
: (tree.course.description ?? '');
|
||||
|
||||
const canonicalUrl = new URL(url.pathname, url.origin).href;
|
||||
const fullTitle = `${itemTitle} · ${tree.course.title}`;
|
||||
const description = itemSummary || tree.course.description || '';
|
||||
const ogImage = tree.course.bannerImage || null;
|
||||
|
||||
const pageMetaTags = Object.freeze({
|
||||
title: `${itemTitle} · ${tree.course.title}`,
|
||||
description: itemSummary || tree.course.description || '',
|
||||
title: fullTitle,
|
||||
description,
|
||||
canonical: canonicalUrl,
|
||||
openGraph: {
|
||||
type: 'article',
|
||||
url: canonicalUrl,
|
||||
title: itemTitle,
|
||||
description: itemSummary || tree.course.description || '',
|
||||
images: tree.course.bannerImage
|
||||
description,
|
||||
images: ogImage
|
||||
? [
|
||||
{
|
||||
url: tree.course.bannerImage,
|
||||
url: ogImage,
|
||||
alt: tree.course.title,
|
||||
width: 1200,
|
||||
height: 630,
|
||||
secureUrl: tree.course.bannerImage,
|
||||
secureUrl: ogImage,
|
||||
type: 'image/jpeg'
|
||||
}
|
||||
]
|
||||
: []
|
||||
},
|
||||
twitter: {
|
||||
handle: '@classroomio',
|
||||
site: '@classroomio',
|
||||
cardType: 'summary_large_image' as const,
|
||||
title: fullTitle,
|
||||
description,
|
||||
image: ogImage ?? undefined,
|
||||
imageAlt: tree.course.title
|
||||
}
|
||||
}) satisfies MetaTagsProps;
|
||||
|
||||
return {
|
||||
tree,
|
||||
item,
|
||||
pageMetaTags
|
||||
};
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/state';
|
||||
import { PublicCourse } from '@cio/ui';
|
||||
import {
|
||||
toPublicExerciseView,
|
||||
toPublicLessonView,
|
||||
toPublicSidebarSections
|
||||
} from '$features/course/utils/public-course-mappers';
|
||||
import { toPublicExerciseView, toPublicLessonView } from '$features/course/utils/public-course-mappers';
|
||||
import {
|
||||
publicExerciseAttemptsStorageKey,
|
||||
type PublicCourseSidebarItem,
|
||||
type PublicCourseSidebarSection,
|
||||
type PublicLessonViewData,
|
||||
type PublicExerciseViewData
|
||||
} from '@cio/ui/custom/public-course';
|
||||
@@ -20,26 +14,8 @@
|
||||
|
||||
let { data } = $props();
|
||||
|
||||
const sections: PublicCourseSidebarSection[] = $derived(toPublicSidebarSections(data.tree));
|
||||
const flatItems = $derived(sections.flatMap((section) => section.items));
|
||||
|
||||
const callout = $derived(data?.tree?.course.callout ?? null);
|
||||
const itemSlug = $derived(data.item.slug);
|
||||
const activeIndex = $derived(flatItems.findIndex((entry) => entry.slug === itemSlug));
|
||||
const activeItem = $derived<PublicCourseSidebarItem | null>(flatItems[activeIndex] ?? null);
|
||||
const prevItem = $derived(activeIndex > 0 ? flatItems[activeIndex - 1] : null);
|
||||
const nextItem = $derived(activeIndex >= 0 && activeIndex < flatItems.length - 1 ? flatItems[activeIndex + 1] : null);
|
||||
|
||||
const callout = $derived(data.tree.course.callout ?? null);
|
||||
const courseTitle = $derived(data.tree.course.title);
|
||||
const org = $derived(data.tree.course.org ?? null);
|
||||
|
||||
const hrefFor = (item: PublicCourseSidebarItem) => `/course/${data.tree.course.slug}/lesson/${item.slug}`;
|
||||
|
||||
async function navigateTo(item: PublicCourseSidebarItem | null) {
|
||||
if (!item) return;
|
||||
|
||||
await goto(hrefFor(item));
|
||||
}
|
||||
|
||||
const lessonView = $derived<PublicLessonViewData | null>(
|
||||
data.item.kind === 'lesson' ? toPublicLessonView(data.item) : null
|
||||
@@ -47,52 +23,61 @@
|
||||
const exerciseView = $derived<PublicExerciseViewData | null>(
|
||||
data.item.kind === 'exercise' ? toPublicExerciseView(data.item) : null
|
||||
);
|
||||
|
||||
const breadcrumbJsonLd = $derived.by(() => {
|
||||
const courseUrl = new URL(`/course/${data?.tree.course.slug}`, page.url.origin).href;
|
||||
const itemTitle = 'title' in data.item ? data.item.title : data?.tree.course.title;
|
||||
|
||||
const schema = {
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'BreadcrumbList',
|
||||
itemListElement: [
|
||||
{
|
||||
'@type': 'ListItem',
|
||||
position: 1,
|
||||
name: data?.tree.course.title,
|
||||
item: courseUrl
|
||||
},
|
||||
{
|
||||
'@type': 'ListItem',
|
||||
position: 2,
|
||||
name: itemTitle,
|
||||
item: page.url.href
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
return JSON.stringify(schema).replace(/</g, '\\u003c');
|
||||
});
|
||||
</script>
|
||||
|
||||
<PublicCourse.PublicCourseShell
|
||||
{sections}
|
||||
{courseTitle}
|
||||
{org}
|
||||
activeSlug={itemSlug}
|
||||
{activeItem}
|
||||
activeFlatIndex={activeIndex >= 0 ? activeIndex : null}
|
||||
totalItems={flatItems.length}
|
||||
hasPrev={!!prevItem}
|
||||
hasNext={!!nextItem}
|
||||
{prevItem}
|
||||
{nextItem}
|
||||
{hrefFor}
|
||||
exploreHref="/courses"
|
||||
signInHref="/login"
|
||||
exploreLabel={$t('public_course.header.explore_courses')}
|
||||
signInLabel={$t('public_course.header.sign_in')}
|
||||
footerPrevLabel={$t('public_course.footer_nav.previous')}
|
||||
footerNextLabel={$t('public_course.footer_nav.next')}
|
||||
courseSlug={data.tree.course.slug}
|
||||
poweredByLabel={$t('public_course.powered_by.label')}
|
||||
poweredByBrand={$t('public_course.powered_by.brand')}
|
||||
onItemClick={navigateTo}
|
||||
onPrev={() => navigateTo(prevItem)}
|
||||
onNext={() => navigateTo(nextItem)}
|
||||
>
|
||||
{#if lessonView}
|
||||
<PublicCourse.PublicLessonView lesson={lessonView} {callout} />
|
||||
{:else if exerciseView}
|
||||
{#key itemSlug}
|
||||
<PublicCourse.PublicExerciseView
|
||||
exercise={exerciseView}
|
||||
{callout}
|
||||
labels={exerciseLabels}
|
||||
attemptsPersistenceKey={publicExerciseAttemptsStorageKey(data.tree.course.slug, itemSlug)}
|
||||
formatAttemptOption={({ attemptNumber, correct, total }) =>
|
||||
t.get('public_course.exercise.attempt_option', { attemptNumber, correct, total })}
|
||||
newAttemptOptionLabel={$t('public_course.exercise.practice_again')}
|
||||
attemptsSelectAriaLabel={$t('public_course.exercise.attempts_select_aria')}
|
||||
submitLabel={$t('public_course.exercise.submit')}
|
||||
tryAgainLabel={$t('public_course.exercise.try_again')}
|
||||
privacyHint={$t('public_course.exercise.privacy_hint')}
|
||||
summaryTemplate={$t('public_course.exercise.summary_template')}
|
||||
/>
|
||||
{/key}
|
||||
<svelte:head>
|
||||
{#if breadcrumbJsonLd}
|
||||
{@html `<script type="application/ld+json">${breadcrumbJsonLd}</script>`}
|
||||
{/if}
|
||||
</PublicCourse.PublicCourseShell>
|
||||
</svelte:head>
|
||||
|
||||
{#key data.item.id}
|
||||
{#if lessonView}
|
||||
<PublicCourse.PublicLessonView
|
||||
lesson={lessonView}
|
||||
videoCaptionsLabel={$t('course.navItem.lessons.materials.tabs.video.transcript.captions_label')}
|
||||
{callout}
|
||||
/>
|
||||
{:else if exerciseView}
|
||||
<PublicCourse.PublicExerciseView
|
||||
exercise={exerciseView}
|
||||
{callout}
|
||||
labels={exerciseLabels}
|
||||
attemptsPersistenceKey={publicExerciseAttemptsStorageKey(data.tree.course.slug, itemSlug)}
|
||||
formatAttemptOption={({ attemptNumber, correct, total }) =>
|
||||
t.get('public_course.exercise.attempt_option', { attemptNumber, correct, total })}
|
||||
newAttemptOptionLabel={$t('public_course.exercise.practice_again')}
|
||||
attemptsSelectAriaLabel={$t('public_course.exercise.attempts_select_aria')}
|
||||
submitLabel={$t('public_course.exercise.submit')}
|
||||
tryAgainLabel={$t('public_course.exercise.try_again')}
|
||||
privacyHint={$t('public_course.exercise.privacy_hint')}
|
||||
summaryTemplate={$t('public_course.exercise.summary_template')}
|
||||
/>
|
||||
{/if}
|
||||
{/key}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { MetaTagsProps } from 'svelte-meta-tags';
|
||||
import { classroomio, type InferResponseType } from '$lib/utils/services/api';
|
||||
import { safeServerApi } from '$lib/utils/services/api/server';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
@@ -49,12 +50,40 @@ export const load = async ({ parent, url }) => {
|
||||
)
|
||||
]);
|
||||
const courseData = coursesResult.ok ? coursesResult.body.data : { courses: [], hasMoreCourses: false };
|
||||
const canonicalUrl = new URL(url.pathname, url.origin).href;
|
||||
const orgTitle = `${org.name} – All Courses`;
|
||||
const orgDescription = `Browse all courses offered by ${org.name}`;
|
||||
|
||||
const pageMetaTags = Object.freeze({
|
||||
title: orgTitle,
|
||||
description: orgDescription,
|
||||
canonical: canonicalUrl,
|
||||
openGraph: {
|
||||
type: 'website',
|
||||
url: canonicalUrl,
|
||||
title: orgTitle,
|
||||
description: orgDescription,
|
||||
...(org.avatarUrl
|
||||
? {
|
||||
images: [
|
||||
{
|
||||
url: org.avatarUrl,
|
||||
alt: `${org.name} logo`,
|
||||
secureUrl: org.avatarUrl,
|
||||
type: 'image/jpeg'
|
||||
}
|
||||
]
|
||||
}
|
||||
: {})
|
||||
}
|
||||
} satisfies MetaTagsProps);
|
||||
|
||||
return {
|
||||
org,
|
||||
courses: courseData.courses,
|
||||
hasMoreCourses: courseData.hasMoreCourses,
|
||||
tagGroups: tagsResult.ok ? tagsResult.body.data : [],
|
||||
activeTags: normalizedTags
|
||||
activeTags: normalizedTags,
|
||||
pageMetaTags
|
||||
};
|
||||
};
|
||||
|
||||
@@ -117,10 +117,6 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{$t('public_courses.page_title')}</title>
|
||||
</svelte:head>
|
||||
|
||||
<PoweredBy />
|
||||
|
||||
<main class={shellClass}>
|
||||
@@ -227,8 +223,7 @@
|
||||
<OrgLandingPageFooter
|
||||
orgName={data.org.name}
|
||||
logoUrl={data.org.avatarUrl ?? undefined}
|
||||
footerLinks={landingSettings.footerLinks}
|
||||
footerText={landingSettings.footerText}
|
||||
footer={landingSettings.footer}
|
||||
variant={landingSettings.theme}
|
||||
/>
|
||||
</main>
|
||||
|
||||
Reference in New Issue
Block a user