Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
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

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/components/DefaultErrorMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ export const DefaultErrorMessagePage = () => {
return (
<PageContainer asChild consumeCss>
<main>
<PageTitle title={t("htmlTitles.errorPage")} />
<PageTitle title={t("htmlTitles.errorPage")} useLocationForCustomPath={true} />
<DefaultErrorMessage applySkipToContentId={true} />
</main>
</PageContainer>
Expand Down
42 changes: 34 additions & 8 deletions src/components/PageTitle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,46 +7,72 @@
*/

import { useContext, useEffect, useRef } from "react";
import { useHref, useLocation } from "react-router";
import { useLocation } from "react-router";
import { isValidLocale } from "../i18n";
import { log } from "../util/logger/logger";
import { getAllDimensions } from "../util/trackingUtil";
import { AuthContext } from "./AuthenticationContext";
import { buildFullUrlFromPath, languagePartIndex } from "./SocialMediaMetadata";

interface Props {
interface BaseProps {
title: string;
}

interface WithCustomUrl extends BaseProps {
customPath: string | undefined;
useLocationForCustomPath?: false;
}

interface WithLocationForCustomUrl extends BaseProps {
customPath?: undefined;
useLocationForCustomPath: true;
}

type Props = WithCustomUrl | WithLocationForCustomUrl;

export const getTrackedUrl = (pathname: string) => {
const parts = pathname.split("/");
const langIdx = languagePartIndex(parts);
if (isValidLocale(parts[langIdx])) {
parts.splice(langIdx, 1);
}

return buildFullUrlFromPath(parts.join("/"));
};

/**
* A component for setting the page title and tracking a page view event.
* @param title - The title of the page. Will update the document title tag and dispatch a page view event. The component expects this title to be stable for the lifetime of the page, meaning it should not change unless the page location changes.
* @param customPath - Optional path to use for page view tracking instead of the canonical URL derived from the current location. Mutually exclusive with `useLocationForCustomPath`.
* @param useLocationForCustomPath - When true, derives the tracked path from the current location's canonical URL. Mutually exclusive with `customUrl`.
*/
export const PageTitle = ({ title }: Props) => {
export const PageTitle = ({ title, customPath }: Props) => {
const { user, authContextLoaded } = useContext(AuthContext);
const hasTracked = useRef(false);

const location = useLocation();
const href = useHref(location);
const trackedPath = customPath ?? location.pathname;
const trackedUrl = getTrackedUrl(trackedPath);

useEffect(() => {
hasTracked.current = false;
}, [href]);
}, [trackedUrl]);

useEffect(() => {
if (!authContextLoaded) return;
if (hasTracked.current) {
log.info("PageTitle: Page view already tracked, skipping duplicate tracking. This should not happen");
return;
}
// for debugging purposes
// log.info(`PageTitle: Tracking page view with title: ${title}`);
const dimensions = getAllDimensions({ user });
window._mtm?.push({
page_title: title,
event: "Pageview",
CustomUrl: trackedUrl,
...dimensions,
});
hasTracked.current = true;
}, [authContextLoaded, title, user]);
}, [authContextLoaded, title, trackedUrl, user]);

return <title>{title}</title>;
};
73 changes: 44 additions & 29 deletions src/components/SocialMediaMetadata.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,27 +11,32 @@ import { useLocation, useHref } from "react-router";
import config from "../config";
import { isValidLocale, preferredLanguages } from "../i18n";

export const buildFullUrlFromPath = (path: string) => {
return `${config.ndlaFrontendDomain}${path}`;
};

export const languagePartIndex = (parts: string[]) => {
return parts.includes("article-iframe") ? 2 : 1;
};

export const getCanonicalUrl = (pathname: string) => {
if (!pathname.includes("article-iframe")) {
return `${config.ndlaFrontendDomain}${pathname}`;
const parts = pathname.split("/");
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.

Mens du har dette friskt i minnet: Kunne du skrevet et par kommentarer om hvorfor getCanonicalUrl og getAlternateUrl må behandles nn, en og nb forskjellig? Fort gjort å glemme når en ikke holder aktivt på med det.

const langIdx = languagePartIndex(parts);
if (parts[langIdx] === "nb") {
parts.splice(langIdx, 1);
}
const paths = pathname.split("/");
if (isValidLocale(paths[2])) {
paths.splice(2, 1);
}
return `${config.ndlaFrontendDomain}${paths.join("/")}`;

return buildFullUrlFromPath(parts.join("/"));
};

export const getAlternateUrl = (pathname: string, alternateLanguage: string) => {
if (!pathname.includes("article-iframe")) {
return `${config.ndlaFrontendDomain}/${alternateLanguage}${pathname}`;
}
const paths = pathname.split("/");
if (isValidLocale(paths[2])) {
paths.splice(2, 1);
const parts = pathname.split("/");
const langIdx = languagePartIndex(parts);
if (isValidLocale(parts[langIdx])) {
parts.splice(langIdx, 1);
}
paths.splice(2, 0, alternateLanguage);
return `${config.ndlaFrontendDomain}${paths.join("/")}`;
parts.splice(langIdx, 0, alternateLanguage);
return buildFullUrlFromPath(parts.join("/"));
};

export const getAlternateLanguages = (trackableContent?: TrackableContent) => {
Expand All @@ -42,43 +47,53 @@ interface TrackableContent {
supportedLanguages?: string[];
}

interface Props {
interface BaseProps {
title: string;
description?: string;
path?: string;
imageUrl?: string;
audioUrl?: string;
trackableContent?: TrackableContent;
children?: ReactNode;
type?: string;
}

interface WithCanonicalPath extends BaseProps {
canonicalPath: string | undefined;
useLocationForCanonicalPath?: false;
}

interface WithLocationForCanonicalPath extends BaseProps {
canonicalPath?: undefined;
useLocationForCanonicalPath: true;
}

type Props = WithCanonicalPath | WithLocationForCanonicalPath;

export const SocialMediaMetadata = ({
title,
imageUrl,
audioUrl,
description,
path,
trackableContent,
children,
canonicalPath,
type = "article",
}: Props) => {
const location = useLocation();
const href = useHref(location);
const hrefLocation = canonicalPath ? { pathname: canonicalPath } : location;
const href = useHref(hrefLocation);
const canonicalUrl = getCanonicalUrl(href);

return (
<>
<link rel="canonical" href={getCanonicalUrl(path ? path : location.pathname)} />
{getAlternateLanguages(trackableContent).map((alternateLanguage) => (
<link
key={alternateLanguage}
rel="alternate"
hrefLang={alternateLanguage}
href={getAlternateUrl(path ? path : location.pathname, alternateLanguage)}
/>
))}
<link rel="canonical" href={canonicalUrl} />
{getAlternateLanguages(trackableContent).map((alternateLanguage) => {
const alternateUrl = getAlternateUrl(canonicalPath ?? location.pathname, alternateLanguage);
return <link key={alternateLanguage} rel="alternate" hrefLang={alternateLanguage} href={alternateUrl} />;
})}
{children}
<meta property="og:type" content={type} />
<meta property="og:url" content={`${config.ndlaFrontendDomain}${href}`} />
<meta property="og:url" content={canonicalUrl} />
{!!title && <meta property="og:title" content={`${title} - NDLA`} />}
{!!description && <meta property="og:description" content={description} />}
{!!description && <meta name="description" content={description} />}
Expand Down
39 changes: 39 additions & 0 deletions src/components/__tests__/pageTitle-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/**
* Copyright (c) 2019-present, NDLA.
*
* This source code is licensed under the GPLv3 license found in the
* LICENSE file in the root directory of this source tree.
*
*/

import { getTrackedUrl } from "../PageTitle";

test("getTrackedUrl with article-url and empty language", () => {
const url = getTrackedUrl("/article/123");
expect(url).toMatch("https://test.ndla.no/article/123");
});

test("getTrackedUrl with article-url and non-default language", () => {
const url = getTrackedUrl("/nn/article/123");
expect(url).toMatch("https://test.ndla.no/article/123");
});

test("getTrackedUrl with article-url and default language", () => {
const url = getTrackedUrl("/nb/article/123");
expect(url).toMatch("https://test.ndla.no/article/123");
});

test("getTrackedUrl with iframe-url and nb language", () => {
const url = getTrackedUrl("/article-iframe/nb/urn:topic:123/1");
expect(url).toMatch("https://test.ndla.no/article-iframe/urn:topic:123/1");
});

test("getTrackedUrl with iframe-url and nn language", () => {
const url = getTrackedUrl("/article-iframe/nn/urn:topic:123/1");
expect(url).toMatch("https://test.ndla.no/article-iframe/urn:topic:123/1");
});

test("getTrackedUrl with iframe-url and no language", () => {
const url = getTrackedUrl("/article-iframe/urn:topic:123/1");
expect(url).toMatch("https://test.ndla.no/article-iframe/urn:topic:123/1");
});
15 changes: 15 additions & 0 deletions src/components/__tests__/socialMediaMetadata-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,26 @@ test("getCanonicalUrl with article-url and empty language", () => {
expect(canonicalUrl).toMatch("https://test.ndla.no/article/123");
});

test("getCanonicalUrl with article-url and non-default language", () => {
const canonicalUrl = getCanonicalUrl("/nn/article/123");
expect(canonicalUrl).toMatch("https://test.ndla.no/nn/article/123");
});

test("getCanonicalUrl with article-url and default language", () => {
const canonicalUrl = getCanonicalUrl("/nb/article/123");
expect(canonicalUrl).toMatch("https://test.ndla.no/article/123");
});

test("getCanonicalUrl with iframe-url and nb language", () => {
const canonicalUrl = getCanonicalUrl("/article-iframe/nb/urn:topic:123/1");
expect(canonicalUrl).toMatch("https://test.ndla.no/article-iframe/urn:topic:123/1");
});

test("getCanonicalUrl with iframe-url and nn language", () => {
const canonicalUrl = getCanonicalUrl("/article-iframe/nn/urn:topic:123/1");
expect(canonicalUrl).toMatch("https://test.ndla.no/article-iframe/nn/urn:topic:123/1");
});

test("getCanonicalUrl with iframe-url and no language", () => {
const canonicalUrl = getCanonicalUrl("/article-iframe/urn:topic:123/1");
expect(canonicalUrl).toMatch("https://test.ndla.no/article-iframe/urn:topic:123/1");
Expand Down
3 changes: 2 additions & 1 deletion src/containers/AboutPageV2/AboutPageLeaf.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ export const AboutPageLeaf = ({ article: _article, crumbs }: Props) => {

return (
<main>
<PageTitle title={getDocumentTitle(t, article.title)} />
<PageTitle title={getDocumentTitle(t, article.title)} useLocationForCustomPath={true} />
<meta name="pageid" content={`${article.id}`} />
{scripts?.map((script) => (
<script key={script.src} src={script.src} type={script.type} async={script.async} defer={script.defer} />
Expand All @@ -108,6 +108,7 @@ export const AboutPageLeaf = ({ article: _article, crumbs }: Props) => {
description={article.metaDescription}
imageUrl={article.metaImage?.image.imageUrl}
trackableContent={article}
useLocationForCanonicalPath={true}
/>
<Hero variant="primary">
<HeroBackground />
Expand Down
3 changes: 2 additions & 1 deletion src/containers/AboutPageV2/AboutPageNode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ export const AboutPageNode = ({ article, menuItems, crumbs }: Props) => {

return (
<main>
<PageTitle title={getDocumentTitle(t, article.title)} />
<PageTitle title={getDocumentTitle(t, article.title)} useLocationForCustomPath={true} />
<meta name="pageid" content={`${article.id}`} />
{scripts?.map((script) => (
<script key={script.src} src={script.src} type={script.type} async={script.async} defer={script.defer} />
Expand All @@ -158,6 +158,7 @@ export const AboutPageNode = ({ article, menuItems, crumbs }: Props) => {
description={transformedArticle.metaDescription}
imageUrl={transformedArticle.metaImage?.image.imageUrl}
trackableContent={transformedArticle}
useLocationForCanonicalPath={true}
/>
<StyledPageContent>
<HomeBreadcrumb items={crumbs.map((bc) => ({ ...bc, to: bc.url }))} />
Expand Down
2 changes: 1 addition & 1 deletion src/containers/AccessDeniedPage/AccessDeniedPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export const AccessDenied = ({ applySkipToContentId }: AccessDeniedProps) => {
return (
<Status code={statusCode}>
<ErrorMessageRoot>
<PageTitle title={t("htmlTitles.accessDenied")} />
<PageTitle title={t("htmlTitles.accessDenied")} useLocationForCustomPath={true} />
<StyledPresentationLine />
<ErrorMessageContent>
<ErrorMessageDescription id={applySkipToContentId ? SKIP_TO_CONTENT_ID : undefined}>
Expand Down
2 changes: 1 addition & 1 deletion src/containers/AllSubjectsPage/AllSubjectsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ export const AllSubjectsPage = () => {
return (
<StyledPageContainer asChild consumeCss>
<main>
<PageTitle title={t("htmlTitles.subjectsPage")} />
<PageTitle title={t("htmlTitles.subjectsPage")} useLocationForCustomPath={true} />
<HeadingWrapper>
<Heading textStyle="heading.medium" id={SKIP_TO_CONTENT_ID}>
{t("subjectsPage.allSubjects")}
Expand Down
5 changes: 3 additions & 2 deletions src/containers/ArticlePage/ArticlePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export const ArticlePage = ({ resource, skipToContentId, loading }: Props) => {
<>
{!!resource && !!resource.article && !!article && (
<>
<PageTitle title={getDocumentTitle(t, resource, root)} />
<PageTitle title={getDocumentTitle(t, resource, root)} customPath={resource.defaultUrl} />
{scripts?.map((script) => (
<script key={script.src} src={script.src} type={script.type} async={script.async} defer={script.defer} />
))}
Expand All @@ -89,7 +89,7 @@ export const ArticlePage = ({ resource, skipToContentId, loading }: Props) => {
trackableContent={article}
description={article.metaDescription}
imageUrl={article.metaImage?.image.imageUrl}
path={resource.url}
canonicalPath={resource.url}
/>
</>
)}
Expand Down Expand Up @@ -127,6 +127,7 @@ ArticlePage.fragments = {
nodeType
name
url
defaultUrl
contentUri
relevanceId
resourceTypes {
Expand Down
4 changes: 2 additions & 2 deletions src/containers/CollectionPage/CollectionPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -135,8 +135,8 @@ const CollectionPageContent = ({ collectionLanguage, subjects, image }: Collecti
return (
<StyledPageContainer padding="large" asChild consumeCss>
<main>
<PageTitle title={pageTitle} />
<SocialMediaMetadata title={metaTitle} imageUrl={image?.image.imageUrl} />
<PageTitle title={pageTitle} useLocationForCustomPath={true} />
<SocialMediaMetadata title={metaTitle} imageUrl={image?.image.imageUrl} useLocationForCanonicalPath={true} />
<div>
{!!image && (
<StyledImage
Expand Down
2 changes: 1 addition & 1 deletion src/containers/ErrorPage/ErrorPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export const ErrorPage = () => {
const { t } = useTranslation();
return (
<Status code={INTERNAL_SERVER_ERROR}>
<PageTitle title={t("htmlTitles.errorPage")} />
<PageTitle title={t("htmlTitles.errorPage")} useLocationForCustomPath={true} />
<meta name="description" content={t("meta.description")} />
<MastheadContainer>
<SafeLink to="/" aria-label="NDLA" title="NDLA">
Expand Down
2 changes: 1 addition & 1 deletion src/containers/ErrorPage/ForbiddenPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export const Forbidden = ({ applySkipToContentId, navigationLink }: Props) => {
return (
<Status code={403}>
<ErrorMessageRoot>
<PageTitle title={t("htmlTitles.forbidden")} />
<PageTitle title={t("htmlTitles.forbidden")} useLocationForCustomPath={true} />
<img src={"/static/not-exist.gif"} alt={t("errorMessage.title")} />
<ErrorMessageContent>
<ErrorMessageTitle id={applySkipToContentId ? SKIP_TO_CONTENT_ID : undefined}>
Expand Down
9 changes: 7 additions & 2 deletions src/containers/FilmFrontpage/FilmFrontpage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,13 @@ export const FilmFrontpage = () => {

return (
<>
{!!node && <PageTitle title={getDocumentTitle(t, node)} />}
<SocialMediaMetadata type="website" title={node?.name ?? ""} description={about?.description} />
{!!node && <PageTitle title={getDocumentTitle(t, node)} useLocationForCustomPath={true} />}
<SocialMediaMetadata
type="website"
title={node?.name ?? ""}
description={about?.description}
useLocationForCanonicalPath={true}
/>
<StyledPageContainer asChild consumeCss>
<main>
<FilmSlideshow slideshow={definedSlideshowMovies} />
Expand Down
Loading
Loading