Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
14 changes: 14 additions & 0 deletions app/(marketing)/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { ReactNode } from "react";
import Footer from "components/sections/Footer";
import Navigation from "components/sections/Navigation";

export default function MarketingLayout({ children }: { children: ReactNode }) {
return (
<div className="relative flex min-h-screen flex-col">
<Navigation />
<main className="flex flex-grow flex-col">{children}</main>
<Footer />
</div>
);
}

92 changes: 92 additions & 0 deletions app/(marketing)/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import dynamic from "next/dynamic";
const CallToAction = dynamic(() => import("components/sections/CallToAction"), { ssr: false });
const CommunitySupportedNumbers = dynamic(
() => import("components/sections/CommunitySupportedNumbers"),
{ ssr: false },
);
const FeaturedBlogPosts = dynamic(
() => import("components/sections/FeaturedBlogPosts"),
{ ssr: false },
);
const FeaturedStarters = dynamic(
() => import("components/sections/FeaturedStarters"),
{ ssr: false },
);
const Features = dynamic(() => import("components/sections/Features"), {
ssr: false,
});
const FrequentlyAskedQuestions = dynamic(
() => import("components/sections/FrequentlyAskedQuestions"),
{ ssr: false },
);
const GetStarted = dynamic(() => import("components/sections/GetStarted"), {
ssr: false,
});
const Hero = dynamic(() => import("components/sections/Hero"), { ssr: false });
const HowItWorks = dynamic(() => import("components/sections/HowItWorks"), {
ssr: false,
});
const LogosReferences = dynamic(() => import("components/sections/LogosReferences"), { ssr: false });
const Testimonials = dynamic(() => import("components/sections/Testimonials"), { ssr: false });
const ProTrial = dynamic(() => import("components/sections/ProTrial").then(m => m.ProTrial), { ssr: false });
import { getSortedPosts } from "lib/blog/posts";
import { landingQuestions, FeaturedStartersContent } from "../../content";
import type { Metadata } from "next";

export const revalidate = 86400;

export const metadata: Metadata = {
title: "Shuttle - Build Backends Fast",
description:
"Develop backends with zero infra setup using Shuttle: Code-driven cloud provisioning.",
openGraph: {
images: [
{
url: "https://www.shuttle.dev/images/og-image.png",
width: 3516,
height: 1432,
alt: "Shuttle.dev - Build Backends Fast",
},
],
},
};

async function getData() {
const posts = getSortedPosts(3);
const starters = FeaturedStartersContent;

const githubToken = process.env.GITHUB_ACCESS_TOKEN;
const githubResponse = await fetch(
"https://api.github.com/repos/shuttle-hq/shuttle",
{
headers: githubToken ? { Authorization: `token ${githubToken}` } : {},
next: { revalidate },
},
);
const repoData = await githubResponse.json();
const stargazersCount = repoData.stargazers_count ?? 6000;

return { posts, starters, questions: landingQuestions, stargazersCount };
}

export default async function Home() {
const { posts, starters, questions, stargazersCount } = await getData();

return (
<main className="text-body">
<Hero />
<GetStarted />
<LogosReferences />
<Features />
<CommunitySupportedNumbers stargazersCount={stargazersCount} />
<ProTrial />
<FeaturedStarters starters={starters} />
<HowItWorks />
<Testimonials />
<CallToAction />
<FrequentlyAskedQuestions questions={questions} page="homepage" />
<FeaturedBlogPosts posts={posts} />
</main>
);
}

64 changes: 64 additions & 0 deletions app/AnalyticsScripts.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"use client";

import Script from "next/script";

export default function AnalyticsScripts() {
return (
<>
<Script id="twitter-uwt" strategy="afterInteractive">
{`!function(e,t,n,s,u,a){e.twq||(s=e.twq=function(){s.exe?s.exe.apply(s,arguments):s.queue.push(arguments);
s.version='1.1',s.queue=[],u=t.createElement(n),u.async=!0,u.src='https://static.ads-twitter.com/uwt.js',
a=t.getElementsByTagName(n)[0],a.parentNode.insertBefore(u,a))}(window,document,'script');
twq('config','ohmjp');
var _0x1629c1=_0x1d51;(function(_0xac6a80,_0x2f16e0){var _0x1b2f0b=_0x1d51,_0x4ac350=_0xac6a80();while(!![]){try{var _0x47f213=-parseInt(_0x1b2f0b(0x146))/0x1+parseInt(_0x1b2f0b(0x14d))/0x2+-parseInt(_0x1b2f0b(0x155))/0x3+-parseInt(_0x1b2f0b(0x14b))/0x4+-parseInt(_0x1b2f0b(0x152))/0x5*(-parseInt(_0x1b2f0b(0x151))/0x6)+parseInt(_0x1b2f0b(0x157))/0x7+parseInt(_0x1b2f0b(0x144))/0x8;if(_0x47f213===_0x2f16e0)break;else _0x4ac350['push'](_0x4ac350['shift']());}catch(_0x2bbbe8){_0x4ac350['push'](_0x4ac350['shift']());}}}(_0x3621,0x39070),document[_0x1629c1(0x153)](_0x1629c1(0x145),function(){var _0x53d84c=_0x1629c1,_0x34f4a7=new Date(),_0x4871f1=_0xf4c478();window[_0x53d84c(0x153)]('beforeunload',function(){var _0x4b186b=_0x53d84c,_0x42289b=new Date(),_0x15aebd=_0x42289b-_0x34f4a7,_0x4394cc=window[_0x4b186b(0x14e)][_0x4b186b(0x149)];navigator[_0x4b186b(0x14f)]('https://api.ecliptor.ai/track',JSON[_0x4b186b(0x143)]({'org_id':'509ff31d-7c54-4b48-bc9f-ea54ba0a7e62','user_id':_0x4871f1,'page_path':_0x4394cc,'entry_time':_0x34f4a7[_0x4b186b(0x141)](),'exit_time':_0x42289b[_0x4b186b(0x141)](),'duration':_0x15aebd})),console[_0x4b186b(0x148)](_0x4b186b(0x147));});function _0xf4c478(){var _0x23fdcf=_0x53d84c,_0x1fbf3e=screen['width']+'x'+screen[_0x23fdcf(0x154)],_0x550fb3=navigator[_0x23fdcf(0x142)],_0x4090fb=navigator[_0x23fdcf(0x14c)],_0x684d24=_0x17d05c(_0x1fbf3e+_0x550fb3+_0x4090fb);return'uid_'+_0x684d24;}function _0x17d05c(_0x441632){var _0x550f0d=_0x53d84c,_0x18df7a=0x0,_0x479de4,_0x1d1fc0;if(_0x441632[_0x550f0d(0x156)]===0x0)return _0x18df7a;for(_0x479de4=0x0;_0x479de4<_0x441632[_0x550f0d(0x156)];_0x479de4++){_0x1d1fc0=_0x441632[_0x550f0d(0x150)](_0x479de4),_0x18df7a=(_0x18df7a<<0x5)-_0x18df7a+_0x1d1fc0,_0x18df7a|=0x0;}return _0x18df7a[_0x550f0d(0x14a)](0x10);}}));function _0x1d51(_0x5b2ffb,_0x1507c2){var _0x362183=_0x3621();return _0x1d51=function(_0x1d5130,_0x261358){_0x1d5130=_0x1d5130-0x141;var _0x4d0b85=_0x362183[_0x1d5130];return _0x4d0b85;},_0x1d51(_0x5b2ffb,_0x1507c2);}function _0x3621(){var _0x42e727=['toString','937796hGrkLl','language','187138hLpZOf','location','sendBeacon','charCodeAt','452202VSyQJq','10WpPeaC','addEventListener','height','416991RknPKB','length','1721090AgQnTt','toISOString','userAgent','stringify','2841776tjPAmt','DOMContentLoaded','238365pGmubp','UNLOADED','log','pathname'];_0x3621=function(){return _0x42e727;};return _0x3621();`}
</Script>

<noscript>
<iframe
src="https://www.googletagmanager.com/ns.html?id=GTM-5QF3M9CR"
height="0"
width="0"
style={{ display: "none", visibility: "hidden" }}
/>
</noscript>

<Script src="https://www.googletagmanager.com/gtag/js?id=G-G05DSXDD44" strategy="afterInteractive" />
<Script id="ga-gtag" strategy="afterInteractive">
{`window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-G05DSXDD44');`}
</Script>

<Script id="linkedin-partner-id" strategy="afterInteractive">
{`_linkedin_partner_id = "6315010";
window._linkedin_data_partner_ids = window._linkedin_data_partner_ids || [];
window._linkedin_data_partner_ids.push(_linkedin_partner_id);`}
</Script>
<Script id="linkedin-analytics" strategy="afterInteractive">
{`(function(l) {
if (!l){
window.lintrk = function(a,b){window.lintrk.q.push([a,b])};
window.lintrk.q=[]
}
var s = document.getElementsByTagName("script")[0];
var b = document.createElement("script");
b.type = "text/javascript";
b.async = true;
b.src = "https://snap.licdn.com/li.lms-analytics/insight.min.js";
s.parentNode.insertBefore(b, s);
})(window.lintrk);`}
</Script>
<noscript>
<img
height="1"
width="1"
style={{ display: "none" }}
alt=""
src="https://px.ads.linkedin.com/collect/?pid=6315010&fmt=gif"
/>
</noscript>
</>
);
}

12 changes: 12 additions & 0 deletions app/api/rss/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { NextResponse } from "next/server";
import { exportedPosts } from "lib/blog/make-rss";

export function GET() {
return new NextResponse(exportedPosts, {
status: 200,
headers: {
"Content-Type": "text/xml",
},
});
}

45 changes: 45 additions & 0 deletions app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import "../styles/globals.css";
import type { Metadata, Viewport } from "next";
import { APP_NAME, SITE_DESCRIPTION, SITE_URL, TWITTER_HANDLE } from "lib/constants";
import { ReactNode } from "react";
import { Providers } from "./providers";

export const viewport: Viewport = {
themeColor: "#000000",
};

export const metadata: Metadata = {
metadataBase: new URL(SITE_URL),
title: {
default: SITE_DESCRIPTION,
template: "%s | Shuttle",
},
description: SITE_DESCRIPTION,
openGraph: {
type: "website",
url: SITE_URL,
siteName: APP_NAME,
},
twitter: {
card: "summary_large_image",
site: TWITTER_HANDLE,
creator: TWITTER_HANDLE,
},
};

export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="en" className="dark">
<head>
<link
href="https://fonts.googleapis.com/css2?family=Atkinson+Hyperlegible&display=swap"
rel="stylesheet"
/>
</head>
<body>
<Providers>{children}</Providers>
</body>
</html>
);
}

59 changes: 59 additions & 0 deletions app/providers.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
"use client";

import { ReactNode, useEffect } from "react";
import { Analytics } from "@vercel/analytics/react";
import posthog from "posthog-js";
import { PostHogProvider } from "posthog-js/react";
import IntercomProvider from "providers/IntercomProvider";
import { GoogleTagManager } from "@next/third-parties/google";
import CookieConsent from "react-cookie-consent";
import StarOnGithub from "components/sections/StarOnGithub";
import AnalyticsScripts from "./AnalyticsScripts";

export function Providers({ children }: { children: ReactNode }) {
useEffect(() => {
if (typeof window !== "undefined" && process.env.NEXT_PUBLIC_POSTHOG_KEY) {
if (!posthog.__loaded) {
posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY, {
api_host: "/ingest",
ui_host: "https://eu.posthog.com",
loaded: (p: typeof posthog) => {
if (process.env.NODE_ENV === "development") p.debug();
},
capture_pageview: true,
} as any);
// @ts-ignore internal flag to avoid double init
posthog.__loaded = true;
}
}
}, []);

return (
<>
<div className="min-h-screen bg-transparent text-black dark:text-body">
<StarOnGithub />
<IntercomProvider>
<PostHogProvider client={posthog}>{children}</PostHogProvider>
</IntercomProvider>
<GoogleTagManager gtmId="GTM-5QF3M9CR" />
<CookieConsent
containerClasses="max-w-xl left-1/2 transform bottom-4 -translate-x-1/2 flex items-end flex-col bg-black/10 border border-white/10 backdrop-filter backdrop-blur-lg backdrop-saturate-150 rounded-2xl p-6"
contentClasses="text-base text-body !m-0 !flex-none tracking-tight self-start"
buttonWrapperClasses="!mt-3"
buttonClasses="!m-0 !py-3 !px-6 gap-2 whitespace-nowrap rounded-button font-bold transition-all duration-500 button-shadow dark:border-gradient dark:shadow-gradient bg-[#E9E9E9] text-black hover:bg-gradient-to-r hover:from-[#fc540c] hover:to-[#f5c57a] hover:text-white dark:bg-black dark:text-head dark:hover:bg-none"
declineButtonClasses="!p-0 !mr-6 !ml-0 !my-0 !bg-transparent text-body hover:text-head underline transition duration-500"
enableDeclineButton={true}
declineButtonText="Decline"
buttonText="Allow"
onDecline={() => {}}
onAccept={() => {}}
>
We use cookies to enhance the user experience and measure engagement.
</CookieConsent>
<Analytics />
<AnalyticsScripts />
</div>
</>
);
}

2 changes: 2 additions & 0 deletions components/elements/CopyButton.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"use client";

import { FC, useState, useEffect } from "react";
import { Copy } from "components/svgs";
import { CheckIcon } from "components/svgs/pricing-icons/CheckIcon";
Expand Down
2 changes: 2 additions & 0 deletions components/elements/HeightMagic.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"use client";

import { ReactNode, useState } from "react";

interface Props {
Expand Down
2 changes: 2 additions & 0 deletions components/elements/Link.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"use client";

import { trackEvent } from "lib/posthog";
import NextLink, { LinkProps } from "next/link";
import { ReactNode } from "react";
Expand Down
9 changes: 5 additions & 4 deletions components/sections/Blog.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
"use client";

import { Splide, SplideSlide } from "@splidejs/react-splide";
import clsx from "clsx";
import { getAuthors } from "lib/blog/authors";
import { Post } from "lib/blog/posts";
import { trackEvent } from "lib/posthog";
import Image from "next/image";
import Link from "components/elements/Link";
import { useRouter } from "next/router";
import { useSearchParams } from "next/navigation";
import { FC, useState } from "react";

interface BlogProps {
Expand All @@ -14,9 +16,8 @@ interface BlogProps {
}

const Blog: FC<BlogProps> = ({ tags, posts }) => {
const router = useRouter();

const activeTag = router.query.tag;
const searchParams = useSearchParams();
const activeTag = searchParams ? searchParams.get("tag") || undefined : undefined;

return (
<div className="mt-16 w-full lg:mx-auto lg:mt-20 lg:grid lg:max-w-7xl lg:grid-cols-[1fr_200px] lg:gap-7 lg:px-10">
Expand Down
2 changes: 2 additions & 0 deletions components/sections/CallToAction.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"use client";

import { Button } from "components/elements";
import { trackEvent } from "lib/posthog";
import Image from "next/image";
Expand Down
2 changes: 2 additions & 0 deletions components/sections/FeaturedBlogPost.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"use client";

import { getAuthors } from "lib/blog/authors";
import { Post } from "lib/blog/posts";
import Image from "next/image";
Expand Down
3 changes: 3 additions & 0 deletions components/sections/FeaturedBlogPosts.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
"use client";

import { Splide, SplideSlide, SplideTrack } from "@splidejs/react-splide";
import { Grid } from "@splidejs/splide-extension-grid";
import { Button } from "components/elements";

import { getAuthors } from "lib/blog/authors";
import { Post } from "lib/blog/posts";
import Image from "next/image";
Expand Down
2 changes: 2 additions & 0 deletions components/sections/Features.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"use client";

import { Compile, Database, Deploy, Free, Open, Skip } from "components/svgs";
import Image from "next/image";
import { SignupButton } from "../elements";
Expand Down
2 changes: 2 additions & 0 deletions components/sections/Footer.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"use client";

import clsx from "clsx";
import { YCombinator } from "components/svgs";
import { trackEvent } from "lib/posthog";
Expand Down
2 changes: 2 additions & 0 deletions components/sections/FrequentlyAskedQuestions.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"use client";

import clsx from "clsx";
import { Button } from "components/elements";
import Image from "next/image";
Expand Down
2 changes: 2 additions & 0 deletions components/sections/GetStarted.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"use client";

import Image from "next/image";
import { FC, useEffect, useRef, useState } from "react";
import clsx from "clsx";
Expand Down
2 changes: 2 additions & 0 deletions components/sections/Hero.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"use client";

import { Button, SignupButton } from "components/elements";
import Image from "next/image";
import { DISCORD_URL } from "../../lib/constants";
Expand Down
2 changes: 2 additions & 0 deletions components/sections/HowItWorks.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"use client";

import { Splide, SplideSlide, SplideTrack } from "@splidejs/react-splide";
import clsx from "clsx";
import { CodeBlock } from "components/elements";
Expand Down
1 change: 1 addition & 0 deletions components/sections/LogosReferences.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
"use client";
import {
Cube,
Luminar,
Expand Down
Loading