├── .prettierignore ├── .env.template ├── .env.development ├── .env.production ├── assets └── index.css ├── README.md ├── config └── builder.ts ├── next-env.d.ts ├── components └── Link │ └── Link.tsx ├── postcss.config.js ├── .gitignore ├── pages ├── api │ └── attributes.ts ├── _app.tsx └── [[...path]].tsx ├── next.config.js ├── middleware.ts ├── tailwind.config.js ├── package.json └── tsconfig.json /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .next 3 | public -------------------------------------------------------------------------------- /.env.template: -------------------------------------------------------------------------------- 1 | BUILDER_PUBLIC_KEY= 2 | BUILDER_PRIVATE_KEY= -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | BUILDER_PUBLIC_KEY=6d585b9923974f03815f710c4ec541a3 2 | BUILDER_PRIVATE_KEY= 3 | -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | BUILDER_PUBLIC_KEY=6d585b9923974f03815f710c4ec541a3 2 | BUILDER_PRIVATE_KEY= 3 | -------------------------------------------------------------------------------- /assets/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | .grayscale { 6 | filter: grayscale(1); 7 | } 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # This code has moved! 2 | 3 | Please use [this repo](https://github.com/BuilderIO/nextjs-edge-personalization-ab-testing) for personalizing at the edge with Vercel and Builder.io 4 | -------------------------------------------------------------------------------- /config/builder.ts: -------------------------------------------------------------------------------- 1 | if (!process.env.BUILDER_PUBLIC_KEY) { 2 | throw new Error('Missing env varialbe BUILDER_PUBLIC_KEY') 3 | } 4 | 5 | export default { 6 | apiKey: process.env.BUILDER_PUBLIC_KEY, 7 | } 8 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /components/Link/Link.tsx: -------------------------------------------------------------------------------- 1 | import NextLink from 'next/link' 2 | 3 | export const Link: React.FC> = ({ 4 | href, 5 | children, 6 | ...props 7 | }) => { 8 | return ( 9 | 10 | {children} 11 | 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | 'tailwindcss', 4 | 'postcss-flexbugs-fixes', 5 | [ 6 | 'postcss-preset-env', 7 | { 8 | autoprefixer: { 9 | flexbox: 'no-2009', 10 | }, 11 | stage: 3, 12 | features: { 13 | 'custom-properties': false, 14 | }, 15 | }, 16 | ], 17 | ], 18 | } 19 | -------------------------------------------------------------------------------- /.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 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | .next/ 13 | out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | 36 | # dev 37 | framework 38 | -------------------------------------------------------------------------------- /pages/api/attributes.ts: -------------------------------------------------------------------------------- 1 | import { getAttributes } from '@builder.io/personalization-context-menu' 2 | import { NextApiRequest, NextApiResponse } from 'next' 3 | 4 | if (!process.env.BUILDER_PRIVATE_KEY) { 5 | throw new Error('No BUILDER_PRIVATE_KEY defined') 6 | } 7 | 8 | /** 9 | * API to get the custom targeting attributes from Builder, only needed for the context menu to show a configurator and allow toggling of attributes 10 | */ 11 | export default async (req: NextApiRequest, res: NextApiResponse) => { 12 | const attributes = await getAttributes(process.env.BUILDER_PRIVATE_KEY!) 13 | res.send(attributes) 14 | } 15 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = { 3 | images: { 4 | domains: ['cdn.builder.io'], 5 | }, 6 | async headers() { 7 | return [ 8 | { 9 | source: '/:path*', 10 | headers: [ 11 | // this will allow site to be framed under builder.io for wysiwyg editing 12 | { 13 | key: 'Content-Security-Policy', 14 | value: 'frame-ancestors https://*.builder.io https://builder.io', 15 | }, 16 | ], 17 | }, 18 | ] 19 | }, 20 | env: { 21 | // expose env to the browser 22 | BUILDER_PUBLIC_KEY: process.env.BUILDER_PUBLIC_KEY, 23 | }, 24 | } 25 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import type { AppProps } from 'next/app' 2 | import { builder } from '@builder.io/react' 3 | import builderConfig from '../config/builder' 4 | import { ContextMenu } from '@builder.io/personalization-context-menu' 5 | import '../assets/index.css' 6 | // only needed for context menu styling 7 | import '@szhsin/react-menu/dist/index.css' 8 | import '@szhsin/react-menu/dist/transitions/slide.css' 9 | import '@builder.io/widgets/dist/lib/builder-widgets-async' 10 | 11 | builder.init(builderConfig.apiKey) 12 | 13 | export default function MyApp({ Component, pageProps }: AppProps) { 14 | return ( 15 | <> 16 | 17 | 18 | 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /middleware.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse, NextRequest } from 'next/server' 2 | import { 3 | getPersonlizedURL 4 | } from '@builder.io/personalization-utils/next' 5 | 6 | const regex = /^(.+\.)/ 7 | 8 | const shouldRewrite = (pathname: string) => { 9 | // only in netlify needed 10 | if (pathname.startsWith('/builder')) { 11 | return false; 12 | } 13 | // do not rewrite api requests 14 | if (pathname.startsWith('/api')) { 15 | return false; 16 | } 17 | // don't rewrite for asset requests (has a file extension) 18 | return !regex.test(pathname); 19 | } 20 | 21 | export default function middleware(request: NextRequest) { 22 | const url = request.nextUrl 23 | if (shouldRewrite(url.pathname)) { 24 | const personalizedURL = getPersonlizedURL(request) 25 | return NextResponse.rewrite(personalizedURL) 26 | } 27 | return NextResponse.next(); 28 | } 29 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | theme: { 3 | extend: { 4 | fontFamily: { 5 | sans: 6 | '-apple-system, "Helvetica Neue", "Segoe UI", Roboto, Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"', 7 | }, 8 | colors: { 9 | 'accent-1': '#FAFAFA', 10 | 'accent-2': '#EAEAEA', 11 | 'accent-7': '#333', 12 | success: '#0070f3', 13 | cyan: '#79FFE1', 14 | }, 15 | spacing: { 16 | 28: '7rem', 17 | }, 18 | letterSpacing: { 19 | tighter: '-.04em', 20 | }, 21 | lineHeight: { 22 | tight: 1.2, 23 | }, 24 | fontSize: { 25 | '5xl': '2.5rem', 26 | '6xl': '2.75rem', 27 | '7xl': '4.5rem', 28 | '8xl': '6.25rem', 29 | }, 30 | boxShadow: { 31 | small: '0 5px 10px rgba(0, 0, 0, 0.12)', 32 | medium: '0 8px 30px rgba(0, 0, 0, 0.12)', 33 | }, 34 | }, 35 | }, 36 | } 37 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "dev": "next dev", 4 | "build": "next build", 5 | "start": "next start", 6 | "analyze": "BUNDLE_ANALYZE=both yarn build", 7 | "find:unused": "next-unused", 8 | "prettier": "prettier" 9 | }, 10 | "dependencies": { 11 | "@builder.io/admin-sdk": "^0.1.0", 12 | "@builder.io/personalization-context-menu": "^0.0.3", 13 | "@builder.io/personalization-utils": "^1.1.1", 14 | "@builder.io/react": "2.0.2-7", 15 | "@builder.io/widgets": "1.2.22-7", 16 | "next": "^12.2.5", 17 | "next-seo": "^5.4.0", 18 | "react": "18.1.0", 19 | "react-dom": "18.1.0" 20 | }, 21 | "devDependencies": { 22 | "@netlify/build": "^26.5.3", 23 | "@netlify/functions": "^1.0.0", 24 | "@netlify/plugin-nextjs": "^4.7.0", 25 | "@types/react": "^18.0.15", 26 | "autoprefixer": "^10.4.7", 27 | "postcss": "^8.4.14", 28 | "postcss-flexbugs-fixes": "^5.0.2", 29 | "postcss-preset-env": "^7.7.2", 30 | "tailwindcss": "^3.1.6" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "target": "esnext", 5 | "lib": [ 6 | "dom", 7 | "dom.iterable", 8 | "esnext" 9 | ], 10 | "allowJs": true, 11 | "skipLibCheck": true, 12 | "strict": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "noEmit": true, 15 | "esModuleInterop": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "jsx": "preserve", 21 | "paths": { 22 | "@lib/*": [ 23 | "lib/*" 24 | ], 25 | "@assets/*": [ 26 | "assets/*" 27 | ], 28 | "@config/*": [ 29 | "config/*" 30 | ], 31 | "@components/*": [ 32 | "components/*" 33 | ], 34 | "@utils/*": [ 35 | "utils/*" 36 | ] 37 | }, 38 | "incremental": true 39 | }, 40 | "include": [ 41 | "next-env.d.ts", 42 | "**/*.ts", 43 | "**/*.tsx", 44 | "**/*.js" 45 | ], 46 | "exclude": [ 47 | "node_modules" 48 | ] 49 | } 50 | -------------------------------------------------------------------------------- /pages/[[...path]].tsx: -------------------------------------------------------------------------------- 1 | import { NextSeo } from 'next-seo' 2 | import { useRouter } from 'next/router' 3 | import { 4 | BuilderComponent, 5 | Builder, 6 | builder, 7 | useIsPreviewing 8 | } from '@builder.io/react' 9 | import builderConfig from '../config/builder' 10 | import DefaultErrorPage from 'next/error' 11 | import Head from 'next/head' 12 | import type { GetStaticPropsContext, InferGetStaticPropsType } from 'next' 13 | import { parsePersonalizedURL } from '@builder.io/personalization-utils/next' 14 | import { useEffect } from 'react' 15 | import '@builder.io/widgets/dist/lib/builder-widgets-async' 16 | 17 | builder.init(builderConfig.apiKey) 18 | 19 | export async function getStaticProps({ params } : GetStaticPropsContext<{ path: string[] }>) { 20 | const { attributes } = parsePersonalizedURL(params?.path!); 21 | const page = 22 | (await builder 23 | .get('page', { 24 | apiKey: builderConfig.apiKey, 25 | userAttributes: attributes!, 26 | cachebust: true 27 | }) 28 | .promise()) || null 29 | 30 | return { 31 | props: { 32 | page, 33 | attributes: attributes, 34 | locale: attributes!.locale || 'en-US' 35 | }, 36 | // Next.js will attempt to re-generate the page: 37 | // - When a request comes in 38 | // - At most once every 1 seconds 39 | revalidate: 1 40 | } 41 | } 42 | 43 | export async function getStaticPaths() { 44 | return { 45 | paths: [], 46 | fallback: true 47 | } 48 | } 49 | 50 | export default function Path({ page, attributes, locale }: InferGetStaticPropsType) { 51 | const router = useRouter() 52 | const isPreviewingInBuilder = useIsPreviewing() 53 | 54 | useEffect(() => { 55 | builder.setUserAttributes(attributes!) 56 | }, []) 57 | 58 | if (router.isFallback) { 59 | return

Loading...

60 | } 61 | 62 | const { title, description, image } = page?.data || {} 63 | return ( 64 | <> 65 | 66 | {!page && } 67 | 68 | 69 | 86 | {(isPreviewingInBuilder || page) ? ( 87 | 93 | 94 | ) : ( 95 | 96 | )} 97 | 98 | ) 99 | } 100 | --------------------------------------------------------------------------------