├── .env.sample ├── .eslintrc.json ├── .gitignore ├── LICENSE ├── README.md ├── app ├── (builder) │ ├── chai │ │ ├── api │ │ │ ├── preview │ │ │ │ └── route.ts │ │ │ ├── revalidate │ │ │ │ └── route.ts │ │ │ └── route.ts │ │ ├── assets │ │ │ └── route.ts │ │ ├── page.tsx │ │ └── users │ │ │ └── route.ts │ ├── chaibuilder.tailwind.ts │ ├── layout.tsx │ └── styles.css ├── (public) │ ├── [[...slug]] │ │ └── page.tsx │ ├── layout.tsx │ └── public.css ├── favicon.ico └── fonts │ ├── GeistMonoVF.woff │ └── GeistVF.woff ├── blocks ├── accordion │ └── Accordion.tsx ├── blogs-grid │ ├── BlogsGrid.tsx │ └── data-provider.ts ├── dropdown │ └── Dropdown.tsx ├── image │ └── Image.tsx ├── index.server.ts ├── index.ts ├── link │ └── Link.tsx └── modal │ └── Modal.tsx ├── chai ├── index.ts └── theme-presets.ts ├── cms └── index.ts ├── components.json ├── components ├── builder │ ├── chaibuilder-pages.tsx │ ├── loader.tsx │ └── logo.tsx ├── preview-banner.tsx └── ui │ ├── accordion.tsx │ ├── avatar.tsx │ ├── badge.tsx │ ├── button.tsx │ ├── card.tsx │ ├── checkbox.tsx │ ├── collapsible.tsx │ ├── command.tsx │ ├── dialog.tsx │ ├── dropdown-menu.tsx │ ├── input.tsx │ ├── label.tsx │ ├── popover.tsx │ ├── progress.tsx │ ├── scroll-area.tsx │ ├── select.tsx │ ├── separator.tsx │ ├── skeleton.tsx │ ├── slider.tsx │ ├── textarea.tsx │ ├── theme-provider.tsx │ └── tooltip.tsx ├── data ├── global.ts └── index.ts ├── declaration.d.ts ├── fonts └── index.ts ├── lib └── utils.ts ├── next.config.mjs ├── package.json ├── page-types ├── blog.ts └── index.ts ├── pnpm-lock.yaml ├── postcss.config.mjs ├── scripts ├── check-env.cjs └── tailwind.cjs ├── tailwind.config.ts ├── tsconfig.json └── utils └── styles-helper.ts /.env.sample: -------------------------------------------------------------------------------- 1 | CHAIBUILDER_API_KEY=#specify the chaibuilder api key here 2 | CHAIBUILDER_WEBHOOK_SECRET=#specify your own webhook secret token here -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next/core-web-vitals"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | .env 31 | 32 | # vercel 33 | .vercel 34 | .idea 35 | 36 | # typescript 37 | *.tsbuildinfo 38 | next-env.d.ts 39 | public/chaistyles.css 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2024, Suraj Air 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name "Chai Builder" nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Chai builder + NextJS Starter 2 | 3 | This is a starter project for Chai builder + NextJS. 4 | 5 | ## Requirements 6 | 7 | - `CHAIBUILDER_API_KEY` - Get API KEY here [https://chaibuilder.com](https://chaibuilder.com/sites) 8 | 9 | ## Features 10 | 11 | - Website builder with drag and drop 12 | - One click publish 13 | - Revisions and restore 14 | - Page lock (prevent multiple users from editing the same page) 15 | - Multi-language support 16 | - SEO ( Basic, Open Graph, JSON-LD ) 17 | - SSR and SSG support 18 | - Built in Vercel ISR support 19 | - Themeable with Tailwind CSS(Shadcn themes) 20 | - Global blocks for reusable content 21 | - Draft preview mode (preview changes before publishing) 22 | - Data binding with external data (e.g. from a CMS) 23 | - Custom page blocks (e.g. a team page with a list of team members) 24 | - Custom page types (e.g. a blog page template for all blogs) 25 | - AI content generation ( with multilingual support ) 26 | - AI style editing 27 | - Dark mode support 28 | - Custom authentication ( Implement your own auth provider ) 29 | - Custom DAM ( Implement your own or use A DAM solution ) 30 | 31 | ## Installation: 32 | 33 | 1. Fork this repo 34 | 2. `pnpm install` 35 | 3. Create a new .env file and add env variables from .env.sample 36 | 4. `pnpm dev` 37 | 5. Goto `/chai` route to login and edit in builder 38 | 39 | ## Stack 40 | 41 | - NextJS15 + React 19 42 | - Tailwind CSS 3.4+ 43 | - Shadcn UI 44 | - TypeScript 45 | 46 | ## Development 47 | 48 | We recomment using `pnpm` for development. 49 | 50 | ```bash 51 | pnpm install 52 | pnpm run dev 53 | ``` 54 | 55 | Navigate to `/chai` route to view the builder and publish. Sign in and start publishing your website. 56 | 57 | ## Deployment 58 | 59 | We recomment using `Vercel` for deployment for better ISR support. 60 | 61 | ## License 62 | 63 | This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. 64 | -------------------------------------------------------------------------------- /app/(builder)/chai/api/preview/route.ts: -------------------------------------------------------------------------------- 1 | // route handler with secret and slug 2 | import { draftMode } from "next/headers"; 3 | import { redirect } from "next/navigation"; 4 | 5 | export async function GET(request: Request) { 6 | // Parse query string parameters 7 | const { searchParams } = new URL(request.url); 8 | const slug = searchParams.get("slug"); 9 | const disable = searchParams.get("disable"); 10 | 11 | // Check the secret and next parameters 12 | // This secret should only be known to this route handler and the CMS 13 | if (!slug) { 14 | return new Response("Invalid token", { status: 401 }); 15 | } 16 | 17 | // Enable Draft Mode by setting the cookie 18 | if (disable === "true") { 19 | (await draftMode()).disable(); 20 | } else { 21 | (await draftMode()).enable(); 22 | } 23 | 24 | // Redirect to the path from the fetched post 25 | // We don't redirect to searchParams.slug as that might lead to open redirect vulnerabilities 26 | redirect(slug); 27 | } 28 | -------------------------------------------------------------------------------- /app/(builder)/chai/api/revalidate/route.ts: -------------------------------------------------------------------------------- 1 | import { revalidatePath, revalidateTag } from 'next/cache' 2 | import { NextRequest, NextResponse } from 'next/server' 3 | 4 | const revalidateRoute = async (req: NextRequest) => { 5 | const { searchParams } = new URL(req.url) 6 | const secret = searchParams.get('secret') 7 | 8 | if (secret !== process.env.CHAI_SECRET_TOKEN) { 9 | return NextResponse.json({ error: 'Invalid secret' }, { status: 401 }) 10 | } 11 | 12 | const tags = req.nextUrl.searchParams.get('tags') || '' 13 | const paths = req.nextUrl.searchParams.get('paths') || '' 14 | 15 | try { 16 | const tagsArray = Array.isArray(tags) ? tags : tags.split(',') 17 | await Promise.all(tagsArray.map((tag) => revalidateTag(tag))) 18 | 19 | const pathsArray = Array.isArray(paths) ? paths : paths.split(',') 20 | await Promise.all(pathsArray.map((path) => revalidatePath(path))) 21 | 22 | return NextResponse.json( 23 | { message: 'Tags and paths revalidated successfully' }, 24 | { status: 200 } 25 | ) 26 | } catch (error) { 27 | console.error(error) 28 | return NextResponse.json( 29 | { error: 'Failed to revalidate tags and paths' }, 30 | { status: 500 } 31 | ) 32 | } 33 | } 34 | 35 | export async function GET(req: NextRequest) { 36 | return revalidateRoute(req) 37 | } 38 | -------------------------------------------------------------------------------- /app/(builder)/chai/api/route.ts: -------------------------------------------------------------------------------- 1 | import { chaiBuilderPages } from "@/chai"; 2 | import "@/data"; 3 | import "@/page-types"; 4 | import { get, has } from "lodash"; 5 | import { revalidateTag } from "next/cache"; 6 | import { NextRequest, NextResponse } from "next/server"; 7 | 8 | export async function POST(req: NextRequest) { 9 | const requestBody = await req.json(); 10 | 11 | try { 12 | // Check for `authorization` header 13 | const authorization = req.headers.get("authorization"); 14 | if (!authorization) { 15 | return NextResponse.json( 16 | { error: "Missing Authorization header" }, 17 | { status: 401 } 18 | ); 19 | } 20 | 21 | // Check and extract, valid token string `authorization` 22 | const authToken = authorization ? authorization.split(" ")[1] : ""; 23 | const response = await chaiBuilderPages.handle(requestBody, authToken); 24 | 25 | const tags = get(response, "tags", []); 26 | for (const tag of tags) { 27 | revalidateTag(tag); 28 | } 29 | 30 | if (has(response, "error")) { 31 | return NextResponse.json(response, { status: response.status }); 32 | } 33 | return NextResponse.json(response); 34 | } catch (error) { 35 | // * On error, throw if firebase auth error, else 500 36 | if (error instanceof Error) { 37 | return NextResponse.json( 38 | { error: "Invalid or expired token" }, 39 | { status: 401 } 40 | ); 41 | } else { 42 | return NextResponse.json( 43 | { error: "Something went wrong." }, 44 | { status: 500 } 45 | ); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/(builder)/chai/assets/route.ts: -------------------------------------------------------------------------------- 1 | import { chaiBuilderPages } from "@/chai"; 2 | import "@/data"; 3 | import "@/page-types"; 4 | import { has } from "lodash"; 5 | import { NextRequest, NextResponse } from "next/server"; 6 | 7 | export async function POST(req: NextRequest) { 8 | const requestBody = await req.json(); 9 | try { 10 | // Check for `authorization` header 11 | const authorization = req.headers.get("authorization"); 12 | if (!authorization) { 13 | return NextResponse.json( 14 | { error: "Missing Authorization header" }, 15 | { status: 401 } 16 | ); 17 | } 18 | 19 | const authToken = authorization ? authorization.split(" ")[1] : ""; 20 | const response = await chaiBuilderPages.handle(requestBody, authToken); 21 | if (has(response, "error")) { 22 | return NextResponse.json(response, { status: response.status }); 23 | } 24 | return NextResponse.json(response); 25 | } catch (error) { 26 | // * On error, throw if firebase auth error, else 500 27 | if (error instanceof Error) { 28 | return NextResponse.json( 29 | { error: "Invalid or expired token" }, 30 | { status: 401 } 31 | ); 32 | } else { 33 | return NextResponse.json( 34 | { error: "Something went wrong." }, 35 | { status: 500 } 36 | ); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/(builder)/chai/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import FullScreenLoader from "@/components/builder/loader"; 4 | import dynamic from "next/dynamic"; 5 | 6 | const ChaiBuilderPages = dynamic( 7 | () => import("@/components/builder/chaibuilder-pages"), 8 | { ssr: false, loading: () => } 9 | ); 10 | 11 | export default function Page() { 12 | return ; 13 | } 14 | -------------------------------------------------------------------------------- /app/(builder)/chai/users/route.ts: -------------------------------------------------------------------------------- 1 | import { chaiBuilderPages } from "@/chai"; 2 | import "@/data"; 3 | import "@/page-types"; 4 | import { has } from "lodash"; 5 | import { NextRequest, NextResponse } from "next/server"; 6 | 7 | export async function POST(req: NextRequest) { 8 | const requestBody = await req.json(); 9 | try { 10 | const authorization = req.headers.get("authorization"); 11 | const authToken = authorization ? authorization.split(" ")[1] : ""; 12 | const response = await chaiBuilderPages.handle(requestBody, authToken); 13 | if (has(response, "error")) { 14 | return NextResponse.json(response, { status: response.status }); 15 | } 16 | return NextResponse.json(response); 17 | } catch (error) { 18 | // * On error, throw if firebase auth error, else 500 19 | if (error instanceof Error) { 20 | return NextResponse.json( 21 | { error: "Invalid or expired token" }, 22 | { status: 401 } 23 | ); 24 | } else { 25 | return NextResponse.json( 26 | { error: "Something went wrong." }, 27 | { status: 500 } 28 | ); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/(builder)/chaibuilder.tailwind.ts: -------------------------------------------------------------------------------- 1 | import { getChaiBuilderTailwindConfig } from "@chaibuilder/pages/tailwind"; 2 | export default getChaiBuilderTailwindConfig([ 3 | "./app/(builder)/**/*.{js,ts,jsx,tsx,mdx}", 4 | "./components/builder/**/*.{js,ts,jsx,tsx,mdx}", 5 | "./node_modules/@chaibuilder/pages/dist/**/*.{js,cjs}", 6 | "./node_modules/@chaibuilder/sdk/dist/**/*.{js,cjs}", 7 | ]); 8 | -------------------------------------------------------------------------------- /app/(builder)/layout.tsx: -------------------------------------------------------------------------------- 1 | import "@/app/(builder)/styles.css"; 2 | import "@chaibuilder/pages/styles"; 3 | 4 | export const metadata = { 5 | title: "Chai Builder - Admin", 6 | description: "Start building your website with Chai Builder", 7 | }; 8 | 9 | export default function RootLayout({ 10 | children, 11 | }: { 12 | children: React.ReactNode; 13 | }) { 14 | return ( 15 | 16 | 17 | 18 | 23 | 24 | {children} 25 | 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /app/(builder)/styles.css: -------------------------------------------------------------------------------- 1 | @config "./chaibuilder.tailwind.ts"; 2 | 3 | @tailwind base; 4 | @tailwind components; 5 | @tailwind utilities; 6 | 7 | /* #TODO: Remove this once we have a better way to handle pointer-events */ 8 | body { 9 | pointer-events: all !important; 10 | } 11 | -------------------------------------------------------------------------------- /app/(public)/[[...slug]]/page.tsx: -------------------------------------------------------------------------------- 1 | import { registerBlocks } from "@/blocks"; 2 | import { registerServerBlocks } from "@/blocks/index.server"; 3 | import { 4 | chaiBuilderPages, 5 | getChaiBuilderPage, 6 | getChaiPageData, 7 | getChaiPageSeoMetadata, 8 | getChaiPageStyles, 9 | getChaiSiteSettings, 10 | NextPageProps, 11 | } from "@/chai"; 12 | import PreviewBanner from "@/components/preview-banner"; 13 | import "@/page-types"; 14 | import { ChaiBlock } from "@chaibuilder/pages/builder"; 15 | import { RenderChaiBlocks } from "@chaibuilder/pages/render"; 16 | import { ChaiPageProps } from "@chaibuilder/pages/runtime"; 17 | import { loadWebBlocks } from "@chaibuilder/pages/web-blocks"; 18 | import { get, noop } from "lodash"; 19 | import isEmpty from "lodash/isEmpty"; 20 | import { draftMode } from "next/headers"; 21 | import { notFound } from "next/navigation"; 22 | 23 | loadWebBlocks(); 24 | registerBlocks(); 25 | registerServerBlocks(); 26 | 27 | export const dynamic = "force-static"; // Remove this if you want to use ssr mode 28 | 29 | export const generateMetadata = async (props: NextPageProps) => { 30 | const nextParams = await props.params; 31 | const slug = nextParams.slug ? `/${nextParams.slug.join("/")}` : "/"; 32 | 33 | const siteSettings = await getChaiSiteSettings(); 34 | chaiBuilderPages.setFallbackLang(get(siteSettings, "fallbackLang", "")); 35 | chaiBuilderPages.setLanguageFromSlug(nextParams.slug); 36 | const chaiPage = await getChaiBuilderPage(slug); 37 | const pageProps: ChaiPageProps = { 38 | slug, 39 | pageType: chaiPage.pageType, 40 | fallbackLang: chaiBuilderPages.getFallbackLang(), 41 | }; 42 | return await getChaiPageSeoMetadata(pageProps); 43 | }; 44 | 45 | export default async function Page({ 46 | params, 47 | }: { 48 | params: Promise<{ slug: string[] }>; 49 | }) { 50 | const { isEnabled } = await draftMode(); 51 | const nextParams = await params; 52 | const slug = nextParams.slug ? `/${nextParams.slug.join("/")}` : "/"; 53 | 54 | const siteSettings = await getChaiSiteSettings(); 55 | chaiBuilderPages.setFallbackLang(get(siteSettings, "fallbackLang", "")); 56 | chaiBuilderPages.setLanguageFromSlug(nextParams.slug); 57 | 58 | const chaiPage = await getChaiBuilderPage(slug); 59 | 60 | if ("error" in chaiPage && chaiPage.error === "PAGE_NOT_FOUND") { 61 | return notFound(); 62 | } 63 | 64 | const pageProps: ChaiPageProps = { 65 | slug, 66 | pageType: chaiPage.pageType, 67 | fallbackLang: chaiBuilderPages.getFallbackLang(), 68 | }; 69 | 70 | const pageStyles = await getChaiPageStyles(chaiPage.blocks as ChaiBlock[]); 71 | const fallbackLang = chaiBuilderPages.getFallbackLang(); 72 | 73 | const pageData = await getChaiPageData( 74 | chaiPage.blocks as unknown as ChaiBlock[], 75 | chaiPage.pageType, 76 | pageProps 77 | ); 78 | 79 | return ( 80 | <> 81 |