├── .gitignore
├── .vscode
└── settings.json
├── README.md
├── app
└── keystatic
│ ├── [[...params]]
│ └── page.tsx
│ ├── keystatic.tsx
│ └── layout.tsx
├── components
├── background-blobs.tsx
├── features.tsx
├── footer.tsx
├── head.tsx
├── hero.tsx
├── logo-link.tsx
├── navbar.tsx
├── pricing.tsx
└── testimonials.tsx
├── content
├── landing-page
│ └── index.yaml
└── testimonials
│ ├── anna
│ └── index.yaml
│ ├── frederik
│ └── index.yaml
│ ├── john-doe
│ └── index.yaml
│ ├── maeva
│ └── index.yaml
│ ├── matthew
│ └── index.yaml
│ ├── simonswiss
│ └── index.yaml
│ └── tamara
│ └── index.yaml
├── keystatic.config.tsx
├── next-env.d.ts
├── next.config.js
├── package.json
├── pages
├── _app.tsx
├── api
│ └── keystatic
│ │ └── [[...params]].tsx
└── index.tsx
├── pnpm-lock.yaml
├── postcss.config.js
├── prettier.config.js
├── public
├── .DS_Store
└── images
│ ├── favicon.ico
│ ├── features-image-cropped.png
│ ├── features-image.png
│ ├── features-image@2x.png
│ ├── features-image@3x.png
│ ├── hero-image.png
│ ├── hero-image@2x.png
│ ├── hero-image@3x.png
│ ├── seo-image.png
│ └── testimonials
│ ├── anna
│ └── avatar.jpg
│ ├── frederik
│ └── avatar.jpg
│ ├── john-doe
│ └── avatar.jpg
│ ├── maeva
│ └── avatar.jpg
│ ├── matthew
│ └── avatar.jpg
│ ├── simonswiss
│ └── avatar.jpg
│ └── tamara
│ └── avatar.jpg
├── styles
└── tailwind.css
├── tailwind.config.js
└── tsconfig.json
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 |
9 | # Diagnostic reports (https://nodejs.org/api/report.html)
10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
11 |
12 | # Runtime data
13 | pids
14 | *.pid
15 | *.seed
16 | *.pid.lock
17 |
18 | # Directory for instrumented libs generated by jscoverage/JSCover
19 | lib-cov
20 |
21 | # Coverage directory used by tools like istanbul
22 | coverage
23 | *.lcov
24 |
25 | # nyc test coverage
26 | .nyc_output
27 |
28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
29 | .grunt
30 |
31 | # Bower dependency directory (https://bower.io/)
32 | bower_components
33 |
34 | # node-waf configuration
35 | .lock-wscript
36 |
37 | # Compiled binary addons (https://nodejs.org/api/addons.html)
38 | build/Release
39 |
40 | # Dependency directories
41 | node_modules/
42 | jspm_packages/
43 |
44 | # TypeScript v1 declaration files
45 | typings/
46 |
47 | # TypeScript cache
48 | *.tsbuildinfo
49 |
50 | # Optional npm cache directory
51 | .npm
52 |
53 | # Optional eslint cache
54 | .eslintcache
55 |
56 | # Microbundle cache
57 | .rpt2_cache/
58 | .rts2_cache_cjs/
59 | .rts2_cache_es/
60 | .rts2_cache_umd/
61 |
62 | # Optional REPL history
63 | .node_repl_history
64 |
65 | # Output of 'npm pack'
66 | *.tgz
67 |
68 | # Yarn Integrity file
69 | .yarn-integrity
70 |
71 | # dotenv environment variables file
72 | .env
73 | .env.test
74 |
75 | # parcel-bundler cache (https://parceljs.org/)
76 | .cache
77 |
78 | # Next.js build output
79 | .next
80 |
81 | # Nuxt.js build / generate output
82 | .nuxt
83 | dist
84 |
85 | # Gatsby files
86 | .cache/
87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js
88 | # https://nextjs.org/blog/next-9-1#public-directory-support
89 | # public
90 |
91 | # vuepress build output
92 | .vuepress/dist
93 |
94 | # Serverless directories
95 | .serverless/
96 |
97 | # FuseBox cache
98 | .fusebox/
99 |
100 | # DynamoDB Local files
101 | .dynamodb/
102 |
103 | # TernJS port file
104 | .tern-port
105 | .vercel
106 |
107 | # Mac files
108 | .DS-Store
109 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "typescript.tsdk": "node_modules/.pnpm/typescript@4.9.5/node_modules/typescript/lib",
3 | "typescript.enablePromptUseWorkspaceTsdk": true
4 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Keystatic Demo – Landing Page
2 |
3 | A marketing landing page demo site, built with Keystatic, Next.js and Tailwind CSS.
4 |
5 | 1. Install dependencies:
6 |
7 | ```sh
8 | npm install
9 | ```
10 | 2. Start the dev server:
11 |
12 | ```sh
13 | npm run dev
14 | ```
15 |
16 | Visit http://127.0.0.1:3000/keystatic to see the Keystatic Admin UI.
--------------------------------------------------------------------------------
/app/keystatic/[[...params]]/page.tsx:
--------------------------------------------------------------------------------
1 | export default function Page() {
2 | return null;
3 | }
4 |
--------------------------------------------------------------------------------
/app/keystatic/keystatic.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { makePage } from "@keystatic/next/ui/app";
4 | import config from "../../keystatic.config";
5 |
6 | export default makePage(config);
7 |
--------------------------------------------------------------------------------
/app/keystatic/layout.tsx:
--------------------------------------------------------------------------------
1 | import KeystaticApp from "./keystatic";
2 |
3 | export const metadata = {
4 | title: "Keystatic: Admin UI",
5 | };
6 |
7 | export default function RootLayout() {
8 | return (
9 |
10 |
12 |
13 |
14 |
15 | );
16 | }
17 |
--------------------------------------------------------------------------------
/components/background-blobs.tsx:
--------------------------------------------------------------------------------
1 | /*
2 | Using some cray-cray arbitrary values over here...
3 | If you're not rocking with it, you can write
4 | normal CSS in `/styles/tailwind.css`.
5 | */
6 |
7 | export default function BackgroundBlobs() {
8 | return (
9 |
10 |
11 | {/* Blob 1 */}
12 |
15 | {/* Blob 2 */}
16 |
19 | {/* Blob 3 */}
20 |
23 |
24 |
25 | );
26 | }
27 |
--------------------------------------------------------------------------------
/components/features.tsx:
--------------------------------------------------------------------------------
1 | import Image from "next/image";
2 | import * as Accordion from "@radix-ui/react-accordion";
3 |
4 | import featuresImage from "../public/images/features-image.png";
5 | import featuresImageCropped from "../public/images/features-image-cropped.png";
6 |
7 | const features = [
8 | {
9 | id: 1,
10 | title: `Created by slate.
11 | Powered by iPhone.`,
12 | text: `Featuring advanced encryption technology, Slate takes full
13 | advantage of the latest device privacy and performance
14 | capabilities available.`,
15 | },
16 | {
17 | id: 2,
18 | title: `No surcharges.
19 | Not even international ones.`,
20 | text: `Featuring advanced encryption technology, Slate takes full
21 | advantage of the latest device privacy and performance
22 | capabilities available.`,
23 | },
24 | {
25 | id: 3,
26 | title: `Peace of mind payments for your everyday transactions.`,
27 | text: `Featuring advanced encryption technology, Slate takes full
28 | advantage of the latest device privacy and performance
29 | capabilities available.`,
30 | },
31 | ];
32 |
33 | export default function Features() {
34 | return (
35 |
36 |
37 |
38 |
45 |
46 |
47 |
54 |
55 |
60 | {features.map((feature) => (
61 |
66 |
67 |
68 |
69 |
70 |
71 | {feature.text}
72 |
73 |
74 | ))}
75 |
76 |
77 |
78 | );
79 | }
80 |
--------------------------------------------------------------------------------
/components/footer.tsx:
--------------------------------------------------------------------------------
1 | export type FooterProps = {
2 | footerHeadline: string;
3 | footerText: string;
4 | };
5 |
6 | type ComponentProps = {
7 | data: FooterProps;
8 | };
9 |
10 | export default function Footer({
11 | data: { footerHeadline, footerText },
12 | }: ComponentProps) {
13 | return (
14 |
51 | );
52 | }
53 |
--------------------------------------------------------------------------------
/components/head.tsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 | import { useRouter } from "next/router";
3 | import NextHead from "next/head";
4 |
5 | const meta = {
6 | title: "Keystatic | Marketing Landing Page Template",
7 | description:
8 | "Slate is a fictive product marketing landing page demo for Keystatic. Built by Thinkmill with Tailwind CSS and Next.js.",
9 | imagePath: "/images/seo-image.png",
10 | };
11 |
12 | export default function Head() {
13 | // Get correct domain to pass it to SEO image
14 | const router = useRouter();
15 | const [rootUrl, setRootUrl] = useState("");
16 | useEffect(() => {
17 | setRootUrl(window.location.origin);
18 | }, [router.pathname]);
19 |
20 | return (
21 |
22 | {meta.title}
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | );
43 | }
44 |
--------------------------------------------------------------------------------
/components/hero.tsx:
--------------------------------------------------------------------------------
1 | import Image from "next/image";
2 |
3 | import heroImage from "../public/images/hero-image.png";
4 |
5 | export type HeroProps = {
6 | heroHeadline: string;
7 | heroIntroText: string;
8 | };
9 |
10 | type ComponentProps = {
11 | data: HeroProps;
12 | };
13 |
14 | export default function Hero({
15 | data: { heroHeadline, heroIntroText },
16 | }: ComponentProps) {
17 | return (
18 |
19 | {/* Blob 2 */}
20 |
21 |
22 |
25 |
30 |
31 | {/* Mobile image */}
32 |
37 |
38 |
39 | {heroHeadline}
40 |
41 |
42 | {heroIntroText}
43 |
44 |
45 | {/* Buttons */}
46 |
72 |
73 |
74 |
75 | );
76 | }
77 |
--------------------------------------------------------------------------------
/components/logo-link.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 |
3 | export default function LogoLink() {
4 | return (
5 |
6 |
25 |
26 | );
27 | }
28 |
--------------------------------------------------------------------------------
/components/navbar.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 |
3 | import LogoLink from "./logo-link";
4 |
5 | export default function Navbar() {
6 | return (
7 |
8 |
9 |
10 |
11 |
12 |
31 |
32 |
33 |
34 | );
35 | }
36 |
37 | function KeystaticBanner() {
38 | return (
39 |
74 | );
75 | }
76 |
--------------------------------------------------------------------------------
/components/pricing.tsx:
--------------------------------------------------------------------------------
1 | const plans = [
2 | {
3 | name: "Starter",
4 | features: [
5 | "Multi-layered encryption",
6 | "Pay later, interest-free",
7 | "$1,000 credit limit",
8 | ],
9 | upgradeFeatures: [],
10 | },
11 | {
12 | name: "Everyday",
13 | monthlyPrice: 15,
14 | features: ["Multi-layered encryption", "Pay later, interest-free"],
15 | upgradeFeatures: ["Approval in minutes", "$5,000 credit limit"],
16 | },
17 | {
18 | name: "Pro",
19 | monthlyPrice: 30,
20 | features: ["Multi-layered encryption", "Pay later, interest-free"],
21 | upgradeFeatures: [
22 | "Approval in minutes",
23 | "Flexible repayments",
24 | "Product protection",
25 | "Unlimited credit limit",
26 | ],
27 | },
28 | ];
29 |
30 | function cx(...classes: string[]) {
31 | return classes.filter(Boolean).join(" ");
32 | }
33 |
34 | export default function Pricing() {
35 | return (
36 |
37 |
38 |
39 | Choose the plan that’s right for you.
40 |
41 |
No commitment. Cancel anytime.
42 |
43 |
44 | {plans.map((plan) => (
45 | -
49 |
50 |
{plan.name}
51 | {/* Plan price */}
52 |
53 | {plan?.monthlyPrice ? (
54 |
55 |
56 | {plan.monthlyPrice}
57 |
58 | /
59 | mth
60 |
61 | ) : (
62 | Free
63 | )}
64 |
65 | {/* Plan description */}
66 |
67 | You just want to give it a try, with no upfront commitment.
68 |
69 |
70 |
80 | {plan?.monthlyPrice ? "Subscribe" : "Get started"}
81 |
82 |
83 |
89 |
90 | {/* Base features */}
91 | {plan.features.map((feature) => (
92 | -
93 |
107 |
108 | {feature}
109 |
110 | ))}
111 | {/* Upgrade features */}
112 | {plan.upgradeFeatures.map((feature) => (
113 | -
114 |
126 |
127 | {feature}
128 |
129 | ))}
130 |
131 |
132 |
133 | ))}
134 |
135 |
136 |
137 | );
138 | }
139 |
--------------------------------------------------------------------------------
/components/testimonials.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import Image from "next/image";
3 |
4 | export type TestimonialProps = {
5 | author: string;
6 | slug: string;
7 | testimonial: string;
8 | featured: boolean;
9 | twitterHandle: string;
10 | avatar: string;
11 | };
12 |
13 | type ComponentProps = {
14 | testimonials: TestimonialProps[];
15 | };
16 |
17 | export default function Testimonials({ testimonials }: ComponentProps) {
18 | /*
19 | The Featured Testimonial will be display in the
20 | more prominent callout above the
21 | other testimonials.
22 | */
23 | const featuredTestimonial =
24 | testimonials.find(
25 | (testimonial: TestimonialProps) => testimonial.featured
26 | ) || testimonials[0];
27 |
28 | const otherTestimonials = [
29 | ...testimonials.filter(
30 | (testimonial: TestimonialProps) => !testimonial.featured
31 | ),
32 | ];
33 |
34 | return (
35 |
36 |
37 |
38 |
39 |
40 | {featuredTestimonial.testimonial}
41 |
42 |
43 |
48 |
49 | {featuredTestimonial.author}
50 |
51 |
55 | @{featuredTestimonial.twitterHandle}
56 |
57 |
58 |
59 |
60 |
61 |
62 | Our users have many reasons to choose Slate.
63 |
64 |
Here's the latest.
65 |
66 |
67 |
68 | {otherTestimonials.map((testimonial: any) => (
69 | -
70 |
71 |
72 |
73 | {testimonial.testimonial}
74 |
75 |
76 |
83 |
84 |
85 | {testimonial.author}
86 |
87 |
91 | @{testimonial.twitterHandle}
92 |
93 |
94 |
95 |
96 |
97 |
98 | ))}
99 |
100 |
101 |
102 |
103 | );
104 | }
105 |
--------------------------------------------------------------------------------
/content/landing-page/index.yaml:
--------------------------------------------------------------------------------
1 | heroHeadline: The future of payments.
2 | heroIntroText: >-
3 | Slate brings the future of credit card payments to all platforms via it’s
4 | simple app. Providing secure, surcharge free transactions anywhere in the
5 | world, with any currency.
6 | footerHeadline: Get started with Slate, today.
7 | footerText: >-
8 | Sign up in minutes and receive the first 3 months free. If you’re not happy
9 | with Slate, simply cancel at anytime. No worries.
10 |
--------------------------------------------------------------------------------
/content/testimonials/anna/index.yaml:
--------------------------------------------------------------------------------
1 | author: Anna
2 | testimonial: >-
3 | I ABSOLUTELY, really love this little GIT-based CMS, looks fun, I want to try
4 | it!
5 | featured: false
6 | twitterHandle: anna_tweets
7 | avatar: avatar.jpg
8 |
--------------------------------------------------------------------------------
/content/testimonials/frederik/index.yaml:
--------------------------------------------------------------------------------
1 | author: Frederik
2 | testimonial: >-
3 | I keep hearing good things about this thing. Something tells me this could be
4 | a special thing.
5 | featured: true
6 | twitterHandle: free-riko
7 | avatar: avatar.jpg
8 |
--------------------------------------------------------------------------------
/content/testimonials/john-doe/index.yaml:
--------------------------------------------------------------------------------
1 | author: John Doe
2 | testimonial: Woooooow, this is super cool! I love that. No but for real!
3 | featured: false
4 | twitterHandle: j_doe
5 | avatar: avatar.jpg
6 |
--------------------------------------------------------------------------------
/content/testimonials/maeva/index.yaml:
--------------------------------------------------------------------------------
1 | author: Maeva
2 | testimonial: I personally know the team behind Keystatic, and... they're wonderful people!
3 | featured: false
4 | twitterHandle: maevs44
5 | avatar: avatar.jpg
6 |
--------------------------------------------------------------------------------
/content/testimonials/matthew/index.yaml:
--------------------------------------------------------------------------------
1 | author: Matthew
2 | testimonial: >-
3 | I can see the potential for a lot of companies out there. Keystatic strikes a
4 | nice niche!
5 | featured: false
6 | twitterHandle: matthew
7 | avatar: avatar.jpg
8 |
--------------------------------------------------------------------------------
/content/testimonials/simonswiss/index.yaml:
--------------------------------------------------------------------------------
1 | author: Simon Vrachliotis
2 | testimonial: >-
3 | I built this template, so you best believe I'm going to save myself a spot in
4 | the testimonials 🤗
5 | featured: false
6 | twitterHandle: simonswiss
7 | avatar: avatar.jpg
8 |
--------------------------------------------------------------------------------
/content/testimonials/tamara/index.yaml:
--------------------------------------------------------------------------------
1 | author: Tamara
2 | testimonial: >-
3 | The idea of not having to manage a database or a hosting provider is warming
4 | up to me!
5 | featured: false
6 | twitterHandle: tamsy_io
7 | avatar: avatar.jpg
8 |
--------------------------------------------------------------------------------
/keystatic.config.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | collection,
3 | config,
4 | fields,
5 | singleton,
6 | LocalConfig,
7 | GitHubConfig,
8 | } from "@keystatic/core";
9 |
10 | const storage: LocalConfig["storage"] | GitHubConfig["storage"] =
11 | process.env.NODE_ENV === "development"
12 | ? { kind: "local" }
13 | : {
14 | kind: "github",
15 | repo: {
16 | owner: process.env.NEXT_PUBLIC_VERCEL_GIT_REPO_OWNER!,
17 | name: process.env.NEXT_PUBLIC_VERCEL_GIT_REPO_SLUG!,
18 | },
19 | };
20 |
21 | export default config({
22 | storage,
23 | singletons: {
24 | landingPage: singleton({
25 | label: "Landing Page",
26 | path: "content/landing-page/",
27 | schema: {
28 | heroHeadline: fields.text({ label: "Hero headline" }),
29 | heroIntroText: fields.text({
30 | label: "Hero intro text",
31 | multiline: true,
32 | }),
33 | footerHeadline: fields.text({ label: "Footer headline" }),
34 | footerText: fields.text({ label: "Footer text", multiline: true }),
35 | },
36 | }),
37 | },
38 | collections: {
39 | testimonials: collection({
40 | path: "content/testimonials/*/",
41 | label: "Testimonials",
42 | slugField: "author",
43 | schema: {
44 | author: fields.slug({ name: { label: "Author" } }),
45 | testimonial: fields.text({ label: "Testimonial", multiline: true }),
46 | featured: fields.checkbox({ label: "Featured testimonial" }),
47 | twitterHandle: fields.text({ label: "Twitter handle" }),
48 | avatar: fields.image({
49 | label: "Avatar",
50 | directory: "public/images/testimonials",
51 | validation: { isRequired: true },
52 | }),
53 | },
54 | }),
55 | },
56 | });
57 |
--------------------------------------------------------------------------------
/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 | ///
4 |
5 | // NOTE: This file should not be edited
6 | // see https://nextjs.org/docs/basic-features/typescript for more information.
7 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | module.exports = {
3 | experimental: {
4 | appDir: true,
5 | },
6 | typescript: { ignoreBuildErrors: true },
7 | };
8 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@keystatic/demo-landing-page",
3 | "dependencies": {
4 | "@keystatic/core": "^0.0.116",
5 | "@keystatic/next": "^0.0.11",
6 | "@radix-ui/react-accordion": "^1.1.2",
7 | "@types/react": "^18.2.21",
8 | "next": "^13.4.19",
9 | "react": "^18.2.0",
10 | "react-dom": "^18.2.0",
11 | "typescript": "^5.1.6"
12 | },
13 | "scripts": {
14 | "dev": "next",
15 | "build": "next build",
16 | "start": "next start",
17 | "postinstall": "touch .env"
18 | },
19 | "devDependencies": {
20 | "autoprefixer": "^10.4.15",
21 | "postcss": "^8.4.28",
22 | "prettier": "^3.0.2",
23 | "prettier-plugin-tailwindcss": "^0.5.3",
24 | "tailwindcss": "^3.3.3"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import type { AppProps } from "next/app";
2 |
3 | import "../styles/tailwind.css";
4 |
5 | export default function MyApp({ Component, pageProps }: AppProps) {
6 | return (
7 |
8 |
9 |
10 | );
11 | }
12 |
--------------------------------------------------------------------------------
/pages/api/keystatic/[[...params]].tsx:
--------------------------------------------------------------------------------
1 | import { makeAPIRouteHandler } from "@keystatic/next/api";
2 | import config from "../../../keystatic.config";
3 |
4 | export default makeAPIRouteHandler({ config });
5 |
--------------------------------------------------------------------------------
/pages/index.tsx:
--------------------------------------------------------------------------------
1 | // Keystatic
2 | import { createReader } from "@keystatic/core/reader";
3 | import keystaticConfig from "../keystatic.config";
4 |
5 | // Page section components
6 | import Head from "../components/head";
7 | import BackgroundBlobs from "../components/background-blobs";
8 | import Features from "../components/features";
9 | import Footer, { FooterProps } from "../components/footer";
10 | import Hero, { HeroProps } from "../components/hero";
11 | import Navbar from "../components/navbar";
12 | import Pricing from "../components/pricing";
13 | import Testimonials, { TestimonialProps } from "../components/testimonials";
14 |
15 | type HomepageProps = {
16 | testimonials: TestimonialProps[];
17 | landingPage: HeroProps & FooterProps;
18 | };
19 |
20 | // ----------
21 |
22 | export default function Index({ testimonials, landingPage }: HomepageProps) {
23 | return (
24 | <>
25 |