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,378 @@
1
+ # Configuration reference (`_config.yml`)
2
+
3
+ *Audience: site owners*
4
+
5
+ Every setting the theme reads from a consuming site's `_config.yml`. The annotated master copy is [`_config.sample.yml`](../../_config.sample.yml); the build reads your site’s `_config.yml`, not the sample automatically. Templates, generators, and the gemspec define current behavior.
6
+
7
+ Per-language settings (name, title, address, data path, URL) live under `languages.<lang>`. Direction, fonts, and UI strings are not config at all: they come from `_data/locales/<lang>.yml` (see [Locale keys](locale-keys.md)). Upgrading from v0.9.0? See [Migrate from v0.9 to v1.0](../how-to/migrate-v0.9-to-v1.0.md). The requirements (Ruby, Jekyll, loading the theme through `group :jekyll_plugins`) are listed in [Getting started](../tutorials/getting-started.md).
8
+
9
+ ## Configuration Reference
10
+
11
+ ### 1. Site Identity
12
+
13
+ | Setting | Type | Default | Description |
14
+ |---|---|---|---|
15
+ | `theme` | String | | **Required.** `jekyll-theme-resume`. |
16
+ | `title` | String | `""` | **Required.** Site title for SEO tags, avatar link title, and footers. |
17
+ | `description` | String | `""` | Site summary emitted by `{% seo %}`. |
18
+ | `url` | String | `""` | **Required.** Protocol and domain (e.g., `https://example.com`). Used for absolute hreflang URLs. |
19
+ | `baseurl` | String | `""` | Subdirectory path if the site is not served from the domain root. |
20
+ | `timezone` | String | unset (system time zone; the sample sets `UTC`) | Timezone for date rendering (e.g., `America/New_York`, `Asia/Riyadh`). Jekyll core key. |
21
+
22
+ The page language and text direction are not site settings. Every layout resolves the language from `page.lang`, then `default_lang`, then `en`, and reads `direction` from that language's locale file.
23
+
24
+ ---
25
+
26
+ ### 2. Favicons & Web App Manifest
27
+
28
+ The theme ships a favicon suite under `assets/favicon/resume/`. Override any asset with a site-level path:
29
+
30
+ | Setting | Type | Default | Description |
31
+ |---|---|---|---|
32
+ | `favicon` | String | `"assets/favicon/resume/favicon.ico"` | Primary `.ico` shortcut icon. |
33
+ | `apple_touch_icon` | String | `"assets/favicon/resume/apple-touch-icon.png"` | 180x180 PNG icon for iOS home screens. |
34
+ | `favicon_32` | String | `"assets/favicon/resume/favicon-32x32.png"` | 32x32 browser favicon. |
35
+ | `favicon_16` | String | `"assets/favicon/resume/favicon-16x16.png"` | 16x16 browser favicon. |
36
+
37
+ All favicon paths pass through `relative_url` in [`_includes/shared-head.html`](../../_includes/shared-head.html), so they work under a `baseurl`.
38
+
39
+ ---
40
+
41
+ ### 3. Languages
42
+
43
+ One entry per language, keyed by language code. The same code names the page's `lang`, the locale file `_data/locales/<code>.yml`, and (usually) the data folder.
44
+
45
+ | Setting | Type | Description |
46
+ |---|---|---|
47
+ | `languages.<lang>.data_path` | String | **Required.** Data folder under `_data/`. Dot paths select nested folders (`"2025-06.v1"` reads `_data/2025-06/v1/`); `""` reads `_data/` itself. |
48
+ | `languages.<lang>.url` | String | CV permalink used by page generation, profile CV buttons, the language switcher fallback, and error Home buttons when no profile page exists. |
49
+ | `languages.<lang>.about` | String | Profile-page bio; accepts Markdown, HTML, or plain text. |
50
+ | `languages.<lang>.name_html` | String | Optional profile-page name markup. Without it the final space-separated word in `name` is bolded. Keep `name` plain text for other uses. |
51
+ | `languages.<lang>.header_intro` | Boolean | `true` renders `intro` from this language's `header.yml` below the header. |
52
+ | `languages.<lang>.name` | String | Full name shown in the resume header. |
53
+ | `languages.<lang>.resume_title` | String | Job title shown under the name. |
54
+ | `languages.<lang>.address` | String | Location shown in the contact row and in Schema.org microdata. |
55
+ | `languages.<lang>.avatar_alt` | String | Avatar alt text. Falls back to `name`, then the locale's `ui.photo_alt`. |
56
+ | `languages.<lang>.postal_code`, `city`, `country_code`, `region` | String | Optional. Exported as `basics.location.postalCode`, `city`, `countryCode`, and `region` in the JSON Resume file; not shown on the page. An invalid `country_code` is omitted with a warning. |
57
+ | `languages.<lang>.auto_generate_pages` | Boolean | Per-language override of `resume_auto_generate_pages` below. Set `false` to require a hand-authored page for just this language even when auto-generation is on globally, or `true` to auto-generate just this language even when it's off globally. |
58
+ | `default_lang` | String | Language used when a page has no `lang`, for the hreflang `x-default` link, and for error page button labels. Default `en`. |
59
+ | `resume_auto_generate_pages` | Boolean | Default `true`. When a `languages.<lang>` entry has no hand-authored CV (`layout: resume`) or profile (`layout: profile`) page, the theme synthesizes one automatically at `languages.<lang>.url` (CV) and `/` for `default_lang` or `/<lang>/` for any other language (profile), each carrying `t_id: resume` / `t_id: profile` so hreflang and the language switcher match them like any hand-authored page. A hand-authored page for a given `layout`+`lang` always wins over auto-generation. Set `false` to require every language to have its own hand-authored page; a language left without a page (auto-generation off and no hand-authored file) logs a build warning instead of failing silently. |
60
+
61
+ ```yaml
62
+ languages:
63
+ en:
64
+ data_path: en
65
+ url: /en/cv/
66
+ header_intro: true
67
+ name: "Jane Doe"
68
+ resume_title: "Senior Product Manager"
69
+ address: "San Francisco, CA"
70
+ avatar_alt: "Jane Doe - Professional Profile"
71
+ ar:
72
+ data_path: ar
73
+ url: /ar/cv/
74
+ header_intro: true
75
+ name: "جين دو"
76
+ resume_title: "مديرة منتج أولى"
77
+ address: "سان فرانسيسكو، كاليفورنيا"
78
+ avatar_alt: "جين دو - الصورة الشخصية"
79
+
80
+ default_lang: en
81
+ ```
82
+
83
+ **Note:** a page occupying the intended permalink also prevents generation, even if its layout differs. Profile generation does not require a CV URL; CV generation does. Disabled generation or a missing CV URL produces a warning when the corresponding page is absent.
84
+
85
+ Adding a language beyond the six shipped ones: [Add a language](../how-to/add-a-language.md).
86
+
87
+ ---
88
+
89
+ ### 4. Profile Picture / Avatar Settings
90
+
91
+ [`_includes/avatar.html`](../../_includes/avatar.html) renders the avatar. Alt text is per language (`languages.<lang>.avatar_alt`, section 3).
92
+
93
+ | Setting | Type | Default | Description |
94
+ |---|---|---|---|
95
+ | `resume_avatar` | Boolean | unset (hidden) | `true` shows the avatar in the resume header. Must be a Boolean. |
96
+ | `avatar_url` | String | `"/assets/images/Profile-min.jpg"` | Local path (passed through `relative_url`) or external URL (anything containing a scheme, used as-is). Only `http`/`https` schemes are allowed: `javascript:`, `data:`, `file:` and others are a validator error and the avatar is not rendered. The theme does not ship the default file: supply it or set `avatar_url`. |
97
+ | `avatar_link` | String / Boolean | `"/"` | Link destination: a relative path or `http`/`https`/`mailto`/`tel` URL. `false`, or a disallowed scheme, renders a plain `<img>`. |
98
+ | `avatar_link_target` | String | `"_self"` | Link `target` attribute. |
99
+
100
+ ```yaml
101
+ resume_avatar: true
102
+ avatar_url: "assets/images/profile.jpg" # or "https://cdn.example.com/avatar.jpg"
103
+ avatar_link: "/"
104
+ avatar_link_target: "_self"
105
+ ```
106
+
107
+ ---
108
+
109
+ ### 5. Contact Information
110
+
111
+ Language-neutral contact details. The address is per language (`languages.<lang>.address`, section 3).
112
+
113
+ | Setting | Type | Default | Description |
114
+ |---|---|---|---|
115
+ | `contact_info.email` | String | `""` | Shown in the contact row and used by the contact button when `resume_looking_for_work: true`. |
116
+ | `contact_info.phone` | String | `""` | Primary phone number. |
117
+ | `contact_info.dob` | Date | unset | Date of birth (`YYYY-MM-DD`), formatted with the locale's month names. |
118
+ | `contact_info.email_live` | String | `""` | Replaces `email` when `enable_live: true`. |
119
+ | `contact_info.phone_live` | String | `""` | Replaces `phone` when `enable_live: true`. |
120
+
121
+ ```yaml
122
+ contact_info:
123
+ email: "jane.doe@example.com"
124
+ phone: "+1 555 555 5555"
125
+ dob: 1992-05-14
126
+ # email_live: "live@janedoe.com"
127
+ # phone_live: "+1 555 000 0000"
128
+ ```
129
+
130
+ ---
131
+
132
+ ### 6. Social Media Links
133
+
134
+ [`_includes/social-links.html`](../../_includes/social-links.html) renders an icon for each configured platform; [`_includes/print-social-links.html`](../../_includes/print-social-links.html) prints the supported platform list as text, labelled from the locale's `ui.social_labels`. Only supported, configured platforms render. `mastodon` currently adds `rel="me"` metadata in the default and profile layouts; it has no social icon or print-list entry yet.
135
+
136
+ `email` renders a `mailto:` link with an accessible label. Every other key takes a full `http`/`https` URL (`whatsapp` is a URL such as `https://wa.me/1234567890`). A value with any other scheme is a validator error and renders no link.
137
+
138
+ ```yaml
139
+ social_links:
140
+ email: "jane.doe@example.com"
141
+ github: https://github.com/yourusername
142
+ linkedin: https://www.linkedin.com/in/yourhandle/
143
+ twitter: https://twitter.com/yourhandle
144
+ telegram: https://t.me/yourhandle
145
+ medium: https://medium.com/@yourhandle
146
+ website: https://yourwebsite.com
147
+ whatsapp: https://wa.me/1234567890
148
+ instagram: https://instagram.com/yourhandle
149
+ facebook: https://facebook.com/yourhandle
150
+ youtube: https://youtube.com/@yourhandle
151
+ devto: https://dev.to/yourhandle
152
+ dribbble: https://dribbble.com/yourhandle
153
+ flickr: https://flickr.com/people/yourhandle
154
+ pinterest: https://pinterest.com/yourhandle
155
+ # mastodon emits <link rel="me"> in default.html and profile.html for Fediverse verification:
156
+ mastodon: https://mastodon.social/@yourhandle
157
+ ```
158
+
159
+ ---
160
+
161
+ ### 7. Resume Display & Behavior Controls
162
+
163
+ | Setting | Type | Default | Description |
164
+ |---|---|---|---|
165
+ | `resume_language_switcher` | Boolean | `true` | Floating switcher listing every other entry in `languages`. `false` hides it; `language_switcher: false` in page front matter hides it on one page. |
166
+ | `display_header_contact_info` | Boolean | unset (hidden) | `true` shows the phone, email, address, and date-of-birth row in the header. |
167
+ | `resume_looking_for_work` | Boolean / omitted | omitted | `true`: contact button; `false`: "not looking" pill; omitted: nothing. |
168
+ | `enable_summary` | Boolean | `false` | Show `summary` fields under roles and courses. |
169
+ | `enable_live` | Boolean | unset | `true` uses `phone_live` and `email_live` instead of `phone` and `email`. An explicit `false` also adds the print-only footer with the page's canonical URL (omitting the key does not). |
170
+ | `resume_print_social_links` | Boolean | unset (hidden) | `true` prints the text list of social links on paper and PDF. |
171
+
172
+ The header intro toggle is per language: `languages.<lang>.header_intro` (section 3).
173
+
174
+ ```yaml
175
+ resume_language_switcher: true
176
+ display_header_contact_info: true
177
+ resume_looking_for_work: true
178
+ enable_summary: false
179
+ enable_live: false
180
+ resume_print_social_links: true
181
+ ```
182
+
183
+ ---
184
+
185
+ ### 8. Resume Sections Toggle & Order
186
+
187
+ [`_includes/resume-section.html`](../../_includes/resume-section.html) renders one section per entry in `resume_section_order`, for every language. A section branch renders when its `resume_section.<name>` flag is truthy; use YAML booleans. Missing or empty data can leave a section heading without items. Section headings come from the locale's `ui.section_titles`.
188
+
189
+ ```yaml
190
+ resume_section:
191
+ experience: true
192
+ education: true
193
+ certifications: true
194
+ courses: true
195
+ volunteering: true
196
+ projects: true
197
+ associations: true
198
+ skills: true
199
+ recognitions: false # plural only; the singular `recognition` key was removed in v1.0.0
200
+ languages: false
201
+ lang_header: true # compact language list in the header (needs display_header_contact_info: true); suppresses the full languages section
202
+ interests: false
203
+ links: false
204
+ publications: false
205
+ references: false
206
+
207
+ resume_section_order:
208
+ - experience
209
+ - education
210
+ - certifications
211
+ - courses
212
+ - volunteering
213
+ - projects
214
+ - associations
215
+ - skills
216
+ - recognitions
217
+ - languages
218
+ - interests
219
+ - links
220
+ - publications
221
+ - references
222
+ ```
223
+
224
+ ---
225
+
226
+ ### 9. Styling, Fonts & Dark Mode
227
+
228
+ | Setting | Type | Default | Description |
229
+ |---|---|---|---|
230
+ | `dark_mode` | String / Boolean | `"auto"` | `"auto"`: CSS-only `prefers-color-scheme`, no toggle. `"enabled"` or `true`: floating toggle with `localStorage` persistence. `false`, or any other value such as `"disabled"`: no toggle. |
231
+ | `resume_theme` | String | `"default"` | Added as a `theme-<value>` body class. `no-custom-fonts` also stops web font loading (same effect as `disable_google_fonts: true`). |
232
+ | `disable_google_fonts` | Boolean | `false` | `true` stops the resume layout from loading any locale `font_url` or the default Lora and Open Sans stylesheet. Supply the font yourself, or set `font_family` to a system font in a site locale override. |
233
+
234
+ Page front matter `dark_mode: false` / `true` controls the toggle on one page. These settings do not disable the system-preference CSS or the stored-preference script; `false` hides the button rather than forcing a light palette.
235
+
236
+ Per-language fonts (`font_url`, `font_family`) are locale settings, not `_config.yml` settings: see [Override locale strings](../how-to/override-locale-strings.md). Why dark mode is off in print: [Dark mode approach](../explanation/dark-mode-approach.md).
237
+
238
+ ---
239
+
240
+ ### 10. Analytics Configuration
241
+
242
+ Configure at most one provider.
243
+
244
+ | Setting | Type | Description |
245
+ |---|---|---|
246
+ | `analytics.gtm` | String | Google Tag Manager container ID (`"GTM-XXXXXXX"`). Injects the `<head>` script and the `<body>` `<noscript>` iframe. |
247
+ | `analytics.gtag` | String | Google Analytics 4 Measurement ID (`"G-XXXXXXXXXX"`). Injects async `gtag.js`. |
248
+
249
+ ```yaml
250
+ analytics:
251
+ # gtm: "GTM-XXXXXXX"
252
+ # gtag: "G-XXXXXXXXXX"
253
+ ```
254
+
255
+ ---
256
+
257
+ ### 11. Build-Time Validation
258
+
259
+ The validator runs on every build by default, logs findings, and never fails the build unless strict mode is on. Full reference: [`validator-cli.md`](validator-cli.md).
260
+
261
+ | Setting | Type | Default | Description |
262
+ |---|---|---|---|
263
+ | `validate_resume` | Boolean | on | Set to `false` to skip validation during builds. |
264
+ | `validate_resume_strict` | Boolean | `false` | `true` aborts the build when validation finds errors. |
265
+ | `validate_resume_fail_on_warnings` | Boolean | `false` | With strict mode, also abort on warnings. |
266
+
267
+ ```yaml
268
+ # validate_resume: false # opt out
269
+ validate_resume_strict: false
270
+ ```
271
+
272
+ ---
273
+
274
+ ### 12. Jekyll Build Settings & Plugins
275
+
276
+ ```yaml
277
+ plugins:
278
+ - jekyll-theme-resume
279
+ - jekyll-feed
280
+ - jekyll-seo-tag
281
+ - jekyll-sitemap
282
+ - jekyll-redirect-from
283
+
284
+ include:
285
+ - _redirects
286
+ - .well-known/
287
+ - _pages/
288
+ - _posts/
289
+
290
+ exclude:
291
+ - scratch.md
292
+ - README.md
293
+ - Gemfile*
294
+ - vendor/
295
+ - node_modules/
296
+ - "*.gemspec"
297
+ - netlify.toml
298
+ - vercel.json
299
+ - WARP.md
300
+ - scripts/
301
+
302
+ defaults: []
303
+ ```
304
+
305
+ ---
306
+
307
+ ### 13. JSON Resume export
308
+
309
+ Exports are on by default. Every key is optional.
310
+
311
+ | Setting | Type | Default | Description |
312
+ |---|---|---|---|
313
+ | `json_resume.enabled` | Boolean | `true` | `false` disables all exports. |
314
+ | `json_resume.root_export` | Boolean | `true` | `false` disables only the root `/resume.json` copy. |
315
+ | `json_resume.languages` | Array | `[]` | Missing or empty means every key in `site.languages`. A nonempty list is an allowlist; unknown languages are warned about and ignored. A malformed non-array value skips generation. Language codes may contain letters, digits, and internal hyphens or underscores; route separators and traversal characters are rejected. |
316
+ | `json_resume.privacy.export_contact_info` | Boolean | `true` | `false` omits email, phone, location and the WhatsApp profile from the export. With `true`, phone and location are exported only if `display_header_contact_info: true`, and email only if that or `resume_looking_for_work` is `true`. |
317
+ | `social_usernames.<network>` | String | unset | Optional profile username. `social_links` values remain URL strings. |
318
+
319
+ ```yaml
320
+ json_resume:
321
+ enabled: true
322
+ root_export: true
323
+ languages: [] # Missing or empty means every key in site.languages
324
+ privacy:
325
+ export_contact_info: true
326
+
327
+ # Optional usernames; existing social_links values remain URL strings.
328
+ social_usernames:
329
+ github: octocat
330
+ ```
331
+
332
+ Routes, collision rules, visibility, and field mappings: [JSON Resume export configuration and privacy](json-resume-fields.md#configuration).
333
+
334
+ ### 14. Content Security Policy
335
+
336
+ The theme ships no CSP header (static sites set headers at the host). It does emit inline code, so a strict `script-src` needs a hash for each inline script, or `'unsafe-inline'`.
337
+
338
+ | Inline code | Where | Directive that governs it |
339
+ |---|---|---|
340
+ | Anti-FOUC theme detector (reads `localStorage`, sets `data-theme`) | `<head>`, every page (`shared-head.html`) | `script-src` |
341
+ | Dark-mode toggle handler | `dark-mode-toggle.html`, when the toggle is enabled | `script-src` |
342
+ | Error-page language detection | `404`/`403`/`500`/`503` pages (`error.html`) | `script-src` |
343
+ | Google Tag Manager or `gtag` snippet | `analytics-head.html`, when `analytics.gtm` / `analytics.gtag` is set | `script-src`, plus `connect-src`/`img-src`/`frame-src` for Google hosts |
344
+ | Locale font and line-height variables | `<style>` in `resume.html` | `style-src` |
345
+ | `onclick="window.location.reload();"` on the error-page Reload button | 500/503 error pages | `script-src-attr` |
346
+ | Some `style="..."` attributes (project and association titles, GTM `<noscript>` iframe) | resume sections, `analytics-body.html` | `style-src-attr` |
347
+
348
+ The language switcher is plain links and needs no script. `<script type="application/json">` blocks are data, not executed, and are not covered by CSP.
349
+
350
+ Starting point without analytics (hashes come from the command below):
351
+
352
+ ```text
353
+ Content-Security-Policy:
354
+ default-src 'self';
355
+ script-src 'self' 'sha256-...' 'sha256-...';
356
+ style-src 'self' 'sha256-...';
357
+ style-src-attr 'unsafe-inline';
358
+ script-src-attr 'none';
359
+ img-src 'self' https: data:;
360
+ object-src 'none'; base-uri 'self'; frame-ancestors 'self'
361
+ ```
362
+
363
+ - `script-src-attr 'none'` blocks the error-page Reload `onclick`; allow it with `'unsafe-hashes' 'sha256-<hash of window.location.reload();>'` if you keep that button.
364
+ - `style-src-attr 'unsafe-inline'` covers the few inline `style` attributes; removing it needs those attributes moved into CSS.
365
+ - With analytics, add the Google hosts you use (for GA4: `https://www.googletagmanager.com` in `script-src`, `https://*.google-analytics.com` in `connect-src`). Tag Manager can load arbitrary tags, which a strict policy cannot control.
366
+ - A hash changes whenever the inline code changes: the theme's scripts change on upgrade, and the analytics snippet changes with your ID. Regenerate after each upgrade or config change.
367
+
368
+ Generate the hashes from a built site (run in the site root after `jekyll build`; Ruby only, no extra gems):
369
+
370
+ ```bash
371
+ ruby -rdigest -rbase64 -e '
372
+ tags = Dir["_site/**/*.html"].flat_map do |f|
373
+ File.read(f).scan(%r{<(script|style)(?![^>]*\s(?:src|type="application/json"))[^>]*>(.*?)</\1>}m)
374
+ end
375
+ tags.uniq.each { |tag, body| puts "#{tag == "script" ? "script-src" : "style-src"} \x27sha256-#{Base64.strict_encode64(Digest::SHA256.digest(body))}\x27" }'
376
+ ```
377
+
378
+ Each output line names the directive to add the hash to. Hashes cover the exact bytes between the tags, whitespace included, so serve the files unminified-after-hash or hash the final output.