{"id":19982476,"url":"https://github.com/netlify/compose-workshop-2023","last_synced_at":"2025-05-04T05:32:26.137Z","repository":{"id":200022020,"uuid":"698000821","full_name":"netlify/compose-workshop-2023","owner":"netlify","description":"Netlify Compose 2023 Workshop: Using core Netlify features to create your first composable project","archived":false,"fork":false,"pushed_at":"2023-12-08T20:22:22.000Z","size":4931,"stargazers_count":4,"open_issues_count":1,"forks_count":21,"subscribers_count":7,"default_branch":"main","last_synced_at":"2025-05-01T00:30:45.364Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/netlify.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null}},"created_at":"2023-09-28T23:29:18.000Z","updated_at":"2023-12-11T13:04:28.000Z","dependencies_parsed_at":"2023-10-15T09:22:33.513Z","dependency_job_id":"d2dad521-3cdb-45c3-af9c-b8955ae97fd6","html_url":"https://github.com/netlify/compose-workshop-2023","commit_stats":null,"previous_names":["netlify/workshop_first-composable-project"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/netlify%2Fcompose-workshop-2023","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/netlify%2Fcompose-workshop-2023/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/netlify%2Fcompose-workshop-2023/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/netlify%2Fcompose-workshop-2023/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/netlify","download_url":"https://codeload.github.com/netlify/compose-workshop-2023/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":252293082,"owners_count":21724960,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-13T04:11:44.082Z","updated_at":"2025-05-04T05:32:25.572Z","avatar_url":"https://github.com/netlify.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Netlify Compose 2023 Workshop\n\nWelcome to Compose! In this workshop, you will learn how to create your first composable website with Netlify.\n\n![](./public/images/og.jpg)\n\n## What are we going to build?\n\nIn two and a half hours, we are going to build a Halloween-themed e-commerce bookstore. The frontend stack will be composed of React, TypeScript, Vite, and Tailwind, and served from Netlify's global high-performance edge network. It will fetch data sources from Contentstack and Storyblok  using [Netlify Connect](https://www.netlify.com/products/connect/). We will build a custom data connector using the [Netlify SDK](https://sdk.netlify.com/connectors/overview/) that reads data from an Amazon S3 bucket. Finally, the site will reach out to third-party services including Stripe and OpenAI using Netlify Functions. The future is composable! \n\nAn example of the finished product is available here: [https://compose-workshop-2023.netlify.app](https://compose-workshop-2023.netlify.app)\n\n## What are we going to learn?\n\nIn this workshop, you will learn how to:\n\n- Create your first site on Netlify\n- Trigger builds with Git and embrace a CI/CD workflow\n- Create Deploy Previews and collaborate using the Netlify Drawer\n- Manage environment variables securely in the Netlify CLI and Netlify UI\n- Stream API responses from OpenAI using Netlify Functions\n- Personalize user experiences with Netlify Edge Functions\n- Persist cache from API responses using fine-grain cache control\n- Fetch content from Contentstack and Storyblok using Netlify Connect\n- Build a custom connector using the Netlify SDK to pull data from Amazon S3\n\n\n## Let's get started\n\n\u003cdetails\u003e\u003csummary\u003eStep 0. Initial setup\u003c/summary\u003e\n\ni. [Fork this repo](https://github.com/netlify/compose-workshop-2023/fork) into your personal account, and uncheck the `Copy the main branch only` checkbox, so that you copy all branches and not just `main`\n\n![Copy all branches](media/copy-all-branches.png)\n\nii. Install the [Netlify GitHub app](https://github.com/apps/netlify/installations/select_target) on your org or repo if you have not done so already\n\niii. Clone your fork, and checkout the `start-here` branch\n\n```bash\ngit clone \u003cFORK_URL\u003e\ngit checkout start-here\n```\n\niv. Install dependencies locally\n\n```bash\nnpm i\n```\n\nv. Ensure you have the latest version of `netlify-cli` installed globally\n\n```bash\nnpm i netlify-cli -g\nnetlify --version\n```\n\n💡 Learn more about the [Netlify CLI](https://docs.netlify.com/cli/get-started/) in our docs.\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\u003csummary\u003eStep 1. Create a new site and run local dev server \u003c/summary\u003e\n\ni. [Create a new site](https://app.netlify.com/start) by going to **Team overview \u003e Add new site \u003e Import an existing project**. Click the Deploy with GitHub button. After you authenticate, search for your fork. For the `Branch to deploy` field, be sure to select `start-here` as your default production branch. You can keep the auto-populated values for all other fields. Click the Deploy button to deploy your site. \n\n![Deploy your project](media/branch-to-deploy.png)\n\nii. Rename site to something more memorable in **Site configuration \u003e Site details \u003e Change site name**.\n\n![Change site name](media/change-site-name.png)\n\niii. Log in to the CLI, link your repo to your site, and start local dev server\n\n```bash\nnetlify login\nnetlify link\nnetlify dev\n```\n\n💡 Learn more about [getting started](https://docs.netlify.com/get-started/) in our docs.\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\u003csummary\u003eStep 2. Function primitives\u003c/summary\u003e\n\nOur site is looking a little bare. Let's add some content! First we'll fetch a list of books that we happen to have as a [CSV file saved inside the /public directory](https://github.com/netlify/compose-workshop-2023/blob/main/public/books.csv).\n\ni. Add the `Bookshelf` component to `src/pages/index.tsx`\n\n```diff\n+import Bookshelf from '~/components/Bookshelf';\nimport Footer from '~/components/Footer';\nimport Hero from '~/components/Hero';\n\nexport default function Home() {\n  return (\n    \u003csection\u003e\n      \u003cHero /\u003e\n+     \u003cBookshelf /\u003e\n      \u003cFooter /\u003e\n    \u003c/section\u003e\n  );\n}\n```\n\nii. Return data from a CSV in an API response in `netlify/functions/books.ts`\n\n```typescript\nimport csv from 'csvtojson';\n\nexport default async (req: Request) =\u003e {\n  const { origin } = new URL(req.url);\n  const response = await fetch(`${origin}/books.csv`);\n  const csvContent = await response.text();\n  const books = await csv().fromString(csvContent);\n  \n  return Response.json(books);\n};\n```\n\n💡 Invoke your function from the CLI:\n\n```\nnetlify functions:invoke books\n```\n\niii. Fetch from the function in `src/context/DataProvider.tsx`\n\n```diff\nfunction StoreProvider({ children }: Props) {\n- const books = [] as Book[];\n+ const [books, setBooks] = useState\u003cBook[]\u003e([]);\n\n  const fetchBooks = async () =\u003e {\n+   if (!books.length) {\n+     const response = await fetch(`/.netlify/functions/books`);\n+     const data = await response.json();\n+     setBooks(data);\n+   }\n  };\n}\n```\n\nThat's nice, but we can only return all the books, when sometimes we only want one book at a time. Let's add a custom path with an optional slug in the API route.\n\niv. Export custom config to control method, route, etc in `netlify/functions/books.ts`\n\n```typescript\nexport const config: Config = {\n  method: 'GET',\n  path: '/api/books{/:id}?',\n};\n```\n\n💡 The `path` parameter follows the [URL Pattern API](https://developer.mozilla.org/en-US/docs/Web/API/URL_Pattern_API) spec.\n\n\nv. Change your clientside API call to new route in `src/context/DataProvider.tsx`\n\n```diff\n-  const fetchBooks = async () =\u003e {\n-   if (!books.length) {\n-     const response = await fetch(`/.netlify/functions/books`);\n-     const data = await response.json();\n-     setBooks(data);\n-   }\n-  };\n+  const fetchBooks = async (id: string = '') =\u003e {\n+    if (books.length \u003c= 1) {\n+      const response = await fetch(`/api/books/${id}`);\n+      const data = await response.json();\n+      setBooks(Array.isArray(data) ? data : [data]);\n+    }\n+  };\n```\n\nvi. Extract and log the id from the URL params in `netlify/functions/books.ts`\n\n```diff\n-export default async (req: Request) =\u003e {\n+export default async (req: Request, context: Context) =\u003e {\n+  const { id } = context.params;\n+  console.log(`Looking up ${id || 'all books'}...`);\n```\n\nvii. Return a single book if the slug is present before the last return statement\n\n```typescript\nif (id) {\n  const book = books.find(b =\u003e b.id === id);\n  if (!book) {\n    return new Response('Not found', { status: 404 });\n  }\n  return Response.json(book);\n}\n```\n\n💡 Learn more about [Functions](https://docs.netlify.com/functions/overview/) in our docs.\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\u003csummary\u003eStep 3. Branches, CI/CD, and Deploy Previews\u003c/summary\u003e\n\nCreate a new branch, commit changes, push the branch, and open a pull request *against the `start-here` branch of your own repo.*\n\n```bash\ngit checkout -b feat/bookshelf\ngit add -A\ngit commit -m \"Adding a list of books to the home page\"\ngit push origin feat/bookshelf\n```\n\nSince you're working in a fork, be sure to change the base repo and branch: \n\nBefore: \n![](media/fork-base-branch-before.png)\n\nAfter: \n![](media/fork-base-branch-after.png)\n\nYou should see a link to the Deploy Preview as a comment by the Netlify bot on the pull request. Pushing to an open pull request [will kick off a new build](https://www.netlify.com/products/build/) in the Continuous Integration pipeline, and you can inspect the deploy logs as the build is building and deploying.\n\nIn addition to deploy logs, the Netlify UI gives you access to function logs as well. You can change the region a function executes by changing the region selector in **Site configuration \u003e Build \u0026 deploy \u003e Functions**.\n\nIn the Deploy Preview itself, you'll notice a floating toolbar anchored to the bottom of your screen. This is the [Netlify Drawer](https://www.netlify.com/products/deploy-previews/). You and your teammates can use this to leave feedback to each other about the Deploy Preview. Any comments you make will sync back to the pull request on GitHub (or any Git service that you may use). \n\nBack in the pull request, merge to main. This will kick off a production build. Every deploy is [atomic](https://jamstack.org/glossary/atomic/) and [immutable](https://jamstack.org/glossary/immutable/), which makes [instant rollbacks](https://docs.netlify.com/site-deploys/manage-deploys/#rollbacks) a breeze.\n\nIn your local repo, sync up with the changes from `start-here` again: \n\n```bash\ngit checkout start-here\ngit pull origin start-here\n```\n\n💡 Learn more about [Git workflows](https://docs.netlify.com/git/overview/) and [site deploys](https://docs.netlify.com/site-deploys/overview/) in our docs.\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\u003csummary\u003eStep 4. Headers and redirects\u003c/summary\u003e\n\nYou'll notice that when you refresh a page on the `/books/{id}` route, the site 404s. Why is that? Since this frontend stack utilizes React as an SPA (Single Page Application), there is only one single HTML file (`/index.html`) inside of the deploy, and routing is managed exclusively by JavaScript referenced in that file. We'll need to add a [redirect](https://docs.netlify.com/routing/redirects/rewrites-proxies/#history-pushstate-and-single-page-apps) that routes 404s to `/index.html`.\n\nInside your publish directory (for this repo, `/public`), add a `_redirects` file that contains the following: \n\n```\n/*  /index.html  200\n```\n\nFor every fallthrough case (i.e. whenever a route is accessed and there isn't a file match), it will now redirect back to `/index.html`, where `react-router` will route accordingly.\n\nSimilar to the `_redirects` file is the `_headers` file. Here you can set custom headers for routes of your choosing. Create a `/public/_headers` file, and save the following: \n\n```\n/* \n  X-Frame-Options: SAMEORIGIN\n```\n\nThis will prevent your site from being loaded in an iframe, a technique that help your site prevent [clickjacking](https://en.wikipedia.org/wiki/Clickjacking) attacks. \n\nYou can also configure both redirects and headers inside the `/netlify.toml` file. Here is the `netlify.toml` equivalents of the above: \n\n```\n[[redirects]]\n  from = \"/*\"\n  to = \"/index.html\"\n  status = 200\n\n[[headers]]\n  for = \"/*\"\n  [headers.values]\n    X-Frame-Options = \"SAMEORIGIN\"\n```\n\n💡 Learn more about [redirects](https://docs.netlify.com/routing/redirects/) and [custom headers](https://docs.netlify.com/routing/headers/) in our docs.\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\u003csummary\u003eStep 5. Advanced fine-grained cache control\u003c/summary\u003e\n\ni. Set fine-grained cache-control headers before fetching in `netlify/functions/books.ts`\n\n```typescript\nconst etag = createHash('md5')\n  .update(id || 'all')\n  .digest('hex');\n\nconst headers = {\n  'Cache-Control': 'public, max-age=0, must-revalidate', // Tell browsers to always revalidate\n  'Netlify-CDN-Cache-Control': 'public, max-age=31536000, must-revalidate', // Tell Edge to cache asset for up to a year\n  'Cache-Tag': `books,promotions`,\n  ETag: `\"${etag}\"`,\n};\n\nif (req.headers.get('if-none-match') === etag) {\n  return new Response('Not modified', { status: 304, headers });\n}\n```\n\nii. Return headers on all Response objects\n\n```diff\nif (id) {\n  const book = books.find(b =\u003e b.id === id);\n  if (!book) {\n-   return new Response('Not found', { status: 404 });\n+   return new Response('Not found', { status: 404, headers });\n  }\n- return Response.json(book);\n+ return Response.json(book, { headers });\n}\n\n-return Response.json(books);\n+return Response.json(books, { headers });\n```\n\niii. Purge cache of specific tags using an API call\n\n```bash\ncurl -X POST 'https://api.netlify.com/api/v1/purge' \\\n  -H 'Authorization: Bearer \u003ctoken\u003e' \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"site_id\":\"$SITE_ID\",\"cache_tags\":[\"books\"]}'\n```\n\n💡 Learn more about [caching](https://docs.netlify.com/platform/caching/) in our docs.\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\u003csummary\u003eStep 6. Edge Functions and personalization\u003c/summary\u003e\n\nWe're going to make a swag section of the site that is personalized to the user based on their geolocation. Edge functions act as middleware for the CDN \u0026mdash; they run in front of other routes!\n\ni. Add the Swag component to the home page in `src/pages/index.tsx`\n\n```diff\nimport Bookshelf from '~/components/Bookshelf';\nimport Footer from '~/components/ui/Footer';\nimport Hero from '~/components/Hero';\n+import Swag from '~/components/Swag';\n\nexport default function Home() {\n  return (\n    \u003csection\u003e\n      \u003cHero /\u003e\n+     \u003cSwag /\u003e\n      \u003cBookshelf /\u003e\n      \u003cFooter /\u003e\n    \u003c/section\u003e\n  );\n}\n```\n\nii. Fetch the swag in `netlify/context/DataProvider.tsx`\n\n```diff\n- const swag = [] as Swag[];\n+ const [swag, setSwag] = useState\u003cSwag[]\u003e([]);\n\n  const fetchSwag = async () =\u003e {\n+    if (!swag.length) {\n+      const response = await fetch('/api/swag');\n+      const data = await response.json();\n+      setSwag(data);\n+    }\n  };\n```\n\niii. Sort items ascending based on distance to user in `netlify/functions/swag.ts`\n\n```diff\nimport { Config, Context } from '@netlify/functions'\n+import haversine from 'haversine';\n\n-export default async (req: Request) =\u003e {\n+export default async (req: Request, context: Context) =\u003e {\n   // ...\n+  const hasGeo = context.geo?.latitude \u0026\u0026 context.geo?.longitude;\n-  const items = selectRandomItems(merchandise, ITEMS_COUNT);\n+  const items = hasGeo\n+    ? merchandise\n+        .sort(\n+          (a, b) =\u003e\n+            haversine(a.location, context.geo) -\n+            haversine(b.location, context.geo)\n+        )\n+        .slice(0, ITEMS_COUNT)\n+    : selectRandomItems(merchandise, ITEMS_COUNT);\n\n  return Response.json(items);\n};\n```\n\niv. Rewrite response bodies to contain geolocation data in `netlify/edge-functions/geo.ts`\n\n```typescript\nimport { Config, Context } from '@netlify/edge-functions';\n\nexport default async (request: Request, context: Context) =\u003e {\n  const response = await context.next();\n  response.headers.set('x-custom-header', 'invoked');\n\n  // html GETs only\n  const isGET = request.method?.toUpperCase() === 'GET';\n  const isHTMLResponse = response.headers\n    .get('content-type')\n    ?.startsWith('text/html');\n  if (!isGET || !isHTMLResponse) {\n    return response;\n  }\n\n  const body = await response.text();\n  const transformedBody = body.replace(\n    'window.geo = {}',\n    `window.geo = ${JSON.stringify(context.geo)}`\n  );\n\n  return new Response(transformedBody, response);\n};\n\nexport const config: Config = {\n  path: '/*',\n  excludedPath: '/(api|assets|images)/*',\n};\n```\n\nv. Edge Functions are also great places to add A/B testing. You can add a cookie at the edge to segment user traffic into groups (also known as  buckets) to run experimentation. Set a new cookie in `netlify/edge-functions/abtest.ts`: \n\n```diff\n+ // set the new \"ab-test-bucket\" cookie\n+ context.cookies.set({\n+   name: bucketName,\n+   value: newBucketValue,\n+ });\n  \n  return response;\n```\n\n💡 Learn more about [Edge Functions](https://docs.netlify.com/edge-functions/overview/) in our docs.\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\u003csummary\u003eStep 7. Environment variables\u003c/summary\u003e\n\nYou can manage environment variables in the UI and CLI. \n\nGo to **Site configuration \u003e Environment variables** to add site-specific env vars to your site. \n\n![Environment variables form](media/env-var-form.png)\n\nIn the CLI, enter the following command to create an environment variable that is scoped to the Functions runtime: \n\n```bash\nnetlify env:set OPENAI_KEY \u003cYOUR_VALUE\u003e --scope functions\n```\n\n💡 Learn more about [environment variables](https://docs.netlify.com/environment-variables/overview/) in our docs.\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\u003csummary\u003eStep 8. Building a content-driven app\u003c/summary\u003e\n\ni. Create a new Connect data layer in **Connect \u003e Add a new data layer**. Then, in **Data layer settings**, save the API URL as the `VITE_CONNECT_API_URL` environment variable.\n\nii. Create a new Connect API token in **Data layer settings \u003e API tokens**. Save this as the `VITE_CONNECT_API_AUTH_TOKEN` environment variable.\n\niii. Create new data sources for Contentstack and Storyblok in **Data layer settings \u003e Data sources**.\n\niv. Replace swag products with data from Contentstack in `src/context/DataProvider.tsx`\n\n```diff\n+import { getProducts } from '~/graphql';\n-import type { Book, Swag } from '~/types/interfaces';\n+import type { Book, Swag, ContentstackProduct } from '~/types/interfaces';\n\n// ...\n\nconst fetchSwag = async () =\u003e {\n  if (!swag.length) {\n-   const response = await fetch('/api/swag');\n-   const data = await response.json();\n-   setSwag(data);\n+   const response = await getProducts();\n+   const products = response.map((product: ContentstackProduct) =\u003e {\n+     return {\n+       ...product,\n+       imagePath: product?.image?.url,\n+       name: product?.title,\n+       slug: product?.id,\n+     };\n+   });\n+   setSwag(products);\n  }\n};\n```\n\nv. Fetch About page content from Storyblok in `src/pages/about.tsx`\n\n```diff\n+import { getAbout } from '~/graphql';\n+import type { AboutPage } from '~/types/interfaces';\n\nexport default function About() {\n+ const [aboutData, setAboutData] = useState\u003cAboutPage\u003e();\n\n+ useEffect(() =\u003e {\n+   getAbout()\n+     .then(data =\u003e {\n+       const content = JSON.parse(data?.content);\n+       setAboutData({\n+         ...data,\n+         content,\n+       });\n+     })\n+     .catch(error =\u003e console.error(error));\n+ }, []);\n\n+ if (!aboutData) {\n+   return null;\n+ }\n\n+ const titleSplit = aboutData.content?.title?.split('Netlify Compose 2023');\n  const linkStyles = 'text-[#30e6e2] hover:underline hover:text-[#defffe]';\n  return (\n```\n\nvi. Replace hardcoded images with dynamic image content from Storyblok in `src/pages/about.tsx`\n\n```diff\n-  src={netlifyLogo}\n+  src={aboutData.content?.headerImage?.filename}\n// ...\n-  src={composeLogo}\n+  src={aboutData.content?.subHeaderImage?.filename}\n// ...\n-  src={netlifyMonogram}\n+  src={aboutData.content?.footerImage?.filename}\n```\n\nvii. Replace hardcoded list items with dynamic list from Storyblok in `src/pages/about.tsx`\n\n```diff\n\u003cul className=\"mt-8 list-disc pl-5\"\u003e\n-  \u003cli\u003e...\u003c/li\u003e\n-  // ...\n+  {aboutData.content?.body?.map(\n+    ({ items }) =\u003e\n+      items?.map(i =\u003e \u003cli key={i._uid}\u003e{i.itemValue}\u003c/li\u003e)\n+  )}\n\u003c/ul\u003e\n```\n\n💡 Learn more about [Netlify Connect](https://docs.netlify.com/connect/overview/) in our docs.\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\u003csummary\u003eStep 9. Utilizing existing custom data sources\u003c/summary\u003e\n\nFollow the instructions in the [Dynamic CMS Connector repo](https://github.com/netlify/workshop_dynamic-cms-connector)!\n\nWe also have the [S3 Connector](https://github.com/netlify/workshop_compose-s3-demo) available.\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\u003csummary\u003eStep 10. Bonus features of the Netlify platform\u003c/summary\u003e\n\nCongrats! You just built a composable website. If we have time, we'll walk through some additional features that you might not know about the Netlify platform. \n\n- [Site protections](https://docs.netlify.com/security/secure-access-to-sites/site-protection/)\n- [Analytics](https://docs.netlify.com/monitor-sites/site-analytics/), [Real User Metrics](https://docs.netlify.com/monitor-sites/real-user-metrics/)\n- [Log Drains](https://docs.netlify.com/monitor-sites/log-drains/)\n- [Slack notifications](https://docs.netlify.com/integrations/slack-app/)\n\n\u003c/details\u003e\n\n## Netlify Connect Credentials\n```\nVITE_CONNECT_API_AUTH_TOKEN=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2OTcyMjgwMzYyNDMsImV4cCI6NDg1MDgyODAzNjI0MywiaXNzIjoibmV0bGlmeS1jb25uZWN0IiwiaHR0cHM6Ly9uZXRsaWZ5LmNvbS9qd3QvY2xhaW1zIjp7ImRhdGFfbGF5ZXJfaWQiOiJjYjM0ZTdmMi00YWVmLTQ1ZDgtOGViOC0zMmFiZWVkNTA1OTAiLCJkYXRhX2xheWVyX3Rva2VuX2lkIjoiYTdjNTIyMWEtODFhNi00MjNlLTg3YjktOGIzNjYwZThhMWJhIiwiYWNjb3VudF9wZXJtaXNzaW9ucyI6eyJyIjp0cnVlfX19.A6lTiggYCXAsHc7o7KfbnunPCju2FMBlcZsmW13smdY\nVITE_CONNECT_API_URL=https://compose-conf-o5lhw4-prod.api.netlify-connect.com/graphql\n```\n\n## Recent Enterprise-focused resources from our blog\n\nRead these recent blog posts focused on Enterprise releases, features,  and use cases.\n\n- Oct 13 2023: [Cache-tags \u0026 Purge API](https://www.netlify.com/blog/cache-tags-and-purge-api-on-netlify/)\n- Oct 12 2023: [Introducing Netlify Functions 2.0](https://www.netlify.com/blog/introducing-netlify-functions-2-0/)\n- Sep 28 2023: [Stale-while-revalidate \u0026 fine-grained cache control](https://www.netlify.com/blog/swr-and-fine-grained-cache-control/)\n- Sep 13 2023: [General Availability of Netlify Software Development Kit (SDK)](https://www.netlify.com/blog/general-availability-netlify-sdk-software-development-kit/)\n- Aug 29 2023: [Elevating enterprise deployment with enhanced monorepo experience](https://www.netlify.com/blog/elevating-enterprise-deployment-introducing-an-enhanced-monorepo-experience-on-netlify/)\n- Aug 24 2023: [How I learned to stop worrying and love the Content Security Policy](https://www.netlify.com/blog/general-availability-content-security-policy-csp-nonce-integration/)\n- Aug 23 2023: [IP and Geo Restrictions for WAF Security](https://www.netlify.com/blog/general-availability-web-application-firewall-traffic-rules/)\n- Aug 22 2023: [Secrets Controller: Proactive security for secret keys](https://www.netlify.com/blog/general-availability-secrets-controller/)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnetlify%2Fcompose-workshop-2023","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnetlify%2Fcompose-workshop-2023","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnetlify%2Fcompose-workshop-2023/lists"}