diff --git a/apps/landing/i18n.config.js b/apps/landing/i18n.config.js
deleted file mode 100644
index 028e542be..000000000
--- a/apps/landing/i18n.config.js
+++ /dev/null
@@ -1,6 +0,0 @@
-const languages = require("@rallly/languages/languages.json");
-
-module.exports = {
- defaultLocale: "en",
- locales: Object.keys(languages),
-};
diff --git a/apps/landing/next-env.d.ts b/apps/landing/next-env.d.ts
index 4f11a03dc..725dd6f24 100644
--- a/apps/landing/next-env.d.ts
+++ b/apps/landing/next-env.d.ts
@@ -1,5 +1,6 @@
///
///
+///
// NOTE: This file should not be edited
-// see https://nextjs.org/docs/basic-features/typescript for more information.
+// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
diff --git a/apps/landing/next-i18next.config.js b/apps/landing/next-i18next.config.js
deleted file mode 100644
index da25de97f..000000000
--- a/apps/landing/next-i18next.config.js
+++ /dev/null
@@ -1,12 +0,0 @@
-const ICU = require("i18next-icu/i18nextICU.js");
-const path = require("path");
-const i18n = require("./i18n.config.js");
-
-module.exports = {
- i18n,
- defaultNS: "common",
- reloadOnPrerender: process.env.NODE_ENV === "development",
- localePath: path.resolve("./public/locales"),
- use: [new ICU()],
- serializeConfig: false,
-};
diff --git a/apps/landing/next.config.js b/apps/landing/next.config.js
index 2476aa89d..ffc1d041d 100644
--- a/apps/landing/next.config.js
+++ b/apps/landing/next.config.js
@@ -3,7 +3,6 @@
// https://nextjs.org/docs/api-reference/next.config.js/introduction
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
-const i18n = require("./i18n.config.js");
const withBundleAnalyzer = require("@next/bundle-analyzer")({
enabled: process.env.ANALYZE === "true",
});
@@ -16,10 +15,14 @@ function createAppUrl(subpath) {
}
const nextConfig = {
- i18n: i18n,
productionBrowserSourceMaps: true,
output: "standalone",
- transpilePackages: ["@rallly/icons", "@rallly/ui", "@rallly/tailwind-config"],
+ transpilePackages: [
+ "@rallly/icons",
+ "@rallly/ui",
+ "@rallly/tailwind-config",
+ "next-mdx-remote",
+ ],
webpack(config) {
config.module.rules.push({
test: /\.svg$/,
diff --git a/apps/landing/package.json b/apps/landing/package.json
index a6a74e7af..c7683b4a2 100644
--- a/apps/landing/package.json
+++ b/apps/landing/package.json
@@ -13,14 +13,14 @@
"prettier": "prettier --write ./src"
},
"dependencies": {
+ "@rallly/billing": "*",
"@rallly/icons": "*",
"@rallly/languages": "*",
"@rallly/tailwind-config": "*",
"@rallly/ui": "*",
- "@rallly/billing": "*",
"@svgr/webpack": "^6.5.1",
"@vercel/analytics": "^0.1.8",
- "class-variance-authority": "^0.7.0",
+ "accept-language-parser": "^1.5.0",
"dayjs": "^1.11.7",
"gray-matter": "^4.0.3",
"i18next": "^22.4.9",
@@ -29,15 +29,15 @@
"lodash": "^4.17.21",
"nanoid": "^4.0.0",
"next-i18next": "^13.0.3",
+ "next-mdx-remote": "^5.0.0",
"next-seo": "^6.1.0",
"react-i18next": "^12.1.4",
- "react-use": "^17.4.0",
- "remark": "^14.0.3",
- "remark-html": "^15.0.2"
+ "react-use": "^17.4.0"
},
"devDependencies": {
"@next/bundle-analyzer": "^12.3.4",
"@rallly/tsconfig": "*",
+ "@rallly/eslint-config": "*",
"@types/color-hash": "^1.0.2",
"@types/lodash": "^4.14.178",
"cross-env": "^7.0.3",
diff --git a/apps/landing/public/locales/en/common.json b/apps/landing/public/locales/en/common.json
index ae9c94562..90c46d33d 100644
--- a/apps/landing/public/locales/en/common.json
+++ b/apps/landing/public/locales/en/common.json
@@ -19,11 +19,11 @@
"pricing": "Pricing",
"bestDoodleAlternative": "Best Doodle Alternative",
"freeSchedulingPoll": "Free Scheduling Poll",
- "findATime": "Find a Time",
"getStarted": "Get started",
"availabilityPoll": "Availability Poll",
"solutions": "Solutions",
"howItWorks": "How it Works",
"status": "Status",
- "when2MeetAlternative": "When2Meet Alternative"
+ "when2MeetAlternative": "When2Meet Alternative",
+ "meetingPoll": "Meeting Poll"
}
diff --git a/apps/landing/public/locales/en/home.json b/apps/landing/public/locales/en/home.json
index 694f03951..e908c34b1 100644
--- a/apps/landing/public/locales/en/home.json
+++ b/apps/landing/public/locales/en/home.json
@@ -10,8 +10,8 @@
"ericQuote": "“If your scheduling workflow lives in emails, I strongly encourage you to try and let Rallly simplify your scheduling tasks for a more organized and less stressful workday.”",
"viaTrustpilot": "via Trustpilot",
"ericJobTitle": "Executive Assistant at MIT",
- "statsUsersRegistered": "45k+ registered users",
- "statsPollsCreated": "100k+ polls created",
+ "statsUsersRegistered": "{count, number, ::compact-short}+ registered users",
+ "statsPollsCreated": "{count, number, ::compact-short}+ polls created",
"statsLanguagesSupported": "10+ languages supported",
"hint": "It's free! No login required.",
"doodleAlternative": "The Best Free Doodle Alternative",
@@ -24,10 +24,6 @@
"createAPoll": "Create a Meeting Poll",
"doodleAlternativeMetaTitle": "Best Free Doodle Alternative | Rallly",
"doodleAlternativeMetaDescription": "Looking for a Doodle alternative? Try Rallly! It's free, easy to use, and doesn't require an account.",
- "findATimeMetaTitle": "Find a Time to Meet | Rallly",
- "findATimeMetaDescription": "Create a meeting poll in seconds, no login required.",
- "findATimeTitle": "Find a Time to Meet",
- "findATimeDescription": "Create a meeting poll and let your participants vote on the best time to meet.",
"createASchedulingPoll": "Create a Scheduling Poll",
"freeSchedulingPollMetaTitle": "Free Scheduling Poll | Rallly",
"freeSchedulingPollMetaDescription": "Create a free scheduling poll in seconds. Ideal for organizing meetings, events, conferences, sports teams and more.",
@@ -40,5 +36,9 @@
"when2meetAlternativeMetaTitle": "Best When2Meet Alternative: Rallly",
"when2meetAlternativeMetaDescription": "Find a better way to schedule meetings with Rallly, the top free alternative to When2Meet. Easy to use and free.",
"when2meetAlternative": "Still using When2Meet?",
- "when2meetAlternativeDescription": "Create professional, ad-free meetings polls for free with Rallly."
+ "when2meetAlternativeDescription": "Create professional, ad-free meetings polls for free with Rallly.",
+ "meetingPoll": "Create professional meetings polls with Rallly",
+ "meetingPollDescription": "Meeting polls are a great way to get people's availability. Rallly lets you create beautiful meeting polls with ease.",
+ "meetingPollMetaTitle": "Meeting Poll",
+ "meetingPollMetaDescription": "Easily schedule meetings with our poll feature, ensuring everyone's availability."
}
diff --git a/apps/landing/public/locales/en/pricing.json b/apps/landing/public/locales/en/pricing.json
index 9d9d48bcc..d6e73b3ef 100644
--- a/apps/landing/public/locales/en/pricing.json
+++ b/apps/landing/public/locales/en/pricing.json
@@ -1,5 +1,4 @@
{
- "pricing": "Pricing",
"pricingDescription": "Get started for free. No login required.",
"freeForever": "free forever",
"planPro": "Pro",
@@ -29,5 +28,7 @@
"whenPollInactive": "When does a poll become inactive?",
"whenPollInactiveAnswer": "Polls become inactive when all date options are in the past AND the poll has not been accessed for over 30 days. Inactive polls are automatically deleted if you do not have a paid subscription.",
"yearlyBillingDescription": "per year",
- "annualBenefit": "{count} months free"
+ "annualBenefit": "{count} months free",
+ "pricingTitle": "Get started for free",
+ "pricingSubtitle": "Upgrade to a paid plan to get access to premium features"
}
diff --git a/apps/landing/src/app/[locale]/(seo)/availability-poll/page.tsx b/apps/landing/src/app/[locale]/(seo)/availability-poll/page.tsx
new file mode 100644
index 000000000..ba6c781d4
--- /dev/null
+++ b/apps/landing/src/app/[locale]/(seo)/availability-poll/page.tsx
@@ -0,0 +1,52 @@
+import { Trans } from "react-i18next/TransWithoutContext";
+
+import Bonus from "@/components/home/bonus";
+import { MarketingHero } from "@/components/home/hero";
+import { BigTestimonial, Marketing, MentionedBy } from "@/components/marketing";
+import { getTranslation } from "@/i18n/server";
+
+export default async function Page({ params }: { params: { locale: string } }) {
+ const { t } = await getTranslation(params.locale, ["common", "home"]);
+ return (
+
+
+ }
+ />
+
+
+
+
+ );
+}
+
+export async function generateMetadata({
+ params,
+}: {
+ params: { locale: string };
+}) {
+ const { t } = await getTranslation(params.locale, "home");
+ return {
+ title: t("availabilityPollMetaTitle", {
+ ns: "home",
+ }),
+ description: t("availabilityPollMetaDescription", {
+ ns: "home",
+ }),
+ };
+}
diff --git a/apps/landing/src/app/[locale]/(seo)/best-doodle-alternative/page.tsx b/apps/landing/src/app/[locale]/(seo)/best-doodle-alternative/page.tsx
new file mode 100644
index 000000000..15c35bbad
--- /dev/null
+++ b/apps/landing/src/app/[locale]/(seo)/best-doodle-alternative/page.tsx
@@ -0,0 +1,42 @@
+import { Trans } from "react-i18next/TransWithoutContext";
+
+import Bonus from "@/components/home/bonus";
+import { MarketingHero } from "@/components/home/hero";
+import { BigTestimonial, Marketing, MentionedBy } from "@/components/marketing";
+import { getTranslation } from "@/i18n/server";
+
+export default async function Page({ params }: { params: { locale: string } }) {
+ const { t } = await getTranslation(params.locale, ["home"]);
+ return (
+
+ }
+ />
+
+
+
+
+ );
+}
+
+export async function generateMetadata({
+ params,
+}: {
+ params: { locale: string };
+}) {
+ const { t } = await getTranslation(params.locale, "home");
+ return {
+ title: t("doodleAlternativeMetaTitle", {
+ ns: "home",
+ }),
+ description: t("doodleAlternativeMetaDescription", {
+ ns: "home",
+ }),
+ };
+}
diff --git a/apps/landing/src/app/[locale]/(seo)/free-scheduling-poll/page.tsx b/apps/landing/src/app/[locale]/(seo)/free-scheduling-poll/page.tsx
new file mode 100644
index 000000000..ebccdbc56
--- /dev/null
+++ b/apps/landing/src/app/[locale]/(seo)/free-scheduling-poll/page.tsx
@@ -0,0 +1,42 @@
+import { Trans } from "react-i18next/TransWithoutContext";
+
+import Bonus from "@/components/home/bonus";
+import { MarketingHero } from "@/components/home/hero";
+import { BigTestimonial, Marketing, MentionedBy } from "@/components/marketing";
+import { getTranslation } from "@/i18n/server";
+
+export default async function Page({ params }: { params: { locale: string } }) {
+ const { t } = await getTranslation(params.locale, ["home"]);
+ return (
+
+ }
+ />
+
+
+
+
+ );
+}
+
+export async function generateMetadata({
+ params,
+}: {
+ params: { locale: string };
+}) {
+ const { t } = await getTranslation(params.locale, "home");
+ return {
+ title: t("freeSchedulingPollMetaTitle", {
+ ns: "home",
+ }),
+ description: t("freeSchedulingPollMetaDescription", {
+ ns: "home",
+ }),
+ };
+}
diff --git a/apps/landing/src/app/[locale]/(seo)/meeting-poll/page.tsx b/apps/landing/src/app/[locale]/(seo)/meeting-poll/page.tsx
new file mode 100644
index 000000000..c5271ea46
--- /dev/null
+++ b/apps/landing/src/app/[locale]/(seo)/meeting-poll/page.tsx
@@ -0,0 +1,48 @@
+import { Trans } from "react-i18next/TransWithoutContext";
+
+import Bonus from "@/components/home/bonus";
+import { MarketingHero } from "@/components/home/hero";
+import { BigTestimonial, Marketing, MentionedBy } from "@/components/marketing";
+import { getTranslation } from "@/i18n/server";
+
+export default async function Page({ params }: { params: { locale: string } }) {
+ const { t } = await getTranslation(params.locale, ["home"]);
+ return (
+
+ }
+ />
+
+
+
+
+ );
+}
+
+export async function generateMetadata({
+ params,
+}: {
+ params: { locale: string };
+}) {
+ const { t } = await getTranslation(params.locale, "home");
+ return {
+ title: t("meetingPollMetaTitle", {
+ ns: "home",
+ defaultValue: "Meeting Poll",
+ }),
+ description: t("meetingPollMetaDescription", {
+ ns: "home",
+ defaultValue:
+ "Easily schedule meetings with our poll feature, ensuring everyone's availability.",
+ }),
+ };
+}
diff --git a/apps/landing/src/app/[locale]/(seo)/when2meet-alternative/page.tsx b/apps/landing/src/app/[locale]/(seo)/when2meet-alternative/page.tsx
new file mode 100644
index 000000000..97a721349
--- /dev/null
+++ b/apps/landing/src/app/[locale]/(seo)/when2meet-alternative/page.tsx
@@ -0,0 +1,42 @@
+import { Trans } from "react-i18next/TransWithoutContext";
+
+import Bonus from "@/components/home/bonus";
+import { MarketingHero } from "@/components/home/hero";
+import { BigTestimonial, Marketing, MentionedBy } from "@/components/marketing";
+import { getTranslation } from "@/i18n/server";
+
+export default async function Page({ params }: { params: { locale: string } }) {
+ const { t } = await getTranslation(params.locale, ["home"]);
+ return (
+
+ }
+ />
+
+
+
+
+ );
+}
+
+export async function generateMetadata({
+ params,
+}: {
+ params: { locale: string };
+}) {
+ const { t } = await getTranslation(params.locale, "home");
+ return {
+ title: t("when2meetAlternativeMetaTitle", {
+ ns: "home",
+ }),
+ description: t("when2meetAlternativeMetaDescription", {
+ ns: "home",
+ }),
+ };
+}
diff --git a/apps/landing/src/app/[locale]/[...notFound]/page.tsx b/apps/landing/src/app/[locale]/[...notFound]/page.tsx
new file mode 100644
index 000000000..2dc2960ac
--- /dev/null
+++ b/apps/landing/src/app/[locale]/[...notFound]/page.tsx
@@ -0,0 +1,5 @@
+import { notFound } from "next/navigation";
+
+export default function CatchAllPage() {
+ notFound();
+}
diff --git a/apps/landing/src/app/[locale]/blog/[slug]/page.tsx b/apps/landing/src/app/[locale]/blog/[slug]/page.tsx
new file mode 100644
index 000000000..78c2337a5
--- /dev/null
+++ b/apps/landing/src/app/[locale]/blog/[slug]/page.tsx
@@ -0,0 +1,101 @@
+import { ArrowLeftIcon } from "lucide-react";
+import Image from "next/image";
+import Link from "next/link";
+import { MDXRemote } from "next-mdx-remote/rsc";
+
+import PostHeader from "@/components/blog/post-header";
+import { getAllPosts, getPostBySlug } from "@/lib/api";
+import { absoluteUrl } from "@/utils/absolute-url";
+
+export default async function Page({ params }: { params: { slug: string } }) {
+ const post = getPostBySlug(params.slug, [
+ "title",
+ "date",
+ "slug",
+ "author",
+ "excerpt",
+ "content",
+ ]);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
Luke Vella
+
+
+ @imlukevella
+
+
+
+
+
+
+ );
+}
+
+export async function generateStaticParams() {
+ const posts = getAllPosts(["slug"]);
+ return posts.map((post) => ({
+ slug: post.slug,
+ }));
+}
+
+export async function generateMetadata({
+ params,
+}: {
+ params: { slug: string };
+}) {
+ const post = getPostBySlug(params.slug, [
+ "title",
+ "date",
+ "slug",
+ "author",
+ "excerpt",
+ ]);
+
+ return {
+ title: post.title,
+ description: post.excerpt,
+ openGraph: {
+ title: post.title,
+ description: post.excerpt,
+ url: absoluteUrl(`/blog/${post.slug}`),
+ images: [
+ {
+ url: absoluteUrl("/api/og-image", {
+ title: post.title,
+ excerpt: post.excerpt,
+ }),
+ width: 1200,
+ height: 630,
+ alt: post.title,
+ type: "image/png",
+ },
+ ],
+ },
+ };
+}
diff --git a/apps/landing/src/app/[locale]/blog/layout.tsx b/apps/landing/src/app/[locale]/blog/layout.tsx
new file mode 100644
index 000000000..6c82e7ad5
--- /dev/null
+++ b/apps/landing/src/app/[locale]/blog/layout.tsx
@@ -0,0 +1,39 @@
+import { NewspaperIcon } from "lucide-react";
+import Script from "next/script";
+
+export default function BlogLayout({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ return (
+
+
+
+ {children}
+
+
+
+
+
+
+
Want to stay up to date?
+
+ Subscribe to our newsletter to get updates on new features and
+ releases.
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/landing/src/app/[locale]/blog/page.tsx b/apps/landing/src/app/[locale]/blog/page.tsx
new file mode 100644
index 000000000..6bae5b048
--- /dev/null
+++ b/apps/landing/src/app/[locale]/blog/page.tsx
@@ -0,0 +1,63 @@
+import { Trans } from "react-i18next/TransWithoutContext";
+
+import type { URLParams } from "@/app/[locale]/types";
+import { getTranslation } from "@/i18n/server";
+import { getAllPosts } from "@/lib/api";
+
+import { PostPreview } from "./post-preview";
+
+export default async function Page({ params }: { params: URLParams }) {
+ const { t } = await getTranslation(params.locale, "blog");
+ const allPosts = getAllPosts([
+ "title",
+ "date",
+ "slug",
+ "author",
+ "coverImage",
+ "excerpt",
+ ]);
+ return (
+
+
+
+ {allPosts.map((post) => (
+
+ ))}
+
+
+ );
+}
+
+export async function generateMetadata({
+ params,
+}: {
+ params: { locale: string };
+}) {
+ const { t } = await getTranslation(params.locale, "blog");
+ return {
+ title: t("blogTitle", {
+ ns: "blog",
+ defaultValue: "Rallly - Blog",
+ }),
+ description: t("blogDescription", {
+ ns: "blog",
+ defaultValue: "News, updates and announcement about Rallly.",
+ }),
+ };
+}
diff --git a/apps/landing/src/components/blog/post-preview.tsx b/apps/landing/src/app/[locale]/blog/post-preview.tsx
similarity index 75%
rename from apps/landing/src/components/blog/post-preview.tsx
rename to apps/landing/src/app/[locale]/blog/post-preview.tsx
index 57b8971e6..797f8ce99 100644
--- a/apps/landing/src/components/blog/post-preview.tsx
+++ b/apps/landing/src/app/[locale]/blog/post-preview.tsx
@@ -1,6 +1,11 @@
+"use client";
+
import dayjs from "dayjs";
+import localizedFormat from "dayjs/plugin/localizedFormat";
import Link from "next/link";
+dayjs.extend(localizedFormat);
+
type Props = {
title: string;
coverImage?: string;
@@ -9,9 +14,9 @@ type Props = {
slug: string;
};
-const PostPreview = ({ title, date, excerpt, slug }: Props) => {
+export const PostPreview = ({ title, date, excerpt, slug }: Props) => {
return (
-
+
@@ -30,8 +35,6 @@ const PostPreview = ({ title, date, excerpt, slug }: Props) => {
{excerpt}
-
+
);
};
-
-export default PostPreview;
diff --git a/apps/landing/src/pages/cookie-policy.tsx b/apps/landing/src/app/[locale]/cookie-policy/page.tsx
similarity index 79%
rename from apps/landing/src/pages/cookie-policy.tsx
rename to apps/landing/src/app/[locale]/cookie-policy/page.tsx
index 389ecbdca..b7dd56275 100644
--- a/apps/landing/src/pages/cookie-policy.tsx
+++ b/apps/landing/src/app/[locale]/cookie-policy/page.tsx
@@ -1,13 +1,6 @@
-import { GetStaticProps } from "next";
-import { NextSeo } from "next-seo";
-
-import PageLayout from "@/components/layouts/page-layout";
-import { getStaticTranslations } from "@/utils/page-translations";
-
-const PrivacyPolicy = () => {
+export default function CookiePolicy() {
return (
-
-
+ <>
Cookie Policy
Last updated: 19 April 2023
@@ -46,10 +39,13 @@ const PrivacyPolicy = () => {
website.
-
+ >
);
-};
+}
-export default PrivacyPolicy;
-
-export const getStaticProps: GetStaticProps = getStaticTranslations();
+export function generateMetadata() {
+ return {
+ title: "Rallly: Cookie Policy",
+ description: "The cookie policy for Rallly.",
+ };
+}
diff --git a/apps/landing/src/components/layouts/page-layout/footer.tsx b/apps/landing/src/app/[locale]/footer.tsx
similarity index 84%
rename from apps/landing/src/components/layouts/page-layout/footer.tsx
rename to apps/landing/src/app/[locale]/footer.tsx
index b0635ae9b..fbd4e6d21 100644
--- a/apps/landing/src/components/layouts/page-layout/footer.tsx
+++ b/apps/landing/src/app/[locale]/footer.tsx
@@ -1,5 +1,7 @@
+"use client";
+
import { DiscordIcon } from "@rallly/icons";
-import languages from "@rallly/languages";
+import languages, { supportedLngs } from "@rallly/languages";
import { Button } from "@rallly/ui/button";
import {
Select,
@@ -16,22 +18,29 @@ import {
} from "lucide-react";
import Image from "next/image";
import Link from "next/link";
-import { useRouter } from "next/router";
+import { usePathname, useRouter } from "next/navigation";
import * as React from "react";
+import { useTranslation } from "react-i18next";
import { Trans } from "@/components/trans";
-export const LanguageSelect = () => {
+const LanguageSelect = () => {
const router = useRouter();
-
+ const pathname = usePathname() ?? "";
+ const { i18n } = useTranslation();
return (