├── .env.template
├── .eslintrc.json
├── .prettierrc
├── .env.development
├── .env.production
├── renovate.json
├── .github
├── .kodiak.toml
└── workflows
│ └── labeller.yml
├── config
└── builder.js
├── netlify.toml
├── styles
├── globals.css
└── Home.module.css
├── assets
└── index.css
├── .gitignore
├── pages
├── api
│ └── attributes.js
├── _app.js
├── _middleware.js
└── builder
│ └── [hash].js
├── package.json
├── next.config.js
└── README.md
/.env.template:
--------------------------------------------------------------------------------
1 | BUILDER_PUBLIC_KEY=
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals"
3 | }
4 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "semi": false
4 | }
5 |
--------------------------------------------------------------------------------
/.env.development:
--------------------------------------------------------------------------------
1 | BUILDER_PUBLIC_KEY=6d585b9923974f03815f710c4ec541a3
2 | BUILDER_PRIVATE_KEY=bpk-b1a3ae141c1b449d99a3d57399f00db8
3 |
--------------------------------------------------------------------------------
/.env.production:
--------------------------------------------------------------------------------
1 | BUILDER_PUBLIC_KEY=6d585b9923974f03815f710c4ec541a3
2 | BUILDER_PRIVATE_KEY=bpk-b1a3ae141c1b449d99a3d57399f00db8
3 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3 | "extends": ["local>netlify/renovate-config"]
4 | }
5 |
--------------------------------------------------------------------------------
/.github/.kodiak.toml:
--------------------------------------------------------------------------------
1 | version = 1
2 |
3 | [merge.automerge_dependencies]
4 | versions = ["minor", "patch"]
5 | usernames = ["renovate"]
6 |
7 | [approve]
8 | auto_approve_usernames = ["renovate"]
9 |
--------------------------------------------------------------------------------
/config/builder.js:
--------------------------------------------------------------------------------
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 |
--------------------------------------------------------------------------------
/netlify.toml:
--------------------------------------------------------------------------------
1 | [build]
2 | command = "next build"
3 | base = "."
4 | publish = ".next"
5 |
6 | [build.environment]
7 | NEXT_USE_NETLIFY_EDGE = "true"
8 |
9 | [[plugins]]
10 | package = "@netlify/plugin-nextjs"
11 |
--------------------------------------------------------------------------------
/styles/globals.css:
--------------------------------------------------------------------------------
1 | html,
2 | body {
3 | padding: 0;
4 | margin: 0;
5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
6 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
7 | }
8 |
9 | a {
10 | color: inherit;
11 | text-decoration: none;
12 | }
13 |
14 | * {
15 | box-sizing: border-box;
16 | }
17 |
--------------------------------------------------------------------------------
/assets/index.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | .grayscale {
6 | filter: grayscale(1);
7 | }
8 |
9 | html,
10 | body {
11 | padding: 0;
12 | margin: 0;
13 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
14 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
15 | }
16 |
17 | a {
18 | color: inherit;
19 | text-decoration: none;
20 | }
21 |
22 | * {
23 | box-sizing: border-box;
24 | }
25 |
--------------------------------------------------------------------------------
/.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 | .pnpm-debug.log*
27 |
28 | # local env files
29 | .env*.local
30 |
31 | # vercel
32 | .vercel
33 |
34 | # netlify
35 | .netlify
36 |
--------------------------------------------------------------------------------
/pages/api/attributes.js:
--------------------------------------------------------------------------------
1 | import { getAttributes } from '@builder.io/personalization-context-menu'
2 |
3 | if (!process.env.BUILDER_PRIVATE_KEY) {
4 | throw new Error('No BUILDER_PRIVATE_KEY defined')
5 | }
6 |
7 | /**
8 | * API to get the custom targeting attributes from Builder, only needed for the context menu to show a configurator and allow toggling of attributes
9 | */
10 | export default async (req, res) => {
11 | const attributes = await getAttributes(process.env.BUILDER_PRIVATE_KEY)
12 | res.send(attributes)
13 | }
14 |
--------------------------------------------------------------------------------
/pages/_app.js:
--------------------------------------------------------------------------------
1 | import { builder } from '@builder.io/react'
2 | import builderConfig from '../config/builder'
3 | import '../assets/index.css'
4 | import { ContextMenu } from '@builder.io/personalization-context-menu'
5 | // only needed for context menu styling
6 | import '@szhsin/react-menu/dist/index.css'
7 | import '@szhsin/react-menu/dist/transitions/slide.css'
8 | import '@builder.io/widgets'
9 |
10 | builder.init(builderConfig.apiKey)
11 |
12 | export default function MyApp({ Component, pageProps }) {
13 | return (
14 | <>
15 |
16 |
17 | >
18 | )
19 | }
20 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "scripts": {
3 | "dev": "yarn next dev",
4 | "build": "yarn next build",
5 | "start": "yarn next start"
6 | },
7 | "dependencies": {
8 | "@builder.io/admin-sdk": "^0.1.0",
9 | "@builder.io/personalization-context-menu": "^0.0.2",
10 | "@builder.io/personalization-utils": "^0.2.1-3",
11 | "@builder.io/react": "^2.0.0",
12 | "@builder.io/widgets": "^1.2.21",
13 | "next": "12.1.5",
14 | "next-seo": "^5.4.0",
15 | "react": "18.1.0",
16 | "react-dom": "18.1.0"
17 | },
18 | "devDependencies": {
19 | "@netlify/build": "^26.5.3",
20 | "@netlify/functions": "^1.0.0",
21 | "@netlify/plugin-nextjs": "^4.7.0"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | reactStrictMode: true,
4 | images: {
5 | domains: ['cdn.builder.io']
6 | },
7 | async headers() {
8 | return [
9 | {
10 | source: '/:path*',
11 | headers: [
12 | // this will allow site to be framed under builder.io for wysiwyg editing
13 | {
14 | key: 'Content-Security-Policy',
15 | value: 'frame-ancestors https://*.builder.io https://builder.io'
16 | }
17 | ]
18 | }
19 | ]
20 | },
21 | env: {
22 | // expose env to the browser
23 | BUILDER_PUBLIC_KEY: process.env.BUILDER_PUBLIC_KEY
24 | }
25 | }
26 |
27 | module.exports = nextConfig
28 |
--------------------------------------------------------------------------------
/.github/workflows/labeller.yml:
--------------------------------------------------------------------------------
1 | name: Label PR
2 | on:
3 | pull_request:
4 | types: [opened, edited]
5 |
6 | jobs:
7 | label-pr:
8 | if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false
9 | runs-on: ubuntu-latest
10 | strategy:
11 | matrix:
12 | pr:
13 | [
14 | { prefix: 'fix', type: 'bug' },
15 | { prefix: 'chore', type: 'chore' },
16 | { prefix: 'test', type: 'chore' },
17 | { prefix: 'ci', type: 'chore' },
18 | { prefix: 'feat', type: 'feature' },
19 | { prefix: 'security', type: 'security' },
20 | ]
21 | steps:
22 | - uses: netlify/pr-labeler-action@v1.0.0
23 | if: startsWith(github.event.pull_request.title, matrix.pr.prefix)
24 | with:
25 | token: '${{ secrets.GITHUB_TOKEN }}'
26 | label: 'type: ${{ matrix.pr.type }}'
27 |
--------------------------------------------------------------------------------
/pages/_middleware.js:
--------------------------------------------------------------------------------
1 | import { NextResponse } from 'next/server'
2 | import {
3 | PersonalizedURL,
4 | getUserAttributes
5 | } from '@builder.io/personalization-utils'
6 |
7 | const personalizedPrefix = '/builder'
8 | const excludededPrefixes = ['/favicon', '/api', '/sw.js', personalizedPrefix]
9 |
10 | export default function middleware(request) {
11 | const url = request.nextUrl
12 | let response = NextResponse.next()
13 | const usePath = url.pathname
14 | if (!excludededPrefixes.find(path => usePath.startsWith(path))) {
15 | const query = Object.fromEntries(url.searchParams)
16 | const personlizedURL = new PersonalizedURL({
17 | pathname: usePath,
18 | attributes: {
19 | ...getUserAttributes({ ...request.cookies, ...query }),
20 | domain: request.headers.get('Host') || '',
21 | country: request.geo?.country || '',
22 | }
23 | })
24 | url.pathname = personlizedURL.rewritePath()
25 | return NextResponse.rewrite(url)
26 | }
27 | return response
28 | }
29 |
--------------------------------------------------------------------------------
/styles/Home.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | padding: 0 2rem;
3 | }
4 |
5 | .main {
6 | min-height: 100vh;
7 | padding: 4rem 0;
8 | flex: 1;
9 | flex-direction: column;
10 | justify-content: center;
11 | align-items: center;
12 | }
13 |
14 | .footer {
15 | display: flex;
16 | flex: 1;
17 | padding: 2rem 0;
18 | border-top: 1px solid #eaeaea;
19 | justify-content: center;
20 | align-items: center;
21 | }
22 |
23 | .footer a {
24 | display: flex;
25 | justify-content: center;
26 | align-items: center;
27 | flex-grow: 1;
28 | }
29 |
30 | .title a {
31 | color: #0070f3;
32 | text-decoration: none;
33 | }
34 |
35 | .title a:hover,
36 | .title a:focus,
37 | .title a:active {
38 | text-decoration: underline;
39 | }
40 |
41 | .title {
42 | margin: 0;
43 | line-height: 1.15;
44 | font-size: 4rem;
45 | }
46 |
47 | .title,
48 | .description {
49 | text-align: center;
50 | }
51 |
52 | .description {
53 | margin: 4rem 0;
54 | line-height: 1.5;
55 | font-size: 1.5rem;
56 | }
57 |
58 | .welcome {
59 | margin-left: 0.8em;
60 | }
61 |
62 | .flag {
63 | vertical-align: middle;
64 | }
65 |
66 | .code {
67 | background: #fafafa;
68 | border-radius: 5px;
69 | padding: 0.75rem;
70 | font-size: 1.1rem;
71 | font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
72 | Bitstream Vera Sans Mono, Courier New, monospace;
73 | }
74 |
75 | .grid {
76 | display: flex;
77 | align-items: center;
78 | justify-content: center;
79 | flex-wrap: wrap;
80 | max-width: 800px;
81 | }
82 |
83 | .card {
84 | margin: 1rem;
85 | padding: 1.5rem;
86 | text-align: left;
87 | color: inherit;
88 | text-decoration: none;
89 | border: 1px solid #eaeaea;
90 | border-radius: 10px;
91 | transition: color 0.15s ease, border-color 0.15s ease;
92 | max-width: 300px;
93 | }
94 |
95 | .card:hover,
96 | .card:focus,
97 | .card:active {
98 | color: #0070f3;
99 | border-color: #0070f3;
100 | }
101 |
102 | .card h2 {
103 | margin: 0 0 1rem 0;
104 | font-size: 1.5rem;
105 | }
106 |
107 | .card p {
108 | margin: 0;
109 | font-size: 1.25rem;
110 | line-height: 1.5;
111 | }
112 |
113 | .logo {
114 | height: 1em;
115 | margin-left: 0.5rem;
116 | }
117 |
118 | @media (max-width: 600px) {
119 | .grid {
120 | width: 100%;
121 | flex-direction: column;
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/pages/builder/[hash].js:
--------------------------------------------------------------------------------
1 | import { NextSeo } from 'next-seo'
2 | import { useRouter } from 'next/router'
3 | import {
4 | BuilderComponent,
5 | Builder,
6 | builder,
7 | isPreviewing
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 { PersonalizedURL } from '@builder.io/personalization-utils'
13 | import { useEffect } from 'react'
14 | import '@builder.io/widgets'
15 |
16 | builder.init(builderConfig.apiKey)
17 |
18 | export async function getStaticProps({ params }) {
19 | const personlizedURL = PersonalizedURL.fromHash(params.hash)
20 | const attributes = personlizedURL.attributes
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 }) {
51 | const router = useRouter()
52 | const isPreviewingInBuilder = isPreviewing()
53 | const show404 = !page && !isPreviewingInBuilder
54 |
55 | useEffect(() => {
56 | builder.setUserAttributes(attributes)
57 | }, [])
58 |
59 | if (router.isFallback) {
60 | return
Loading...
61 | }
62 |
63 | const { title, description, image } = page?.data || {}
64 | return (
65 | <>
66 |
67 | {!page && }
68 |
69 |
70 |
87 | {show404 ? (
88 |
89 | ) : (
90 |
96 | )}
97 | >
98 | )
99 | }
100 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Next.js 12 Edge Middleware Demo with Builder.io
2 |
3 |
4 |
5 | Try the demo live here: https://builder-edge-personalized.netlify.app/
6 |
7 | ## Introduction
8 |
9 | This is a demo of building personalized landing pages with Builder and Next.js middleware running on Netlify Edge Functions.
10 | All pages are generated with SSG/ISR with a middleware function that rewrites the generic URL to a personalized one.
11 |
12 |
13 |
14 |
15 |
16 |
17 | ## Prerequisites
18 |
19 | Before using this example, make sure you have the following:
20 |
21 | * A [Builder.io](builder.io) account. Check out the [plans](https://www.builder.io/m/pricing), which range from our free tier to custom.
22 | * Save private key / public key from your space [settings page](https://builder.io/account/space) and create copy both keys to `.env.development` and `.env.production` for the next step.
23 |
24 |
25 | ## Quick Start
26 |
27 | ```shell
28 | git clone git@github.com:BuilderIO/netlify-next-edge-middleware.git
29 | cd netlify-next-edge-middleware
30 | ```
31 | Then update env files with the keys from last step
32 | ```
33 | yarn
34 | yarn dev
35 | ```
36 |
37 |
38 | ## Why Builder.io?
39 |
40 |
41 |
42 |
43 |
44 | Drag and drop page builder and CMS for React, Vue, Angular, and more
45 |
46 |
47 | Integrate with any site or app. Drag and drop with the components already in your codebase. High speed, full control, no compromises
48 |
49 |
50 |
51 |
52 | Hardcoding layouts for frequently changing content bottlenecks your team and makes releases messy
53 |
54 | Using an API-driven UI allows you to:
55 |
56 | - Decouple page updates from deploys
57 | - Schedule, a/b test, and personalize via APIs
58 | - Reduce code + increase composability
59 |
60 |
61 |
62 |
63 | ## Why edge middleware for landing pages?
64 | Edge middlewares allow us to factor in many details about the request than just the URLs which is usually a limitation of statically generating pages, you'll see in this example we're using cookies, and geolocation information to fetch the correct content and render it statically.
65 |
66 |
67 |
68 |
69 | Additional framework support:
70 |
71 | - [Gatsby](https://github.com/BuilderIO/builder/tree/master/examples/gatsby)
72 | - [Next.js](https://github.com/BuilderIO/builder/tree/master/examples/next-js)
73 | - [Angular](https://github.com/BuilderIO/builder/tree/master/packages/angular)
74 | - [HTML API (for any framework)](https://builder.io/c/docs/html-api)
75 |
76 | As well as some handy power features like:
77 |
78 | - [Symbols](https://builder.io/c/docs/guides/symbols)
79 | - [Dynamic data fetching and binding](https://builder.io/c/docs/guides/advanced-data)
80 | - [State handling](https://builder.io/c/docs/guides/state-and-actions)
81 | - [Content API](https://builder.io/c/docs/query-api)
82 | - [GraphQL API](https://builder.io/c/docs/graphql-api)
83 | - [Webhooks](https://builder.io/c/docs/webhooks)
84 | - [Targeting and scheduling content](https://builder.io/c/docs/guides/targeting-and-scheduling)
85 | - [Extending Builder.io with plugins](https://github.com/BuilderIO/builder/tree/master/plugins)
86 |
87 | ## Join the community!
88 |
89 | Questions? Requests? Feedback? Chat with us in our [official forum](https://forum.builder.io)!
90 |
--------------------------------------------------------------------------------