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 +
+
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. +
+
+
+
+
+
+
+
+