├── .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 |
--------------------------------------------------------------------------------