rawfeed 0.1.4 → 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.
Files changed (102) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +20 -21
  3. data/README.md +12 -130
  4. data/_data/options.yml +270 -0
  5. data/_data/resume.yml +8 -8
  6. data/_includes/alert +3 -1
  7. data/_includes/chart +13 -32
  8. data/_includes/details +1 -57
  9. data/_includes/image +12 -4
  10. data/_includes/layout/blog_search.html +6 -4
  11. data/_includes/layout/data.liquid +21 -3
  12. data/_includes/layout/disqus.html +12 -26
  13. data/_includes/layout/footer.html +33 -17
  14. data/_includes/layout/giscus.html +27 -19
  15. data/_includes/layout/head.html +41 -41
  16. data/_includes/layout/header.html +127 -101
  17. data/_includes/layout/maintenance.html +6 -10
  18. data/_includes/layout/paginator.html +6 -4
  19. data/_includes/socials +7 -5
  20. data/_includes/tabs +1 -94
  21. data/_includes/toc +11 -194
  22. data/_includes/video +4 -1
  23. data/_layouts/blog.html +8 -7
  24. data/_layouts/contact.html +90 -196
  25. data/_layouts/default.html +42 -341
  26. data/_layouts/error.html +6 -4
  27. data/_layouts/home.html +45 -36
  28. data/_layouts/licenses.html +10 -0
  29. data/_layouts/page.html +4 -4
  30. data/_layouts/pixel.html +48 -0
  31. data/_layouts/pixels.html +71 -1
  32. data/_layouts/post.html +28 -29
  33. data/_layouts/resume.html +41 -34
  34. data/_layouts/tag.html +14 -3
  35. data/_layouts/tag_posts.html +3 -3
  36. data/_sass/base/_index.scss +39 -3
  37. data/_sass/components/_badges.scss +10 -0
  38. data/_sass/components/_markdown.scss +8 -5
  39. data/_sass/includes/_footer.scss +5 -2
  40. data/_sass/includes/_header.scss +23 -19
  41. data/_sass/includes/_highlight.scss +20 -7
  42. data/_sass/includes/_maintenance.scss +2 -3
  43. data/_sass/includes/_terminal.scss +35 -12
  44. data/_sass/layouts/_blog.scss +13 -9
  45. data/_sass/layouts/_contact.scss +6 -5
  46. data/_sass/layouts/_default.scss +5 -5
  47. data/_sass/layouts/_index.scss +3 -0
  48. data/_sass/layouts/_licenses.scss +7 -0
  49. data/_sass/layouts/_page.scss +1 -0
  50. data/_sass/layouts/_pixel.scss +61 -0
  51. data/_sass/layouts/_pixels.scss +86 -0
  52. data/_sass/layouts/_post.scss +4 -11
  53. data/_sass/layouts/_resume.scss +16 -3
  54. data/_sass/layouts/_tag-posts.scss +1 -2
  55. data/_sass/layouts/_tag.scss +12 -1
  56. data/_sass/main.scss +16 -1
  57. data/_sass/theme/_dark.scss +8 -1
  58. data/_sass/theme/_light.scss +8 -1
  59. data/assets/images/blog/.keep +0 -0
  60. data/assets/images/pixels/luffy.jpg +0 -0
  61. data/assets/js/blog.coffee +102 -0
  62. data/assets/js/contact.coffee +105 -0
  63. data/assets/js/default.coffee +172 -0
  64. data/assets/js/discus.coffee +30 -0
  65. data/assets/js/fallback/README.md +3 -0
  66. data/assets/js/fallback/blog.js +113 -0
  67. data/assets/js/fallback/contact.js +116 -0
  68. data/assets/js/{default.js → fallback/default.js} +50 -0
  69. data/assets/js/fallback/discus.js +32 -0
  70. data/{_includes/layout/google_analytics.html → assets/js/fallback/google_analytics.js} +7 -3
  71. data/assets/js/fallback/home.js +275 -0
  72. data/assets/js/fallback/no_inframe.js +4 -0
  73. data/assets/js/fallback/page.js +423 -0
  74. data/assets/js/fallback/pixels.js +1 -0
  75. data/assets/js/fallback/resume.js +13 -0
  76. data/assets/js/fallback/tags.js +1 -0
  77. data/{_includes/layout/capture_scripts.liquid → assets/js/fallback/theme_load.js} +0 -2
  78. data/assets/js/google_analytics.coffee +24 -0
  79. data/assets/js/home.coffee +250 -0
  80. data/assets/js/no_inframe.coffee +9 -0
  81. data/assets/js/page.coffee +379 -0
  82. data/assets/js/pixels.coffee +2 -0
  83. data/assets/js/resume.coffee +9 -0
  84. data/assets/js/tags.coffee +2 -0
  85. data/assets/js/theme_load.coffee +6 -0
  86. data/assets/json/blog_search.json +2 -2
  87. data/lib/rawfeed/author.rb +59 -0
  88. data/lib/rawfeed/csp_filters.rb +3 -0
  89. data/lib/rawfeed/draft.rb +1 -1
  90. data/lib/rawfeed/layout.rb +7 -0
  91. data/lib/rawfeed/page.rb +2 -2
  92. data/lib/rawfeed/pixel.rb +32 -0
  93. data/lib/rawfeed/post.rb +2 -2
  94. data/lib/rawfeed/resume.rb +1 -0
  95. data/lib/rawfeed/typescript_liquid.rb +172 -0
  96. data/lib/rawfeed/utils.rb +1 -0
  97. data/lib/rawfeed/version.rb +1 -1
  98. data/lib/rawfeed/with_class.rb +20 -0
  99. data/lib/rawfeed.rb +5 -1
  100. metadata +44 -12
  101. data/assets/js/avatar.js +0 -59
  102. data/assets/js/terminal.js +0 -18
data/_includes/toc CHANGED
@@ -1,203 +1,20 @@
1
- <nav class="toc" id="toc" data-toc-selector="{{ include.selector | default: '.post-content' }}"
1
+ {%- include layout/data.liquid -%}
2
+
3
+ <nav id="toc"
4
+ style="border-top-left-radius: 0px !important; border-top-right-radius: 0px !important;"
5
+ class="toc{% if default_.rounding %} rounding-plugins{% endif %}{% if default_.background_focus %} background_focus{% endif %}"
6
+ data-toc-selector="{{ include.selector | default: '.post-content' }}"
2
7
  data-toc-max-level="{{ include.max_level | default: 3 }}"
3
- data-toc-scroll-offset="{{ include.scroll_offset | default: 20 }}">
8
+ data-toc-scroll-offset="{{ include.scroll_offset | default: 20 }}"
9
+ data-btn-show="{{ include.btn_show }}"
10
+ data-btn-hidden="{{ include.btn_hidden }}">
11
+
4
12
  <div class="toc-head">
5
13
  <h2>{{ include.title | default: "TOC" }}</h2>
6
- <button class="toc-toggle" aria-expanded="true" type="button">{{ include.btn_hidden }}</button>
14
+ <button class="toc-toggle" aria-expanded="true" type="button"></button>
7
15
  </div>
8
16
  <div class="toc-list-wrapper">
9
17
  <ul class="toc-list" role="list"></ul>
10
18
  <p class="toc-empty" style="display:none">No titles found. Remove TOC!</p>
11
19
  </div>
12
20
  </nav>
13
-
14
- <script>
15
- (function () {
16
- const minLayoutWidth = 1830;
17
-
18
- const toc = document.getElementById('toc');
19
- if (toc) {
20
- const sentinel = document.createElement('div');
21
- toc.parentNode.insertBefore(sentinel, toc);
22
-
23
- function shouldApplyFixed() {
24
- return window.innerWidth > minLayoutWidth;
25
- }
26
-
27
- const observer = new IntersectionObserver(
28
- ([entry]) => {
29
- if (shouldApplyFixed()) {
30
- if (!entry.isIntersecting) {
31
- toc.classList.add('toc-fixed');
32
- } else {
33
- toc.classList.remove('toc-fixed');
34
- }
35
- } else {
36
- toc.classList.remove('toc-fixed');
37
- }
38
- },
39
- { threshold: 0 }
40
- );
41
- observer.observe(sentinel);
42
-
43
- window.addEventListener('resize', () => {
44
- if (!shouldApplyFixed()) {
45
- toc.classList.remove('toc-fixed');
46
- }
47
- });
48
- }
49
-
50
- function slugify(text) {
51
- if (!text) return '';
52
- return text.toString().toLowerCase().trim()
53
- .normalize('NFKD').replace(/[\u0300-\u036f]/g, '')
54
- .replace(/[^\w\s-]/g, '').replace(/\s+/g, '-').replace(/--+/g, '-');
55
- }
56
-
57
- function buildTOC(tocEl) {
58
- const selector = tocEl.dataset.tocSelector || '.post-content' || '.page-content';
59
- const maxLevel = parseInt(tocEl.dataset.tocMaxLevel || '3', 10);
60
- const offset = parseInt(tocEl.dataset.tocScrollOffset || '20', 10);
61
- const root = document.querySelector(selector);
62
-
63
- if (!root) {
64
- tocEl.querySelector('.toc-empty').textContent = `Content not found (${selector})`;
65
- tocEl.querySelector('.toc-empty').style.display = 'block';
66
- return;
67
- }
68
-
69
- const headings = Array.from(root.querySelectorAll(Array(maxLevel).fill(0).map((_, i) => `h${i + 1}`).join(',')))
70
- .filter(h => !tocEl.contains(h))
71
- .filter(h => parseInt(h.tagName.substring(1)) <= maxLevel);
72
-
73
- if (headings.length === 0) {
74
- tocEl.querySelector('.toc-empty').style.display = 'block';
75
- return;
76
- }
77
-
78
- const tocRoot = tocEl.querySelector('.toc-list');
79
- tocRoot.innerHTML = '';
80
-
81
- const idCounts = {};
82
- headings.forEach(h => {
83
- if (!h.id) {
84
- let id = slugify(h.textContent);
85
- if (!id) id = 'section';
86
- if (idCounts[id]) { idCounts[id] += 1; id = id + '-' + idCounts[id]; }
87
- else idCounts[id] = 1;
88
- h.id = id;
89
- }
90
- });
91
-
92
- // Build hierarchical tree
93
- const stack = [{ level: 0, ul: tocRoot }];
94
- headings.forEach((h, i) => {
95
- const level = parseInt(h.tagName.substring(1));
96
- const li = document.createElement('li');
97
- const a = document.createElement('a');
98
- a.href = '#' + h.id;
99
- a.textContent = h.textContent.trim();
100
- a.addEventListener('click', e => {
101
- e.preventDefault();
102
- window.scrollTo({ top: h.getBoundingClientRect().top + window.scrollY - offset, behavior: 'smooth' });
103
- history.replaceState(null, '', '#' + h.id);
104
-
105
- // // Clicking on the TOC menu closes the TOC
106
- // const wrapper = tocEl.querySelector('.toc-list-wrapper');
107
- // const toggle = tocEl.querySelector('.toc-toggle');
108
- // wrapper.style.display = 'none';
109
- // toggle.setAttribute('aria-expanded', 'false');
110
- // toggle.textContent = '{ { include.btn_show } }';
111
- });
112
- li.appendChild(a);
113
-
114
- while (stack.length > 1 && level <= stack[stack.length - 1].level) stack.pop();
115
- const parent = stack[stack.length - 1].ul;
116
- parent.appendChild(li);
117
-
118
- const next = headings[i + 1];
119
- if (next) {
120
- const nextLevel = parseInt(next.tagName.substring(1));
121
- if (nextLevel > level) {
122
- const newUl = document.createElement('ul');
123
- li.appendChild(newUl);
124
- stack.push({ level, ul: newUl });
125
- }
126
- }
127
- });
128
-
129
- // Active Highlight
130
- const links = tocRoot.querySelectorAll('a');
131
- function onScroll() {
132
- const fromTop = window.scrollY + offset + 1;
133
- let current = headings[0];
134
- for (let i = 0; i < headings.length; i++) {
135
- if (headings[i].offsetTop <= fromTop) current = headings[i];
136
- }
137
- links.forEach(l => l.classList.toggle('active', l.getAttribute('href') === '#' + current.id));
138
- }
139
- window.addEventListener('scroll', onScroll, { passive: true });
140
- onScroll();
141
- }
142
-
143
- document.addEventListener('DOMContentLoaded', function () {
144
- document.querySelectorAll('.toc').forEach(toc => {
145
- buildTOC(toc);
146
- const toggle = toc.querySelector('.toc-toggle');
147
- const wrapper = toc.querySelector('.toc-list-wrapper');
148
-
149
- wrapper.style.display = 'none';
150
- toggle.setAttribute('aria-expanded', 'false');
151
- toggle.textContent = '{{ include.btn_show }}';
152
-
153
- toggle.addEventListener('click', () => {
154
- const expanded = toggle.getAttribute('aria-expanded') === 'true';
155
- wrapper.style.display = expanded ? 'none' : 'block';
156
- toggle.setAttribute('aria-expanded', (!expanded).toString());
157
- toggle.textContent = expanded ? '{{ include.btn_show }}' : '{{ include.btn_hidden }}';
158
- });
159
-
160
- const tocTop = toc.offsetTop;
161
-
162
- function handleScrollFix() {
163
- if (window.innerWidth <= minLayoutWidth) {
164
- toc.classList.remove('fixed');
165
- toc.style.position = '';
166
- toc.style.top = '';
167
- toc.style.zIndex = '';
168
- toc.style.width = '';
169
- return;
170
- }
171
-
172
- const scrollTop = window.scrollY || document.documentElement.scrollTop;
173
-
174
- // Fixes pinning behavior in Chrome
175
- if (scrollTop >= tocTop) {
176
- toc.classList.add('fixed');
177
- toc.style.position = 'fixed';
178
- toc.style.top = '0';
179
- toc.style.zIndex = '9999';
180
- // toc.style.width = toc.offsetWidth + 'px';
181
- } else {
182
- toc.classList.remove('fixed');
183
- toc.style.position = '';
184
- toc.style.top = '';
185
- toc.style.width = '';
186
- }
187
-
188
- // // Automatically closes the TOC if it is open and scrolled to it.
189
- // const rect = toc.getBoundingClientRect();
190
- // if (rect.top <= 0 && toggle.getAttribute('aria-expanded') === 'true') {
191
- // wrapper.style.display = 'none';
192
- // toggle.setAttribute('aria-expanded', 'false');
193
- // toggle.textContent = '{ { include.btn_show } }';
194
- // }
195
- }
196
-
197
- window.addEventListener('scroll', handleScrollFix, { passive: true });
198
- window.addEventListener('resize', handleScrollFix);
199
- handleScrollFix();
200
- });
201
- });
202
- })();
203
- </script>
data/_includes/video CHANGED
@@ -1,4 +1,7 @@
1
- <div class="video-wrapper">
1
+ {%- include layout/data.liquid -%}
2
+
3
+
4
+ <div class="video-wrapper{% if default_.rounding %} rounding-plugins{% endif %}">
2
5
  <iframe src="{{ include.url }}" title="{{ include.title | default: 'Video' }}"
3
6
  frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen>
4
7
  </iframe>
data/_layouts/blog.html CHANGED
@@ -12,12 +12,15 @@ layout: default
12
12
  {%- endfor -%}
13
13
  {%- endif -%}
14
14
 
15
- <div class="container blog">
15
+ <div id="blog" class="blog{% if default_.background_focus %} background_focus{% endif %} {% if default_.rounding %} rounding{% endif %}">
16
16
  {%- include layout/blog_search.html -%}
17
17
 
18
18
  <div id="posts" class="row">
19
+ {%- if page.description -%}
20
+ <div class="blog-description"><span>»&nbsp;</span>{{ page.description | markdownify }}</div>
21
+ {%- endif -%}
19
22
  {%- if page.title -%}
20
- <h2 class="blog-subtitle">[&nbsp;{{ site.strings.blog.subtitle | default: "posts" }}:&nbsp;&nbsp;{{ count_posts }}&nbsp;]</h2>
23
+ <h2 class="blog-subtitle">[&nbsp;{{ blog_.subtitle | default: "posts" }}:&nbsp;&nbsp;{{ count_posts }}&nbsp;]</h2>
21
24
  {%- endif -%}
22
25
 
23
26
  {%- if site.posts.size > 0 -%}
@@ -30,14 +33,12 @@ layout: default
30
33
 
31
34
  {%- for post in posts -%}
32
35
  <li class="blog-list__item">
33
- {%- if site.datelang -%}
34
- <span class="blog-list__meta">{% datelang post.date format:date_format %}</span>&nbsp;»&nbsp;
36
+ {%- if datelang_ -%}
37
+ <span class="blog-list__meta">{% datelang post.date format:datelang_.format %}</span>&nbsp;»&nbsp;
35
38
  {%- else -%}
36
39
  <span class="blog-list__meta">{{ post.date | date: "%b %-d, %Y" }}</span>&nbsp;»&nbsp;
37
40
  {%- endif -%}
38
- <a class="blog-list__link" href="{{ post.url | relative_url }}">
39
- {{ post.title | escape }}
40
- </a>
41
+ <a class="blog-list__link" href="{{ post.url | relative_url }}">{{ post.title | escape }}</a>
41
42
  {%- if post.draft -%}
42
43
  <span class="draft-badge">[&nbsp;this is a draft&nbsp;]</span>
43
44
  {%- endif -%}
@@ -2,18 +2,21 @@
2
2
  layout: default
3
3
  ---
4
4
 
5
- {%- if site.google.recaptcha.pubkey and site.google.apps_script.url -%}
5
+ {%- include layout/data.liquid -%}
6
+
7
+ {%- if head_.google.recaptcha.pubkey and head_.google.apps_script.url -%}
6
8
 
7
9
  <script src="https://www.google.com/recaptcha/api.js" async defer></script>
8
10
 
9
- <div class="container contact">
11
+ <div id="contact" class="contact{% if default_.background_focus %} background_focus{% endif %} {% if default_.rounding %} rounding{% endif %}">
12
+
10
13
  <div class="modal fade"
11
- id="contactMessageModal"
12
- tabindex="-1"
13
- aria-labelledby="contactMessageModalLabel"
14
- aria-hidden="true"
15
- data-bs-backdrop="static"
16
- data-bs-keyboard="false">
14
+ id="contactMessageModal"
15
+ tabindex="-1"
16
+ aria-labelledby="contactMessageModalLabel"
17
+ aria-hidden="true"
18
+ data-bs-backdrop="static"
19
+ data-bs-keyboard="false">
17
20
  <div class="modal-dialog">
18
21
  <div class="modal-content">
19
22
  <div class="modal-header">
@@ -29,41 +32,39 @@ layout: default
29
32
  <span class="contact-title"><strong>[&nbsp;{{ page.title | downcase }}&nbsp;]</strong></span>
30
33
  <form id="contactForm" class="mt-4 contact-form">
31
34
  <div class="mb-3">
32
- {% comment %} <label for="inputName" class="form-label">{{ site.strings.contact.name | default: "Name" | downcase }}</label> {% endcomment %}
33
35
  <input id="inputName"
34
36
  name="name"
35
37
  type="text"
36
- placeholder="{{ site.strings.contact.name.placeholder | default: 'First and last name' }}"
38
+ placeholder="{{ contact_.name.placeholder | default: 'First and last name' }}"
37
39
  class="form-control contact-form__name"
38
40
  required>
39
41
  </div>
40
42
  <div class="mb-3">
41
- {% comment %} <label for="inputEmail" class="form-label">{{ site.strings.contact.name | default: "Email address" | downcase }}</label> {% endcomment %}
42
43
  <input id="inputEmail"
43
44
  name="email"
44
45
  type="email"
45
- placeholder="{{ site.strings.contact.email.placeholder | default: 'Your best email address' }}"
46
+ placeholder="{{ contact_.email.placeholder | default: 'Your best email address' }}"
46
47
  class="form-control contact-form__email"
47
48
  aria-describedby="emailHelp"
48
49
  required>
49
50
  <div id="emailHelp" class="form-text contact-form__help">
50
- {{ site.strings.contact.email.help | default: "We'll never share your email with anyone else." }}
51
+ {{ contact_.email.help | default: "We'll never share your email with anyone else." }}
51
52
  </div>
52
53
  </div>
53
54
  <textarea id="textMessage"
54
55
  name="message"
55
56
  class="form-control contact-form__message"
56
- placeholder="{{ site.strings.contact.message.placeholder | default: 'Write your message here' }}"
57
+ placeholder="{{ contact_.message.placeholder | default: 'Write your message here' }}"
57
58
  style="min-height: 150px"
58
59
  required></textarea>
59
- <small style="display: block; font-size: 8pt; opacity: .6">{{ site.strings.contact.message.caracters.warning.content }}</small>
60
- <!-- TODO: version: 0.2.0 Make reCaptcha change themes instantly -->
61
- <div id="g-recaptcha" class="g-recaptcha mt-2" data-sitekey="{{ site.google.recaptcha.pubkey }}" style="display: inline-block; margin: 5px 0;"></div>
60
+ <small style="display: block; font-size: 8pt; opacity: .6">{{ contact_.message.caracters.warning.content }}</small>
61
+ <!-- TODO: version: 0.3.0 Make reCaptcha change themes instantly -->
62
+ <div id="g-recaptcha" class="g-recaptcha mt-2" data-sitekey="{{ head_.google.recaptcha.pubkey }}" style="display: inline-block; margin: 5px 0;"></div>
62
63
  <div class="d-flex justify-content-end mb-5">
63
64
  <button id="submitButton"
64
65
  type="submit"
65
66
  class="btn contact-form__submit">
66
- {{ site.strings.contact.button.text | default: 'Send!' }}
67
+ {{ contact_.button.text | default: 'Send!' }}
67
68
  </button>
68
69
  </div>
69
70
  </form>
@@ -73,119 +74,9 @@ layout: default
73
74
  </div>
74
75
  </div>
75
76
 
76
- <script>
77
- const form = document.getElementById("contactForm");
78
- const submitButton = document.getElementById("submitButton");
79
- const endpoint = "{{ site.google.apps_script.url }}"; // URL Google Apps Script
80
-
81
- // get modal
82
- function showModal(title, message, type = 'success') {
83
- const modalEl = document.getElementById('contactMessageModal');
84
- const modalTitle = modalEl.querySelector('.modal-title');
85
- const modalBody = modalEl.querySelector('.modal-body');
86
- const modalContent = modalEl.querySelector('.modal-content');
87
-
88
- modalContent.classList.remove('contact-message-success', 'contact-message-error', 'contact-message-warning');
89
-
90
-
91
- // Apply the color according to the type
92
- if (type === 'success') {
93
- modalContent.classList.add('contact-message-success');
94
- } else if (type === 'error') {
95
- modalContent.classList.add('contact-message-error');
96
- } else if (type === 'warning') {
97
- modalContent.classList.add('contact-message-warning');
98
- }
99
-
100
- modalTitle.innerHTML = title;
101
- modalBody.innerHTML = message;
102
-
103
- const bsModal = new bootstrap.Modal(modalEl);
104
- bsModal.show();
105
- }
106
-
107
- form.addEventListener("submit", async (e) => {
108
- e.preventDefault();
109
-
110
- const recaptchaResponse = grecaptcha.getResponse();
111
- if (!recaptchaResponse) {
112
- showModal(
113
- "{{ site.strings.contact.recaptcha.warning.title | default: 'Warning' }}",
114
- "{{ site.strings.contact.recaptcha.warning.content | default: "Please tick the 'I'm not a robot' box." }}",
115
- "warning"
116
- );
117
- return;
118
- }
119
-
120
- const textarea = document.getElementById('textMessage');
121
- const text = textarea.value.trim();
122
- if (text.length < {{ site.strings.contact.message.caracters.min }}) {
123
- showModal(
124
- "{{ site.strings.contact.message.caracters.warning.title | default: 'Warning' }}",
125
- "{{ site.strings.contact.message.caracters.warning.content | default: "The message must have at least 50 characters." }}",
126
- "warning"
127
- );
128
- return;
129
- }
130
-
131
- submitButton.disabled = true;
132
- submitButton.textContent = "{{ site.strings.contact.message.status | default: "Sending...Wait" }}";
133
-
134
- const formData = new FormData(form);
135
- const data = Object.fromEntries(formData.entries());
136
-
137
- try {
138
- const response = await fetch(endpoint, {
139
- method: "POST",
140
- redirect: "follow",
141
- body: JSON.stringify(data)
142
- });
143
-
144
- const result = await response.json();
145
-
146
- if (result.result === 'success') {
147
- form.reset();
148
- grecaptcha.reset();
149
- showModal(
150
- "{{ site.strings.contact.message.success.title | default: 'Message Sent' }}",
151
- "{{ site.strings.contact.message.success.content | default: 'Your message has been sent successfully!' }}",
152
- "success"
153
- );
154
- } else {
155
- showModal(
156
- "{{ site.strings.contact.message.error.title | default: 'Error' }}",
157
- "{{ site.strings.contact.message.error.content | default: 'Something went wrong while sending your message.' }}",
158
- "error"
159
- );
160
- throw new Error(result.message || "{{ site.strings.contact.message.error.content | default: "An unknown error has occurred." }}");
161
- }
162
-
163
- } catch (error) {
164
- console.error("Error sending:", error);
165
- if (error.message.includes("reCAPTCHA")) {
166
- showModal(
167
- "{{ site.strings.contact.message.error.title | default: 'Error' }}",
168
- "{{ site.strings.contact.recaptcha.fail | default: "Verification failed. Please reload the page and try again." }}",
169
- "error"
170
- );
171
- } else {
172
- showModal(
173
- "{{ site.strings.contact.message.error.title | default: 'Error' }}",
174
- "{{ site.strings.contact.recaptcha.error | default: "An error occurred while sending the message. Please try again." }}",
175
- "error"
176
- );
177
- }
178
- grecaptcha.reset();
179
-
180
- } finally {
181
- submitButton.disabled = false;
182
- submitButton.textContent = "{{ site.strings.contact.button.text | default: "Send!" }}";
183
- }
184
- });
185
- </script>
186
77
  {%- else -%}
187
78
 
188
- <div class="contat-disabled">
79
+ <div class="contact-disabled{% if default_.background_focus %} background_focus{% endif %} {% if default_.rounding %} rounding{% endif %}">
189
80
  <h1 style="background-color: yellow;color: black;padding: 10px">Warning: Email form disabled</h1>
190
81
  <p>To use the email submission form, you need to:</p>
191
82
  <p>1 - Copy the script below and implement it in <a href="https://script.google.com" target="_blank">Google Apps Script</a>:</p>
@@ -194,92 +85,95 @@ layout: default
194
85
  <p>Note2: Without editing the script in Google Apps Script, you need to deploy it again.</p>
195
86
  </blockquote>
196
87
 
197
- {% highlight javascript linenos %}
198
- // IMPORTANT! You must put your gmail email here.
199
- const TO_ADDRESS = "YOUR EMAIL GMAIL";
88
+ {% highlight javascript linenos %}
89
+ // IMPORTANT! You must put your gmail email here.
90
+ const TO_ADDRESS = "YOUR EMAIL GMAIL";
200
91
 
201
- // Get the secret key from the Script Properties
202
- const RECAPTCHA_SECRET_KEY = PropertiesService.getScriptProperties().getProperty('RECAPTCHA_SECRET_KEY');
92
+ // Get the secret key from the Script Properties
93
+ const RECAPTCHA_SECRET_KEY = PropertiesService.getScriptProperties().getProperty('RECAPTCHA_SECRET_KEY');
203
94
 
204
- // Function to validate the reCAPTCHA token
205
- function validateRecaptcha(token) {
206
- if (!token) {
207
- throw new Error("Missing reCAPTCHA token.");
208
- }
209
- const url = "https://www.google.com/recaptcha/api/siteverify";
210
- const payload = {
211
- secret: RECAPTCHA_SECRET_KEY,
212
- response: token
213
- };
95
+ // Function to validate the reCAPTCHA token
96
+ function validateRecaptcha(token) {
97
+ if (!token) {
98
+ throw new Error("Missing reCAPTCHA token.");
99
+ }
100
+ const url = "https://www.google.com/recaptcha/api/siteverify";
101
+ const payload = {
102
+ secret: RECAPTCHA_SECRET_KEY,
103
+ response: token
104
+ };
105
+
106
+ const response = UrlFetchApp.fetch(url, {
107
+ method: "post",
108
+ payload: payload
109
+ });
214
110
 
215
- const response = UrlFetchApp.fetch(url, {
216
- method: "post",
217
- payload: payload
218
- });
111
+ const result = JSON.parse(response.getContentText());
219
112
 
220
- const result = JSON.parse(response.getContentText());
113
+ if (!result.success) {
114
+ throw new Error("reCAPTCHA verification failed: " + (result['error-codes'] || 'Unknown error.'));
115
+ }
221
116
 
222
- if (!result.success) {
223
- throw new Error("reCAPTCHA verification failed: " + (result['error-codes'] || 'Unknown error.'));
117
+ return true;
224
118
  }
225
119
 
226
- return true;
227
- }
228
-
229
-
230
- function doPost(e) {
231
- try {
232
- const data = JSON.parse(e.postData.contents);
233
120
 
234
- // 1. Validate the reCAPTCHA token first!
235
- validateRecaptcha(data['g-recaptcha-response']);
121
+ function doPost(e) {
122
+ try {
123
+ const data = JSON.parse(e.postData.contents);
236
124
 
237
- // 2. If validation passed, continue with the rest of the code
238
- const { name, email, message } = data;
125
+ // 1. Validate the reCAPTCHA token first!
126
+ validateRecaptcha(data['g-recaptcha-response']);
239
127
 
240
- if (!name || !email || !message) {
241
- throw new Error("Missing form data.");
242
- }
128
+ // 2. If validation passed, continue with the rest of the code
129
+ const { name, email, message } = data;
243
130
 
244
- const subject = "New website message " + name;
245
- const htmlBody = `
246
- <p>You have received a new message from your website.:</p><hr>
247
- <p><b>Name:</b> ${name}</p>
248
- <p><b>Email:</b> <a href="mailto:${email}">${email}</a></p>
249
- <p><b>Message:</b></p>
250
- <p style="white-space: pre-wrap;">${message}</p><hr>
251
- `;
131
+ if (!name || !email || !message) {
132
+ throw new Error("Missing form data.");
133
+ }
252
134
 
253
- MailApp.sendEmail({
254
- to: TO_ADDRESS,
255
- subject: subject,
256
- htmlBody: htmlBody,
257
- replyTo: email
258
- });
135
+ const subject = "New website message " + name;
136
+ const htmlBody = `
137
+ <p>You have received a new message from your website.:</p><hr>
138
+ <p><b>Name:</b> ${name}</p>
139
+ <p><b>Email:</b> <a href="mailto:${email}">${email}</a></p>
140
+ <p><b>Message:</b></p>
141
+ <p style="white-space: pre-wrap;">${message}</p><hr>
142
+ `;
143
+
144
+ MailApp.sendEmail({
145
+ to: TO_ADDRESS,
146
+ subject: subject,
147
+ htmlBody: htmlBody,
148
+ replyTo: email
149
+ });
259
150
 
260
- return ContentService
261
- .createTextOutput(JSON.stringify({ 'result': 'success', 'message': 'Message sent!' }))
262
- .setMimeType(ContentService.MimeType.JSON);
151
+ return ContentService
152
+ .createTextOutput(JSON.stringify({ 'result': 'success', 'message': 'Message sent!' }))
153
+ .setMimeType(ContentService.MimeType.JSON);
263
154
 
264
- } catch (err) {
265
- Logger.log(err.toString());
266
- return ContentService
267
- .createTextOutput(JSON.stringify({ 'result': 'error', 'message': err.toString() }))
268
- .setMimeType(ContentService.MimeType.JSON);
155
+ } catch (err) {
156
+ Logger.log(err.toString());
157
+ return ContentService
158
+ .createTextOutput(JSON.stringify({ 'result': 'error', 'message': err.toString() }))
159
+ .setMimeType(ContentService.MimeType.JSON);
160
+ }
269
161
  }
270
- }
271
- {% endhighlight %}
162
+ {% endhighlight %}
163
+
272
164
  <p>2 - Create a <a href="https://console.cloud.google.com/security/recaptcha" target="_blank">reCaptcha</a>
273
165
  on Google and add the reCaptcha <strong>PRIVATE</strong> key to the <strong>Google Apps Script</strong> script property.</p>
274
166
  <p>3 - Copy the reCaptcha <strong>PUBLIC</strong> key and the <strong>Google Apps Script</strong> URL and place them in <strong>_config.yml:</strong></p>
167
+
275
168
  {% highlight yml linenos %}
276
- google:
277
- ###
278
- ###
279
- apps_script:
280
- url: "https://script.google.com/macros/s/BuD..."
281
- recaptcha:
282
- pubkey: "8Lci194rAAAAA70Sv..."
169
+ google:
170
+ ###
171
+ ###
172
+ apps_script:
173
+ url: "https://script.google.com/macros/s/BuD..."
174
+ recaptcha:
175
+ pubkey: "8Lci194rAAAAA70Sv..."
283
176
  {% endhighlight %}
177
+
284
178
  </div>
285
179
  {%- endif -%}