jekyll-theme-resume 1.3.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 (123) hide show
  1. checksums.yaml +7 -0
  2. data/403.html +6 -0
  3. data/404.html +6 -0
  4. data/500.html +6 -0
  5. data/CHANGELOG.md +498 -0
  6. data/CODE_OF_CONDUCT.md +92 -0
  7. data/LICENSE.txt +22 -0
  8. data/README.md +184 -0
  9. data/SECURITY.md +30 -0
  10. data/_config.sample.yml +329 -0
  11. data/_data/locales/ar.yml +85 -0
  12. data/_data/locales/de.yml +86 -0
  13. data/_data/locales/en.yml +84 -0
  14. data/_data/locales/es.yml +86 -0
  15. data/_data/locales/fr.yml +86 -0
  16. data/_data/locales/ur.yml +86 -0
  17. data/_data/social_networks.yml +83 -0
  18. data/_includes/analytics-body.html +12 -0
  19. data/_includes/analytics-head.html +36 -0
  20. data/_includes/avatar.html +84 -0
  21. data/_includes/dark-mode-toggle.html +202 -0
  22. data/_includes/data-loader.html +42 -0
  23. data/_includes/date-formatter.html +63 -0
  24. data/_includes/grouped-item-list.html +53 -0
  25. data/_includes/hreflang.html +58 -0
  26. data/_includes/language-switcher.html +72 -0
  27. data/_includes/print-social-links.html +23 -0
  28. data/_includes/resume-section.html +547 -0
  29. data/_includes/safe-url.html +21 -0
  30. data/_includes/shared-head.html +51 -0
  31. data/_includes/social-links.html +54 -0
  32. data/_includes/vendors/svg-icons/ATTRIBUTION.md +34 -0
  33. data/_includes/vendors/svg-icons/dev.svg +3 -0
  34. data/_includes/vendors/svg-icons/dribbble-symbol.svg +3 -0
  35. data/_includes/vendors/svg-icons/envelope.svg +4 -0
  36. data/_includes/vendors/svg-icons/facebook.svg +3 -0
  37. data/_includes/vendors/svg-icons/flickr.svg +4 -0
  38. data/_includes/vendors/svg-icons/github.svg +3 -0
  39. data/_includes/vendors/svg-icons/globe-1.svg +3 -0
  40. data/_includes/vendors/svg-icons/instagram.svg +4 -0
  41. data/_includes/vendors/svg-icons/linkedin.svg +3 -0
  42. data/_includes/vendors/svg-icons/medium.svg +3 -0
  43. data/_includes/vendors/svg-icons/phone.svg +13 -0
  44. data/_includes/vendors/svg-icons/pinterest.svg +3 -0
  45. data/_includes/vendors/svg-icons/postcard.svg +14 -0
  46. data/_includes/vendors/svg-icons/telegram.svg +3 -0
  47. data/_includes/vendors/svg-icons/whatsapp.svg +3 -0
  48. data/_includes/vendors/svg-icons/x.svg +3 -0
  49. data/_includes/vendors/svg-icons/youtube.svg +3 -0
  50. data/_layouts/default.html +65 -0
  51. data/_layouts/error.html +138 -0
  52. data/_layouts/profile.html +123 -0
  53. data/_layouts/resume.html +238 -0
  54. data/_plugins/error_pages_generator.rb +71 -0
  55. data/_plugins/json_resume_generator.rb +79 -0
  56. data/_plugins/resume_pages_generator.rb +71 -0
  57. data/_plugins/resume_validator.rb +40 -0
  58. data/_sass/_all-pages.scss +335 -0
  59. data/_sass/_base.scss +130 -0
  60. data/_sass/_dark-mode.scss +387 -0
  61. data/_sass/_layout.scss +116 -0
  62. data/_sass/_mixins.scss +136 -0
  63. data/_sass/_normalize.scss +379 -0
  64. data/_sass/_profile-page.scss +60 -0
  65. data/_sass/_resume-ltr.scss +436 -0
  66. data/_sass/_resume-rtl.scss +91 -0
  67. data/_sass/_variables.scss +45 -0
  68. data/assets/css/cv-ltr.scss +31 -0
  69. data/assets/css/cv-rtl.scss +36 -0
  70. data/assets/css/main.scss +8 -0
  71. data/assets/css/profile.scss +9 -0
  72. data/assets/favicon/resume/about.txt +6 -0
  73. data/assets/favicon/resume/android-chrome-192x192.png +0 -0
  74. data/assets/favicon/resume/android-chrome-512x512.png +0 -0
  75. data/assets/favicon/resume/apple-touch-icon.png +0 -0
  76. data/assets/favicon/resume/favicon-16x16.png +0 -0
  77. data/assets/favicon/resume/favicon-32x32.png +0 -0
  78. data/assets/favicon/resume/favicon.ico +0 -0
  79. data/assets/favicon/resume/site.webmanifest +1 -0
  80. data/bin/validate-resume +89 -0
  81. data/bin/verify +27 -0
  82. data/docs/README.md +89 -0
  83. data/docs/explanation/accessibility-decisions.md +27 -0
  84. data/docs/explanation/architecture.md +44 -0
  85. data/docs/explanation/dark-mode-approach.md +25 -0
  86. data/docs/explanation/data-driven-model.md +25 -0
  87. data/docs/explanation/multilingual-and-rtl-design.md +40 -0
  88. data/docs/how-to/add-a-language.md +57 -0
  89. data/docs/how-to/add-a-section.md +35 -0
  90. data/docs/how-to/add-a-social-platform.md +32 -0
  91. data/docs/how-to/add-a-test.md +34 -0
  92. data/docs/how-to/create-a-custom-layout.md +64 -0
  93. data/docs/how-to/enable-dark-mode.md +43 -0
  94. data/docs/how-to/link-translations-with-hreflang.md +25 -0
  95. data/docs/how-to/migrate-v0.9-to-v1.0.md +46 -0
  96. data/docs/how-to/override-locale-strings.md +51 -0
  97. data/docs/how-to/override-sass-partials.md +26 -0
  98. data/docs/how-to/proof-built-html.md +22 -0
  99. data/docs/how-to/publish-json-resume.md +38 -0
  100. data/docs/how-to/show-language-proficiency-in-header.md +13 -0
  101. data/docs/how-to/switch-resume-versions.md +42 -0
  102. data/docs/how-to/troubleshoot-builds.md +39 -0
  103. data/docs/how-to/validate-in-ci.md +38 -0
  104. data/docs/how-to/verify-accessibility.md +28 -0
  105. data/docs/reference/accessibility-coverage.md +39 -0
  106. data/docs/reference/config.md +378 -0
  107. data/docs/reference/data-schemas.md +529 -0
  108. data/docs/reference/glossary.md +39 -0
  109. data/docs/reference/includes.md +197 -0
  110. data/docs/reference/json-resume-fields.md +148 -0
  111. data/docs/reference/layouts.md +144 -0
  112. data/docs/reference/locale-keys.md +76 -0
  113. data/docs/reference/repository-map.md +83 -0
  114. data/docs/reference/sass-tokens.md +279 -0
  115. data/docs/reference/testing-suites.md +62 -0
  116. data/docs/reference/validator-cli.md +203 -0
  117. data/docs/tutorials/getting-started.md +173 -0
  118. data/lib/jekyll-theme-resume/json_resume_exporter.rb +334 -0
  119. data/lib/jekyll-theme-resume/resume_validator.rb +853 -0
  120. data/lib/jekyll-theme-resume/schemas/LICENSE.md +21 -0
  121. data/lib/jekyll-theme-resume/schemas/json_resume_v1.0.0.json +500 -0
  122. data/lib/jekyll-theme-resume.rb +21 -0
  123. metadata +371 -0
@@ -0,0 +1,202 @@
1
+ {%- comment -%}
2
+ Component: Dark Mode Toggle Widget
3
+ - Purpose: Provides an accessible, persistent floating toggle button for switching between Light and Dark color themes.
4
+ - Activation Cascade:
5
+ 1. Evaluates the site-level switch: `site.dark_mode` (boolean or "enabled").
6
+ 2. Applies page-level override if specified in front matter (`page.dark_mode: true/false/enabled`).
7
+ 3. Renders button markup and client controller only when enabled.
8
+ - Localization: aria-label/title come from the current page's locale.ui.dark_mode_toggle.
9
+ - Visual Design: Floating circular action button fixed in the viewport.
10
+ - Print Media: Decorated with `class="no-print"` to ensure it is excluded from printed resumes or PDF generation.
11
+ - CSS Integration: Operates with CSS custom properties defined in `_sass/_dark-mode.scss`.
12
+ {%- endcomment -%}
13
+
14
+ {%- assign lang = page.lang | default: site.default_lang | default: 'en' -%}
15
+ {%- assign locale = site.data.locales[lang] | default: site.data.locales[site.default_lang] -%}
16
+ {% assign dark_mode_enabled = false %}
17
+ {% if site.dark_mode == "enabled" or site.dark_mode == true %}
18
+ {% assign dark_mode_enabled = true %}
19
+ {% endif %}
20
+ {% if page.dark_mode == false %}
21
+ {% assign dark_mode_enabled = false %}
22
+ {% elsif page.dark_mode == true or page.dark_mode == "enabled" %}
23
+ {% assign dark_mode_enabled = true %}
24
+ {% endif %}
25
+
26
+ {% if dark_mode_enabled %}
27
+ <!-- Dark Mode Toggle Button (Two-state: System Default vs Pinned Override) -->
28
+ <button
29
+ id="dark-mode-toggle"
30
+ class="dark-mode-toggle no-print"
31
+ type="button"
32
+ aria-label="{{ locale.ui.dark_mode_toggle }}"
33
+ title="{{ locale.ui.dark_mode_toggle }}"
34
+ aria-pressed="false"
35
+ data-pinned="false"
36
+ >
37
+ {%- comment -%}
38
+ Inline SVG Icons:
39
+ - Both icons have `aria-hidden="true"` and `focusable="false"` to prevent assistive technologies
40
+ from announcing raw vector graphics and to keep keyboard focus solely on the parent <button>.
41
+ - Visibility is toggled via CSS selectors based on `data-theme` or system color-scheme preference.
42
+ {%- endcomment -%}
43
+ <!-- Sun Icon: Visible in dark mode / indicates switching to light mode -->
44
+ <svg
45
+ class="dark-mode-icon dark-mode-icon-sun"
46
+ xmlns="http://www.w3.org/2000/svg"
47
+ width="20"
48
+ height="20"
49
+ viewBox="0 0 24 24"
50
+ fill="none"
51
+ stroke="currentColor"
52
+ stroke-width="2"
53
+ stroke-linecap="round"
54
+ stroke-linejoin="round"
55
+ aria-hidden="true"
56
+ focusable="false"
57
+ >
58
+ <circle cx="12" cy="12" r="5"></circle>
59
+ <line x1="12" y1="1" x2="12" y2="3"></line>
60
+ <line x1="12" y1="21" x2="12" y2="23"></line>
61
+ <line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
62
+ <line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
63
+ <line x1="1" y1="12" x2="3" y2="12"></line>
64
+ <line x1="21" y1="12" x2="23" y2="12"></line>
65
+ <line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
66
+ <line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
67
+ </svg>
68
+
69
+ <!-- Moon Icon: Visible in light mode / indicates switching to dark mode -->
70
+ <svg
71
+ class="dark-mode-icon dark-mode-icon-moon"
72
+ xmlns="http://www.w3.org/2000/svg"
73
+ width="20"
74
+ height="20"
75
+ viewBox="0 0 24 24"
76
+ fill="none"
77
+ stroke="currentColor"
78
+ stroke-width="2"
79
+ stroke-linecap="round"
80
+ stroke-linejoin="round"
81
+ aria-hidden="true"
82
+ focusable="false"
83
+ >
84
+ <path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
85
+ </svg>
86
+ </button>
87
+
88
+ <script>
89
+ /**
90
+ * Dark Mode Client Controller
91
+ * Encapsulated in an IIFE to prevent polluting the global window namespace.
92
+ *
93
+ * Two-State Logic Architecture:
94
+ * 1. Unpinned (System Default): Follows the OS `prefers-color-scheme`. `localStorage` contains no value.
95
+ * Listens dynamically to OS dark/light mode switches.
96
+ * 2. Pinned (User Override): Explicit user choice saved in `localStorage.setItem('color-scheme', ...)`.
97
+ * Clicking the button when pinned resets the theme back to the system default (clearing localStorage).
98
+ */
99
+ (function () {
100
+ var toggleBtn = document.getElementById('dark-mode-toggle');
101
+ if (!toggleBtn) return;
102
+
103
+ var metaTheme = document.querySelector('meta[name="color-scheme"]');
104
+
105
+ // Detects user's operating system level theme preference
106
+ function getSystemTheme() {
107
+ if (window.matchMedia) {
108
+ return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
109
+ }
110
+ return 'light';
111
+ }
112
+
113
+ // Storage Error Boundary: Accesses localStorage inside try/catch to safely handle
114
+ // private browsing modes (e.g. Safari Private Browsing) or restricted cookie security settings.
115
+ function getSavedTheme() {
116
+ try {
117
+ var val = localStorage.getItem('color-scheme');
118
+ if (val === 'dark' || val === 'light') {
119
+ return val;
120
+ }
121
+ return null;
122
+ } catch (e) {
123
+ return null;
124
+ }
125
+ }
126
+
127
+ // Synchronizes data attributes on <html> and <meta name="color-scheme">
128
+ function syncState() {
129
+ var saved = getSavedTheme();
130
+ if (saved !== null) {
131
+ document.documentElement.setAttribute('data-color-scheme', saved);
132
+ document.documentElement.setAttribute('data-theme', saved);
133
+ if (metaTheme) {
134
+ metaTheme.setAttribute('content', saved);
135
+ }
136
+ } else {
137
+ document.documentElement.removeAttribute('data-color-scheme');
138
+ document.documentElement.removeAttribute('data-theme');
139
+ if (metaTheme) {
140
+ metaTheme.setAttribute('content', 'light dark');
141
+ }
142
+ }
143
+ }
144
+
145
+ // Updates ARIA accessibility attributes and data-pinned status.
146
+ // aria-label/title are set once from locale.ui.dark_mode_toggle in the
147
+ // server-rendered markup and are not re-localized client-side.
148
+ function updateUI() {
149
+ var saved = getSavedTheme();
150
+ var isPinned = (saved !== null);
151
+ var effectiveTheme = isPinned ? saved : getSystemTheme();
152
+
153
+ toggleBtn.setAttribute('data-pinned', isPinned ? 'true' : 'false');
154
+ toggleBtn.setAttribute('aria-pressed', isPinned ? 'true' : 'false');
155
+ toggleBtn.setAttribute('data-current-theme', effectiveTheme);
156
+ }
157
+
158
+ // Toggles theme state: cycles between user-pinned override and system default
159
+ function toggleTheme() {
160
+ var saved = getSavedTheme();
161
+ if (saved !== null) {
162
+ // Currently pinned -> reset to system default
163
+ try {
164
+ localStorage.removeItem('color-scheme');
165
+ } catch (e) {}
166
+ syncState();
167
+ } else {
168
+ // Currently unpinned -> pin opposite of current system setting
169
+ var systemTheme = getSystemTheme();
170
+ var targetTheme = (systemTheme === 'dark') ? 'light' : 'dark';
171
+ try {
172
+ localStorage.setItem('color-scheme', targetTheme);
173
+ } catch (e) {}
174
+ syncState();
175
+ }
176
+ updateUI();
177
+ }
178
+
179
+ toggleBtn.addEventListener('click', toggleTheme);
180
+
181
+ // Reactive System Theme Listener: Adapts UI in real-time when OS theme changes (if unpinned)
182
+ if (window.matchMedia) {
183
+ var darkQuery = window.matchMedia('(prefers-color-scheme: dark)');
184
+ var mqListener = function () {
185
+ if (!getSavedTheme()) {
186
+ updateUI();
187
+ }
188
+ };
189
+ if (darkQuery.addEventListener) {
190
+ darkQuery.addEventListener('change', mqListener);
191
+ } else if (darkQuery.addListener) {
192
+ darkQuery.addListener(mqListener);
193
+ }
194
+ }
195
+
196
+ // Initial hydration on page load
197
+ syncState();
198
+ updateUI();
199
+ })();
200
+ </script>
201
+ {% endif %}
202
+
@@ -0,0 +1,42 @@
1
+ {%- comment -%}
2
+ ================================================================================
3
+ DYNAMIC DATA LOADER HELPER (_includes/data-loader.html)
4
+ ================================================================================
5
+ PURPOSE:
6
+ Dynamically resolves the root data object (`resume_data`) for the current layout
7
+ by traversing nested YAML folders in `_data/` based on a dot-separated path.
8
+
9
+ HOW IT WORKS:
10
+ 1. Default Assignment:
11
+ Starts with top-level `site.data`. If no custom subpath is configured,
12
+ the template reads directly from root YAML files (e.g. `_data/experience.yml`).
13
+
14
+ 2. Subpath Splitting (`split: '.'`):
15
+ Breaks dot-separated strings into array segments.
16
+ Example: "2025-06.20250621-PM" -> ["2025-06", "20250621-PM"]
17
+ Example: "en" -> ["en"]
18
+
19
+ 3. Recursive Bracket Notation (`resume_data[part]`):
20
+ Liquid dot notation (site.data.2025-06) breaks on numbers and hyphens.
21
+ Bracket notation safely accesses arbitrary string keys at any nesting depth:
22
+ - Level 1: site.data["2025-06"]
23
+ - Level 2: site.data["2025-06"]["20250621-PM"]
24
+
25
+ PARAMETERS:
26
+ - `path` (String, Optional): The dot-path string to resolve (e.g. "en", "ar", "2025-06.v1").
27
+ Defaults to `site.languages[lang].data_path` (current page.lang, or site.default_lang) if omitted.
28
+
29
+ OUTPUT:
30
+ - Sets the global template variable `resume_data` in the calling layout context.
31
+ ================================================================================
32
+ {%- endcomment -%}
33
+ {%- assign default_lang_cfg = site.languages[page.lang | default: site.default_lang | default: 'en'] -%}
34
+ {%- assign data_path = include.path | default: default_lang_cfg.data_path -%}
35
+ {%- assign resume_data = site.data -%}
36
+
37
+ {%- if data_path.size > 0 -%}
38
+ {%- assign path_parts = data_path | split: '.' -%}
39
+ {%- for part in path_parts -%}
40
+ {%- assign resume_data = resume_data[part] -%}
41
+ {%- endfor -%}
42
+ {%- endif -%}
@@ -0,0 +1,63 @@
1
+ {%- comment -%}
2
+ Universal Date Formatter
3
+ Formats a single date for any locale from _data/locales/<lang>.yml. Single canonical date
4
+ formatting hook for every language, designed to cleanly host future calendar-system
5
+ extensions (e.g. Feature 2.9 Dual Gregorian / Hijri Localization) without reintroducing
6
+ language-isolated includes such as the legacy _includes/ar-date.html.
7
+
8
+ Usage:
9
+ {% include date-formatter.html date=role.startdate %}
10
+ {% include date-formatter.html date=role.startdate style="MDY" %}
11
+ {% include date-formatter.html date=role.startdate lang="ar" %}
12
+
13
+ Parameters:
14
+ - date: the date to format, or a present-tense marker string (e.g. "Present"). Required.
15
+ - style: "MDY" for "Month Day, Year"; anything else (default "MY") for "Month Year".
16
+ - lang: locale code to format in. Defaults to the active page/site language.
17
+ {%- endcomment -%}
18
+ {%- assign lang = include.lang | default: page.lang | default: site.default_lang | default: 'en' -%}
19
+ {%- assign locale = site.data.locales[lang] | default: site.data.locales[site.default_lang] -%}
20
+ {%- assign style = include.style | default: "MY" -%}
21
+
22
+ {%- if include.date and include.date != "" -%}
23
+ {%- comment -%} Case-insensitive match against locale.present_values (e.g. "Present", "حتى الآن") {%- endcomment -%}
24
+ {%- assign date_downcase = include.date | downcase -%}
25
+ {%- assign is_present = false -%}
26
+ {%- for present_value in locale.present_values -%}
27
+ {%- assign present_value_downcase = present_value | downcase -%}
28
+ {%- if present_value_downcase == date_downcase -%}
29
+ {%- assign is_present = true -%}
30
+ {%- endif -%}
31
+ {%- endfor -%}
32
+
33
+ {%- if is_present -%}
34
+ {{ locale.ui.present }}
35
+ {%- else -%}
36
+ {%- comment -%}
37
+ Split the ISO string ourselves: Liquid's `date` filter reads a bare year (2018) as a
38
+ Unix timestamp and cannot parse YYYY-MM. YAML Date objects stringify to YYYY-MM-DD.
39
+ Anything that is not YYYY, YYYY-MM or YYYY-MM-DD is printed verbatim.
40
+ {%- endcomment -%}
41
+ {%- assign date_parts = include.date | append: "" | slice: 0, 10 | split: "-" -%}
42
+ {%- assign year = date_parts[0] -%}
43
+ {%- assign year_check = year | plus: 0 | append: "" -%}
44
+ {%- assign month_num = date_parts[1] | plus: 0 -%}
45
+ {%- assign day = date_parts[2] | plus: 0 -%}
46
+ {%- assign month_name = nil -%}
47
+ {%- if date_parts[1].size == 2 and month_num >= 1 and month_num <= 12 -%}
48
+ {%- assign month_index = month_num | minus: 1 -%}
49
+ {%- assign month_name = locale.months[month_index] -%}
50
+ {%- endif -%}
51
+ {%- if year.size != 4 or year_check != year or date_parts.size > 3 -%}
52
+ {{ include.date }}
53
+ {%- elsif date_parts.size == 1 -%}
54
+ {{ year }}
55
+ {%- elsif month_name == nil -%}
56
+ {{ include.date }}
57
+ {%- elsif style == "MDY" and date_parts.size == 3 and day > 0 -%}
58
+ {{ month_name }} {{ day }}, {{ year }}
59
+ {%- else -%}
60
+ {{ month_name }} {{ year }}
61
+ {%- endif -%}
62
+ {%- endif -%}
63
+ {%- endif -%}
@@ -0,0 +1,53 @@
1
+ {%- comment -%}
2
+ Shared renderer for the Experience and Volunteering sections: groups entries by
3
+ company, sorts each group's roles newest-first, and formats each role's duration
4
+ or date range. Extracted so a fix to one section's logic always applies to both.
5
+
6
+ Params:
7
+ items - the section's data array (e.g. resume_data.experience)
8
+ lang - the current language code
9
+ {%- endcomment -%}
10
+ {%- assign active_entries = include.items | where: "active", true -%}
11
+ {%- assign groups = active_entries | group_by: "company" -%}
12
+
13
+ {%- for group in groups -%}
14
+ <div class="resume-item" itemscope itemprop="worksFor" itemtype="http://schema.org/Organization">
15
+ <h3 class="resume-item-title" itemprop="name">{{ group.name }}</h3>
16
+
17
+ {%- assign roles = group.items | sort: "startdate" | reverse -%}
18
+ {%- for role in roles -%}
19
+ {%- assign has_dates = false -%}
20
+ {%- if role.durations or role.startdate -%}
21
+ {%- assign has_dates = true -%}
22
+ {%- endif -%}
23
+
24
+ {%- if role.position.size > 0 or has_dates or role.location.size > 0 -%}
25
+ <h4 class="resume-item-details" itemprop="description">
26
+ {%- if role.position.size > 0 -%}
27
+ {{ role.position }}
28
+ {%- endif -%}
29
+
30
+ {%- if has_dates -%}
31
+ {% if role.position.size > 0 %} &bull; {% endif %}
32
+ {% if role.durations %}
33
+ {% for time in role.durations %}{{ time.duration }} {% endfor %}
34
+ {% else %}
35
+ {%- assign role_enddate = role.enddate | default: "Present" -%}
36
+ {% include date-formatter.html date=role.startdate lang=include.lang style="MY" %} &ndash;
37
+ {% include date-formatter.html date=role_enddate lang=include.lang style="MY" %}
38
+ {% endif %}
39
+ {%- endif -%}
40
+
41
+ {%- if role.location.size > 0 -%}
42
+ {% if role.position.size > 0 or has_dates %} &bull; {% endif %}
43
+ {{ role.location }}
44
+ {%- endif -%}
45
+ </h4>
46
+ {%- endif -%}
47
+
48
+ {%- if site.enable_summary == true and role.summary -%}
49
+ <div class="resume-item-copy">{{ role.summary }}</div>
50
+ {%- endif -%}
51
+ {%- endfor -%}
52
+ </div><!-- end of grouped resume-item -->
53
+ {%- endfor -%}
@@ -0,0 +1,58 @@
1
+ {% comment %}
2
+ ==================================================================
3
+ START: HREFLANG TAGS FOR MULTILINGUAL SEO
4
+ This logic generates the <link rel="alternate"> tags that tell
5
+ search engines about the different language versions of this page.
6
+ It only runs if a 't_id' (Translation ID) is set in the page's front matter.
7
+
8
+ WHY THIS MATTERS:
9
+ Search engines (Google, Bing, Yandex) require bidirectional hreflang links across
10
+ all localized versions of equivalent content to prevent duplicate content penalties
11
+ and to direct international visitors to the correct regional/language variant.
12
+
13
+ HOW IT WORKS:
14
+ - Pages representing the same document in different languages (e.g. English and Arabic CVs)
15
+ share an identical `t_id` identifier in front matter.
16
+ - site.pages is queried to locate all translation peers.
17
+ - Absolute URLs are required by RFC 8288 and Google Search Console standards.
18
+ - The 'x-default' fallback is explicitly mapped to the English edition for unmatched locales.
19
+ ==================================================================
20
+ {% endcomment %}
21
+ {% if page.t_id %}
22
+
23
+ {% comment %}
24
+ 1. Find all pages on the site that share the *same* 't_id' as this
25
+ current page. Store them in a new list called 'translations'.
26
+ {% endcomment %}
27
+ {% assign translations = site.pages | where: "t_id", page.t_id %}
28
+
29
+ {% comment %}
30
+ 2. Loop through every page found in the 'translations' list.
31
+ 't' represents each individual page in the loop.
32
+ {% endcomment %}
33
+ {% for t in translations %}
34
+ {% comment %}
35
+ Print an 'alternate' tag for each page, using its
36
+ 'lang' (e.g., "en", "ar") and its full 'url'.
37
+ {% endcomment %}
38
+ <link rel="alternate" hreflang="{{ t.lang | escape }}" href="{{ t.url | absolute_url }}" />
39
+ {% endfor %}
40
+
41
+ {% comment %}
42
+ 3. Find the site's default-language page from the 'translations' list
43
+ to use as the "default" version.
44
+ {% endcomment %}
45
+ {% assign default_lang = site.default_lang | default: "en" %}
46
+ {% assign default_page = translations | where: "lang", default_lang | first %}
47
+
48
+ {% comment %}
49
+ 4. If an English version was found, print the 'x-default' tag.
50
+ This tells Google which page to show if the user's
51
+ language doesn't match any of our versions.
52
+ {% endcomment %}
53
+ {% if default_page %}
54
+ <link rel="alternate" hreflang="x-default" href="{{ default_page.url | absolute_url }}" />
55
+ {% endif %}
56
+
57
+ {% endif %}
58
+ {% comment %} ================= End HREFLANG logic ================= {% endcomment %}
@@ -0,0 +1,72 @@
1
+ {%- comment -%}
2
+ Include: Reusable Multilingual Language Switcher Component (Feature 1.2; dropdown redesign Feature 2.12)
3
+ - Purpose: Floating dropdown listing links to every other configured language's resume.
4
+ - Positioning: Fixed top-left viewport corner (see _sass/_layout.scss).
5
+ - Markup: Native <details>/<summary> disclosure — zero JavaScript, full keyboard support
6
+ (Enter/Space toggles the panel open and closed; browsers do not natively close it on Esc
7
+ or outside click, matching plain <details> behavior everywhere else). Scales to any number
8
+ of configured languages without growing a row of buttons.
9
+ - Link Resolution (two-tier), per target language:
10
+ 1. Discovers the counterpart page via page.t_id + lang match (site.pages).
11
+ 2. Falls back to languages.<lang>.url from _config.yml.
12
+ - Accessibility: The trigger's visible text is the current locale's ui.language_switcher
13
+ string. The panel is a <nav> landmark labelled with the same string. Each link is labelled
14
+ with its own locale's ui.language_name.
15
+ - Print Safety: Decorated with class="no-print" — hidden on printed resume sheets.
16
+ - Toggle: Disabled via site.resume_language_switcher: false in _config.yml.
17
+ {%- endcomment -%}
18
+
19
+ {% if site.resume_language_switcher != false and page.language_switcher != false %}
20
+ {%- assign current_lang = page.lang | default: site.default_lang | default: 'en' -%}
21
+ {%- assign current_locale = site.data.locales[current_lang] | default: site.data.locales[site.default_lang] -%}
22
+
23
+ <details class="language-switcher no-print">
24
+ <summary class="language-switcher-trigger">
25
+ <span class="lang-icon" aria-hidden="true">🌐</span>
26
+ <span class="language-switcher-label">{{ current_locale.ui.language_switcher }}</span>
27
+ </summary>
28
+ <nav class="language-switcher-panel" aria-label="{{ current_locale.ui.language_switcher }}">
29
+ {%- comment -%}
30
+ IMPORTANT: {% include %} shares Liquid variable scope with its caller (resume.html),
31
+ so every variable assigned in this loop body must use a switch_-prefixed name that
32
+ cannot collide with resume.html's own lang/locale/lang_cfg - otherwise the last loop
33
+ iteration's values would silently leak out and corrupt the rest of the caller's render.
34
+ {%- endcomment -%}
35
+ {% for switch_entry in site.languages %}
36
+ {%- assign switch_lang = switch_entry[0] -%}
37
+ {%- assign switch_lang_cfg = switch_entry[1] -%}
38
+ {%- if switch_lang != current_lang -%}
39
+ {%- assign switch_locale = site.data.locales[switch_lang] -%}
40
+
41
+ {%- comment -%}
42
+ TIER 1: Auto-discover counterpart page by matching page.t_id + target language.
43
+ Both pages must have the same t_id value in their front matter (e.g., t_id: cv-main).
44
+ {%- endcomment -%}
45
+ {%- assign switch_url = nil -%}
46
+ {%- if page.t_id -%}
47
+ {%- assign switch_matched_page = site.pages | where: "t_id", page.t_id | where: "lang", switch_lang | first -%}
48
+ {%- if switch_matched_page -%}
49
+ {%- assign switch_url = switch_matched_page.url | relative_url -%}
50
+ {%- endif -%}
51
+ {%- endif -%}
52
+
53
+ {%- comment -%}
54
+ TIER 2: Fall back to languages.<lang>.url from _config.yml.
55
+ {%- endcomment -%}
56
+ {%- unless switch_url -%}
57
+ {%- assign switch_url = switch_lang_cfg.url | relative_url -%}
58
+ {%- endunless -%}
59
+
60
+ <a href="{{ switch_url }}"
61
+ class="lang-switch-btn"
62
+ aria-label="{{ switch_locale.ui.language_name }}"
63
+ title="{{ switch_locale.ui.language_name }}"
64
+ dir="{{ switch_locale.direction }}">
65
+ <span class="lang-icon" aria-hidden="true">🌐</span>
66
+ <span class="lang-label-target">{{ switch_locale.ui.language_name }}</span>
67
+ </a>
68
+ {%- endif -%}
69
+ {% endfor %}
70
+ </nav>
71
+ </details>
72
+ {% endif %}
@@ -0,0 +1,23 @@
1
+ {%- comment -%}
2
+ Component: Print-Only Social & Contact Links
3
+ - Purpose: Renders full plaintext URLs for physical paper printouts and PDF exports
4
+ where clickable icon-only links cannot be followed.
5
+ - Localization: Platform labels come from locale.ui.social_labels.
6
+ - Bidi Protection: Every URL is wrapped in `<span dir="ltr">` to ensure web addresses,
7
+ slashes, and domain names are never reversed or scrambled in an RTL reading context.
8
+ {%- endcomment -%}
9
+ <!-- and guess where these are defined? Yup, you guessed it: the _config.yml file -->
10
+ {% if site.resume_print_social_links %}
11
+ {%- assign lang = page.lang | default: site.default_lang | default: 'en' -%}
12
+ {%- assign locale = site.data.locales[lang] | default: site.data.locales[site.default_lang] -%}
13
+ <ul>
14
+
15
+ {% for network in site.data.social_networks %}
16
+ {% assign value = site.social_links[network.key] %}
17
+ {% if value %}
18
+ <li><strong>{{ locale.ui.social_labels[network.key] }}</strong>: <span dir="ltr">{{ value | escape }}</span></li>
19
+ {% endif %}
20
+ {% endfor %}
21
+
22
+ </ul>
23
+ {% endif %}