{"id":21269544,"url":"https://github.com/fardjad/blog","last_synced_at":"2026-05-22T05:19:40.446Z","repository":{"id":250820870,"uuid":"835568498","full_name":"fardjad/blog","owner":"fardjad","description":"Fardjad's Blog","archived":false,"fork":false,"pushed_at":"2024-08-04T13:35:35.000Z","size":1221,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-01-22T02:19:56.982Z","etag":null,"topics":["blog","deno","gist"],"latest_commit_sha":null,"homepage":"https://blog.fardjad.com","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/fardjad.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-07-30T05:30:10.000Z","updated_at":"2024-08-23T17:52:32.000Z","dependencies_parsed_at":"2024-08-03T19:24:48.048Z","dependency_job_id":"5e1d31fa-eafd-4907-bc9b-a2834bbd950c","html_url":"https://github.com/fardjad/blog","commit_stats":null,"previous_names":["fardjad/blog"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fardjad%2Fblog","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fardjad%2Fblog/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fardjad%2Fblog/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fardjad%2Fblog/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/fardjad","download_url":"https://codeload.github.com/fardjad/blog/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":243725538,"owners_count":20337667,"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":["blog","deno","gist"],"created_at":"2024-11-21T08:08:53.927Z","updated_at":"2026-05-22T05:19:40.360Z","avatar_url":"https://github.com/fardjad.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Fardjad's Blog\n\nThis repository contains the source code of my\n[personal blog](https://blog.fardjad.com). It automatically scans my published\n[GitHub Gists](https://gist.github.com/fardjad) and shows the ones that are\ntagged as blog posts in a nice and clean way, with a link to the original Gist\nfor comments and reactions.\n\nIn my opinion GitHub Gists are great for writing blog posts because:\n\n- They support Markdown and hosting images.\n- Readers can leave comments on the Gists.\n- Gists are discoverable in search engines.\n- Gists are version controlled.\n\n## Running the Project\n\n### Local Development\n\n1. Clone the repository\n2. Install [Deno](https://docs.deno.com/runtime/manual/#install-deno)\n3. Install the Git hooks by running the following command:\n\n   ```shell\n   deno task hooks:install\n   ```\n4. Install [Turso CLI](https://docs.turso.tech/quickstart)\n\n   _Hint: You can get a local server up and running by running `turso dev`._\n5. Create a `.env` file in the root directory similar to\n   [`.env.example`](./.env.example) and set the environment variables.\n6. Run the following command to start the application:\n\n   ```shell\n   deno task dev\n   ```\n\nRunning the application in development mode means:\n\n1. The HTML output is pretty-printed.\n2. Tailwind CSS is compiled on the fly.\n\n### Running in Production\n\nFollow the same steps as above, but instead of running `deno task dev`, run the\nfollowing commands:\n\n```shell\ndeno task start\n```\n\n### Deploying to Deno Deploy\n\nInstall [deployctl](https://docs.deno.com/deploy/manual/), run the following\ncommand, and follow the prompts:\n\n```shell\ndeployctl deploy --env-file .env\n# a production deployment is needed to enable the cron job\ndeployctl deploy --prod\n```\n\n### GitHub Token\n\nThe synchronization job requires a\n[GitHub token](https://github.com/settings/tokens/new) to fetch the Gists.\nEnsure that the token is granted the `gist` scope; otherwise, it won't be able\nto fetch secret Gists.\n\n## How it works\n\nThe following diagram shows the high-level architecture of the blog:\n\n```mermaid\ngraph LR\n   User --\u003e |Publish or update Gists in| Gists[GitHub Gists]\n   SyncPostsJob[\"Sync Posts Job\"] --\u003e |Fetch updated Gists from| Gists[GitHub Gists]\n   SyncPostsJob --\u003e |Store Blog Gists in| Database[Database]\n\n   User --\u003e |View the home page| WebApp[\"Web Application\"]\n   User --\u003e |View a post| WebApp\n   WebApp[\"Web Application\"] --\u003e |Read posts from| Database\n```\n\nThere are two main components that can run independently of each other:\n\n1. [The synchronization job](./src/cron.ts) that scans my Gists and stores the\n   ones that are tagged as blog posts in a database.\n2. The [web application](./src/server/app.ts) that reads the stored Gists and\n   shows them in a nice and clean way.\n\n### Synchronization Job\n\nGists are fetched from GitHub using the\n[GitHub API](https://docs.github.com/en/rest/gists/gists?apiVersion=2022-11-28#list-gists-for-the-authenticated-user).\nA **Blog Gist** is a Gist that meets the following criteria:\n\n1. Its description matches the pattern: `[Title] Description #tag1 #tag2 ...`.\n2. It has a non-empty title.\n3. It has a `blog` tag.\n4. It has at least one Markdown file.\n\nWhen a Gist matches the above criteria, the synchronization job fetches the\ncontent of its first Markdown file and stores it in the database. The job runs\nevery minute and fetches only the updated Gists since the last run (see the\n`since` parameter in the\n[API docs](https://docs.github.com/en/rest/gists/gists?apiVersion=2022-11-28#list-gists-for-the-authenticated-user--parameters)).\n\n### Web Application\n\nEvery Blog Gist that gets stored in the database is called a\n[**Post**](./src/blog/model/post.ts). Posts are assigned a **slug** based on\ntheir **title**. For example a post with the title `Hello World` will have the\nslug `hello-world` and can be accessed at `/posts/hello-world`. When two posts\nhave the same slug, the newer post will get a numeric suffix. For example, if\nthere are two posts with the title \"Hello World\", the first post will have the\nslug `hello-world` and the second post will have the slug `hello-world-1`.\n\nThere are three routes in the web application:\n\n1. The OpenGraph image route (`/og-image/:slug`) that generates an image for a\n   post based on its content\n1. The home page (`/`) that\n   [shows some hard-coded content followed by the list of the posts stored in the database](./src/server/page/home.tsx)\n1. The post page (`/posts/:slug`) that shows the content of a post\n\nThe posts are rendered on the server using a\n[Markdown renderer](./src/markdown/markdown-renderer.ts). The renderer removes\nthe first heading with depth 1 (i.e., the first `h1` element) from the Markdown\ncontent and replaces it with the post title followed by some extra information,\nsuch as my name, the date of the post, and a link to the original Gist (see\n[here](./src/server/page/post.tsx) for more details).\n\n## Noteworthy Dependencies\n\n| Name                                                                       | Description                                              |\n| -------------------------------------------------------------------------- | -------------------------------------------------------- |\n| [Deno](https://deno.com/)                                                  | Javascript Runtime                                       |\n| [Turso](https://turso.tech/)                                               | Database                                                 |\n| [Hono](https://hono.dev/)                                                  | Web Application Framework                                |\n| [Tailwind CSS](https://tailwindcss.com/)                                   | CSS Framework                                            |\n| [remark](https://github.com/remarkjs)                                      | Markdown Processor                                       |\n| [rehype](https://github.com/rehypejs)                                      | HTML Processor                                           |\n| [github-markdown-css](https://github.com/sindresorhus/github-markdown-css) | CSS to replicate the style of GitHub Markdown            |\n| [satori](https://github.com/vercel/satori)                                 | HTML+CSS to SVG converter (used for OG image generation) |\n| [resvg](https://github.com/RazrFalcon/resvg)                               | SVG to PNG converter (used for OG image generation)      |\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffardjad%2Fblog","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ffardjad%2Fblog","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffardjad%2Fblog/lists"}