{"id":19554027,"url":"https://github.com/felixjung/blog-headless","last_synced_at":"2026-04-13T18:01:23.680Z","repository":{"id":40708760,"uuid":"309469028","full_name":"felixjung/blog-headless","owner":"felixjung","description":"Simple serverless content API built on Vercel serverless functions and DynamoDB.","archived":false,"fork":false,"pushed_at":"2023-01-07T22:03:41.000Z","size":2029,"stargazers_count":0,"open_issues_count":16,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-10-22T00:38:53.678Z","etag":null,"topics":["blog","cms","nodejs","serverless","typescript","vercel"],"latest_commit_sha":null,"homepage":"https://felixjung.io","language":"TypeScript","has_issues":false,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/felixjung.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":"2020-11-02T19:06:42.000Z","updated_at":"2021-01-14T11:56:45.000Z","dependencies_parsed_at":"2023-02-08T02:32:57.508Z","dependency_job_id":null,"html_url":"https://github.com/felixjung/blog-headless","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/felixjung/blog-headless","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/felixjung%2Fblog-headless","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/felixjung%2Fblog-headless/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/felixjung%2Fblog-headless/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/felixjung%2Fblog-headless/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/felixjung","download_url":"https://codeload.github.com/felixjung/blog-headless/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/felixjung%2Fblog-headless/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31764317,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-13T15:25:13.801Z","status":"ssl_error","status_checked_at":"2026-04-13T15:25:09.162Z","response_time":93,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"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":["blog","cms","nodejs","serverless","typescript","vercel"],"created_at":"2024-11-11T04:25:38.046Z","updated_at":"2026-04-13T18:01:23.659Z","avatar_url":"https://github.com/felixjung.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cdiv align=\"center\"\u003e\n\u003ch1\u003eblog-headless\u003c/h1\u003e\nSimple serverless content API built on Vercel serverless functions and DynamoDB.\n\u003c/div\u003e\n\n### About\n\nI needed something simple and quick to develop to serve\n[the content](https://github.com/felixjung/blog-posts) for\n[my blog](https://github.com/felixjung/felixjung.io). GitHub is the single\nsource of truth for the blog's content. Posts are stored as MDX files, exported\nfrom [Ulysses](https://ulysses.app). Metadata is stored together with the posts\nin YAML files. Querying the GitHub API for these files when\n[fetching routes](https://nextjs.org/docs/basic-features/data-fetching#getstaticpaths-static-generation)\nand\n[static props](https://nextjs.org/docs/basic-features/data-fetching#getstaticprops-static-generation)\nin my Next.js website was not very efficient, especially when trying to paginate\ncontent.\n\n![Sequence diagram depicting the content syncronization from GitHub to DynamoDB via webhooks.](docs/content-sync.svg)\n\nThis little project allows me to sync content for the blog to an AWS DynamoDB\ntable as JSON and then access it from the Next.js website through a small Posts\nAPI. GitHub remains the single source of truth but content can be queried more\nefficiently via the API.\n\n![Sequence diagram depicting the list posts operation on the API.](docs/content-fetch.svg)\n\nThe project structure follows Vercel's recommendation of keeping serverless request handlers\nin an `api` folder at the project root and defining routes and route params in the\nfile names.\n\n```\napi\n├── index.ts        // Responds with a 404\n├── posts\n│  ├── [slug].ts    // GET /posts/{slug}\n│  └── index.ts     // GET /posts\n└── webhooks.ts     // POST /webhooks\n```\n\n### Serverless Design Approach\n\nThe functions handling requests are composed from middleware functions and a\nrequest handler. The composition approach is heavily inspired by the\n[Koa.js](https://koajs.com) web framework.\n\nThe original Vercel request handler function\n`(req: NowRequest, res: NowResponse): void` is\n[mapped](https://github.com/felixjung/blog-headless/tree/main/middleware/context.ts#L20-L43)\nto a middleware `async (ctx: Context, next: NextFn): void`. The `Context` type\nprovides middleware functions with a logger, a request ID, configuration, and\nthe original request and response objects, `req` and `res`.\n\nEvery middleware can `await` the next middleware's return value. As in Koa, this\nallows for simple centralized error handling and pre-/post-middleware executing\ntasks, such as measuring execution time. Here is an example of the headers\nmiddleware.\n\n```ts\nexport function withHeaders(headers: { [key: string]: string | number }) {\n  return async ({ logger, res }: Context, next: NextFn) =\u003e {\n    logger.info('adding headers', { headers });\n\n    Object.keys(headers).forEach((key) =\u003e {\n      res.setHeader(key, headers[key]);\n    });\n\n    await next();\n  };\n}\n```\n\nIn a handler this middleware is then used as\n`export default withContext(withHeaders({ 'x-my-header': 'some-value' }), myHandlerFunction)`.\n\n### Requirements\n\n- A GitHub repository storing content\n  [the same way I do](https://github.com/felixjung/blog-posts).\n- A Vercel project to run the serverless functions.\n- An AWS DynamoDB table to write and read JSON content from.\n- Vercel CLI for the development environment.\n\n### Installation\n\nRun `npm install` to install dependencies.\n\n### Development\n\nFor development, populate a `.env` file derived from `.env` example and run\n`vercel dev`.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffelixjung%2Fblog-headless","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ffelixjung%2Fblog-headless","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffelixjung%2Fblog-headless/lists"}