├── .nvmrc ├── public ├── sw.js ├── og.png ├── ditto.png ├── favicon.ico └── porygon.png ├── cdk.json ├── .github └── FUNDING.yml ├── next-env.d.ts ├── src ├── components │ ├── button.tsx │ ├── spacer.tsx │ └── pokemon.tsx ├── pages │ ├── _app.tsx │ ├── [ditto].tsx │ ├── random.tsx │ ├── ssr.tsx │ ├── index.tsx │ ├── isr.tsx │ ├── pokemons │ │ └── [porygon].tsx │ └── _document.tsx └── styles.css ├── lib ├── fetch.ts └── gtag.ts ├── .gitignore ├── bin.ts ├── stack.ts ├── next.config.js ├── package.json ├── LICENSE ├── tsconfig.json ├── README.md └── types ├── Pokedex.ts └── Pokemon.ts /.nvmrc: -------------------------------------------------------------------------------- 1 | 14 -------------------------------------------------------------------------------- /public/sw.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npm run deploy" 3 | } -------------------------------------------------------------------------------- /public/og.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibrahimcesar/nextjs-ssr-isr-cdk-aws/HEAD/public/og.png -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [ibrahimcesar] 4 | -------------------------------------------------------------------------------- /public/ditto.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibrahimcesar/nextjs-ssr-isr-cdk-aws/HEAD/public/ditto.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibrahimcesar/nextjs-ssr-isr-cdk-aws/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/porygon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ibrahimcesar/nextjs-ssr-isr-cdk-aws/HEAD/public/porygon.png -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/components/button.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link" 2 | 3 | const Button = () => { 4 | return ( 5 | 6 | 7 | 14 | 15 | 16 | ) 17 | } 18 | 19 | export default Button 20 | -------------------------------------------------------------------------------- /lib/fetch.ts: -------------------------------------------------------------------------------- 1 | import axios from "redaxios"; 2 | 3 | export async function getPokemons(n?: number) { 4 | const pokemons = n || 3000; 5 | const { data: response } = await axios( 6 | `https://pokeapi.co/api/v2/pokemon?limit=${pokemons}` 7 | ); 8 | return response; 9 | } 10 | 11 | export async function getPokemonData(name: string) { 12 | const { data: response } = await axios( 13 | `https://pokeapi.co/api/v2/pokemon/${name}` 14 | ); 15 | return response; 16 | } 17 | -------------------------------------------------------------------------------- /.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 | 21 | # debug 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | 26 | # local env files 27 | .env.local 28 | .env.development.local 29 | .env.test.local 30 | .env.production.local 31 | 32 | cdk.out 33 | .vscode -------------------------------------------------------------------------------- /bin.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from "@aws-cdk/core"; 2 | import { Builder } from "@sls-next/lambda-at-edge"; 3 | import { NextStack } from "./stack"; 4 | 5 | const builder = new Builder(".", "./build", {args: ['build']}); 6 | 7 | builder 8 | .build(true) 9 | .then(() => { 10 | const app = new cdk.App(); 11 | new NextStack(app, "NextJsPokeStack", { 12 | env: { 13 | region: 'us-east-1', 14 | }, 15 | analyticsReporting: true, 16 | description: "Testing deploying NextJS Serverless Construct" 17 | }); 18 | }) 19 | .catch((e) => { 20 | console.error(e); 21 | process.exit(1); 22 | }); 23 | -------------------------------------------------------------------------------- /src/components/spacer.tsx: -------------------------------------------------------------------------------- 1 | interface Spacer { 2 | size: string 3 | axis?: 'vertical' | 'horizontal' 4 | delegated?: {} 5 | style?: {} 6 | } 7 | 8 | const Spacer = (props: Spacer) => { 9 | const width = props.axis === 'vertical' ? 1 : props.size; 10 | const height = props.axis === 'horizontal' ? 1 : props.size; 11 | return ( 12 | 23 | ); 24 | }; 25 | 26 | export default Spacer; 27 | -------------------------------------------------------------------------------- /stack.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from "@aws-cdk/core"; 2 | import { Duration } from "@aws-cdk/core"; 3 | import { NextJSLambdaEdge } from "@sls-next/cdk-construct"; 4 | import { Runtime } from "@aws-cdk/aws-lambda"; 5 | 6 | export class NextStack extends cdk.Stack { 7 | constructor(scope: cdk.Construct, id: string, props: cdk.StackProps) { 8 | super(scope, id, props); 9 | new NextJSLambdaEdge(this, "NextJsApp", { 10 | serverlessBuildOutDir: "./build", 11 | runtime: Runtime.NODEJS_12_X, 12 | memory: 1024, 13 | timeout: Duration.seconds(30), 14 | withLogging: true, 15 | name: { 16 | apiLambda: `${id}Api`, 17 | defaultLambda: `Fn${id}`, 18 | imageLambda: `${id}Image`, 19 | }, 20 | }); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/gtag.ts: -------------------------------------------------------------------------------- 1 | export const GA_TRACKING_ID = process.env.NEXT_PUBLIC_GA_ID; 2 | 3 | type GTagEvent = { 4 | action: string; 5 | category: string; 6 | label: string; 7 | value: number; 8 | }; 9 | 10 | // https://developers.google.com/analytics/devguides/collection/gtagjs/pages 11 | export const pageview = (url: URL) => { 12 | if (typeof GA_TRACKING_ID === "string") { 13 | window.gtag("config", GA_TRACKING_ID, { 14 | page_path: url, 15 | }); 16 | } 17 | }; 18 | 19 | // https://developers.google.com/analytics/devguides/collection/gtagjs/events 20 | export const event = ({ action, category, label, value }: GTagEvent) => { 21 | window.gtag("event", action, { 22 | event_category: category, 23 | event_label: label, 24 | value: value, 25 | }); 26 | }; 27 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | const terryPratchett = { 2 | key: 'X-Clacks-Overhead', 3 | value: 'GNU Terry Pratchett', 4 | } 5 | 6 | module.exports = { 7 | async headers() { 8 | return [ 9 | { 10 | source: '/', 11 | headers: [ 12 | terryPratchett 13 | ], 14 | }, 15 | { 16 | source: '/:ditto', 17 | headers: [ 18 | terryPratchett 19 | ], 20 | }, 21 | { 22 | source: '/_next\/([^\/]+\/?)*', 23 | headers: [ 24 | terryPratchett 25 | ], 26 | }, 27 | ] 28 | }, 29 | env: { 30 | baseUrl: "https://aws-ssr-pokemon.ibrahimcesar.cloud", 31 | NEXT_PUBLIC_GA_ID: "G-0H4982YVLL" 32 | }, 33 | images: { 34 | domains: ['raw.githubusercontent.com'], 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pokeserverless", 3 | "version": "2.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "cdk:deploy": "cdk deploy --profile demo", 10 | "deploy": "ts-node bin.ts" 11 | }, 12 | "dependencies": { 13 | "next": "^12.1.0", 14 | "react": "17.0.2", 15 | "react-dom": "17.0.2", 16 | "redaxios": "^0.4.1" 17 | }, 18 | "devDependencies": { 19 | "@aws-cdk/aws-lambda": "^1.119.0", 20 | "@aws-cdk/core": "^1.119.0", 21 | "@sls-next/cdk-construct": "3.2.0", 22 | "@sls-next/lambda-at-edge": "3.2.0", 23 | "@types/gtag.js": "^0.0.7", 24 | "@types/node": "^16.6.1", 25 | "@types/react": "^17.0.18", 26 | "aws-cdk": "^1.119.0", 27 | "ts-node": "^10.2.0", 28 | "typescript": "^4.3.5" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Ibrahim Cesar 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import type { AppProps, NextWebVitalsMetric } from 'next/app' 2 | import { useRouter } from 'next/router' 3 | import { useEffect } from 'react' 4 | import * as gtag from '@/lib/gtag' 5 | 6 | import '../styles.css' 7 | 8 | function PokeServerless({ Component, pageProps }: AppProps) { 9 | const router = useRouter() 10 | useEffect(() => { 11 | const handleRouteChange = (url: URL) => { 12 | gtag.pageview(url) 13 | } 14 | router.events.on('routeChangeComplete', handleRouteChange) 15 | return () => { 16 | router.events.off('routeChangeComplete', handleRouteChange) 17 | } 18 | }, [router.events]) 19 | return 20 | } 21 | 22 | export function reportWebVitals(metric: NextWebVitalsMetric) { 23 | if (window) { 24 | window.gtag('event', metric.name, { 25 | event_category: 26 | metric.label === 'web-vital' ? 'Web Vitals' : 'Next.js custom metric', 27 | value: Math.round(metric.name === 'CLS' ? metric.value * 1000 : metric.value), // values must be integers 28 | event_label: metric.id, // id unique to current page load 29 | non_interaction: true, // avoids affecting bounce rate. 30 | }) 31 | } 32 | } 33 | 34 | export default PokeServerless 35 | -------------------------------------------------------------------------------- /src/pages/[ditto].tsx: -------------------------------------------------------------------------------- 1 | import { GetServerSideProps } from 'next' 2 | import Head from "next/head" 3 | import { getPokemonData } from '@/lib/fetch' 4 | import PokemonForm from '@/components/pokemon' 5 | 6 | import type { Pokemon } from '@/types/Pokemon' 7 | 8 | interface PokemonApi { 9 | data: Pokemon 10 | } 11 | 12 | const Ditto = (props: PokemonApi) => { 13 | if (!props?.data?.name) return null; 14 | 15 | const pokeName = props.data.species.name.charAt(0).toUpperCase() + props.data.species.name.slice(1) 16 | 17 | return ( 18 |
19 | 20 | {pokeName} | PokéServeless - AWS Serverless Lambda@Edge 21 | 22 | 23 |
24 |

PokéServerless — Server Side Rendering

25 |
26 | 27 |
28 | ) 29 | 30 | } 31 | 32 | 33 | export const getServerSideProps: GetServerSideProps = async (context) => { 34 | let data; 35 | 36 | const { ditto} = context.query; 37 | 38 | if (typeof ditto === 'string') { 39 | data = await getPokemonData(ditto) 40 | } else { 41 | data = {} 42 | } 43 | 44 | return { props: { data } } 45 | } 46 | 47 | export default Ditto 48 | -------------------------------------------------------------------------------- /src/pages/random.tsx: -------------------------------------------------------------------------------- 1 | import { GetServerSideProps } from 'next' 2 | import Head from "next/head" 3 | import { getPokemons, getPokemonData } from '@/lib/fetch' 4 | import PokemonForm from '@/components/pokemon' 5 | 6 | import type { Pokemon } from '@/types/Pokemon' 7 | import type { Pokedex } from '@/types/Pokedex' 8 | 9 | interface PokemonApi { 10 | data: Pokemon 11 | } 12 | 13 | const Ditto = (props: PokemonApi) => { 14 | if (!props.data.name) return null; 15 | 16 | const pokeName = props.data.species.name.charAt(0).toUpperCase() + props.data.species.name.slice(1) 17 | 18 | return ( 19 |
20 | 21 | A wild {pokeName} appears! | PokéSSR - AWS Serverless Lambda@Edge 22 | 23 | 24 | 25 |
26 | ) 27 | } 28 | 29 | 30 | export const getServerSideProps: GetServerSideProps = async () => { 31 | const random = await getPokemons() as Pokedex 32 | 33 | const ditto = random?.results[random.results.length * Math.random() | 0]?.name 34 | 35 | let data; 36 | 37 | if (typeof ditto === 'string') { 38 | data = await getPokemonData(ditto) 39 | } else { 40 | data = {} 41 | } 42 | 43 | return { 44 | props: { 45 | data 46 | } 47 | } 48 | } 49 | 50 | export default Ditto -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "alwaysStrict": true, 4 | "downlevelIteration": true, 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "inlineSourceMap": true, 8 | "lib": [ 9 | "es2020", 10 | "DOM" 11 | ], 12 | "moduleResolution": "node", 13 | "noEmitOnError": true, 14 | "noFallthroughCasesInSwitch": true, 15 | "noImplicitAny": true, 16 | "noImplicitThis": true, 17 | "noImplicitReturns": true, 18 | "noUncheckedIndexedAccess": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "resolveJsonModule": true, 22 | "strict": true, 23 | "strictBindCallApply": true, 24 | "strictFunctionTypes": true, 25 | "strictNullChecks": true, 26 | "strictPropertyInitialization": true, 27 | "stripInternal": true, 28 | "target": "ES2020", 29 | "typeRoots": [ 30 | "node_modules/@types" 31 | ], 32 | "useDefineForClassFields": true, 33 | "allowJs": true, 34 | "skipLibCheck": true, 35 | "noEmit": true, 36 | "module": "commonjs", 37 | "isolatedModules": true, 38 | "jsx": "preserve", 39 | "baseUrl": ".", 40 | "paths": { 41 | "@/components/*": [ 42 | "./src/components/*" 43 | ], 44 | "@/lib/*": [ 45 | "./lib/*" 46 | ], 47 | "@/pages/*": [ 48 | "./src/pages/*" 49 | ], 50 | "@/types/*": [ 51 | "./types/*" 52 | ] 53 | }, 54 | }, 55 | "exclude": [ 56 | "node_modules" 57 | ], 58 | "include": [ 59 | "src", 60 | "next-env.d.ts", 61 | "**/*.ts", 62 | "**/*.tsx", 63 | "lib" 64 | ] 65 | } 66 | -------------------------------------------------------------------------------- /src/pages/ssr.tsx: -------------------------------------------------------------------------------- 1 | import { GetServerSideProps } from 'next' 2 | import Link from "next/link" 3 | import { getPokemons } from '@/lib/fetch' 4 | import Button from "@/components/button" 5 | import Spacer from "@/components/spacer" 6 | 7 | import type { Pokedex, Result } from '@/types/Pokedex' 8 | 9 | interface PokeDexApi { 10 | data: Pokedex 11 | } 12 | 13 | const PokemonsPage = (props: PokeDexApi) => { 14 | 15 | return ( 16 | <> 17 |
18 |
19 |
20 |

Pokémons

21 |

Page to test SSR deploy with Next.js.

22 |

Total of Pokémons: {props.data.count}

23 | 24 |
40 |
41 |
42 | 43 | ) 44 | } 45 | 46 | export const getServerSideProps: GetServerSideProps = async () => { 47 | const data = await getPokemons() 48 | return { 49 | props: { 50 | data 51 | } 52 | } 53 | } 54 | 55 | export default PokemonsPage 56 | -------------------------------------------------------------------------------- /src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image" 2 | import Link from "next/link" 3 | import Button from "@/components/button" 4 | import Spacer from "@/components/spacer" 5 | 6 | 7 | const PokemonsPage = () => { 8 | 9 | return ( 10 | <> 11 |
12 |
13 |
14 |

PokéServerless

15 | 16 | 40 | 41 |
44 |
45 |
46 | 47 | ) 48 | } 49 | 50 | 51 | 52 | export default PokemonsPage 53 | -------------------------------------------------------------------------------- /src/pages/isr.tsx: -------------------------------------------------------------------------------- 1 | import { GetServerSideProps } from 'next' 2 | import Link from "next/link" 3 | import { getPokemons } from '@/lib/fetch' 4 | import Button from "@/components/button" 5 | import Spacer from "@/components/spacer" 6 | 7 | import type { Pokedex, Result } from '@/types/Pokedex' 8 | 9 | interface PokeDexApi { 10 | data: Pokedex 11 | } 12 | 13 | const PokemonsPage = (props: PokeDexApi) => { 14 | 15 | return ( 16 | <> 17 |
18 |
19 |
20 |

Pokémons

21 |

Page to test ISR deploy with NextJS.

22 |

Total of Pokémons: {props.data.count}

23 | 24 |
40 |
41 |
42 | 43 | ) 44 | } 45 | 46 | export const getServerSideProps: GetServerSideProps = async () => { 47 | const data = await getPokemons() 48 | return { 49 | props: { 50 | data 51 | } 52 | } 53 | } 54 | 55 | export default PokemonsPage 56 | -------------------------------------------------------------------------------- /src/pages/pokemons/[porygon].tsx: -------------------------------------------------------------------------------- 1 | // Example of ISG 2 | import { GetStaticPaths, GetStaticProps } from 'next'; 3 | import Head from "next/head" 4 | import { useRouter } from 'next/router'; 5 | import { getPokemons, getPokemonData } from '@/lib/fetch' 6 | import PokemonForm from '@/components/pokemon' 7 | 8 | import type { Pokemon } from '@/types/Pokemon' 9 | import type { Pokedex } from '@/types/Pokedex' 10 | 11 | interface PokemonApi { 12 | data: Pokemon, 13 | date: string 14 | } 15 | 16 | const Porygon = (props: PokemonApi) => { 17 | if (!props?.data?.name) return null; 18 | const router = useRouter(); 19 | 20 | if (router.isFallback) { 21 | return
Loading......I had to fetch incrementally!!
; 22 | } 23 | 24 | const pokeName = props.data.species.name.charAt(0).toUpperCase() + props.data.species.name.slice(1) 25 | 26 | return ( 27 | <> 28 |
29 | 30 | {pokeName} | PokéServerless - AWS Serverless Lambda@Edge 31 | 32 | 33 |
34 |

PokéServerless — Incremental Static Regeneration

35 |
36 | 37 |
38 |

{`Generated at ${new Date(props.date).toLocaleString()}`}

39 | 40 | ) 41 | } 42 | 43 | export const getStaticProps: GetStaticProps = async (context) => { 44 | let data 45 | 46 | if (context.params) { 47 | 48 | data = await getPokemonData(context.params.porygon as string) 49 | } else { 50 | data = {} 51 | } 52 | 53 | return { 54 | props: { 55 | data, 56 | date: new Date().toISOString(), 57 | }, 58 | revalidate: 60 * 5 59 | } 60 | }; 61 | 62 | export const getStaticPaths: GetStaticPaths<{ porygon: string }> = async () => { 63 | 64 | const pokemons = await getPokemons(25) as Pokedex 65 | 66 | const paths = pokemons.results.map((pokemon) => { 67 | return { params: { porygon: pokemon.name.toString() } }; 68 | }); 69 | 70 | return { 71 | fallback: true, 72 | paths, 73 | }; 74 | }; 75 | 76 | export default Porygon 77 | -------------------------------------------------------------------------------- /src/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import Document, { DocumentContext, Html, Head, Main, NextScript } from 'next/document' 2 | import Link from 'next/link' 3 | import { GA_TRACKING_ID } from '@/lib/gtag' 4 | 5 | class PokeDoc extends Document { 6 | static async getInitialProps(ctx: DocumentContext) { 7 | const initialProps = await Document.getInitialProps(ctx) 8 | 9 | return initialProps 10 | } 11 | 12 | render() { 13 | return ( 14 | 15 | 16 | PokéServerless - AWS Serverless Lambda@Edge 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | {/* Global Site Tag (gtag.js) - Google Analytics */} 28 |