jekyll-devto 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5de7210b54c1eeda3f3930fd529ab2acb06d08578b098be5b0bcc9389f2decbb
4
- data.tar.gz: af15eb800bd515a2554b93c431a054edabc161883108301e6f542d10eefa0d43
3
+ metadata.gz: 1aa5e1f448c0e19e840473d8829de5c76d9db2f9f27c9e919243086d07d2b7f9
4
+ data.tar.gz: 9abb009ac797c5009074a1120fca05c0fd15771a7404afa4b3f3ea44f53f4574
5
5
  SHA512:
6
- metadata.gz: 6bf236df31a78ac816447e7fc62f5424cf369e4e1f94287b618e18b16ca15e18e6f629b11b62313e046f8e808ffe4d45bf792ae61979c5c64e96345229ce27d0
7
- data.tar.gz: e30ab831596ed0880e33bac1eeec3874adf4da9fe2670a67cbc48dc215366fc34955f315f6d92d48ec7175424106d7d4450a41a81591b59efbad465ec74b03a5
6
+ metadata.gz: 3557c4a83b0c514893f5acbce8f0ff78fecab9c2139d02a06c65bbfd5299d2324c8d2f88d83a432358a58d1ff93e7accbb941d552e0421364088d052af4d80a8
7
+ data.tar.gz: 25efecb7281df14f9aadb89cf25a756aba07fa99dc1f73b07b63e7ee401bb0e7f155367f91002a9efd26ed334f40433dc3e2af70005a6440e07fe4db847dba65
data/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.2.0](https://github.com/JuanVqz/jekyll-devto/compare/v0.1.0...v0.2.0) (2026-10-02)
4
+
5
+
6
+ ### Features
7
+
8
+ * choose dev.to tags and series from the post's front matter ([#13](https://github.com/JuanVqz/jekyll-devto/issues/13)) ([1aa48f7](https://github.com/JuanVqz/jekyll-devto/commit/1aa48f7a362781f660a54d6e041f043db5a11eb8))
9
+ * **html:** bring code block languages to dev.to ([#11](https://github.com/JuanVqz/jekyll-devto/issues/11)) ([2e62edb](https://github.com/JuanVqz/jekyll-devto/commit/2e62edb47809cc945af3988973e6c44ad9fe2a27))
10
+ * **publisher:** use the post image as the dev.to cover ([#12](https://github.com/JuanVqz/jekyll-devto/issues/12)) ([90707e7](https://github.com/JuanVqz/jekyll-devto/commit/90707e71fbaa5d309bed29ddc455c04bea07237d))
11
+
12
+
13
+ ### Documentation
14
+
15
+ * add gem version and CI badges to the README ([#8](https://github.com/JuanVqz/jekyll-devto/issues/8)) ([a0d2a9c](https://github.com/JuanVqz/jekyll-devto/commit/a0d2a9c47a871597bd4d3f4c1ac113c18b41a323))
16
+
3
17
  ## 0.1.0 (2026-10-01)
4
18
 
5
19
 
data/README.md CHANGED
@@ -1,5 +1,8 @@
1
1
  # jekyll-devto
2
2
 
3
+ [![Gem Version](https://badge.fury.io/rb/jekyll-devto.svg)](https://badge.fury.io/rb/jekyll-devto)
4
+ [![CI](https://github.com/JuanVqz/jekyll-devto/actions/workflows/ci.yml/badge.svg)](https://github.com/JuanVqz/jekyll-devto/actions/workflows/ci.yml)
5
+
3
6
  Cross-post a Jekyll blog to [dev.to](https://dev.to), complete, through dev.to's own RSS import.
4
7
 
5
8
  dev.to can import posts from your feed, but with a typical Jekyll feed you get:
@@ -50,6 +53,19 @@ devto:
50
53
 
51
54
  To keep a post off dev.to, set `devto: false` in its front matter.
52
55
 
56
+ **Cover image.** `jekyll-devto publish` gives each post a dev.to cover: `devto_cover` from the post's front matter, or else the post's `image` (a path, a URL, or `{ path: ... }`, which is what [jekyll-og-image](https://github.com/igor-alexandrov/jekyll-og-image) and Chirpy use). A cover already set on the dev.to draft is kept.
57
+
58
+ ```yaml
59
+ devto_cover: /assets/img/devto-cover.png # optional
60
+ ```
61
+
62
+ **Tags and series.** dev.to keeps only the first four tags, as lowercase letters and digits (`tailwind-css` becomes `tailwindcss`). `devto_tags` picks them for dev.to; it is used for the import and also replaces the tags of a draft that was already imported. `devto_series` puts the post in a dev.to series, created if it does not exist; a series already set on the draft is kept.
63
+
64
+ ```yaml
65
+ devto_tags: [ruby, rails, jekyll, opensource]
66
+ devto_series: "Rails upgrades"
67
+ ```
68
+
53
69
  ## Publishing the drafts
54
70
 
55
71
  ```sh
@@ -89,9 +105,9 @@ Checked against [Forem's source](https://github.com/forem/forem), the software d
89
105
  - **The imported body says `published: false` in its own front matter**, and that wins over the
90
106
  API's `published` field (`Article#evaluate_front_matter`). The publish command flips it inside the
91
107
  body. A plain `published: true` request leaves the post a draft.
92
- - **Code blocks arrive without a language.** dev.to removes every `class` attribute before
93
- converting (`Feeds::CleanHtml`), so syntax highlighting is lost on import whatever the feed says.
94
- - **Only the first four tags are kept**, stripped to letters and digits.
108
+ - **The import drops the language of every code block.** dev.to removes every `class` attribute before converting (`Feeds::CleanHtml`), so a fence never gets a language from the feed. The feed marks each block with `data-lang`, which survives, and `jekyll-devto publish` writes that language into the draft's fences, matching blocks by their first line of code.
109
+ - **Sometimes the import keeps the raw HTML instead of Markdown.** dev.to converts the feed only when block tags outnumber blank lines (`Feeds::AssembleArticleMarkdown#html_content?`). The feed removes blank lines between tags and writes newlines inside code as `
`, so every post takes the Markdown path. Drafts imported before this change may still be raw HTML; their code keeps no language.
110
+ - **Only the first four tags are kept**, stripped to letters and digits and lowercased. Use `devto_tags` to choose them.
95
111
  - **"Replace self-referential links with DEV-specific links"** rewrites links between your posts to
96
112
  their dev.to articles at import time, drafts included. Publish the linked post first, or leave
97
113
  that option off.
@@ -2,9 +2,10 @@
2
2
  {%- assign devto = site.devto | default: empty -%}
3
3
  {%- comment -%} Not site.url + baseurl: links in content already carry baseurl (relative_url adds it). {%- endcomment -%}
4
4
  {%- assign site_url = site.url -%}
5
+ {%- assign site_root = site.url | append: site.baseurl -%}
5
6
  {%- assign posts = site.posts | where_exp: "post", "post.devto != false" -%}
6
7
  {%- if devto.limit %}{% assign posts = posts | slice: 0, devto.limit %}{% endif %}
7
- <rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom">
8
+ <rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:devto="https://github.com/JuanVqz/jekyll-devto">
8
9
  <channel>
9
10
  <title>{{ site.title | default: site.name | xml_escape }}</title>
10
11
  <link>{{ "/" | absolute_url }}</link>
@@ -21,11 +22,25 @@
21
22
  <link>{{ post_url }}</link>
22
23
  <guid isPermaLink="true">{{ post_url }}</guid>
23
24
  <pubDate>{{ post.date | date_to_rfc822 }}</pubDate>
24
- {%- assign tags = post.tags | default: post.categories %}
25
+ {%- comment -%} devto_tags first, so new imports already carry the tags chosen for dev.to. {%- endcomment -%}
26
+ {%- assign devto_tags = post.devto_tags | devto_list %}
27
+ {%- assign tags = devto_tags | default: post.tags | default: post.categories %}
25
28
  {%- for tag in tags %}
26
29
  <category>{{ tag | xml_escape }}</category>
27
30
  {%- endfor %}
28
31
  <description>{{ post.description | default: post.excerpt | strip_html | strip_newlines | truncatewords: 60 | xml_escape }}</description>
32
+ {%- comment -%} The dev.to cover: devto_cover, else the post image (jekyll-og-image sets it). {%- endcomment -%}
33
+ {%- comment -%} Unlike content links, image paths leave baseurl out (Chirpy and jekyll-og-image add it when rendering). {%- endcomment -%}
34
+ {%- assign cover = post.devto_cover | default: post.image | devto_url: site_root %}
35
+ {%- if cover != "" %}
36
+ <devto:cover>{{ cover | xml_escape }}</devto:cover>
37
+ {%- endif %}
38
+ {%- if devto_tags %}
39
+ <devto:tags>{{ devto_tags | join: "," | xml_escape }}</devto:tags>
40
+ {%- endif %}
41
+ {%- if post.devto_series %}
42
+ <devto:series>{{ post.devto_series | xml_escape }}</devto:series>
43
+ {%- endif %}
29
44
  <content:encoded>{{ post.content | devto_html: site_url | xml_escape }}</content:encoded>
30
45
  </item>
31
46
  {%- endfor %}
@@ -13,12 +13,16 @@ module Jekyll
13
13
  # up inside the code.
14
14
  # - Root-relative links and images point nowhere once the post lives on
15
15
  # dev.to.
16
+ #
17
+ # The code language cannot travel as a class, so each block carries it as
18
+ # data-lang, which CleanHtml keeps. ReverseMarkdown ignores it; the
19
+ # publisher reads it from the feed and writes it into the draft's fences.
16
20
  module HTML
17
21
  # Kramdown block options such as {: .nolineno } or {: file="..." } add
18
22
  # classes and attributes to the wrapper, so the class is matched anywhere
19
23
  # in the tag.
20
24
  ROUGE_BLOCK = %r{
21
- <div\b[^>]*\bclass="[^"]*\bhighlighter-rouge\b[^"]*"[^>]*>\s*
25
+ <div\b(?<attrs>[^>]*\bclass="[^"]*\bhighlighter-rouge\b[^"]*"[^>]*)>\s*
22
26
  <div\ class="highlight">\s*<pre\ class="highlight"><code>
23
27
  (?<body>.*?)
24
28
  </code></pre>\s*</div>\s*</div>
@@ -26,7 +30,7 @@ module Jekyll
26
30
 
27
31
  # What the {% highlight %} Liquid tag renders.
28
32
  HIGHLIGHT_TAG = %r{
29
- <figure\ class="highlight"><pre><code\b[^>]*>
33
+ <figure\ class="highlight"><pre><code\b(?<attrs>[^>]*)>
30
34
  (?<body>.*?)
31
35
  </code></pre></figure>
32
36
  }mx
@@ -40,31 +44,103 @@ module Jekyll
40
44
  TAG = /<[a-zA-Z][^>]*>/
41
45
  ROOT_RELATIVE = %r{\b(src|href)="/(?!/)}
42
46
 
47
+ # Kramdown's name for a fence with no language.
48
+ NO_LANGUAGE = %w[plaintext text].freeze
49
+
43
50
  module_function
44
51
 
45
52
  def convert(html, base_url)
46
53
  html.to_s
47
- .gsub(ROUGE_BLOCK) { plain_code(Regexp.last_match[:body]) }
48
- .gsub(HIGHLIGHT_TAG) { plain_code(Regexp.last_match[:body]) }
54
+ .gsub(ROUGE_BLOCK) { plain_code(Regexp.last_match[:body], rouge_language(Regexp.last_match[:attrs])) }
55
+ .gsub(HIGHLIGHT_TAG) { plain_code(Regexp.last_match[:body], Regexp.last_match[:attrs][/\bdata-lang="([^"]+)"/, 1]) }
49
56
  .gsub(TAG) { |tag| tag.gsub(ROOT_RELATIVE, %(\\1="#{base_url.to_s.chomp('/')}/)) }
50
57
  end
51
58
 
52
- # The language is not kept: dev.to strips every class before converting,
53
- # so it would never arrive.
54
- def plain_code(body)
59
+ # Forem converts the HTML to Markdown only when block tags outnumber blank
60
+ # lines (Feeds::AssembleArticleMarkdown#html_content?); otherwise it stores
61
+ # the raw HTML, and none of the conversion (fences, languages) happens.
62
+ # Blank lines between tags are only formatting, and a newline inside code
63
+ # written as &#10; is the same character once parsed, so the post reads
64
+ # the same while the count of blank lines drops to zero.
65
+ def without_blank_lines(html)
66
+ html.split(%r{(<pre\b[^>]*>.*?</pre>)}m).map do |part|
67
+ part.start_with?('<pre') ? part.gsub("\n", '&#10;') : part.gsub(/\n\s*\n/, "\n")
68
+ end.join
69
+ end
70
+
71
+ # Kramdown writes the fence's language as a language-* class, and the
72
+ # language can hold any non-space character (c#, shell.session). It is
73
+ # read from the class attribute only, so another attribute such as
74
+ # file="docs/language-notes.md" cannot pass for it.
75
+ def rouge_language(attrs)
76
+ attrs[/\bclass="([^"]*)"/, 1].to_s[/(?:\A|\s)language-(\S+)/, 1]
77
+ end
78
+
79
+ def plain_code(body, language)
55
80
  body = Regexp.last_match[:code] if body =~ GUTTER
81
+ attribute = language && !NO_LANGUAGE.include?(language) ? %( data-lang="#{language}") : ''
82
+
83
+ "<pre#{attribute}><code>#{body.gsub(/<[^>]+>/, '')}</code></pre>"
84
+ end
85
+
86
+ # A post image as an absolute URL. Takes a path, a URL, or Chirpy's
87
+ # { "path" => ... } hash; nil when there is nothing to point at.
88
+ def absolute_url(value, base_url)
89
+ path = value.is_a?(Hash) ? value['path'] : value
90
+ return if path.to_s.empty?
91
+ return path if path.match?(%r{\Ahttps?://})
92
+ return "#{base_url.to_s[/\A[a-z][a-z0-9+.-]*:/i] || 'https:'}#{path}" if path.start_with?('//')
93
+
94
+ "#{base_url.to_s.chomp('/')}/#{path.sub(%r{\A/}, '')}"
95
+ end
96
+
97
+ CODE_BLOCK = %r{<pre\b(?<attrs>[^>]*)><code\b[^>]*>(?<code>.*?)</code></pre>}m
98
+
99
+ # Every code block in a converted body, in order, as [language, first
100
+ # line of code] (language is nil when the block has none). The first line
101
+ # is what identifies the block in the dev.to draft, whose fences do not
102
+ # always line up one to one with the blocks.
103
+ def code_blocks(html)
104
+ html.to_s.scan(CODE_BLOCK).map do |attrs, code|
105
+ [attrs[/\bdata-lang="([^"]+)"/, 1], first_line(unescape(code))]
106
+ end
107
+ end
108
+
109
+ # Whitespace is collapsed for the comparison: some Markdown converters
110
+ # squeeze runs of spaces inside code.
111
+ def first_line(code)
112
+ code.to_s.each_line.map { |line| line.split.join(' ') }.find { |line| !line.empty? }.to_s
113
+ end
56
114
 
57
- "<pre><code>#{body.gsub(/<[^>]+>/, '')}</code></pre>"
115
+ # The entities this HTML can carry inside code; enough to compare it with
116
+ # the Markdown dev.to stores. &amp; goes last so "&amp;lt;" stays "&lt;".
117
+ def unescape(text)
118
+ text.gsub(/&#(\d+);/) { Regexp.last_match(1).to_i.chr(Encoding::UTF_8) }
119
+ .gsub('&lt;', '<').gsub('&gt;', '>').gsub('&quot;', '"').gsub('&#39;', "'").gsub('&amp;', '&')
58
120
  end
59
121
  end
60
122
 
61
123
  # Liquid filter for the feed template.
62
124
  module Filters
125
+ # devto_tags as a list, whether the front matter wrote a YAML list or a
126
+ # string ("ruby, rails" or "ruby rails", as Jekyll accepts for tags), so
127
+ # the import and the publisher see the same tags. nil when there are none.
128
+ def devto_list(value)
129
+ list = value.is_a?(Array) ? value.map(&:to_s) : value.to_s.split(/[,\s]+/)
130
+ list = list.map(&:strip).reject(&:empty?)
131
+ list unless list.empty?
132
+ end
133
+
134
+ def devto_url(value, base_url)
135
+ HTML.absolute_url(value, base_url).to_s
136
+ end
137
+
63
138
  def devto_html(html, base_url)
64
- HTML.convert(html, base_url)
139
+ HTML.without_blank_lines(HTML.convert(html, base_url))
65
140
  end
66
141
  end
67
142
  end
68
143
  end
69
144
 
70
- Liquid::Template.register_filter(Jekyll::Devto::Filters)
145
+ # The CLI loads this file without Jekyll, only for HTML.code_blocks.
146
+ Liquid::Template.register_filter(Jekyll::Devto::Filters) if defined?(Liquid::Template)
@@ -4,6 +4,7 @@ require 'net/http'
4
4
  require 'rexml/document'
5
5
  require 'time'
6
6
  require_relative 'client'
7
+ require_relative 'html'
7
8
 
8
9
  module Jekyll
9
10
  module Devto
@@ -17,7 +18,7 @@ module Jekyll
17
18
  class Publisher
18
19
  MAX_REDIRECTS = 5
19
20
 
20
- Post = Struct.new(:title, :link, :date, keyword_init: true)
21
+ Post = Struct.new(:title, :link, :date, :code_blocks, :cover, :tags, :series, keyword_init: true)
21
22
 
22
23
  def initialize(feed:, client:, days: 7, publish: false, now: Time.now, out: $stdout, err: $stderr)
23
24
  @feed = feed
@@ -51,7 +52,7 @@ module Jekyll
51
52
 
52
53
  # One rejected post must not stop the rest, or every retry would stop at it.
53
54
  begin
54
- result = @client.update(draft['id'], published: true, body_markdown: self.class.published_body(draft['body_markdown']))
55
+ result = @client.update(draft['id'], published: true, body_markdown: self.class.prepared_body(draft['body_markdown'], post, cover: new_cover(draft, post)))
55
56
  sent[draft['id']] = post
56
57
  @out.puts " sent #{post.title.inspect} -> #{result['url']}"
57
58
  rescue StandardError => e
@@ -76,17 +77,102 @@ module Jekyll
76
77
  end
77
78
  end
78
79
 
80
+ # The draft body to send: published, with each code fence given the
81
+ # language its block had on the site.
82
+ def self.prepared_body(markdown, post, cover: post.cover)
83
+ body = published_body(markdown)
84
+ body = with_front_matter(body, 'cover_image', cover) if cover
85
+ tags = devto_tags(post.tags.to_a)
86
+ body = with_front_matter(body, 'tags', tags, replace: true) unless tags.empty?
87
+ body = with_front_matter(body, 'series', yaml_string(post.series)) if post.series
88
+ with_code_languages(body, post.code_blocks.to_a)
89
+ end
90
+
91
+ # Adds `key: value` to the draft's front matter, which dev.to reads on
92
+ # every save (Article#evaluate_front_matter: cover_image, tags, series).
93
+ # A key the draft already has is kept, since it was set on dev.to on
94
+ # purpose, unless `replace:` says the site's value wins (devto_tags,
95
+ # which the author chose for dev.to explicitly).
96
+ def self.with_front_matter(markdown, key, value, replace: false)
97
+ # The key line plus any continuation lines (an indented or "- " list), so
98
+ # a replaced block list does not leave its items behind.
99
+ line = /^#{Regexp.escape(key)}:[^\r\n]*(?:\r?\n(?:[ \t]|-(?=[ \t\r\n]))[^\r\n]*)*/
100
+ markdown.to_s.sub(/\A---(\r?\n).*?^---[ \t]*\r?$/m) do |front_matter|
101
+ newline = Regexp.last_match(1)
102
+ if front_matter.match?(line)
103
+ next replace ? front_matter.sub(line) { "#{key}: #{value}" } : front_matter
104
+ end
105
+
106
+ front_matter.sub(/^---[ \t]*\r?\z/) { |closing| "#{key}: #{value}#{newline}#{closing}" }
107
+ end
108
+ end
109
+
110
+ # The cleanup dev.to's import applies to tags (Feeds::AssembleArticleMarkdown
111
+ # #get_tags): at most four, spaces and anything but letters and digits
112
+ # removed, 20 characters each. Lowercased as dev.to stores them
113
+ # (ActsAsTaggableOn.force_lowercase), so the dry run shows the real tags.
114
+ def self.devto_tags(tags)
115
+ tags.first(4).map { |tag| tag.delete(' ').gsub(/[^[:alnum:]]/, '')[0..19].downcase }.reject(&:empty?).join(',')
116
+ end
117
+
118
+ # A double-quoted YAML scalar, so a value such as "MDN: Spanish" stays a
119
+ # string instead of breaking the front matter. A JSON string is one.
120
+ def self.yaml_string(value)
121
+ JSON.generate(value.to_s)
122
+ end
123
+
124
+ # Writes each block's language into the draft's code fence. Blocks are
125
+ # matched by their first line of code, not by position: dev.to does not
126
+ # turn every block into a fence (one inside a list item can come out
127
+ # unfenced), and an edited draft may not line up either. Each block is
128
+ # used once, in order: a fence only matches blocks after the last one
129
+ # matched, so a block missing from the draft cannot lend its language to
130
+ # a later fence that starts the same way. A fence nothing matches, or
131
+ # one that already names a language, is left alone.
132
+ def self.with_code_languages(markdown, blocks)
133
+ lines = markdown.to_s.lines
134
+ cursor = 0
135
+ inside = false
136
+ lines.each_with_index do |line, index|
137
+ next unless line =~ /\A\s*```[^\s`]*\s*\z/
138
+
139
+ inside = !inside
140
+ next unless inside && line =~ /\A\s*```\s*\z/
141
+
142
+ first = HTML.first_line(lines[(index + 1)..].take_while { |l| l !~ /\A\s*```/ }.join)
143
+ match = (cursor...blocks.size).find { |i| blocks[i][1] == first }
144
+ next unless match
145
+
146
+ cursor = match + 1
147
+ language = blocks[match].first
148
+ lines[index] = line.sub('```', "```#{language}") if language
149
+ end
150
+ lines.join
151
+ end
152
+
79
153
  def due_posts
80
154
  REXML::Document.new(read_feed).get_elements('//item').filter_map do |item|
81
155
  date = Time.rfc2822(item.elements['pubDate'].text)
82
156
  next if date > @now || date < @now - (@days * 86_400)
83
157
 
84
- Post.new(title: item.elements['title'].text.to_s.strip, link: item.elements['link'].text.to_s.strip, date: date)
158
+ Post.new(title: item.elements['title'].text.to_s.strip, link: item.elements['link'].text.to_s.strip, date: date,
159
+ code_blocks: HTML.code_blocks(item.elements['content:encoded']&.text),
160
+ cover: item.elements['devto:cover']&.text,
161
+ tags: item.elements['devto:tags']&.text&.split(','),
162
+ series: item.elements['devto:series']&.text)
85
163
  end
86
164
  end
87
165
 
88
166
  private
89
167
 
168
+ # A cover chosen in dev.to's editor lives in the article (main_image),
169
+ # not in the body's front matter, and a cover_image key in the front
170
+ # matter would replace it (Article#set_main_image). The API reports it
171
+ # as cover_image, so a draft that has one is left with it.
172
+ def new_cover(draft, post)
173
+ post.cover if draft['cover_image'].to_s.empty?
174
+ end
175
+
90
176
  # Follows redirects (http to https, apex to www), which Net::HTTP does not.
91
177
  def read_feed
92
178
  return File.read(@feed) unless @feed.match?(%r{\Ahttps?://})
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Jekyll
4
4
  module Devto
5
- VERSION = '0.1.0'
5
+ VERSION = '0.2.0'
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jekyll-devto
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Juan Vásquez
@@ -48,7 +48,7 @@ description: |
48
48
  Rouge line numbers, absolute links and images), and a command that
49
49
  publishes the imported drafts once their post is live on your site.
50
50
  email:
51
- - juan@ombulabs.com
51
+ - hello@juanvasquez.dev
52
52
  executables:
53
53
  - jekyll-devto
54
54
  extensions: []