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.
- checksums.yaml +7 -0
- data/403.html +6 -0
- data/404.html +6 -0
- data/500.html +6 -0
- data/CHANGELOG.md +498 -0
- data/CODE_OF_CONDUCT.md +92 -0
- data/LICENSE.txt +22 -0
- data/README.md +184 -0
- data/SECURITY.md +30 -0
- data/_config.sample.yml +329 -0
- data/_data/locales/ar.yml +85 -0
- data/_data/locales/de.yml +86 -0
- data/_data/locales/en.yml +84 -0
- data/_data/locales/es.yml +86 -0
- data/_data/locales/fr.yml +86 -0
- data/_data/locales/ur.yml +86 -0
- data/_data/social_networks.yml +83 -0
- data/_includes/analytics-body.html +12 -0
- data/_includes/analytics-head.html +36 -0
- data/_includes/avatar.html +84 -0
- data/_includes/dark-mode-toggle.html +202 -0
- data/_includes/data-loader.html +42 -0
- data/_includes/date-formatter.html +63 -0
- data/_includes/grouped-item-list.html +53 -0
- data/_includes/hreflang.html +58 -0
- data/_includes/language-switcher.html +72 -0
- data/_includes/print-social-links.html +23 -0
- data/_includes/resume-section.html +547 -0
- data/_includes/safe-url.html +21 -0
- data/_includes/shared-head.html +51 -0
- data/_includes/social-links.html +54 -0
- data/_includes/vendors/svg-icons/ATTRIBUTION.md +34 -0
- data/_includes/vendors/svg-icons/dev.svg +3 -0
- data/_includes/vendors/svg-icons/dribbble-symbol.svg +3 -0
- data/_includes/vendors/svg-icons/envelope.svg +4 -0
- data/_includes/vendors/svg-icons/facebook.svg +3 -0
- data/_includes/vendors/svg-icons/flickr.svg +4 -0
- data/_includes/vendors/svg-icons/github.svg +3 -0
- data/_includes/vendors/svg-icons/globe-1.svg +3 -0
- data/_includes/vendors/svg-icons/instagram.svg +4 -0
- data/_includes/vendors/svg-icons/linkedin.svg +3 -0
- data/_includes/vendors/svg-icons/medium.svg +3 -0
- data/_includes/vendors/svg-icons/phone.svg +13 -0
- data/_includes/vendors/svg-icons/pinterest.svg +3 -0
- data/_includes/vendors/svg-icons/postcard.svg +14 -0
- data/_includes/vendors/svg-icons/telegram.svg +3 -0
- data/_includes/vendors/svg-icons/whatsapp.svg +3 -0
- data/_includes/vendors/svg-icons/x.svg +3 -0
- data/_includes/vendors/svg-icons/youtube.svg +3 -0
- data/_layouts/default.html +65 -0
- data/_layouts/error.html +138 -0
- data/_layouts/profile.html +123 -0
- data/_layouts/resume.html +238 -0
- data/_plugins/error_pages_generator.rb +71 -0
- data/_plugins/json_resume_generator.rb +79 -0
- data/_plugins/resume_pages_generator.rb +71 -0
- data/_plugins/resume_validator.rb +40 -0
- data/_sass/_all-pages.scss +335 -0
- data/_sass/_base.scss +130 -0
- data/_sass/_dark-mode.scss +387 -0
- data/_sass/_layout.scss +116 -0
- data/_sass/_mixins.scss +136 -0
- data/_sass/_normalize.scss +379 -0
- data/_sass/_profile-page.scss +60 -0
- data/_sass/_resume-ltr.scss +436 -0
- data/_sass/_resume-rtl.scss +91 -0
- data/_sass/_variables.scss +45 -0
- data/assets/css/cv-ltr.scss +31 -0
- data/assets/css/cv-rtl.scss +36 -0
- data/assets/css/main.scss +8 -0
- data/assets/css/profile.scss +9 -0
- data/assets/favicon/resume/about.txt +6 -0
- data/assets/favicon/resume/android-chrome-192x192.png +0 -0
- data/assets/favicon/resume/android-chrome-512x512.png +0 -0
- data/assets/favicon/resume/apple-touch-icon.png +0 -0
- data/assets/favicon/resume/favicon-16x16.png +0 -0
- data/assets/favicon/resume/favicon-32x32.png +0 -0
- data/assets/favicon/resume/favicon.ico +0 -0
- data/assets/favicon/resume/site.webmanifest +1 -0
- data/bin/validate-resume +89 -0
- data/bin/verify +27 -0
- data/docs/README.md +89 -0
- data/docs/explanation/accessibility-decisions.md +27 -0
- data/docs/explanation/architecture.md +44 -0
- data/docs/explanation/dark-mode-approach.md +25 -0
- data/docs/explanation/data-driven-model.md +25 -0
- data/docs/explanation/multilingual-and-rtl-design.md +40 -0
- data/docs/how-to/add-a-language.md +57 -0
- data/docs/how-to/add-a-section.md +35 -0
- data/docs/how-to/add-a-social-platform.md +32 -0
- data/docs/how-to/add-a-test.md +34 -0
- data/docs/how-to/create-a-custom-layout.md +64 -0
- data/docs/how-to/enable-dark-mode.md +43 -0
- data/docs/how-to/link-translations-with-hreflang.md +25 -0
- data/docs/how-to/migrate-v0.9-to-v1.0.md +46 -0
- data/docs/how-to/override-locale-strings.md +51 -0
- data/docs/how-to/override-sass-partials.md +26 -0
- data/docs/how-to/proof-built-html.md +22 -0
- data/docs/how-to/publish-json-resume.md +38 -0
- data/docs/how-to/show-language-proficiency-in-header.md +13 -0
- data/docs/how-to/switch-resume-versions.md +42 -0
- data/docs/how-to/troubleshoot-builds.md +39 -0
- data/docs/how-to/validate-in-ci.md +38 -0
- data/docs/how-to/verify-accessibility.md +28 -0
- data/docs/reference/accessibility-coverage.md +39 -0
- data/docs/reference/config.md +378 -0
- data/docs/reference/data-schemas.md +529 -0
- data/docs/reference/glossary.md +39 -0
- data/docs/reference/includes.md +197 -0
- data/docs/reference/json-resume-fields.md +148 -0
- data/docs/reference/layouts.md +144 -0
- data/docs/reference/locale-keys.md +76 -0
- data/docs/reference/repository-map.md +83 -0
- data/docs/reference/sass-tokens.md +279 -0
- data/docs/reference/testing-suites.md +62 -0
- data/docs/reference/validator-cli.md +203 -0
- data/docs/tutorials/getting-started.md +173 -0
- data/lib/jekyll-theme-resume/json_resume_exporter.rb +334 -0
- data/lib/jekyll-theme-resume/resume_validator.rb +853 -0
- data/lib/jekyll-theme-resume/schemas/LICENSE.md +21 -0
- data/lib/jekyll-theme-resume/schemas/json_resume_v1.0.0.json +500 -0
- data/lib/jekyll-theme-resume.rb +21 -0
- metadata +371 -0
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
{%- assign lang = page.lang | default: site.default_lang | default: 'en' -%}
|
|
2
|
+
{%- assign locale = site.data.locales[lang] | default: site.data.locales[site.default_lang] -%}
|
|
3
|
+
{%- assign lang_cfg = site.languages[lang] -%}
|
|
4
|
+
{% include data-loader.html path=lang_cfg.data_path %}
|
|
5
|
+
<!DOCTYPE html>
|
|
6
|
+
<html lang="{{ lang | escape }}" dir="{{ locale.direction }}">
|
|
7
|
+
<!-- This page was last updated on {{ "now" | date: "%Y-%m-%d %H:%M:%S" }}Z -->
|
|
8
|
+
|
|
9
|
+
<head>
|
|
10
|
+
{%- comment -%}
|
|
11
|
+
Head Assets:
|
|
12
|
+
- shared-head: Shared charset, viewport, SEO base, and favicon links
|
|
13
|
+
- Locale web font (locale.font_url) and locale-driven CSS (assets/css/cv-{{ locale.direction }}.css)
|
|
14
|
+
- seo: Jekyll SEO tag for social graph, search indexing, and canonical URLs
|
|
15
|
+
- analytics-head: Google Analytics / GTM tracking scripts (optional)
|
|
16
|
+
{%- endcomment -%}
|
|
17
|
+
{% include shared-head.html %}
|
|
18
|
+
|
|
19
|
+
{% if site.disable_google_fonts != true and site.resume_theme != 'no-custom-fonts' %}
|
|
20
|
+
{% if locale.font_url != "" %}
|
|
21
|
+
<!-- Locale web font, from _data/locales/{{ lang }}.yml -->
|
|
22
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
23
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
24
|
+
<link href="{{ locale.font_url }}" rel="stylesheet">
|
|
25
|
+
{% else %}
|
|
26
|
+
<!-- Default web font (Lora + Open Sans) for locales without a dedicated font_url -->
|
|
27
|
+
<link href='https://fonts.googleapis.com/css?family=Lora:400,700%7COpen+Sans:400,300,800,700' rel='stylesheet' type='text/css'>
|
|
28
|
+
{% endif %}
|
|
29
|
+
{% endif %}
|
|
30
|
+
|
|
31
|
+
<style>
|
|
32
|
+
:root {
|
|
33
|
+
{% if locale.font_family != "" %}--font-locale: {{ locale.font_family }};{% endif %}
|
|
34
|
+
--line-height-locale: {{ locale.line_height }};
|
|
35
|
+
}
|
|
36
|
+
</style>
|
|
37
|
+
|
|
38
|
+
<link rel="stylesheet" type="text/css" href="{{ 'assets/css/cv-' | append: locale.direction | append: '.css' | relative_url }}">
|
|
39
|
+
|
|
40
|
+
{% include hreflang.html %}
|
|
41
|
+
|
|
42
|
+
{% seo %}
|
|
43
|
+
|
|
44
|
+
{% include analytics-head.html %}
|
|
45
|
+
</head>
|
|
46
|
+
|
|
47
|
+
<body class="theme-{% if site.resume_theme %}{{ site.resume_theme }}{% endif %}">
|
|
48
|
+
<!-- Accessible skip-to-content link (WCAG 2.4.1) -->
|
|
49
|
+
<a href="#main-content" class="skip-link no-print">{{ locale.ui.skip_to_content }}</a>
|
|
50
|
+
|
|
51
|
+
<!-- Analytics body snippet (noscript, dataLayer, etc.) -->
|
|
52
|
+
{% include analytics-body.html %}
|
|
53
|
+
|
|
54
|
+
<div class="wrapper" itemscope itemtype="https://schema.org/Person">
|
|
55
|
+
{% include dark-mode-toggle.html %}
|
|
56
|
+
{% include language-switcher.html %}
|
|
57
|
+
|
|
58
|
+
{%- comment -%}
|
|
59
|
+
Schema.org Microdata & Live Contact Protection:
|
|
60
|
+
When site.enable_live is true, phone_live and email_live are used for privacy.
|
|
61
|
+
Fallback to standard phone/email if live versions are not provided.
|
|
62
|
+
{%- endcomment -%}
|
|
63
|
+
{% if site.enable_live == true and site.contact_info.phone_live %}
|
|
64
|
+
<meta itemprop="telephone" content="{{ site.contact_info.phone_live }}" />
|
|
65
|
+
{% elsif site.contact_info.phone %}
|
|
66
|
+
<meta itemprop="telephone" content="{{ site.contact_info.phone }}" />
|
|
67
|
+
{% endif %}
|
|
68
|
+
|
|
69
|
+
{% if site.enable_live == true and site.contact_info.email_live %}
|
|
70
|
+
<meta itemprop="email" content="{{ site.contact_info.email_live }}" />
|
|
71
|
+
{% elsif site.contact_info.email %}
|
|
72
|
+
<meta itemprop="email" content="{{ site.contact_info.email }}" />
|
|
73
|
+
{% endif %}
|
|
74
|
+
|
|
75
|
+
{% if lang_cfg.address %}
|
|
76
|
+
<meta itemprop="address" content="{{ lang_cfg.address }}" />
|
|
77
|
+
{% endif %}
|
|
78
|
+
|
|
79
|
+
<header class="page-header" role="banner">
|
|
80
|
+
|
|
81
|
+
{% if site.resume_avatar == true %}
|
|
82
|
+
<!-- You can turn off the avatar in _config.yml by setting resume_avatar to false -->
|
|
83
|
+
{% include avatar.html lang=lang %}
|
|
84
|
+
{% endif %}
|
|
85
|
+
|
|
86
|
+
<!-- Your name is defined in _config.yml under languages.{{ lang }}.name -->
|
|
87
|
+
<h1 class="header-name" itemprop="name">
|
|
88
|
+
{{ lang_cfg.name }}
|
|
89
|
+
</h1>
|
|
90
|
+
|
|
91
|
+
{%- comment -%}
|
|
92
|
+
Contact Information Bar (Feature 2.8, icon-first for every direction):
|
|
93
|
+
Displays contact channels (phone, email, address, date of birth, languages).
|
|
94
|
+
When site.enable_live is true, sanitized live contact points are rendered.
|
|
95
|
+
Phone numbers and email addresses are wrapped with dir="ltr" to prevent
|
|
96
|
+
bidirectional punctuation distortion in RTL locales, and are harmless no-ops in LTR ones.
|
|
97
|
+
{%- endcomment -%}
|
|
98
|
+
{% if site.display_header_contact_info == true %}
|
|
99
|
+
<!-- To display contact info here, change `display_header_contact_info` value in _config.yml to true -->
|
|
100
|
+
<div class="header-contact-info">
|
|
101
|
+
<p>
|
|
102
|
+
{%- assign contact_phone = site.contact_info.phone -%}
|
|
103
|
+
{%- if site.enable_live == true and site.contact_info.phone_live -%}
|
|
104
|
+
{%- assign contact_phone = site.contact_info.phone_live -%}
|
|
105
|
+
{%- endif -%}
|
|
106
|
+
{% if contact_phone %}
|
|
107
|
+
{% include vendors/svg-icons/phone.svg %} <a href="tel:{{ contact_phone | remove: ' ' | remove: '-' | remove: '(' | remove: ')' }}" dir="ltr" style="text-decoration: none; color: inherit;">{{ contact_phone }}</a>
|
|
108
|
+
{% endif %}
|
|
109
|
+
|
|
110
|
+
{%- assign contact_email_display = site.contact_info.email -%}
|
|
111
|
+
{%- if site.enable_live == true and site.contact_info.email_live -%}
|
|
112
|
+
{%- assign contact_email_display = site.contact_info.email_live -%}
|
|
113
|
+
{%- endif -%}
|
|
114
|
+
{% if contact_email_display %}
|
|
115
|
+
{% include vendors/svg-icons/envelope.svg %} <a href="mailto:{{ contact_email_display }}" dir="ltr" style="text-decoration: none; color: inherit;">{{ contact_email_display }}</a>
|
|
116
|
+
{% endif %}
|
|
117
|
+
|
|
118
|
+
{% if lang_cfg.address %}
|
|
119
|
+
{% include vendors/svg-icons/postcard.svg %} {{ lang_cfg.address }}
|
|
120
|
+
{% endif %}
|
|
121
|
+
|
|
122
|
+
{% if site.contact_info.dob %}
|
|
123
|
+
{{ locale.ui.date_of_birth }} {% include date-formatter.html date=site.contact_info.dob style="MDY" lang=lang %}
|
|
124
|
+
{% endif %}
|
|
125
|
+
</p>
|
|
126
|
+
|
|
127
|
+
{%- comment -%}
|
|
128
|
+
Active Languages Header Summary:
|
|
129
|
+
Filters languages where active: true and renders a dedicated centered line,
|
|
130
|
+
joined with the locale's list_separator (e.g. Arabic comma "، ").
|
|
131
|
+
{%- endcomment -%}
|
|
132
|
+
{% assign active_languages = resume_data.languages | where: "active", true %}
|
|
133
|
+
{% if site.resume_section.lang_header and active_languages.size > 0 %}
|
|
134
|
+
<p class="header-languages">
|
|
135
|
+
<strong>{{ locale.ui.languages }}</strong>
|
|
136
|
+
{% for language in active_languages %}
|
|
137
|
+
{{ language.language }} ({{ language.descrp_short }}){% unless forloop.last %}{{ locale.ui.list_separator }}{% endunless %}
|
|
138
|
+
{% endfor %}
|
|
139
|
+
</p>
|
|
140
|
+
{% endif %}
|
|
141
|
+
</div>
|
|
142
|
+
{% endif %}
|
|
143
|
+
|
|
144
|
+
<div class="title-bar no-print">
|
|
145
|
+
<!-- Your title is defined in _config.yml under languages.{{ lang }}.resume_title -->
|
|
146
|
+
<h2 class="header-title" itemprop="jobTitle">{{ lang_cfg.resume_title }}</h2>
|
|
147
|
+
|
|
148
|
+
<!-- This is the markup for the icon links; moved out to an include because it's very verbose, and you shouldn't ever need to edit the markup (unless you want to re-order the icons); if you want to customize which links appear, define them in the _config.yml file -->
|
|
149
|
+
{% if site.social_links %}
|
|
150
|
+
<!-- and guess where these are defined? Yup, you guessed it: the _config.yml file -->
|
|
151
|
+
<ul class="social-links">
|
|
152
|
+
{% include social-links.html %}
|
|
153
|
+
</ul>
|
|
154
|
+
{% endif %}
|
|
155
|
+
</div>
|
|
156
|
+
|
|
157
|
+
{% if lang_cfg.header_intro == true %}
|
|
158
|
+
<div class="executive-summary" itemprop="description">
|
|
159
|
+
<p>{{ resume_data.header.intro }}</p>
|
|
160
|
+
</div>
|
|
161
|
+
{% endif %}
|
|
162
|
+
|
|
163
|
+
{%- comment -%}
|
|
164
|
+
Job Seeking / Availability Call-to-Action:
|
|
165
|
+
- true: Displays localized "Contact me" mailto CTA.
|
|
166
|
+
- false: Displays localized "I'm not looking for work" badge.
|
|
167
|
+
- nil: Hidden entirely.
|
|
168
|
+
{%- endcomment -%}
|
|
169
|
+
{% if site.resume_looking_for_work == true %}
|
|
170
|
+
{% assign contact_email = site.contact_info.email %}
|
|
171
|
+
{% if site.enable_live == true and site.contact_info.email_live %}
|
|
172
|
+
{% assign contact_email = site.contact_info.email_live %}
|
|
173
|
+
{% endif %}
|
|
174
|
+
<a href="mailto:{{ contact_email }}" class="contact-button no-print" itemprop="email">{{ locale.ui.contact_me }}</a>
|
|
175
|
+
{% elsif site.resume_looking_for_work == false %}
|
|
176
|
+
<a class="contact-button not-looking no-print">{{ locale.ui.not_looking_for_work }}</a>
|
|
177
|
+
{% else %}
|
|
178
|
+
{% endif %}
|
|
179
|
+
</header>
|
|
180
|
+
|
|
181
|
+
<!-- Main resume content landmark (WCAG 1.3.1 / 2.4.1) -->
|
|
182
|
+
<main id="main-content" class="resume-main-content" role="main" tabindex="-1">
|
|
183
|
+
{% comment %}
|
|
184
|
+
=============================================================================
|
|
185
|
+
DYNAMIC SECTION RENDERING
|
|
186
|
+
=============================================================================
|
|
187
|
+
Loop through resume_section_order from _config.yml and render each section
|
|
188
|
+
through the locale-driven dispatcher. This allows you to customize section
|
|
189
|
+
order without editing the layout.
|
|
190
|
+
{% endcomment %}
|
|
191
|
+
{%- for section_name in site.resume_section_order -%}
|
|
192
|
+
{% include resume-section.html section_name=section_name lang=lang %}
|
|
193
|
+
{%- endfor -%}
|
|
194
|
+
|
|
195
|
+
{% if site.resume_print_social_links %}
|
|
196
|
+
<!-- begin Print Social Links -->
|
|
197
|
+
<section class="content-section print-only">
|
|
198
|
+
|
|
199
|
+
<header class="section-header">
|
|
200
|
+
<h2>{{ locale.ui.social_links }}</h2>
|
|
201
|
+
</header>
|
|
202
|
+
|
|
203
|
+
<div class="resume-item">
|
|
204
|
+
{% include print-social-links.html %}
|
|
205
|
+
</div>
|
|
206
|
+
|
|
207
|
+
</section>
|
|
208
|
+
<!-- end Print Social Links -->
|
|
209
|
+
{% endif %}
|
|
210
|
+
</main>
|
|
211
|
+
|
|
212
|
+
<footer class="page-footer no-print" role="contentinfo">
|
|
213
|
+
<p class="footer-line">
|
|
214
|
+
{{ locale.ui.page_last_generated_on }} {{ "now" | date: "%Y-%m-%d" }} {{ locale.ui.at }} {{ "now" | date: "%H:%M:%S" }}Z.
|
|
215
|
+
</p>
|
|
216
|
+
<p class="copyright">{{ site.title }} © 2018 - {{ "now" | date: "%Y" }}</p>
|
|
217
|
+
</footer>
|
|
218
|
+
|
|
219
|
+
{%- comment -%}
|
|
220
|
+
Print-Only Footer:
|
|
221
|
+
When site.enable_live == false, appends the canonical permalink of the online resume
|
|
222
|
+
to printed versions/PDF exports for offline verification.
|
|
223
|
+
{%- endcomment -%}
|
|
224
|
+
{% if site.enable_live == false %}
|
|
225
|
+
<footer class="page-footer print-only" role="contentinfo">
|
|
226
|
+
<p class="footer-line">
|
|
227
|
+
{{ locale.ui.cv_last_generated_on }} {{ "now" | date: "%Y-%m-%d" }} {{ locale.ui.at }} {{ "now" | date: "%H:%M:%S" }}Z.
|
|
228
|
+
<br>
|
|
229
|
+
<a href="{{ page.url | replace:'index.html','' | prepend: site.baseurl | prepend: site.url }}" target="_blank" rel="noopener noreferrer" style="font-weight: 100; color: #999;">
|
|
230
|
+
{{ page.url | replace:'index.html','' | prepend: site.baseurl | prepend: site.url }}
|
|
231
|
+
</a>
|
|
232
|
+
</p>
|
|
233
|
+
</footer>
|
|
234
|
+
{% endif %}
|
|
235
|
+
</div>
|
|
236
|
+
|
|
237
|
+
</body>
|
|
238
|
+
</html>
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JekyllThemeResume
|
|
4
|
+
# Programmatically synthesizes standard HTTP error pages (404, 403, 500)
|
|
5
|
+
# into site.pages if they are not already defined by the site.
|
|
6
|
+
#
|
|
7
|
+
# WHY THIS IS NECESSARY:
|
|
8
|
+
# Jekyll gem-based themes only distribute assets, _layouts, _includes, _sass,
|
|
9
|
+
# and _data. Root-level HTML files in a gem theme are NOT automatically copied
|
|
10
|
+
# or rendered into the consuming site's destination directory (_site/).
|
|
11
|
+
#
|
|
12
|
+
# HOW IT WORKS:
|
|
13
|
+
# This generator executes during the Jekyll build pipeline. For each error page
|
|
14
|
+
# defined in ERROR_PAGES, it checks whether the consuming site already provides
|
|
15
|
+
# an override. If no override exists, it creates an in-memory virtual page
|
|
16
|
+
# (Jekyll::PageWithoutAFile) and adds it to site.pages so Jekyll compiles it
|
|
17
|
+
# using _layouts/error.html.
|
|
18
|
+
class ErrorPagesGenerator < Jekyll::Generator
|
|
19
|
+
# safe true: Allows plugin execution in Jekyll safe mode / GitHub Pages environments
|
|
20
|
+
safe true
|
|
21
|
+
# priority :low: Ensures all physical pages in the consuming site are parsed and
|
|
22
|
+
# added to site.pages first, allowing our collision checks to be fully accurate.
|
|
23
|
+
priority :low
|
|
24
|
+
|
|
25
|
+
# Standard HTTP error codes, target output filenames, and default bilingual titles.
|
|
26
|
+
ERROR_PAGES = {
|
|
27
|
+
"404" => {
|
|
28
|
+
"file" => "404.html",
|
|
29
|
+
"title" => "404 - Page Not Found"
|
|
30
|
+
},
|
|
31
|
+
"403" => {
|
|
32
|
+
"file" => "403.html",
|
|
33
|
+
"title" => "403 - Access Forbidden"
|
|
34
|
+
},
|
|
35
|
+
"500" => {
|
|
36
|
+
"file" => "500.html",
|
|
37
|
+
"title" => "500 - Internal Server Error"
|
|
38
|
+
}
|
|
39
|
+
}.freeze
|
|
40
|
+
|
|
41
|
+
def generate(site)
|
|
42
|
+
ERROR_PAGES.each do |code, info|
|
|
43
|
+
filename = info["file"]
|
|
44
|
+
# Skip if page already exists in site.pages or in site source files
|
|
45
|
+
#
|
|
46
|
+
# THREE-TIER COLLISION PREVENTION:
|
|
47
|
+
# 1. Check in-memory site.pages for existing page name or permalink URL
|
|
48
|
+
next if site.pages.any? { |p| p.name == filename || p.url == "/#{filename}" }
|
|
49
|
+
# 2. Check if a physical file exists at the root of the consuming site
|
|
50
|
+
next if File.exist?(site.in_source_dir(filename))
|
|
51
|
+
# 3. Check if a physical file exists in the _pages/ collection directory
|
|
52
|
+
next if File.exist?(site.in_source_dir("_pages", filename))
|
|
53
|
+
|
|
54
|
+
# Synthesize virtual page in memory without requiring a physical file on disk.
|
|
55
|
+
# Arguments: site, base directory, sub-directory (""), file name
|
|
56
|
+
page = Jekyll::PageWithoutAFile.new(site, site.source, "", filename)
|
|
57
|
+
page.content = ""
|
|
58
|
+
# Assign Front Matter metadata consumed by _layouts/error.html:
|
|
59
|
+
page.data["layout"] = "error"
|
|
60
|
+
page.data["code"] = code
|
|
61
|
+
page.data["title"] = info["title"]
|
|
62
|
+
page.data["permalink"] = "/#{filename}"
|
|
63
|
+
# Exclude synthetic error pages from sitemap generators (e.g. jekyll-sitemap)
|
|
64
|
+
page.data["sitemap"] = false
|
|
65
|
+
|
|
66
|
+
# Append virtual page to site.pages so Jekyll renders and writes it to _site/
|
|
67
|
+
site.pages << page
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../lib/jekyll-theme-resume/json_resume_exporter"
|
|
4
|
+
require_relative "../lib/jekyll-theme-resume/resume_validator"
|
|
5
|
+
|
|
6
|
+
module JekyllThemeResume
|
|
7
|
+
class JsonResumePage < Jekyll::PageWithoutAFile
|
|
8
|
+
def initialize(site, route, document)
|
|
9
|
+
super(site, site.source, File.dirname(route).delete_prefix("/"), "resume.json")
|
|
10
|
+
self.content = "#{JSON.pretty_generate(document)}\n"
|
|
11
|
+
data.merge!("layout" => nil, "permalink" => route, "sitemap" => false)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Jekyll::Site uses Renderer directly, not Page#render.
|
|
15
|
+
def render_with_liquid?
|
|
16
|
+
false
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def place_in_layout?
|
|
20
|
+
false
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
class JsonResumeGenerator < Jekyll::Generator
|
|
25
|
+
safe true
|
|
26
|
+
priority :lowest
|
|
27
|
+
|
|
28
|
+
def generate(site)
|
|
29
|
+
site.config["json_resume_routes"] = {}
|
|
30
|
+
options = site.config["json_resume"].is_a?(Hash) ? site.config["json_resume"] : {}
|
|
31
|
+
languages = site.config["languages"]
|
|
32
|
+
return if options["enabled"] == false || !languages.is_a?(Hash)
|
|
33
|
+
|
|
34
|
+
selected_languages(languages, options).each do |lang|
|
|
35
|
+
unless ResumeValidator::LANG_KEY_REGEX.match?(lang.to_s)
|
|
36
|
+
Jekyll.logger.warn "JSON Resume:", "invalid language route; skipping export"
|
|
37
|
+
next
|
|
38
|
+
end
|
|
39
|
+
route = "/#{lang}/resume.json"
|
|
40
|
+
next if collision?(site, route)
|
|
41
|
+
|
|
42
|
+
document = JsonResumeExporter.export(site, lang)
|
|
43
|
+
next unless document
|
|
44
|
+
|
|
45
|
+
site.pages << JsonResumePage.new(site, route, document)
|
|
46
|
+
site.config["json_resume_routes"][lang] = route
|
|
47
|
+
next unless lang == (site.config["default_lang"] || "en") && options["root_export"] != false
|
|
48
|
+
next if collision?(site, "/resume.json")
|
|
49
|
+
|
|
50
|
+
site.pages << JsonResumePage.new(site, "/resume.json", document)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def selected_languages(languages, options)
|
|
57
|
+
requested = options["languages"]
|
|
58
|
+
return languages.keys if requested.nil? || requested == []
|
|
59
|
+
|
|
60
|
+
unless requested.is_a?(Array)
|
|
61
|
+
Jekyll.logger.warn "JSON Resume:", "languages must be an array; skipping exports"
|
|
62
|
+
return []
|
|
63
|
+
end
|
|
64
|
+
requested.uniq.select do |lang|
|
|
65
|
+
known = languages.key?(lang)
|
|
66
|
+
Jekyll.logger.warn "JSON Resume:", "unknown export language '#{lang}'; ignored" unless known
|
|
67
|
+
known
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def collision?(site, route)
|
|
72
|
+
target = File.expand_path(route.delete_prefix("/"), site.dest)
|
|
73
|
+
resources = site.pages + site.static_files + site.collections.values.flat_map(&:docs)
|
|
74
|
+
occupied = resources.any? { |item| File.expand_path(item.destination(site.dest)) == target }
|
|
75
|
+
Jekyll.logger.warn "JSON Resume:", "#{route}: destination occupied; keeping existing resource" if occupied
|
|
76
|
+
occupied
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../lib/jekyll-theme-resume/resume_validator"
|
|
4
|
+
|
|
5
|
+
module JekyllThemeResume
|
|
6
|
+
# Synthesizes a CV (layout: resume) and profile (layout: profile) page for every
|
|
7
|
+
# languages.<lang> entry in _config.yml that doesn't already have a hand-authored one,
|
|
8
|
+
# the same way ErrorPagesGenerator synthesizes 404/403/500 pages (see
|
|
9
|
+
# error_pages_generator.rb). t_id matches the documented convention (t_id: resume /
|
|
10
|
+
# t_id: profile) so hreflang.html and language-switcher.html match generated and
|
|
11
|
+
# hand-authored pages identically.
|
|
12
|
+
class ResumePagesGenerator < Jekyll::Generator
|
|
13
|
+
safe true
|
|
14
|
+
# priority :low: run after physical pages are parsed, so collision checks are accurate.
|
|
15
|
+
priority :low
|
|
16
|
+
|
|
17
|
+
def generate(site)
|
|
18
|
+
languages = site.config["languages"]
|
|
19
|
+
return unless languages.is_a?(Hash)
|
|
20
|
+
|
|
21
|
+
default_lang = site.config["default_lang"] || "en"
|
|
22
|
+
|
|
23
|
+
languages.each do |lang, lang_cfg|
|
|
24
|
+
unless ResumeValidator::LANG_KEY_REGEX.match?(lang.to_s)
|
|
25
|
+
Jekyll.logger.warn "ResumePagesGenerator:",
|
|
26
|
+
"skipping language #{lang.to_s.inspect}: keys may only use letters, digits, '-' and '_'."
|
|
27
|
+
next
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
lang_cfg ||= {}
|
|
31
|
+
synthesize_or_warn(site, "resume", lang, lang_cfg["url"], lang_cfg)
|
|
32
|
+
|
|
33
|
+
profile_url = lang == default_lang ? "/" : "/#{lang}/"
|
|
34
|
+
synthesize_or_warn(site, "profile", lang, profile_url, lang_cfg)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def synthesize_or_warn(site, layout, lang, permalink, lang_cfg)
|
|
41
|
+
return if existing_page?(site, layout, lang, permalink)
|
|
42
|
+
|
|
43
|
+
unless permalink && auto_generate?(site, lang_cfg)
|
|
44
|
+
Jekyll.logger.warn "ResumePagesGenerator:",
|
|
45
|
+
"no #{layout} page for language '#{lang}' - auto-generation is " \
|
|
46
|
+
"disabled or languages.#{lang}.url is missing, and no " \
|
|
47
|
+
"hand-authored page exists. This language will not be built."
|
|
48
|
+
return
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
page = Jekyll::PageWithoutAFile.new(site, site.source, "", "#{layout}-#{lang}.html")
|
|
52
|
+
page.content = ""
|
|
53
|
+
page.data["layout"] = layout
|
|
54
|
+
page.data["lang"] = lang
|
|
55
|
+
page.data["t_id"] = layout
|
|
56
|
+
page.data["permalink"] = permalink
|
|
57
|
+
site.pages << page
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def existing_page?(site, layout, lang, permalink)
|
|
61
|
+
site.pages.any? { |p| p.data["layout"] == layout && p.data["lang"] == lang } ||
|
|
62
|
+
(permalink && site.pages.any? { |p| p.url == permalink })
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def auto_generate?(site, lang_cfg)
|
|
66
|
+
return lang_cfg["auto_generate_pages"] unless lang_cfg["auto_generate_pages"].nil?
|
|
67
|
+
|
|
68
|
+
site.config["resume_auto_generate_pages"] != false
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../lib/jekyll-theme-resume/resume_validator"
|
|
4
|
+
|
|
5
|
+
module JekyllThemeResume
|
|
6
|
+
# Jekyll Generator that validates resume YAML data during Jekyll build (on by default).
|
|
7
|
+
#
|
|
8
|
+
# _config.yml switches:
|
|
9
|
+
# validate_resume: false # Opt out of validation entirely
|
|
10
|
+
# validate_resume_strict: true # Abort the build on validation errors
|
|
11
|
+
class ResumeValidatorGenerator < Jekyll::Generator
|
|
12
|
+
safe true
|
|
13
|
+
priority :low
|
|
14
|
+
|
|
15
|
+
def generate(site)
|
|
16
|
+
return if site.config["validate_resume"] == false
|
|
17
|
+
|
|
18
|
+
data_dir = site.config["data_dir"] || "_data"
|
|
19
|
+
full_data_path = site.in_source_dir(data_dir)
|
|
20
|
+
|
|
21
|
+
# Check if the data directory or language subdirectories exist
|
|
22
|
+
unless Dir.exist?(full_data_path)
|
|
23
|
+
Jekyll.logger.warn("ResumeValidator:", "Configured data directory '#{full_data_path}' not found. Skipping.")
|
|
24
|
+
return
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Hand over the already-parsed config so languages come from `languages:` without re-reading disk.
|
|
28
|
+
validator = ResumeValidator.new(full_data_path, config: site.config)
|
|
29
|
+
strict = site.config["validate_resume_strict"] == true
|
|
30
|
+
fail_warnings = site.config["validate_resume_fail_on_warnings"] == true
|
|
31
|
+
validator.validate(fail_on_warnings: fail_warnings)
|
|
32
|
+
|
|
33
|
+
return unless strict && (validator.errors.any? || (fail_warnings && validator.warnings.any?))
|
|
34
|
+
|
|
35
|
+
msg = "Resume validation failed with #{validator.errors.size} error(s)"
|
|
36
|
+
msg += " and #{validator.warnings.size} warning(s)" if fail_warnings
|
|
37
|
+
raise Jekyll::Errors::FatalException, "#{msg} in strict mode."
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|