{"id":16629167,"url":"https://github.com/bradgarropy/cloudflare-link-tree","last_synced_at":"2025-10-30T03:31:29.639Z","repository":{"id":43913613,"uuid":"381098037","full_name":"bradgarropy/cloudflare-link-tree","owner":"bradgarropy","description":"🌲 cloudflare link tree","archived":false,"fork":false,"pushed_at":"2021-09-22T01:13:34.000Z","size":283,"stargazers_count":9,"open_issues_count":0,"forks_count":3,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-02-02T06:11:20.036Z","etag":null,"topics":["cloudflare","cloudflare-worker","cloudflare-workers","eslint","interview","link-tree","prettier","typescript","webpack"],"latest_commit_sha":null,"homepage":"https://cloudflare-link-tree.bradgarropy.workers.dev","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/bradgarropy.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}},"created_at":"2021-06-28T16:36:12.000Z","updated_at":"2023-04-23T23:07:10.000Z","dependencies_parsed_at":"2022-09-18T09:02:11.592Z","dependency_job_id":null,"html_url":"https://github.com/bradgarropy/cloudflare-link-tree","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bradgarropy%2Fcloudflare-link-tree","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bradgarropy%2Fcloudflare-link-tree/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bradgarropy%2Fcloudflare-link-tree/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/bradgarropy%2Fcloudflare-link-tree/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/bradgarropy","download_url":"https://codeload.github.com/bradgarropy/cloudflare-link-tree/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":238930130,"owners_count":19554122,"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":["cloudflare","cloudflare-worker","cloudflare-workers","eslint","interview","link-tree","prettier","typescript","webpack"],"created_at":"2024-10-12T04:40:00.133Z","updated_at":"2025-10-30T03:31:29.294Z","avatar_url":"https://github.com/bradgarropy.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"_I posted [this article][article] on [my blog][blog] for a better reading experience._\n\n# 🌲 cloudflare worker link tree\n\nI've heard a lot about [Cloudflare Workers][workers], but until recently I've never created one. Today I built and deployed my first worker, so I'd like to teach you how to do the same thing!\n\n## 🤔 what are cloudflare workers?\n\n[Cloudflare Workers][workers] is a globally distributed, auto-scaling severless application platform where you can execute your application code in many different languages. It comes with a [web dashboard][dashboard] and a [cli tool][cli] for easy management of your workers.\n\n## 👨🏼‍💻 building a link tree\n\nThe first worker I built was a clone of [linktree][linktree], but with a little added flare. I'll walk you through how to build a simpler version of this link tree.\n\nAll of the code is located on [GitHub][github] in the [cloudflare-link-tree][repo] repository.\n\n![link tree][finished]\n\nOur application will have two endpoints. `/links` will return raw `json` data for all of our links, and any other route will return a rendered HTML version of the link tree.\n\n### create a cloudflare account\n\nBefore we get started, you'll have to [sign up][signup] for a Cloudflare account. Don't worry, there's a great [free tier][pricing] and you don't have to hand over a credit card to get started.\n\n### install the workers cli\n\nCloudflare made it easy to manage your workers and test them locally with a cli tool called [wrangler][cli]. Install it globally as shown below.\n\n```bash\nnpm install --global @cloudflare/wrangler\n```\n\nThen, login to Cloudflare from `wrangler`.\n\n```bash\nwrangler login\n```\n\nNow you're ready to start building!\n\n### generate a worker\n\nWhile you can start from scratch, Cloudflare provides lots of [starter templates][templates] to get a worker up and running quickly. These templates are simply GitHub repositories, so you can even create your own.\n\nTo begin our link tree, let's use `wrangler` to generate a basic [worker template][worker-template].\n\n```bash\nwrangler generate link-tree https://github.com/cloudflare/worker-template\n```\n\nThen, head over to the directory and install the dependencies.\n\n```bash\ncd link-tree\nnpm install\n```\n\nFinally, we'll modify the `wrangler.toml` file to use `webpack` as a build tool, as well as add our account id to connect the worker with Cloudflare. If you can't find your account id, run `wrangler whoami` to see it.\n\n```toml\nname = \"link-tree\"\ntype = \"webpack\"\n\naccount_id = \"\u003cACCOUNT_ID\u003e\"\nworkers_dev = true\nroute = \"\"\nzone_id = \"\"\n```\n\nWe're all set for local development and ready for deployments to Cloudflare Workers.\n\n### link tree data\n\nBefore we can get to implenting the link tree, we need some data to work with. Create a `links.js` file and add some links to share. I've included an example of mine below.\n\n```javascript\nconst links = [\n    {name: \"Website\", url: \"bradgarropy.com\"},\n    {name: \"GitHub\", url: \"https://github.com/bradgarropy\"},\n    {name: \"YouTube\", url: \"https://youtube.com/bradgarropy\"},\n]\n\nexport default links\n```\n\n### routing requests\n\nYou can think of workers like a miniature web application that lives on a CDN. Web applications typically handle many different kinds of requests across many different endpoints. So let's include a router to lay out the infrastructure of our worker.\n\nI chose [itty-router][router] because it was specifically designed for use with Cloudflare Workers. Install it by using the command below.\n\n```bash\nnpm install itty-router\n```\n\nNow we can define our two application routes. The first will be `/links`, which will serve the raw link data we defined in `links.js`. The second will be `*`, which means any other endpoint, and will return our rendered link tree.\n\n```javascript\nimport {Router} from \"itty-router\"\n\nconst router = Router()\n\nrouter.get(\"/links\", () =\u003e {\n    return new Response(\"GET /links\")\n})\n\nrouter.get(\"*\", () =\u003e {\n    return new Response(\"GET *\")\n})\n\naddEventListener(\"fetch\", event =\u003e {\n    event.respondWith(router.handle(event.request))\n})\n```\n\nThe call to `addEventListener` is standard boilerplate for listening to events in a worker. You can see that we're responding with the result from our router's handler method.\n\n### spin up a development environment\n\nLet's take a minute here to ensure our two routes are working and learn how to start our development server. Run the following command to start a local instance of our worker.\n\n```bash\nwrangler dev\n```\n\nNow we've got a server running our application at `127.0.0.1:8787`. Try the following endpoints and double check that the correct test responses are coming back.\n\n| url                    | response     |\n| ---------------------- | ------------ |\n| `127.0.0.1:8787`       | `GET *`      |\n| `127.0.0.1:8787/links` | `GET /links` |\n\nYou can do this in a browser or with an API tool like [Insomnia][insomnia] or [Postman][postman].\n\n### link json api\n\nThe `/links` endpoint should return our links as raw `json` data. We can use the router to handle this endpoint with the `router.get()` method. Inside of the route handler, we create a new response, where we stringify the `links` data and send it back as `json` in the body.\n\n```javascript\nimport links from \"./links\"\n\nrouter.get(\"/links\", () =\u003e {\n    const response = new Response(JSON.stringify(links), {\n        headers: {\"content-type\": \"application/json\"},\n    })\n\n    return response\n})\n```\n\nOpen a browser and check that `127.0.0.1:8787/links` is returning the `json` data as expected.\n\n### link tree template\n\nAll other routes should render our link tree to HTML. I've placed a static HTML template [here][template], which we can fetch from our worker, and then modify the response to include our `links`. Let's start by returning the template from all other routes.\n\nAgain we use the `router.get()` method, but specify `*` to indicate all routes. Then we can use the `fetch` API to grab the static HTML template. In addition to `fetch`, Cloudflare Workers provide many other [Runtime APIs][runtime-apis] for you to leverage.\n\n```javascript\nrouter.get(\"*\", async () =\u003e {\n    const response = await fetch(\n        \"https://static-links-page.signalnerve.workers.dev\",\n    )\n\n    return response\n})\n```\n\nNow is a good time to open a browser and check that `127.0.0.1:8787` is returning the static HTML template.\n\n### modify the link tree template\n\nAnother powerful feature of Cloudflare Workers is the ability to modify a request or response as needed based on your application logic. In our case, we'll be using our worker to change the static HTML template in a few different ways.\n\n-   Add a profile image\n-   Include our name\n-   Insert our links\n\nThis can be accomplished using the [HTMLRewriter API][htmlrewriter]. Based on the template, we'll need four rewriters to handle modifying different HTML elements. The `HTMLRewriter` API maps a document query selector to a rewriter class which commits the necessary changes.\n\nLet's start by importing our four rewriters (we'll implement these in the next steps), and map them to the appropriate query selectors. Note how this acts on the response from the HTML template. Feel free to inspect the [HTML template][template] to understand the DOM structure and the relevant element ids.\n\n```javascript\nimport {\n    AvatarRewriter,\n    LinkRewriter,\n    NameRewriter,\n    ProfileRewriter,\n} from \"./rewriters\"\n\nrouter.get(\"*\", async () =\u003e {\n    const response = await fetch(\n        \"https://static-links-page.signalnerve.workers.dev\",\n    )\n\n    const rewrittenResponse = new HTMLRewriter()\n        .on(\"#profile\", new ProfileRewriter())\n        .on(\"#avatar\", new AvatarRewriter())\n        .on(\"#name\", new NameRewriter())\n        .on(\"#links\", new LinkRewriter())\n        .transform(response)\n\n    return rewrittenResponse\n})\n```\n\nNow we'll move on and define each of the rewriters that we'll need in a `rewriters.js` file. Let's start with the `ProfileRewriter`, which should remove the `display: none` style to show the profile section.\n\n```javascript\nclass ProfileRewriter {\n    element(element) {\n        element.removeAttribute(\"style\")\n    }\n}\n```\n\nYou'll see that a rewriter is a class that can contain three methods, `element`, `comments`, and `text`. In our case, we'll only need the `element` method. We'll use the `removeAttribute` method to get rid of the `style` attribute.\n\nNext we'll update the avatar to include our own photo. Define an `AvatarRewriter` class with an `element` method, but this time we'll use the `setAttribute` method to modify the image source.\n\n```javascript\nclass AvatarRewriter {\n    element(element) {\n        const src = \"https://github.com/bradgarropy.png\"\n        element.setAttribute(\"src\", src)\n    }\n}\n```\n\nThen let's insert our name by creating a `NameRewriter` and using the `setInnerContent` method, which replaces the text content of the element.\n\n```javascript\nclass NameRewriter {\n    element(element) {\n        element.setInnerContent(\"Brad Garropy\")\n    }\n}\n```\n\nFinally, let's do what we came here for and include our links in the `#links` section of the document. We'll start by defining our `LinkRewriter`, then iterating over the `links` we previously defined in the `links.js` file.\n\nFor each link, we'll need to create an `\u003ca\u003e` tag and populate it with the link's name and url. Then we'll use the `append` method to add the markup right before the `element` closing tag.\n\n```javascript\nimport links from \"./links\"\n\nclass LinkRewriter {\n    element(element) {\n        links.forEach(link =\u003e\n            {\n                cont html = `\u003ca href=\"${link.url}\"\u003e${link.name}\u003c/a\u003e`\n                element.append(html, {html: true})\n            }\n        )\n    }\n}\n```\n\nYou may have noticed the additional `{html: true}` argument, this tells the `append` method that the first argument should be treated as HTML instead of plain text, which would get HTML encoded.\n\n### final product\n\nHead over to `127.0.0.1:8787` to see the new HTML response, which has been rewritten by our worker to include our links! It should look something like this.\n\n![link tree][final]\n\nNow you can use `wrangler` to publish your worker to Cloudflare.\n\n```bash\nwrangler publish\n```\n\nYour worker should be available globally at the url shown in the console, which should look something like this one.\n\n```text\nhttps://link-tree.\u003cSUBDOMAIN\u003e.workers.dev\n```\n\n### conclusion\n\nI'm definitely fascinated by [Cloudflare Workers][workers], and I'm still exploring the possibilites of what they can enable. If you build something cool with a worker, share it with me on [Twitter][twitter]!\n\n[workers]: https://workers.cloudflare.com\n[dashboard]: https://dash.cloudflare.com\n[cli]: https://developers.cloudflare.com/workers/cli-wrangler\n[finished]: images/link-tree.png\n[linktree]: https://linktr.ee\n[signup]: https://dash.cloudflare.com/sign-up\n[pricing]: https://workers.cloudflare.com/#plans\n[templates]: https://developers.cloudflare.com/workers/get-started/quickstarts\n[worker-template]: https://github.com/cloudflare/worker-template\n[router]: https://github.com/kwhitley/itty-router\n[insomnia]: https://insomnia.rest\n[postman]: https://postman.com\n[template]: https://static-links-page.signalnerve.workers.dev\n[runtime-apis]: https://developers.cloudflare.com/workers/runtime-apis\n[htmlrewriter]: https://developers.cloudflare.com/workers/runtime-apis/html-rewriter\n[final]: images/final.png\n[twitter]: https://twitter.com/bradgarropy\n[repo]: https://github.com/bradgarropy/cloudflare-link-tree\n[github]: https://github.com/bradgarropy\n[article]: https://bradgarropy.com/blog/cloudflare-worker-link-tree\n[blog]: https://bradgarropy.com/blog\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbradgarropy%2Fcloudflare-link-tree","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbradgarropy%2Fcloudflare-link-tree","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbradgarropy%2Fcloudflare-link-tree/lists"}