diff --git a/public/nextjs_migration/client/js/landing.js b/public/nextjs_migration/client/js/landing.js
new file mode 100644
index 00000000000..c3ff1256b2e
--- /dev/null
+++ b/public/nextjs_migration/client/js/landing.js
@@ -0,0 +1,27 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/** @type {HTMLDivElement} */
+// @ts-ignore: We guard against a null value by not calling init():
+const landingPartial = document.querySelector("[data-partial='landing']")
+
+if (landingPartial) {
+ init()
+}
+
+async function init () {
+ const landingForm = landingPartial.querySelector('form.exposure-scan')
+ if (!(landingForm instanceof HTMLFormElement)) {
+ return
+ }
+ landingForm.addEventListener('submit', (e) => {
+ e.preventDefault()
+ const emailField = landingForm.elements.namedItem('email')
+ const newLocation = new URL(document.location)
+ newLocation.pathname = '/scan/'
+ newLocation.hash = `#email=${encodeURIComponent(emailField.value)}`
+ document.location = newLocation.href
+ })
+ landingForm.hidden = false
+}
diff --git a/public/nextjs_migration/client/js/transitionObserver.js b/public/nextjs_migration/client/js/transitionObserver.js
new file mode 100644
index 00000000000..3238fd61a4c
--- /dev/null
+++ b/public/nextjs_migration/client/js/transitionObserver.js
@@ -0,0 +1,81 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// percentage a section that has to be in view in order to appear
+const sectionThreshold = 0.1
+const queueIntervalDuration = 150
+
+let observers
+let queueInterval
+// holds the sections so they can appear one after another
+const entryQueue = []
+
+function handleShowSection () {
+ if (!entryQueue.length && queueInterval) {
+ clearInterval(queueInterval)
+ queueInterval = null
+ return
+ }
+
+ const nextEntry = entryQueue.shift()
+ nextEntry.target.dataset.enterTransition = 'visible'
+}
+
+function setQueueInterval () {
+ queueInterval = setInterval(handleShowSection, queueIntervalDuration)
+}
+
+function handleScroll (entries) {
+ entries.forEach(entry => {
+ const sectionElement = entry.target
+ const hasEntered = sectionElement.getAttribute('data-enter-transition') === 'visible'
+
+ if (hasEntered) {
+ return
+ }
+
+ const isInViewport = entry.isIntersecting
+ if (isInViewport) {
+ entryQueue.push(entry)
+ }
+
+ if (!queueInterval) {
+ setQueueInterval()
+ }
+ })
+}
+
+function init (sections) {
+ const observer = new IntersectionObserver(handleScroll, {
+ threshold: sectionThreshold
+ })
+
+ observers = [...sections].map(section => {
+ section.dataset.enterTransition = 'entering'
+
+ observer.observe(section)
+ return observer
+ })
+
+ setQueueInterval()
+}
+
+if (!observers) {
+ const mediaQuery = window.matchMedia('(prefers-reduced-motion: no-preference)')
+ const allowMotion = mediaQuery && mediaQuery.matches
+
+ const sections = document.querySelectorAll('[data-enter-transition]')
+
+ // Don’t hide elements that are marked for entering transitions
+ // while users set their motion preferences.
+ mediaQuery.addEventListener('change', () => {
+ const documentStyle = document.documentElement.style
+ documentStyle.setProperty('--enter-transition-opacity', '1')
+ documentStyle.setProperty('--enter-transition-y', '0')
+ })
+
+ if (allowMotion) {
+ init(sections)
+ }
+}
diff --git a/src/app/(nextjs_migration)/(guest)/page.tsx b/src/app/(nextjs_migration)/(guest)/page.tsx
new file mode 100644
index 00000000000..0fe134f423d
--- /dev/null
+++ b/src/app/(nextjs_migration)/(guest)/page.tsx
@@ -0,0 +1,155 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import Image from "next/image";
+import Script from "next/script";
+import "../../../client/css/partials/landing.css";
+
+import { getL10n } from "../../functions/server/l10n";
+
+import HeroImage from "../../../client/images/landing-hero@2x.webp";
+import LaptopImage from "../../../client/images/landing-laptop@2x.webp";
+import LockImage from "../../../client/images/landing-lock@2x.webp";
+import MailImage from "../../../client/images/landing-mail@2x.webp";
+import NaturePhoneImage from "../../../client/images/landing-nature-phone@2x.webp";
+
+export default function Home() {
+ const l10n = getL10n();
+
+ return (
+