[
  {
    "path": ".dockerignore",
    "content": "/nextjs/.next\n/nextjs/dist\n/nextjs/node_modules"
  },
  {
    "path": "Dockerfile",
    "content": "ARG GO_VERSION=1.18\nARG NODE_VERSION=14.16.1\nARG ALPINE_VERSION=3.13.5\n\nFROM node:${NODE_VERSION}-alpine AS node-builder\nWORKDIR /app\nCOPY nextjs/package.json nextjs/yarn.lock ./\nRUN yarn install --frozen-lockfile\nCOPY nextjs/ .\nENV NEXT_TELEMETRY_DISABLED=1\nRUN yarn run export\n\nFROM golang:${GO_VERSION}-alpine AS go-builder\nWORKDIR /app\nCOPY go.mod main.go ./\nCOPY --from=node-builder /app/dist ./nextjs/dist\nRUN go build .\n\nFROM alpine:${ALPINE_VERSION}\nWORKDIR /app\nCOPY --from=go-builder /app/golang-nextjs-portable .\n\nENTRYPOINT [\"./golang-nextjs-portable\"]\n\nEXPOSE 8080"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2021 David Stotijn\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE."
  },
  {
    "path": "Makefile",
    "content": ".PHONY: build-nextjs\nbuild-nextjs:\n\tcd nextjs; \\\n\tyarn install; \\\n\tNEXT_TELEMETRY_DISABLED=1 yarn run export\n\n.PHONY: build\nbuild: build-nextjs\n\tgo build ."
  },
  {
    "path": "README.md",
    "content": "# golang-nextjs-portable\n\n**golang-nextjs-portable** is a small Go program to showcase the `embed` package\nfor bundling a static HTML export of a Next.js app.\n\n👉 Read the companion\n[article](https://v0x.nl/articles/portable-apps-go-nextjs) that walks\nthrough this project.\n\n<img src=\"https://v0x.nl/assets/articles/golang-nextjs-portable-og.png\">\n\n## Requirements\n\n- Go 1.18\n- Yarn\n\n**Note:** While the `embed` package was added in Go 1.16, the `all:` prefix of\nthe `embed` directive was added in 1.18. We use `all:` in this project because\nNext.js static exports contain files and direcories with underscore prefixes.\n\n## Installing\n\nClone or download the repository:\n\n```sh\n$ git clone git@github.com:dstotijn/golang-nextjs-portable.git\n```\n\n## Usage\n\nFrom the repository root directory, generate the static HTML export of the Next.js\napp, and build the Go binary:\n\n```sh\n$ cd nextjs\n$ yarn install\n$ yarn run export\n$ cd ..\n$ go build .\n```\n\nThen run the binary:\n\n```sh\n$ ./golang-nextjs-portable\n\n2021/04/27 14:55:38 Starting HTTP server at http://localhost:8080 ...\n```\n\n## License\n\n[MIT](/LICENSE)\n\n---\n\n© 2021 David Stotijn — [Twitter](https://twitter.com/dstotijn), [Email](mailto:dstotijn@gmail.com), [Homepage](https://v0x.nl)\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/dstotijn/golang-nextjs-portable\n\ngo 1.18\n"
  },
  {
    "path": "main.go",
    "content": "package main\n\nimport (\n\t\"embed\"\n\t\"io/fs\"\n\t\"log\"\n\t\"net/http\"\n\t\"runtime/pprof\"\n)\n\n//go:embed all:nextjs/dist\nvar nextFS embed.FS\n\nfunc main() {\n\t// Root at the `dist` folder generated by the Next.js app.\n\tdistFS, err := fs.Sub(nextFS, \"nextjs/dist\")\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\n\t// The static Next.js app will be served under `/`.\n\thttp.Handle(\"/\", http.FileServer(http.FS(distFS)))\n\t// The API will be served under `/api`.\n\thttp.HandleFunc(\"/api\", handleAPI)\n\n\t// Start HTTP server at :8080.\n\tlog.Println(\"Starting HTTP server at http://localhost:8080 ...\")\n\tlog.Fatal(http.ListenAndServe(\":8080\", nil))\n}\n\nfunc handleAPI(w http.ResponseWriter, _ *http.Request) {\n\t// Gather memory allocations profile.\n\tprofile := pprof.Lookup(\"allocs\")\n\n\t// Write profile (human readable, via debug: 1) to HTTP response.\n\terr := profile.WriteTo(w, 1)\n\tif err != nil {\n\t\tlog.Printf(\"Error: Failed to write allocs profile: %v\", err)\n\t}\n}\n"
  },
  {
    "path": "nextjs/.gitignore",
    "content": "/dist\n/.next\n/node_modules"
  },
  {
    "path": "nextjs/next-env.d.ts",
    "content": "/// <reference types=\"next\" />\n/// <reference types=\"next/types/global\" />\n"
  },
  {
    "path": "nextjs/next.config.js",
    "content": "module.exports = {\n  async rewrites() {\n    // When running Next.js via Node.js (e.g. `dev` mode), proxy API requests\n    // to the Go server.\n    return [\n      {\n        source: \"/api\",\n        destination: \"http://localhost:8080/api\",\n      },\n    ];\n  },\n  future: {\n    webpack5: true,\n  },\n  trailingSlash: true,\n};\n"
  },
  {
    "path": "nextjs/package.json",
    "content": "{\n  \"name\": \"golang-nextjs-portable\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"next dev\",\n    \"build\": \"next build\",\n    \"start\": \"next start\",\n    \"export\": \"rm -rf .next && next build && next export -o dist\"\n  },\n  \"dependencies\": {\n    \"next\": \"10.1.3\",\n    \"react\": \"17.0.2\",\n    \"react-dom\": \"17.0.2\",\n    \"swr\": \"0.5.5\"\n  },\n  \"devDependencies\": {\n    \"@types/react\": \"17.0.3\",\n    \"typescript\": \"4.2.4\"\n  }\n}\n"
  },
  {
    "path": "nextjs/pages/foo/bar.tsx",
    "content": "import Link from \"next/link\";\n\nfunction Bar(): JSX.Element {\n  return (\n    <div>\n      <h1>Bar</h1>\n      <p>\n        This is <code>pages/foo/bar.tsx</code>.\n      </p>\n      <p>\n        Check out <Link href=\"/\">the homepage</Link>.\n      </p>\n    </div>\n  );\n}\n\nexport default Bar;\n"
  },
  {
    "path": "nextjs/pages/foo/index.tsx",
    "content": "import Link from \"next/link\";\n\nfunction Foo(): JSX.Element {\n  return (\n    <div>\n      <h1>Foo</h1>\n      <p>\n        This is <code>pages/foo/index.tsx</code>.\n      </p>\n      <p>\n        Check out <Link href=\"/foo/bar\">bar</Link>.\n      </p>\n    </div>\n  );\n}\n\nexport default Foo;\n"
  },
  {
    "path": "nextjs/pages/index.tsx",
    "content": "import Link from \"next/link\";\nimport useSWR from \"swr\";\n\nasync function fetcher(url: string) {\n  const resp = await fetch(url);\n  return resp.text();\n}\n\nfunction Index(): JSX.Element {\n  const { data, error } = useSWR(\"/api\", fetcher, { refreshInterval: 1000 });\n\n  return (\n    <div>\n      <h1>Hello, world!</h1>\n      <p>\n        This is <code>pages/index.tsx</code>.\n      </p>\n      <p>\n        Check out <Link href=\"/foo\">foo</Link>.\n      </p>\n\n      <h2>Memory allocation stats from Go server</h2>\n      {error && (\n        <p>\n          Error fetching profile: <strong>{error}</strong>\n        </p>\n      )}\n      {!error && !data && <p>Loading ...</p>}\n      {!error && data && <pre>{data}</pre>}\n    </div>\n  );\n}\n\nexport default Index;\n"
  },
  {
    "path": "nextjs/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"es5\",\n    \"lib\": [\n      \"dom\",\n      \"dom.iterable\",\n      \"esnext\"\n    ],\n    \"allowJs\": true,\n    \"skipLibCheck\": true,\n    \"strict\": false,\n    \"forceConsistentCasingInFileNames\": true,\n    \"noEmit\": true,\n    \"esModuleInterop\": true,\n    \"module\": \"esnext\",\n    \"moduleResolution\": \"node\",\n    \"resolveJsonModule\": true,\n    \"isolatedModules\": true,\n    \"jsx\": \"preserve\"\n  },\n  \"include\": [\n    \"next-env.d.ts\",\n    \"**/*.ts\",\n    \"**/*.tsx\"\n  ],\n  \"exclude\": [\n    \"node_modules\"\n  ]\n}\n"
  }
]