diff --git a/package-lock.json b/package-lock.json
index bf2f15a2d4..d1a07a88f1 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -29,6 +29,7 @@
"prop-types": "15.7.2",
"react": "16.14.0",
"react-dom": "16.14.0",
+ "react-helmet": "^6.1.0",
"react-redux": "7.1.3",
"react-responsive": "8.1.0",
"react-router": "5.1.2",
@@ -20427,6 +20428,25 @@
}
}
},
+ "node_modules/react-helmet": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/react-helmet/-/react-helmet-6.1.0.tgz",
+ "integrity": "sha512-4uMzEY9nlDlgxr61NL3XbKRy1hEkXmKNXhjbAIOVw5vcFrsdYbH2FEwcNyWvWinl103nXgzYNlns9ca+8kFiWw==",
+ "dependencies": {
+ "object-assign": "^4.1.1",
+ "prop-types": "^15.7.2",
+ "react-fast-compare": "^3.1.1",
+ "react-side-effect": "^2.1.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.3.0"
+ }
+ },
+ "node_modules/react-helmet/node_modules/react-fast-compare": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz",
+ "integrity": "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA=="
+ },
"node_modules/react-intl": {
"version": "5.25.1",
"resolved": "https://registry.npmjs.org/react-intl/-/react-intl-5.25.1.tgz",
@@ -20647,6 +20667,14 @@
"react": ">=15"
}
},
+ "node_modules/react-side-effect": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/react-side-effect/-/react-side-effect-2.1.2.tgz",
+ "integrity": "sha512-PVjOcvVOyIILrYoyGEpDN3vmYNLdy1CajSFNt4TDsVQC5KpTijDvWVoR+/7Rz2xT978D8/ZtFceXxzsPwZEDvw==",
+ "peerDependencies": {
+ "react": "^16.3.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
"node_modules/react-style-singleton": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz",
@@ -40777,6 +40805,24 @@
"use-sidecar": "^1.1.2"
}
},
+ "react-helmet": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/react-helmet/-/react-helmet-6.1.0.tgz",
+ "integrity": "sha512-4uMzEY9nlDlgxr61NL3XbKRy1hEkXmKNXhjbAIOVw5vcFrsdYbH2FEwcNyWvWinl103nXgzYNlns9ca+8kFiWw==",
+ "requires": {
+ "object-assign": "^4.1.1",
+ "prop-types": "^15.7.2",
+ "react-fast-compare": "^3.1.1",
+ "react-side-effect": "^2.1.0"
+ },
+ "dependencies": {
+ "react-fast-compare": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.0.tgz",
+ "integrity": "sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA=="
+ }
+ }
+ },
"react-intl": {
"version": "5.25.1",
"resolved": "https://registry.npmjs.org/react-intl/-/react-intl-5.25.1.tgz",
@@ -40928,6 +40974,12 @@
"tiny-warning": "^1.0.0"
}
},
+ "react-side-effect": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/react-side-effect/-/react-side-effect-2.1.2.tgz",
+ "integrity": "sha512-PVjOcvVOyIILrYoyGEpDN3vmYNLdy1CajSFNt4TDsVQC5KpTijDvWVoR+/7Rz2xT978D8/ZtFceXxzsPwZEDvw==",
+ "requires": {}
+ },
"react-style-singleton": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz",
diff --git a/package.json b/package.json
index 9bde1eb2ae..acdcb8bd22 100644
--- a/package.json
+++ b/package.json
@@ -53,6 +53,7 @@
"prop-types": "15.7.2",
"react": "16.14.0",
"react-dom": "16.14.0",
+ "react-helmet": "^6.1.0",
"react-redux": "7.1.3",
"react-responsive": "8.1.0",
"react-router": "5.1.2",
diff --git a/src/head/Head.jsx b/src/head/Head.jsx
new file mode 100644
index 0000000000..f093b87514
--- /dev/null
+++ b/src/head/Head.jsx
@@ -0,0 +1,21 @@
+import React from 'react';
+import { Helmet } from 'react-helmet';
+import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
+import { getConfig } from '@edx/frontend-platform';
+
+import messages from './messages';
+
+const Head = ({ intl }) => (
+
+
+ {intl.formatMessage(messages['course-authoring.page.title'], { siteName: getConfig().SITE_NAME })}
+
+
+
+);
+
+Head.propTypes = {
+ intl: intlShape.isRequired,
+};
+
+export default injectIntl(Head);
diff --git a/src/head/Head.test.jsx b/src/head/Head.test.jsx
new file mode 100644
index 0000000000..cdd54311fe
--- /dev/null
+++ b/src/head/Head.test.jsx
@@ -0,0 +1,17 @@
+import React from 'react';
+import { IntlProvider } from '@edx/frontend-platform/i18n';
+import { Helmet } from 'react-helmet';
+import { mount } from 'enzyme';
+import { getConfig } from '@edx/frontend-platform';
+import Head from './Head';
+
+describe('Head', () => {
+ const props = {};
+ it('should match render title tag and favicon with the site configuration values', () => {
+ mount();
+ const helmet = Helmet.peek();
+ expect(helmet.title).toEqual(`Course Authoring | ${getConfig().SITE_NAME}`);
+ expect(helmet.linkTags[0].rel).toEqual('shortcut icon');
+ expect(helmet.linkTags[0].href).toEqual(getConfig().FAVICON_URL);
+ });
+});
diff --git a/src/head/messages.js b/src/head/messages.js
new file mode 100644
index 0000000000..4fca090383
--- /dev/null
+++ b/src/head/messages.js
@@ -0,0 +1,11 @@
+import { defineMessages } from '@edx/frontend-platform/i18n';
+
+const messages = defineMessages({
+ 'course-authoring.page.title': {
+ id: 'course-authoring.page.title',
+ defaultMessage: 'Course Authoring | {siteName}',
+ description: 'Title tag',
+ },
+});
+
+export default messages;
diff --git a/src/index.jsx b/src/index.jsx
index 0fdf1db709..594f8d0ab9 100755
--- a/src/index.jsx
+++ b/src/index.jsx
@@ -16,10 +16,12 @@ import appMessages from './i18n';
import initializeStore from './store';
import './index.scss';
import CourseAuthoringRoutes from './CourseAuthoringRoutes';
+import Head from './head/Head';
subscribe(APP_READY, () => {
ReactDOM.render(
+