{"id":16861779,"url":"https://github.com/maelvls/hudevto","last_synced_at":"2025-04-11T10:44:47.075Z","repository":{"id":57581245,"uuid":"338779423","full_name":"maelvls/hudevto","owner":"maelvls","description":"🧵 Push your Hugo posts to your dev.to account!","archived":false,"fork":false,"pushed_at":"2023-02-22T16:30:45.000Z","size":221,"stargazers_count":10,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-03-25T07:12:20.095Z","etag":null,"topics":["devto","forem","hugo"],"latest_commit_sha":null,"homepage":"","language":"Go","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/maelvls.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,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-02-14T10:29:01.000Z","updated_at":"2024-07-28T07:06:51.000Z","dependencies_parsed_at":"2024-06-20T11:10:32.293Z","dependency_job_id":null,"html_url":"https://github.com/maelvls/hudevto","commit_stats":null,"previous_names":[],"tags_count":2,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/maelvls%2Fhudevto","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/maelvls%2Fhudevto/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/maelvls%2Fhudevto/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/maelvls%2Fhudevto/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/maelvls","download_url":"https://codeload.github.com/maelvls/hudevto/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248378598,"owners_count":21094018,"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":["devto","forem","hugo"],"created_at":"2024-10-13T14:33:21.439Z","updated_at":"2025-04-11T10:44:47.039Z","avatar_url":"https://github.com/maelvls.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# hudevto, the CLI for pushing and synchronizing your Hugo blog posts to Dev.to\n\n![Screenshot of the hudevto push command](https://user-images.githubusercontent.com/2195781/108324642-737e7f80-71c8-11eb-9e4f-8f23fd14d644.png)\n\n**Content:**\n\n- [Install](#install)\n- [Usage](#usage)\n- [Use it](#use-it)\n  - [List your dev.to articles](#list-your-devto-articles)\n  - [Preview the Markdown content that will be pushed to dev.to](#preview-the-markdown-content-that-will-be-pushed-to-devto)\n  - [Push one blog post to dev.to](#push-one-blog-post-to-devto)\n  - [Push all blog posts to dev.to](#push-all-blog-posts-to-devto)\n- [Notes](#notes)\n  - [Hugo's hard breaks versus dev.to hard breaks](#hugos-hard-breaks-versus-devto-hard-breaks)\n  - [Known errors](#known-errors)\n\n## Install\n\n```sh\n# Requirement: Go is installed and $(go env GOPATH)/bin is in your PATH.\n(cd \u0026\u0026 GO111MODULE=on go get github.com/maelvls/hudevto@latest)\n```\n\n## Usage\n\n```bash\n% hudevto help\nhudevto allows you to synchronize your Hugo posts with your DEV articles. The\nsynchronization is one way (Hugo to DEV). A Hugo post is only pushed when a\nchange is detected. When pushed to DEV, the Hugo article is transformed a bit,\ne.g., relative image links are absolutified (see TRANSFORMATIONS).\n\nCOMMANDS\n\n  hudevto status [POST]\n      Shows the status of each post (or of a single post). The status shows\n      whether it is mapped to a DEV article and if a push is required when the\n      Hugo post has changes that are not on DEV yet.\n\n  hudevto preview [POST]\n      Displays a Markdown preview of the Hugo post that has been converted into\n      the DEV article Markdown format. You can use this command to check that\n      the tranformations were correctly applied.\n\n  hudevto diff [POST]\n      Displays a diff between the Hugo post and the DEV article. It is useful\n      when you want to see what changes will be pushed.\n\n  hudevto push [POST]\n      Pushes the given Hugo Markdown post to DEV. If no post is given, then\n      all posts are pushed.\n\n  hudevto devto list\n      Lists all the articles you have on your DEV account.\n\nIMPORTANT\n\nhudevto has been mainly built for pushing https://maelvls.dev, and the following\nassumptions are made:\n\n1. Each blog post is in its own folder and the article itself is in index.md,\n   e.g. ./content/post-1/index.md.\n2. The images are hosted along with the index.md file.\n3. The base_url is set in config.yml.\n4. Each article has the \"url\" field set in its front-matter.\n\nHOW TO USE IT\n\nIn order to operate, hudevto requires you to have your DEV account configured\nwith \"Publish to DEV Community from your blog's RSS\". You can configure that at\nhttps://dev.to/settings/extensions. DEV will create a draft article for\nevery Hugo post that you have published on your blog. For example, Let us\nimagine that your Hugo blog layout is:\n\n    .\n    └── content\n       ├── brick-chest.md\n       ├── cloth-impossible.md\n       └── powder-farmer.md\n\nAfter configuring the RSS feed of your blog at https://maelvls.dev/index.xml,\nDEV should create one draft article per post. You can check that these articles\nhave been created on DEV with:\n\n    % hudevto devto list\n    386001: unpublished at https://dev.to/maelvls/brick-chest/edit\n    386002: unpublished at https://dev.to/maelvls/cloth-impossible/edit\n    386003: unpublished at https://dev.to/maelvls/powder-farmer/edit\n\nThe next step is to map each article that you want to sync to DEV. Let us see\nthe state of the mapping:\n\n    % hudevto status\n    error: ./content/brick-chest.md: missing devtoId field in front matter, might be 386001: https://dev.to/maelvls/brick-chest/edit\n    error: ./content/cloth-impossible.md: missing devtoId field in front matter, might be 386002: https://dev.to/maelvls/cloth-impossible/edit\n    error: ./content/powder-farmer.md: missing devtoId field in front matter, might be 386003: https://dev.to/maelvls/powder-farmer/edit\n\nAt this point, you need to open each of your Hugo post and add some fields to\ntheir front matters. For example, in ./content/brick-chest.md, we add this:\n\n    devtoId: 386001       # This is the DEV ID as seen in hudevto devto list\n    devtoPublished: true  # When false, the DEV article will stay a draft\n    devtoSkip: false      # When true, hudevto will ignore this post.\n\nThe status should have changed:\n\n    % hudevto status\n    info: ./content/brick-chest.md will be pushed published to https://dev.to/maelvls/brick-chest/edit\n    info: ./content/cloth-impossible.md will be pushed published to https://dev.to/maelvls/cloth-impossible/edit\n    info: ./content/powder-farmer.md will be pushed published to https://dev.to/maelvls/powder-farmer/edit\n\nFinally, you can push to DEV:\n\n    % hudevto push\n    success: ./content/brick-chest.md pushed to https://dev.to/maelvls/brick-chest-2588\n    success: ./content/cloth-impossible.md pushed to https://dev.to/maelvls/cloth-impossible-95dc\n    success: ./content/powder-farmer.md pushed to https://dev.to/maelvls/powder-farmer-6a18\n\nTRANSFORMATIONS\nThe Markdown for Hugo posts and dev.to articles have slight differences.\nBefore pushing to dev.to, hudevto does some transformations to the Markdown\nfile. To see the transformations before pushing the Hugo post to dev.to, use one of:\n\n    % hudevto diff ./debug-k8s/index.md\n\nThe transformations are:\n\n1. ABSOLUTE MARKDOWN IMAGES: the relative image links are absolutified since\n   dev.to needs the image path to be absolute (the base URL itself is not\n   required).\n\n   The following Hugo Markdown snippet:\n\n     ![wireshark](wireshark.png)\n\n    becomes:\n\n      ![wireshark](/debug-k8s/wireshark.png)\n                   \u0026lt;--(1)---\u003e\n\n    where (1) is the article's Hugo permalink to the ./debug-k8s/index.md post.\n    Note that the ![]() tag must span a single line. Otherwise, it won't be\n    transformed.\n\n2. ABSOLUTE HTML IMG TAGS: unlike with Markdown images, the \u003cimg\u003e HTML tags\n   need to be absolute and needs to contain the base URL. For example, the\n   following HTML:\n\n        \u003cimg src=\"wireshark.png\"\u003e\n\n    gets transformed to:\n\n        \u003cimg src=\"https://maelvls/debug-k8s/wireshark.png\"\u003e\n\n    The \u003cimg\u003e tag must be on a single line, and the \"src\" value must end with\n\tone of the following extensions: png, PNG, jpeg, JPG, jpg, gif, GIF, svg,\n\tSVG.\n\n3. SHORTCODES: Hugo shortcodes for embedding (like for embedding a Youtube video)\n   are turned into Liquid tags that dev.to knows about.\n4. ANCHOR IDS: Hugo and Devto have different anchor ID syntaxes.\n\nOPTIONS\n  -apikey string\n    \tThe API key for Dev.to. You can also set DEVTO_APIKEY instead.\n  -debug\n    \tPrint debug information such as the HTTP requests that are being made in curl format.\n  -root string\n    \tRoot directory of the Hugo project.\n```\n\n## Use it\n\nFirst, copy your dev.to token from your dev.to settings and set it as an\nenvironment variable:\n\n```sh\nexport DEVTO_APIKEY=$(lpass show dev.to -p)\n```\n\n### List your dev.to articles\n\nThis is useful because I have dev.to configured with the RSS feed of my\nblog so that dev.to automatically creates a draft of each of my new posts.\n\n```sh\n% hudevto devto list\n410260: unpublished at https://dev.to/maelvls/it-s-always-the-dns-fault-3lg3-temp-slug-8953915/edit (It's always the DNS' fault)\n365847: unpublished at https://dev.to/maelvls/stuff-about-wireshark-28c-temp-slug-8030102/edit (Stuff about Wireshark)\n365846: unpublished at https://dev.to/maelvls/how-client-server-ssh-authentication-works-5e7-temp-slug-7868012/edit (How client-server SSH authentication works)\n313908: unpublished at https://dev.to/maelvls/about-3896-temp-slug-7318594/edit (About)\n365849: published at https://dev.to/maelvls/epic-journey-with-statically-and-dynamically-linked-libraries-a-so-1khn (Epic journey with statically and dynamically-linked libraries (.a, .so))\n331169: published at https://dev.to/maelvls/github-actions-with-a-private-terraform-module-5b85 (Github Actions with a private Terraform module)\n317339: published at https://dev.to/maelvls/learning-kubernetes-controllers-496j (Learning Kubernetes Controllers)\n```\n\n### Preview the Markdown content that will be pushed to dev.to\n\nI use the `hudevto preview` command because I do some transformations and I need a way to preview the changes to make sure the Markdown and front matter make sense. The transformations are:\n\n- Generate a new front matter which is used by dev.to for setting the dev.to post title and canonical URL;\n- Change the Hugo \"tags\" into Liquid tags, such as:\n\n  ```md\n  {{\u003c youtube 30a0WrfaS2A \u003e}}\n  ```\n\n  is changed to the Liquid tag:\n\n  ```md\n  {% youtube 30a0WrfaS2A %}\n  ```\n\n- Add the base URL of the post to the markdown images so that images are not\n  broken. ONLY WORKS if your images are stored along side your blog post, such\n  as:\n\n  ```sh\n  % ls --tree ./content/2020/avoid-gke-lb-using-hostport\n  ./content/2020/avoid-gke-lb-using-hostport\n  ├── cost-load-balancer-gke.png\n  ├── cover-external-dns.png\n  ├── how-service-controller-works-on-gke.png\n  ├── index.md                                             # The actual blog post.\n  └── packet-routing-with-akrobateo.png\n  ```\n\n- The relative image links are \"absolutified\". This is needed so that Devto can\n  access the images. For example, the following post:\n\n  \u003chttps://maelvls.dev/you-should-write-comments/index.md\u003e\n\n  then I need to replace the relative image paths such as\n\n  ```markdown\n  ![My image](cover-you-should-write-comments.png)\n  ```\n\n  with:\n\n  ```text\n  ![My image](/you-should-write-comments/cover-you-should-write-comments.png)\n              \u003c-----------------------\u003e\n                        url\n               (from front matter)\n  ```\n\n  The prefix that gets added comes from the front matter of the Hugo post. Here\n  is an example of front matter:\n\n  ```yaml\n  ---\n  title: \"Writing useful comments\"\n  date: 2021-06-05\n  url: \"/writing-useful-comments\" # \u003c--- THIS\n  ---\n  ```\n\n  The `url` part is only added if you are storing the images alongside your\n  post.\n\n  Note that the images using the syntax `![]()` tag must span a single line.\n  Otherwise, it won't be transformed.\n\n  ```sh\n  % ls --tree ./content/2020/avoid-gke-lb-using-hostport\n  ./content/2020/avoid-gke-lb-using-hostport\n  ├── cost-load-balancer-gke.png\n  ├── cover-external-dns.png\n  ├── how-service-controller-works-on-gke.png            # The image.\n  ├── index.md                                           # The post.\n  └── packet-routing-with-akrobateo.png\n  ```\n\n  If your images are stored in the `static` directory, it should still work.\n\n  Since you can also embed `\u003cimg\u003e` tags in markdown, these are also converted.\n  For example:\n\n  ```markdown\n  \u003cimg src=\"dnat-google-vpc-how-comes-back.svg\"/\u003e\n  ```\n\n  becomes:\n\n  ```text\n  \u003cimg src=\"https://maelvls.dev/you-should-write-comments/dnat-google-vpc-how-comes-back.svg\"/\u003e\n            \u003c------------------\u003e\u003c-----------------------\u003e\n                   base_url                url\n              (from config.yaml)    (from front matter)\n  ```\n\n  Like above, the HTML `\u003cimg\u003e` tag must span a single line.\n\n  Only the following image extensions are converted: png, PNG, jpeg, JPG, jpg,\n  gif, GIF, svg, SVG.\n\n- The GitHub-style anchor IDs are converted to Devto anchor IDs. This is because\n  GitHub-style anchor IDs, which is what Hugo produces, are different from the\n  ones produced by Devto. For example, take the following Markdown:\n\n  ```markdown\n  [`go get -u` vs. `go.mod` (= _*Problem*_)](#go-get--u-vs-gomod--_problem_)\n  ```\n\n  becomes:\n\n  ```markdown\n  [`go get -u` vs. `go.mod` (= _*Problem*_)](#-raw-go-get-u-endraw-vs-raw-gomod-endraw-problem)\n  ```\n\n**Note:** that Hugo uses soft breaks for new lines as per the CommonMark\nspec, but dev.to uses the \"Markdown Here\" conventions which use a hard\nbreak on new lines; to work around that, see the below\n[section](#hugos-hard-breaks-versus-devto-hard-breaks).\n\n```sh\n% hudevto preview ./content/2020/avoid-gke-lb-using-hostport/index.md\n---\ntitle: \"Avoid GKE's expensive load balancer by using hostPort\"\ndescription: \"I want to avoid using the expensive Google Network Load Balancer and instead do the load balancing in-cluster using akrobateo, which acts as a LoadBalancer controller.\"\npublished: true\ntags: \"\"\ndate: 20200120T00:00Z\nseries: \"\"\ncanonical_url: \"https://maelvls.dev/avoid-gke-lb-with-hostport/\"\ncover_image: \"https://maelvls.dev/avoid-gke-lb-with-hostport/cover-external-dns.png\"\n---\n\n\u003e **⚠️ Update 25 April 2020**: Akrobateo has been EOL in January 2020 due to the company going out of business. Their blog post regarding the EOL isn't available anymore and was probably shut down. Fortunately, the Wayback Machine [has a snapshot of the post](https://web.archive.org/web/20200107111252/https://blog.kontena.io/farewell/) (7th January 2020). Here is an excerpt:\n\u003e\n\u003e \u003e This is a sad day for team Kontena. We tried to build something amazing but our plans of creating business around open source software has failed. We couldn't build a sustainable business. Despite all the effort, highs and lows, as of today, Kontena has ceased operations. The team is no more and the official support for Kontena products is no more available.\n\u003e\n\u003e This is so sad... 😢 Note that the Github repo [kontena/akrobateo](https://github.com/kontena/akrobateo) is still there (and has not been archived yet), but their Docker registry has been shut down which means most of this post is broken.\n\nIn my spare time, I maintain a tiny \"playground\" Kubernetes cluster on [GKE](https://cloud.google.com/kubernetes-engine) (helm charts [here](https://github.com/maelvls/k.maelvls.dev)). I quickly realized that realized using `Service type=LoadBalancer` in GKE was spawning a _[Network Load Balancer](https://cloud.google.com/load-balancing/docs/network)_ which costs approximately **\\$15 per month**! In this post, I present a way of avoiding the expensive Google Network Load Balancer by load balancing in-cluster using akrobateo, which acts as a Service type=LoadBalancer controller.\n```\n\n### Push one blog post to dev.to\n\n```sh\n% hudevto push ./content/2020/avoid-gke-lb-using-hostport/index.md\nsuccess: ./content/2020/avoid-gke-lb-using-hostport/index.md pushed published to https://dev.to/maelvls/avoid-gke-s-expensive-load-balancer-by-using-hostport-2ab9 (devtoId: 241275, devtoPublished: true)\n```\n\n### Push all blog posts to dev.to\n\n```sh\n% hudevto push\nsuccess: ./content/notes/dns.md pushed unpublished to https://dev.to/maelvls/it-s-always-the-dns-fault-3lg3-temp-slug-8953915/edit (devtoId: 410260, devtoPublished: false)\nsuccess: ./content/2020/deployment-available-condition/index.md pushed published to https://dev.to/maelvls/understanding-the-available-condition-of-a-kubernetes-deployment-51li (devtoId: 386691, devtoPublished: true)\nsuccess: ./content/2020/docker-proxy-registry-kind/index.md pushed published to https://dev.to/maelvls/pull-through-docker-registry-on-kind-clusters-cpo (devtoId: 410837, devtoPublished: true)\nsuccess: ./content/2020/mitmproxy-kubectl/index.md pushed published to https://dev.to/maelvls/using-mitmproxy-to-understand-what-kubectl-does-under-the-hood-36om (devtoId: 377876, devtoPublished: true)\nsuccess: ./content/2020/static-libraries-and-autoconf-hell/index.md pushed published to https://dev.to/maelvls/epic-journey-with-statically-and-dynamically-linked-libraries-a-so-1khn (devtoId: 365849, devtoPublished: true)\nsuccess: ./content/2020/gh-actions-with-tf-private-repo/index.md pushed published to https://dev.to/maelvls/github-actions-with-a-private-terraform-module-5b85 (devtoId: 331169, devtoPublished: true)\n...\n```\n\n## Notes\n\n### Hugo's hard breaks versus dev.to hard breaks\n\nOne major difference between Hugo and dev.to markdown is that Hugo uses\nsoft breaks whenever it parses a new lines (as per the CommonMark spec); on\nthe other side, dev.to uses the \"Markdown Here\" conventions where a hard\nbreak is used when a new line is parsed.\n\nI was not able to find a way to do the transformation in `hudevto` itself.\nWhat I currently do is to keep my hugo blog source with lines \"unwrapped\"\nsince I used to wrap my markdown files at 80 characters.\n\nTo \"unwrap\" all your markdown line from 80 chars to \"no width limit\", you\ncan use `prettier`:\n\n```sh\nnpm i -g prettier\nprettier --write --prose-wrap=never content/**/*.md\n```\n\n### Known errors\n\n**Validation failed: Canonical url has already been taken** means that\nanother article of yours exists with the same `canonical_url` field in its\nfront matter; it often means that there is a duplicate article.\n\n**Validation failed: Body markdown has already been taken** means that the\nsame markdown body already existings in one of your articles on dev.to.\nOften means that there is a duplicate article.\n\n**Validation failed: (\u003cunknown\u003e): could not find expected ':' while scanning a simple key at line 4 column 1**: you can use the command\n\n```sh\nhudevto preview ./content/2020/gh-actions-with-tf-private-repo/index.md\n```\n\nto see what is being uploaded to dev.to. I often got this error when trying\nto do a multi-line description. I had to change from:\n\n```yaml\ndescription: |\n  We often talk about avoiding unecessary comments that needlessly paraphrase\n  what the code does. In this article, I gathered some thoughts about why\n  writing comments is as important as writing the code itself.\n```\n\nto:\n\n```yaml\ndescription: \"We often talk about avoiding unecessary comments that needlessly paraphrase what the code does. In this article, I gathered some thoughts about why writing comments is as important as writing the code itself.\"\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmaelvls%2Fhudevto","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmaelvls%2Fhudevto","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmaelvls%2Fhudevto/lists"}