jekyll-theme-resume 1.3.0 → 1.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0e076d6b78d7877746a77bdc4a8d059ae08cb2e7bdd377152ab4567b32a2ed78
4
- data.tar.gz: 8e9ea90a8e10b66755160e3e39475489c9965a4a47f2aa73446db9eb8c2b294c
3
+ metadata.gz: f52cbf7a52fc458e7c41a07de7dbf3a48a397c8f6e78456e507afe7cc2882df6
4
+ data.tar.gz: df0da124295d8a02e85189962387c5b908a40632a4a23fc7739cc86de9a1f6b1
5
5
  SHA512:
6
- metadata.gz: b569cdd97368ba6b59c593cde172b12fc828082291bc8920ca7acfe4459e086e39f334c51f4692c38a4173162d6266a27483342ebd0f4f4435b63f0da39d225f
7
- data.tar.gz: a341f452fce3e1c298b7f3b106a35d1d1b46a6979795dc9ded191e6e9cb5521df4deb198498520b8ac2c1c1b5a4636416089a5a1227bb4e177cb9a2db70b056c
6
+ metadata.gz: 3303c1b21f23fb2e08a0aaf1932219a11ab1f254da4d15a4d4f205cb7087bdfbf8c3069b9de52af22c1365db9be597448a97e99be16f9a2e536dad9a5bd69394
7
+ data.tar.gz: 83565e16549fb42737512e3efbb18fba991603f07b7ddbc79b40e055511d2aac6b22ebf575b17d0255c826a98a0f41a2b4b310019c54afc90b99f5a8a38d1f02
data/CHANGELOG.md CHANGED
@@ -4,6 +4,50 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+ ## [1.3.2] - 2026-10-26
8
+
9
+ ### Added
10
+ * Add initial trail setup and documentation for coding sessions ([`907835b`](https://github.com/kmutahar/jekyll-theme-resume/commit/907835b55da7d4f0f84f0028f22e6868e5458186))
11
+
12
+ * Add Windows and JRuby gems to the sample Gemfile ([`ec1223d`](https://github.com/kmutahar/jekyll-theme-resume/commit/ec1223da04393f4abc5dd5673b978044deede65c))
13
+
14
+ * Add Discord and Behance icons ([`49ede2c`](https://github.com/kmutahar/jekyll-theme-resume/commit/49ede2c9e0eea0fe0edf2440d11e1f11e52bc34a))
15
+
16
+
17
+ ### Changed
18
+ * Update trail learnings ([`05dfe16`](https://github.com/kmutahar/jekyll-theme-resume/commit/05dfe164f2760e936773d43ee7a82e41d3c8f790))
19
+
20
+ * Retire SEC-04 ([`aea06d8`](https://github.com/kmutahar/jekyll-theme-resume/commit/aea06d8ef42716613ad9959445130da1c46fb030))
21
+
22
+ * Link every feature to a GitHub issue and trim shipped social platforms ([`3aca714`](https://github.com/kmutahar/jekyll-theme-resume/commit/3aca7141e89864973cbd504dfec0539f0fa9b222))
23
+
24
+ * Emit JSON-LD ProfilePage/Person structured data ([`5f7a989`](https://github.com/kmutahar/jekyll-theme-resume/commit/5f7a989d1ccec6190d3c381ec2ac3d0abeaf0079))
25
+
26
+ * Drop duplicated lists, derive export networks, cover aria-hidden ([`ec6dda8`](https://github.com/kmutahar/jekyll-theme-resume/commit/ec6dda87d8430d89c17882d44afd11cab48b151f))
27
+
28
+
29
+ ### Fixed
30
+ * Order full dates by locale pattern ([`87c892c`](https://github.com/kmutahar/jekyll-theme-resume/commit/87c892c74f1ddc1ce633f2b110e581d8edf9f9d4))
31
+
32
+ * Correct homepage URL formatting in documentation and gemspec ([`b649efd`](https://github.com/kmutahar/jekyll-theme-resume/commit/b649efdcc270d91bc639be35aa5d71d7367a48b5))
33
+
34
+ * Reject plain-string education awards ([`15aa799`](https://github.com/kmutahar/jekyll-theme-resume/commit/15aa7998bda7da8e670ecc1e2ead55b13d1200d0))
35
+
36
+ ## [1.3.1] - 2026-10-05
37
+
38
+ ### Added
39
+ * Add Rule 9, release files are owned by bin/release ([`4324433`](https://github.com/kmutahar/jekyll-theme-resume/commit/43244338cc4a027a8c2296f4a028ef0ce67dd4ee))
40
+
41
+
42
+ ### Changed
43
+ * Stop tracking local implementation plan ([`0ad72dc`](https://github.com/kmutahar/jekyll-theme-resume/commit/0ad72dc330ca8205b248c382c9980462ba6eb557))
44
+
45
+ * Publish to RubyGems via Trusted Publishing (SEC-04) ([`02a4c4b`](https://github.com/kmutahar/jekyll-theme-resume/commit/02a4c4bec139568aae4c673bf7d00bbafd85a71b))
46
+
47
+
48
+ ### Fixed
49
+ * Read files as UTF-8 so the gem loads under a US-ASCII default encoding ([`0fb3e43`](https://github.com/kmutahar/jekyll-theme-resume/commit/0fb3e43d9943021308150e93b25bed54d5a0fc57))
50
+
7
51
  ## [1.3.0] - 2026-10-04
8
52
 
9
53
  ### Added
@@ -476,6 +520,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
476
520
 
477
521
  * Initial commit (New Theme Template) ([`00af662`](https://github.com/kmutahar/jekyll-theme-resume/commit/00af6628dfec7aefe0ef7d7083bf98c9713a5ffd))
478
522
 
523
+ [1.3.2]: https://github.com/kmutahar/jekyll-theme-resume/compare/v1.3.1...v1.3.2
524
+ [1.3.1]: https://github.com/kmutahar/jekyll-theme-resume/compare/v1.3.0...v1.3.1
479
525
  [1.3.0]: https://github.com/kmutahar/jekyll-theme-resume/compare/v1.1.0...v1.3.0
480
526
  [1.1.0]: https://github.com/kmutahar/jekyll-theme-resume/compare/v1.0.2...v1.1.0
481
527
  [1.0.2]: https://github.com/kmutahar/jekyll-theme-resume/compare/v1.0.1...v1.0.2
data/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  A flexible Jekyll theme for clean, data-driven, multilingual resume/CV websites. Ships English, Arabic, Spanish, French, German, and Urdu; any other language is added from your site alone. Created and maintained by Khaldoon Mutahar. See the latest version on the [Releases page](https://github.com/kmutahar/jekyll-theme-resume/releases).
6
6
 
7
- **Links:** [RubyGems](https://rubygems.org/gems/jekyll-theme-resume) · [Live demo](https://www.mutahr.me/jekyll-theme-resume) · [Source](https://github.com/kmutahar/jekyll-theme-resume)
7
+ **Links:** [RubyGems](https://rubygems.org/gems/jekyll-theme-resume) · [Live demo](https://www.mutahar.me/jekyll-theme-resume) · [Source](https://github.com/kmutahar/jekyll-theme-resume)
8
8
  Inspired by and originally forked from [Joel Glovier’s resume template](https://github.com/jglovier/resume-template/). Joel’s version was a basic English-only theme with limited customization (e.g., no section reordering); this project has since evolved into a fully separate theme authored by Khaldoon.
9
9
 
10
10
  ## Features
@@ -17,6 +17,7 @@ Inspired by and originally forked from [Joel Glovier’s resume template](https:
17
17
  - **Modern favicon suite**: High-resolution favicons (Apple touch icon, 32x32, 16x16, webmanifest) with subpath-safe URLs and `_config.yml` override support
18
18
  - **Print-friendly**: Optimized for PDF generation and printing with bidirectional text isolation (`dir="ltr"`) for URLs
19
19
  - **SEO ready**: Built-in support for multilingual SEO, standardized canonical tags via `jekyll-seo-tag`, sitemaps, and feeds
20
+ - **JSON-LD structured data**: Each CV page carries a Schema.org `ProfilePage` + `Person` block built from the same data as the JSON Resume export (on by default; opt out with `json_ld.enabled: false`). It does not guarantee ATS parsing or rich results.
20
21
  - **JSON Resume Export**: Multilingual builds generate standards-validated JSON Resume files at `/<lang>/resume.json` (on by default; opt out with `json_resume.enabled: false`).
21
22
  - **Automatic pages**: Missing CV and profile pages are generated for each configured language; hand-authored pages take precedence.
22
23
  - **Data validation**: `validate-resume` CLI and build-time checks for schemas, dates, URLs, and parity across every configured language
data/_config.sample.yml CHANGED
@@ -88,6 +88,8 @@ social_links:
88
88
  # dribbble: https://dribbble.com/yourhandle
89
89
  # flickr: https://flickr.com/people/yourhandle
90
90
  # pinterest: https://pinterest.com/yourhandle
91
+ # discord: https://discord.gg/yourinvite
92
+ # behance: https://behance.net/yourhandle
91
93
  # mastodon: https://mastodon.social/@yourhandle # Emits <link rel="me"> in default layout
92
94
 
93
95
  # ==============================================================================
@@ -324,6 +326,11 @@ json_resume:
324
326
  languages: [] # Empty/missing: all configured languages
325
327
  privacy:
326
328
  export_contact_info: true # False removes email, phone, location, WhatsApp from JSON
329
+ # JSON-LD (Schema.org ProfilePage + Person) in each CV page's <head>.
330
+ # Independent of json_resume.enabled; honors json_resume.privacy.export_contact_info.
331
+ # Details: docs/reference/json-ld-fields.md in the theme repository.
332
+ json_ld:
333
+ enabled: true
327
334
  # Optional; social_links values remain URL strings.
328
335
  # social_usernames:
329
336
  # github: octocat
data/_data/locales/ar.yml CHANGED
@@ -53,6 +53,8 @@ ui:
53
53
  pinterest: "بينتيريست"
54
54
  telegram: "تيليجرام"
55
55
  youtube: "يوتيوب"
56
+ discord: "ديسكورد"
57
+ behance: "بيهانس"
56
58
  error_pages:
57
59
  "404":
58
60
  title: "الصفحة غير موجودة"
@@ -70,6 +72,7 @@ present_values:
70
72
  - "present"
71
73
  - "حتى الآن"
72
74
  - "حاليًا"
75
+ date_format_mdy: "{day} {month} {year}"
73
76
  months:
74
77
  - "يناير"
75
78
  - "فبراير"
data/_data/locales/de.yml CHANGED
@@ -53,6 +53,8 @@ ui:
53
53
  pinterest: "Pinterest"
54
54
  telegram: "Telegram"
55
55
  youtube: "YouTube"
56
+ discord: "Discord"
57
+ behance: "Behance"
56
58
  error_pages:
57
59
  "404":
58
60
  title: "Seite Nicht Gefunden"
@@ -71,6 +73,7 @@ present_values:
71
73
  - "heute"
72
74
  - "aktuell"
73
75
  - "gegenwärtig"
76
+ date_format_mdy: "{day}. {month} {year}"
74
77
  months:
75
78
  - "Januar"
76
79
  - "Februar"
data/_data/locales/en.yml CHANGED
@@ -53,6 +53,8 @@ ui:
53
53
  pinterest: "Pinterest"
54
54
  telegram: "Telegram"
55
55
  youtube: "YouTube"
56
+ discord: "Discord"
57
+ behance: "Behance"
56
58
  error_pages:
57
59
  "404":
58
60
  title: "Page Not Found"
@@ -69,6 +71,7 @@ error_pages:
69
71
  present_values:
70
72
  - "present"
71
73
  - "current"
74
+ date_format_mdy: "{month} {day}, {year}"
72
75
  months:
73
76
  - "January"
74
77
  - "February"
data/_data/locales/es.yml CHANGED
@@ -53,6 +53,8 @@ ui:
53
53
  pinterest: "Pinterest"
54
54
  telegram: "Telegram"
55
55
  youtube: "YouTube"
56
+ discord: "Discord"
57
+ behance: "Behance"
56
58
  error_pages:
57
59
  "404":
58
60
  title: "Página No Encontrada"
@@ -71,6 +73,7 @@ present_values:
71
73
  - "actualidad"
72
74
  - "actualmente"
73
75
  - "presente"
76
+ date_format_mdy: "{day} de {month} de {year}"
74
77
  months:
75
78
  - "enero"
76
79
  - "febrero"
data/_data/locales/fr.yml CHANGED
@@ -53,6 +53,8 @@ ui:
53
53
  pinterest: "Pinterest"
54
54
  telegram: "Telegram"
55
55
  youtube: "YouTube"
56
+ discord: "Discord"
57
+ behance: "Behance"
56
58
  error_pages:
57
59
  "404":
58
60
  title: "Page Introuvable"
@@ -71,6 +73,7 @@ present_values:
71
73
  - "actuel"
72
74
  - "actuellement"
73
75
  - "présent"
76
+ date_format_mdy: "{day} {month} {year}"
74
77
  months:
75
78
  - "janvier"
76
79
  - "février"
data/_data/locales/ur.yml CHANGED
@@ -53,6 +53,8 @@ ui:
53
53
  pinterest: "پن ٹریسٹ"
54
54
  telegram: "ٹیلیگرام"
55
55
  youtube: "یوٹیوب"
56
+ discord: "ڈسکورڈ"
57
+ behance: "بیہانس"
56
58
  error_pages:
57
59
  "404":
58
60
  title: "صفحہ نہیں ملا"
@@ -71,6 +73,7 @@ present_values:
71
73
  - "حال"
72
74
  - "موجودہ"
73
75
  - "تاحال"
76
+ date_format_mdy: "{day} {month} {year}"
74
77
  months:
75
78
  - "جنوری"
76
79
  - "فروری"
@@ -7,11 +7,21 @@
7
7
  # itemprop: schema.org microdata value social-links.html sets on the <a>
8
8
  # label: English fallback for the icon's aria-label/title; each locale's
9
9
  # ui.social_labels.<key> wins when present (print labels use it too)
10
+ - key: behance
11
+ icon: behance
12
+ itemprop: sameAs
13
+ label: Behance
14
+
10
15
  - key: devto
11
16
  icon: dev
12
17
  itemprop: url
13
18
  label: Dev.to
14
19
 
20
+ - key: discord
21
+ icon: discord
22
+ itemprop: url
23
+ label: Discord
24
+
15
25
  - key: dribbble
16
26
  icon: dribbble-symbol
17
27
  itemprop: sameAs
@@ -12,7 +12,7 @@
12
12
 
13
13
  Parameters:
14
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".
15
+ - style: "MDY" for a full date, ordered by the locale's date_format_mdy; anything else (default "MY") for "Month Year".
16
16
  - lang: locale code to format in. Defaults to the active page/site language.
17
17
  {%- endcomment -%}
18
18
  {%- assign lang = include.lang | default: page.lang | default: site.default_lang | default: 'en' -%}
@@ -55,7 +55,9 @@
55
55
  {%- elsif month_name == nil -%}
56
56
  {{ include.date }}
57
57
  {%- elsif style == "MDY" and date_parts.size == 3 and day > 0 -%}
58
- {{ month_name }} {{ day }}, {{ year }}
58
+ {%- assign mdy_pattern = locale.date_format_mdy | default: "{month} {day}, {year}" -%}
59
+ {%- assign mdy_text = mdy_pattern | replace: "{month}", month_name | replace: "{day}", day | replace: "{year}", year -%}
60
+ {{ mdy_text }}
59
61
  {%- else -%}
60
62
  {{ month_name }} {{ year }}
61
63
  {%- endif -%}
@@ -0,0 +1,12 @@
1
+ {%- comment -%}
2
+ Include: JSON-LD structured data (Schema.org ProfilePage + Person) for a CV page.
3
+ - Parameters: lang: the active language key.
4
+ - Source: site.json_ld_pages[lang].script, built in Ruby by JsonLdBuilder from the JSON Resume export,
5
+ so inactive entries, live contacts and privacy match /<lang>/resume.json. Already JSON-escaped
6
+ ("<" as \u003c): print it as is, never through jsonify or escape.
7
+ - Emits nothing when json_ld.enabled is false or the language has no export.
8
+ {%- endcomment -%}
9
+ {%- assign json_ld_page = site.json_ld_pages[include.lang] -%}
10
+ {%- if json_ld_page.script -%}
11
+ <script type="application/ld+json">{{ json_ld_page.script }}</script>
12
+ {%- endif -%}
@@ -1,8 +1,7 @@
1
1
  {%- comment -%}
2
2
  Component: Social & Professional Links List
3
3
  - Purpose: Renders interactive social network and contact links defined in `site.social_links` in `_config.yml`.
4
- - Supported Platforms: Email, GitHub, LinkedIn, Telegram, Twitter/X, Medium, Dribbble, Facebook, Instagram,
5
- Website, WhatsApp, Dev.to, Flickr, Pinterest, YouTube.
4
+ - Supported Platforms: see `_data/social_networks.yml`.
6
5
 
7
6
  Architecture & Implementation Details:
8
7
  1. Bundled Local SVGs: Vector icons are sourced locally from `_includes/vendors/svg-icons/`.
@@ -27,6 +27,8 @@ folders. Original folder (version) each file shipped in:
27
27
  | `flickr.svg` | `vendors/lineicons-v5.1/` |
28
28
  | `pinterest.svg` | `vendors/lineicons-v5.1/` |
29
29
  | `youtube.svg` | `vendors/lineicons-v5.1/` |
30
+ | `discord.svg` | `vendors/lineicons-v5.1/` |
31
+ | `behance.svg` | `vendors/lineicons-v5.1/` |
30
32
 
31
33
  To add a new icon (from Lineicons or elsewhere), drop the SVG into this folder and reference it
32
34
  by filename — from `_data/social_networks.yml` for a social platform, or directly via
@@ -0,0 +1,5 @@
1
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false">
2
+ <path d="M10.0968 11.3229C10.6452 11.0971 11.0645 10.7422 11.3226 10.3229C11.5806 9.90351 11.7419 9.35512 11.7419 8.77448C11.7419 8.22609 11.6452 7.74222 11.4839 7.32286C11.2903 6.90351 11.0323 6.58093 10.6774 6.35512C10.3226 6.12932 9.93548 5.93577 9.45161 5.83899C8.93548 5.74222 8.41936 5.70996 7.87097 5.70996H2V17.968H7.96774C8.51613 17.968 9.06452 17.9358 9.54839 17.7422C10.0968 17.6132 10.5484 17.3874 10.9677 17.0971C11.3871 16.839 11.7097 16.4519 11.9355 15.968C12.1613 15.5164 12.2903 14.968 12.2903 14.3229C12.2903 13.5487 12.0968 12.8713 11.7419 12.3551C11.3548 11.9035 10.8065 11.5487 10.0968 11.3229ZM4.74194 7.83899H7.29032C7.51613 7.83899 7.74194 7.87125 7.96774 7.87125C8.19355 7.87125 8.41935 8.00028 8.54839 8.09706C8.74194 8.19383 8.87097 8.32286 8.96774 8.54867C9.06452 8.74222 9.09677 8.96803 9.09677 9.29061C9.09677 9.77448 8.96774 10.1938 8.64516 10.4196C8.32258 10.6132 7.90323 10.7422 7.45161 10.7422H4.74194C4.74194 10.6777 4.74194 7.83899 4.74194 7.83899ZM9.45161 15.0971C9.35484 15.3229 9.22581 15.4519 9 15.5809C8.80645 15.71 8.58065 15.8067 8.35484 15.8067C8.12903 15.839 7.87097 15.839 7.6129 15.839H4.70968V12.5164H7.67742C8.25806 12.5164 8.70968 12.6454 9.09677 12.9358C9.45161 13.1938 9.64516 13.6777 9.64516 14.2906C9.58065 14.6132 9.58065 14.8713 9.45161 15.0971Z" fill="#323544"/>
3
+ <path d="M21.8065 12.1938C21.6774 11.5486 21.3871 11.0002 21.0323 10.5164C20.6774 10.0325 20.2258 9.64541 19.6774 9.32283C19.129 9.06476 18.4516 8.87121 17.7097 8.87121C17.0323 8.87121 16.3871 9.00024 15.9032 9.22605C15.3548 9.45186 14.871 9.77444 14.4516 10.2261C14.0323 10.6454 13.7097 11.1293 13.4839 11.7099C13.2581 12.2583 13.129 12.9035 13.129 13.5809C13.129 14.2583 13.2258 14.9035 13.4516 15.4841C13.6774 16.0648 14 16.5164 14.3548 16.968C14.7742 17.3873 15.2258 17.7099 15.8065 17.9357C16.3548 18.1615 17 18.2906 17.7097 18.2906C18.7097 18.2906 19.5806 18.0648 20.2903 17.6131C21.0323 17.1615 21.5161 16.3873 21.871 15.2906H19.7419C19.6452 15.5486 19.4194 15.839 19.0968 16.0648C18.7742 16.2906 18.3226 16.4196 17.8387 16.4196C17.1613 16.4196 16.6129 16.2261 16.2581 15.8712C15.9032 15.5164 15.6774 14.839 15.6774 14.1293H22C22 13.4519 21.9677 12.7744 21.8065 12.1938ZM15.5484 12.6131C15.5484 12.3873 15.6452 12.1615 15.7097 11.968C15.8065 11.7422 15.9032 11.5164 16.0645 11.3551C16.2581 11.1615 16.5161 10.968 16.7742 10.839C17.0323 10.7099 17.3548 10.7099 17.6452 10.7099C18.2258 10.7099 18.7419 10.9035 19 11.1938C19.2581 11.5164 19.4839 11.968 19.5484 12.6131H15.5484Z" fill="#323544"/>
4
+ <path d="M20.0319 6.54859H15.0319V7.7744H20.0319V6.54859Z" fill="#323544"/>
5
+ </svg>
@@ -0,0 +1,3 @@
1
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false">
2
+ <path d="M18.9419 5.55541C17.6473 4.94967 16.263 4.50945 14.8158 4.25879C14.638 4.58013 14.4304 5.01234 14.2872 5.35616C12.7488 5.1248 11.2244 5.1248 9.71431 5.35616C9.57116 5.01234 9.35878 4.58013 9.17947 4.25879C7.73069 4.50945 6.34478 4.95129 5.05016 5.5586C2.43887 9.5046 1.73099 13.3526 2.08493 17.1459C3.81688 18.4393 5.49534 19.225 7.14547 19.7391C7.55291 19.1784 7.91628 18.5823 8.22931 17.9541C7.63313 17.7276 7.06209 17.448 6.52256 17.1235C6.66569 17.0174 6.80572 16.9066 6.94097 16.7925C10.2318 18.3317 13.8074 18.3317 17.0589 16.7925C17.1958 16.9066 17.3358 17.0174 17.4774 17.1235C16.9362 17.4496 16.3637 17.7292 15.7675 17.9557C16.0805 18.5823 16.4423 19.18 16.8513 19.7407C18.503 19.2266 20.1831 18.4409 21.915 17.1459C22.3303 12.7485 21.2056 8.93585 18.9419 5.55541ZM8.67766 14.8131C7.68978 14.8131 6.87963 13.8908 6.87963 12.7678C6.87963 11.6447 7.67247 10.7209 8.67766 10.7209C9.68284 10.7209 10.493 11.6431 10.4757 12.7678C10.4772 13.8908 9.68284 14.8131 8.67766 14.8131ZM15.3223 14.8131C14.3344 14.8131 13.5243 13.8908 13.5243 12.7678C13.5243 11.6447 14.3171 10.7209 15.3223 10.7209C16.3275 10.7209 17.1376 11.6431 17.1203 12.7678C17.1203 13.8908 16.3275 14.8131 15.3223 14.8131Z" fill="#323544"/>
3
+ </svg>
@@ -1,4 +1,4 @@
1
- <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
1
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false">
2
2
  <path d="M3.73586 4.5C2.7772 4.5 2.00008 5.27724 2.00025 6.2359L2.00024 6.26829C2.01058 6.81904 2.28193 7.33272 2.73193 7.65165L2.7428 7.65929L10.713 13.2171C11.3896 13.689 12.2608 13.7479 12.9861 13.3941C13.0897 13.3435 13.1903 13.2845 13.287 13.2171L21.2569 7.65949C21.7224 7.33485 21.9998 6.8031 21.9998 6.23554C21.9996 5.27702 21.2229 4.5 20.2644 4.5H3.73586Z" fill="#323544"/>
3
3
  <path opacity="0.4" d="M22.0001 8.96973L14.145 14.4473C12.8562 15.346 11.1438 15.346 9.85507 14.4473L2.00023 8.96991L2 17.2498C2 18.4924 3.00736 19.4998 4.25 19.4998H19.75C20.9926 19.4998 22 18.4924 22 17.2498L22.0001 8.96973Z" fill="#323544"/>
4
4
  </svg>
@@ -1,3 +1,3 @@
1
- <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
1
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false">
2
2
  <path d="M12.0014 2.75C17.1095 2.75 21.2505 6.89178 21.2505 11.9999C21.2505 17.108 17.1095 21.2498 12.0014 21.2498M12.0014 2.75C6.89329 2.75 2.75235 6.89178 2.75235 11.9999C2.75235 17.108 6.89329 21.2498 12.0014 21.2498M12.0014 2.75C14.2101 2.75 16.0005 6.89136 16.0005 11.9993C16.0005 17.1073 14.2102 21.2498 12.0014 21.2498M12.0014 2.75C9.79266 2.75 8.00196 6.89137 8.00196 11.9993C8.00196 17.1073 9.79266 21.2498 12.0014 21.2498M11.9996 15.9986C17.1076 15.9986 21.2492 14.2081 21.2492 11.9993C21.2492 9.79058 17.1076 8.00004 11.9996 8.00004C6.89165 8.00004 2.75 9.79058 2.75 11.9993C2.75 14.2081 6.89166 15.9986 11.9996 15.9986Z" stroke="#323544" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
3
3
  </svg>
data/_layouts/resume.html CHANGED
@@ -12,6 +12,7 @@
12
12
  - shared-head: Shared charset, viewport, SEO base, and favicon links
13
13
  - Locale web font (locale.font_url) and locale-driven CSS (assets/css/cv-{{ locale.direction }}.css)
14
14
  - seo: Jekyll SEO tag for social graph, search indexing, and canonical URLs
15
+ - json-ld-resume: Schema.org ProfilePage + Person JSON-LD for this CV (built in Ruby from the JSON Resume export)
15
16
  - analytics-head: Google Analytics / GTM tracking scripts (optional)
16
17
  {%- endcomment -%}
17
18
  {% include shared-head.html %}
@@ -41,6 +42,8 @@
41
42
 
42
43
  {% seo %}
43
44
 
45
+ {% include json-ld-resume.html lang=lang %}
46
+
44
47
  {% include analytics-head.html %}
45
48
  </head>
46
49
 
@@ -51,28 +54,35 @@
51
54
  <!-- Analytics body snippet (noscript, dataLayer, etc.) -->
52
55
  {% include analytics-body.html %}
53
56
 
54
- <div class="wrapper" itemscope itemtype="https://schema.org/Person">
57
+ {%- assign person_id = site.json_ld_pages[lang].person_id -%}
58
+ <div class="wrapper" itemscope itemtype="https://schema.org/Person"{% if person_id %} itemid="{{ person_id | escape }}"{% endif %}>
55
59
  {% include dark-mode-toggle.html %}
56
60
  {% include language-switcher.html %}
57
61
 
58
62
  {%- comment -%}
59
63
  Schema.org Microdata & Live Contact Protection:
64
+ Telephone and address are emitted only when site.display_header_contact_info is true;
65
+ email also when site.resume_looking_for_work is true (same rule as the JSON Resume export).
60
66
  When site.enable_live is true, phone_live and email_live are used for privacy.
61
67
  Fallback to standard phone/email if live versions are not provided.
62
68
  {%- 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 }}" />
69
+ {% if site.display_header_contact_info == true %}
70
+ {% if site.enable_live == true and site.contact_info.phone_live %}
71
+ <meta itemprop="telephone" content="{{ site.contact_info.phone_live }}" />
72
+ {% elsif site.contact_info.phone %}
73
+ <meta itemprop="telephone" content="{{ site.contact_info.phone }}" />
74
+ {% endif %}
67
75
  {% endif %}
68
76
 
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 }}" />
77
+ {% if site.display_header_contact_info == true or site.resume_looking_for_work == true %}
78
+ {% if site.enable_live == true and site.contact_info.email_live %}
79
+ <meta itemprop="email" content="{{ site.contact_info.email_live }}" />
80
+ {% elsif site.contact_info.email %}
81
+ <meta itemprop="email" content="{{ site.contact_info.email }}" />
82
+ {% endif %}
73
83
  {% endif %}
74
84
 
75
- {% if lang_cfg.address %}
85
+ {% if site.display_header_contact_info == true and lang_cfg.address %}
76
86
  <meta itemprop="address" content="{{ lang_cfg.address }}" />
77
87
  {% endif %}
78
88
 
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../lib/jekyll-theme-resume/json_ld_builder"
3
4
  require_relative "../lib/jekyll-theme-resume/json_resume_exporter"
4
5
  require_relative "../lib/jekyll-theme-resume/resume_validator"
5
6
 
@@ -21,15 +22,29 @@ module JekyllThemeResume
21
22
  end
22
23
  end
23
24
 
25
+ # Publishes each language's JSON Resume export as /<lang>/resume.json and the matching JSON-LD
26
+ # script and Person @id to site.json_ld_pages[lang] (read by _includes/json-ld-resume.html).
27
+ # Each language is exported at most once per build, so export warnings are not repeated.
24
28
  class JsonResumeGenerator < Jekyll::Generator
25
29
  safe true
26
30
  priority :lowest
27
31
 
28
32
  def generate(site)
29
33
  site.config["json_resume_routes"] = {}
30
- options = site.config["json_resume"].is_a?(Hash) ? site.config["json_resume"] : {}
34
+ site.config["json_ld_pages"] = {}
31
35
  languages = site.config["languages"]
32
- return if options["enabled"] == false || !languages.is_a?(Hash)
36
+ return unless languages.is_a?(Hash)
37
+
38
+ documents = Hash.new { |cache, lang| cache[lang] = JsonResumeExporter.export(site, lang) }
39
+ publish_json_resume(site, languages, documents)
40
+ publish_json_ld(site, languages, documents)
41
+ end
42
+
43
+ private
44
+
45
+ def publish_json_resume(site, languages, documents)
46
+ options = site.config["json_resume"].is_a?(Hash) ? site.config["json_resume"] : {}
47
+ return if options["enabled"] == false
33
48
 
34
49
  selected_languages(languages, options).each do |lang|
35
50
  unless ResumeValidator::LANG_KEY_REGEX.match?(lang.to_s)
@@ -39,19 +54,34 @@ module JekyllThemeResume
39
54
  route = "/#{lang}/resume.json"
40
55
  next if collision?(site, route)
41
56
 
42
- document = JsonResumeExporter.export(site, lang)
57
+ document = documents[lang]
43
58
  next unless document
44
59
 
45
60
  site.pages << JsonResumePage.new(site, route, document)
46
61
  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)
62
+ publish_root_export(site, lang, document, options)
51
63
  end
52
64
  end
53
65
 
54
- private
66
+ def publish_root_export(site, lang, document, options)
67
+ return unless lang == (site.config["default_lang"] || "en") && options["root_export"] != false
68
+ return if collision?(site, "/resume.json")
69
+
70
+ site.pages << JsonResumePage.new(site, "/resume.json", document)
71
+ end
72
+
73
+ def publish_json_ld(site, languages, documents)
74
+ options = site.config["json_ld"].is_a?(Hash) ? site.config["json_ld"] : {}
75
+ return if options["enabled"] == false
76
+
77
+ languages.each_key do |lang|
78
+ next unless ResumeValidator::LANG_KEY_REGEX.match?(lang.to_s) && documents[lang]
79
+
80
+ graph = JsonLdBuilder.build(documents[lang], lang)
81
+ site.config["json_ld_pages"][lang] = { "script" => JsonLdBuilder.script(graph),
82
+ "person_id" => graph.dig("mainEntity", "@id") }
83
+ end
84
+ end
55
85
 
56
86
  def selected_languages(languages, options)
57
87
  requested = options["languages"]
data/docs/README.md CHANGED
@@ -35,6 +35,7 @@ Steps for one task each.
35
35
  | [Show language proficiency in the header](how-to/show-language-proficiency-in-header.md) | Site owners | Show a one-line language summary in the resume header. |
36
36
  | [Switch resume versions](how-to/switch-resume-versions.md) | Site owners | Load a different set of resume data for a language. |
37
37
  | [Troubleshoot builds](how-to/troubleshoot-builds.md) | Site owners and theme developers | Diagnose a build problem from the symptom. |
38
+ | [Validate structured data](how-to/validate-structured-data.md) | Site owners | Check the JSON-LD block of a CV page and turn it off. |
38
39
  | [Validate resume data in CI](how-to/validate-in-ci.md) | Site owners and theme developers | Gate builds on validation and run the checks in GitHub Actions. |
39
40
  | [Verify accessibility](how-to/verify-accessibility.md) | Site owners and theme developers | Run the automated and manual accessibility checks. |
40
41
 
@@ -48,6 +49,7 @@ Facts to look up: keys, schemas, flags, and structure.
48
49
  | [Data schemas](reference/data-schemas.md) | Site owners | The YAML schema of every resume data file. |
49
50
  | [Locale keys](reference/locale-keys.md) | Site owners and theme developers | The locale file schema, shipped locales, and override rules. |
50
51
  | [JSON Resume export reference](reference/json-resume-fields.md) | Site owners | Export configuration, privacy, and field mappings. |
52
+ | [JSON-LD fields reference](reference/json-ld-fields.md) | Site owners | The JSON-LD output, field mappings, and privacy rules. |
51
53
  | [Validator and build checks](reference/validator-cli.md) | Site owners and theme developers | `validate-resume`, `check-data-keys`, rules by section, and the Ruby API. |
52
54
  | [Accessibility coverage](reference/accessibility-coverage.md) | Site owners and theme developers | What the theme implements and what the automated checks cover. |
53
55
  | [Layouts reference](reference/layouts.md) | Site owners and theme developers | The layouts, language resolution, and data loading. |
@@ -0,0 +1,48 @@
1
+ # Validate structured data
2
+
3
+ *Audience: site owners*
4
+
5
+ Check the JSON-LD block on a built CV page, run it through the public validators, and turn it off if you do not want it. What the block contains is in the [JSON-LD fields reference](../reference/json-ld-fields.md).
6
+
7
+ JSON-LD does not guarantee ATS parsing or rich results. These checks show that the markup is valid, not that a crawler will use it.
8
+
9
+ ## 1. Build and extract the block
10
+
11
+ Build the site, then print the JSON-LD from a CV page. With Ruby and Nokogiri (already in the theme's bundle):
12
+
13
+ ```bash
14
+ bundle exec jekyll build
15
+ bundle exec ruby -rnokogiri -rjson -e '
16
+ doc = Nokogiri::HTML(File.read("_site/en/cv/index.html"))
17
+ doc.css(%q(script[type="application/ld+json"])).each do |node|
18
+ data = JSON.parse(node.text)
19
+ puts JSON.pretty_generate(data) if data["@type"] == "ProfilePage"
20
+ end'
21
+ ```
22
+
23
+ Adjust the path to your CV page. The page has two JSON-LD blocks: the theme's `ProfilePage` and the `WebPage` block from `jekyll-seo-tag`. The filter above prints ours.
24
+
25
+ In a browser, open the page, view source, and search for `application/ld+json`.
26
+
27
+ ## 2. Check it with the validators
28
+
29
+ To check syntax and vocabulary, paste the block into [validator.schema.org](https://validator.schema.org/). To see how Google reads it, use the [Rich Results Test](https://search.google.com/test/rich-results) with the published URL, or with pasted code for a local build.
30
+
31
+ ## 3. Read the common warnings
32
+
33
+ | Warning | Cause | Fix |
34
+ |---|---|---|
35
+ | The block has no `@id`, `url` or `Person.@id` | `site.url` is not set, so the theme cannot build absolute URLs. | Set `url` in `_config.yml`. |
36
+ | No `email`, `telephone` or `address` | The CV hides them, or `json_resume.privacy.export_contact_info` is `false`. | See [Visibility and privacy](../reference/json-ld-fields.md#visibility-and-privacy). |
37
+ | No `worksFor` | `worksFor` lists only roles with a start date and no end date. | Give the current role a `startdate` and set `enddate` to `Present` or a word from the locale's `present_values`, or leave it out. |
38
+ | No `description` or `image` | Only emitted when `header_intro: true` and `resume_avatar: true`. | Enable them if you want the values. |
39
+ | Two blocks, different `author` | `site.author` feeds `jekyll-seo-tag`'s block. | Set `site.author` to the CV name. |
40
+
41
+ ## 4. Turn it off
42
+
43
+ ```yaml
44
+ json_ld:
45
+ enabled: false
46
+ ```
47
+
48
+ The block disappears from every CV page. The JSON Resume files are not affected; control them with [`json_resume.enabled`](../reference/config.md#13-json-resume-export).
@@ -152,6 +152,8 @@ social_links:
152
152
  dribbble: https://dribbble.com/yourhandle
153
153
  flickr: https://flickr.com/people/yourhandle
154
154
  pinterest: https://pinterest.com/yourhandle
155
+ discord: https://discord.gg/yourinvite
156
+ behance: https://behance.net/yourhandle
155
157
  # mastodon emits <link rel="me"> in default.html and profile.html for Fediverse verification:
156
158
  mastodon: https://mastodon.social/@yourhandle
157
159
  ```
@@ -331,6 +333,23 @@ social_usernames:
331
333
 
332
334
  Routes, collision rules, visibility, and field mappings: [JSON Resume export configuration and privacy](json-resume-fields.md#configuration).
333
335
 
336
+ ### 13a. JSON-LD structured data
337
+
338
+ On by default. The key is optional.
339
+
340
+ | Setting | Type | Default | Description |
341
+ |---|---|---|---|
342
+ | `json_ld.enabled` | Boolean | `true` | `false` removes the `application/ld+json` block from every CV page. |
343
+
344
+ `json_ld` is independent of `json_resume.enabled` and `json_resume.languages`: turning off the JSON Resume files does not turn off JSON-LD, and every configured language whose export passes validation gets a block. It does honor `json_resume.privacy.export_contact_info`: with `false`, the block omits email, phone, address and the WhatsApp profile.
345
+
346
+ ```yaml
347
+ json_ld:
348
+ enabled: true
349
+ ```
350
+
351
+ Output shape, field mappings and limits: [JSON-LD fields reference](json-ld-fields.md). Checking the output: [Validate structured data](../how-to/validate-structured-data.md).
352
+
334
353
  ### 14. Content Security Policy
335
354
 
336
355
  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'`.
@@ -341,6 +360,7 @@ The theme ships no CSP header (static sites set headers at the host). It does em
341
360
  | Dark-mode toggle handler | `dark-mode-toggle.html`, when the toggle is enabled | `script-src` |
342
361
  | Error-page language detection | `404`/`403`/`500`/`503` pages (`error.html`) | `script-src` |
343
362
  | 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 |
363
+ | JSON-LD block (`<script type="application/ld+json">`, `json-ld-resume.html`) | `<head>` of CV pages | None. It is a data block that browsers never run, so `script-src` does not govern it and it needs no hash |
344
364
  | Locale font and line-height variables | `<style>` in `resume.html` | `style-src` |
345
365
  | `onclick="window.location.reload();"` on the error-page Reload button | 500/503 error pages | `script-src-attr` |
346
366
  | Some `style="..."` attributes (project and association titles, GTM `<noscript>` iframe) | resume sections, `analytics-body.html` | `style-src-attr` |
@@ -15,6 +15,7 @@ _layouts/resume.html
15
15
  ├── data-loader.html (binds resume_data from languages.<lang>.data_path)
16
16
  ├── shared-head.html (anti-FOUC script, metadata, favicon suite)
17
17
  ├── hreflang.html (alternate-language links)
18
+ ├── json-ld-resume.html (Schema.org JSON-LD script for this CV)
18
19
  ├── analytics-head.html (GTM / GA4 head script)
19
20
  ├── analytics-body.html (GTM noscript iframe)
20
21
  ├── dark-mode-toggle.html (floating theme toggle)
@@ -45,6 +46,15 @@ _layouts/resume.html
45
46
  - **Favicons:** `favicon`, `apple_touch_icon`, `favicon_32`, `favicon_16`, and the web manifest, all through `relative_url`.
46
47
  - **Robots:** `noindex noarchive nosnippet noimageindex` when `page.noindex: true`.
47
48
 
49
+ ### 1a. `json-ld-resume.html`
50
+
51
+ - **Consumed by:** `resume.html`, right after `{% seo %}`.
52
+ - **Parameters:** `lang` (the active language key).
53
+ - **Source:** `site.json_ld_pages[lang].script`, built in Ruby by `JsonLdBuilder` from the JSON Resume export and published by `json_resume_generator.rb`.
54
+ - **Output:** one `<script type="application/ld+json">` block. Nothing is emitted when `json_ld.enabled` is `false` or the language has no export.
55
+ - **Escaping:** the script is already JSON-escaped (`<` as `\u003c`). Print it as is; do not pass it through `jsonify` or `escape`.
56
+ - **Override:** a site can replace the include by adding `_includes/json-ld-resume.html`. The override then owns the output and its escaping. See [JSON-LD fields](json-ld-fields.md).
57
+
48
58
  ### 2. Stylesheet links (inline in each layout)
49
59
 
50
60
  - `default.html` (therefore also error pages) links `assets/css/main.css` directly, no include.
@@ -101,7 +111,7 @@ _layouts/resume.html
101
111
  ### 7. `social-links.html`
102
112
 
103
113
  - **Consumed by:** `resume.html` and `profile.html`, inside `<ul class="social-links">`, when `site.social_links` is set.
104
- - `email` renders a `mailto:` link with `itemprop="email"`. The other 14 platforms (`github`, `linkedin`, `telegram`, `twitter`, `medium`, `dribbble`, `facebook`, `instagram`, `website`, `whatsapp`, `devto`, `flickr`, `pinterest`, `youtube`) open in a new tab with `rel="noopener nofollow noreferrer"`.
114
+ - `email` renders a `mailto:` link with `itemprop="email"`. Every other platform in `_data/social_networks.yml` opens in a new tab with `rel="noopener nofollow noreferrer"`.
105
115
  - Every icon link carries `aria-label`, `title`, and a `.sr-only` text span, all taken from the page locale's `ui.social_labels.<key>` (falling back to the English `label` in `_data/social_networks.yml`).
106
116
  - `profile.html` adds one extra email icon for `contact_info.email` only when `social_links.email` is not set, so the profile never shows two email icons.
107
117
 
@@ -0,0 +1,84 @@
1
+ # JSON-LD fields reference
2
+
3
+ *Audience: site owners and theme developers*
4
+
5
+ Reference for the Schema.org JSON-LD block on each CV page: output shape, field mappings, visibility and privacy rules, and limits. To check the output, see [Validate structured data](../how-to/validate-structured-data.md).
6
+
7
+ JSON-LD is metadata for search engines and other crawlers. It does not guarantee that an applicant tracking system (ATS) parses the CV, and it does not guarantee rich results in search. Crawlers decide for themselves what to use.
8
+
9
+ ## Output
10
+
11
+ Each CV page has one `<script type="application/ld+json">` in `<head>`, right after `{% seo %}`. It is a `ProfilePage` whose `mainEntity` is a `Person`.
12
+
13
+ ```json
14
+ {
15
+ "@context": "https://schema.org",
16
+ "@type": "ProfilePage",
17
+ "@id": "https://example.org/en/cv/",
18
+ "url": "https://example.org/en/cv/",
19
+ "inLanguage": "en",
20
+ "dateModified": "2026-10-05T12:00:00Z",
21
+ "mainEntity": {
22
+ "@type": "Person",
23
+ "@id": "https://example.org/en/cv/#person",
24
+ "name": "Sherlock Holmes",
25
+ "jobTitle": "Consulting Detective",
26
+ "worksFor": [{ "@type": "Organization", "name": "Acme" }],
27
+ "knowsAbout": ["Ruby"]
28
+ }
29
+ }
30
+ ```
31
+
32
+ The example is shortened; the full property list is below. The theme builds the block in Ruby (`JsonLdBuilder`) from the [JSON Resume export](json-resume-fields.md) of the same language, then prints it through [`json-ld-resume.html`](includes.md#1a-json-ld-resumehtml). Because it reads the export, inactive entries, live contact values, privacy settings, HTML stripping and URL checks are already applied.
33
+
34
+ Every empty string, `nil`, empty array and empty object is dropped at every level. A node left with only `@type` is dropped too.
35
+
36
+ ## Field mapping
37
+
38
+ | Schema.org | JSON Resume source | Rule |
39
+ |---|---|---|
40
+ | `ProfilePage.@id`, `ProfilePage.url`, `Person.url` | `basics.url` | All omitted when the export has no URL (no `site.url`). Relative values are never emitted. |
41
+ | `ProfilePage.inLanguage` | the language key | Same value as `<html lang>`. |
42
+ | `ProfilePage.dateModified` | `meta.lastModified` | Build time, UTC, ISO 8601. |
43
+ | `Person.@id` | `basics.url` plus `#person` | Omitted when `basics.url` is absent. |
44
+ | `name`, `jobTitle`, `description`, `image` | `basics.name`, `basics.label`, `basics.summary`, `basics.image` | `summary` exists only when `header_intro: true`; `image` only when `resume_avatar: true`. |
45
+ | `email`, `telephone` | `basics.email`, `basics.phone` | See [Visibility and privacy](#visibility-and-privacy). |
46
+ | `address` (`PostalAddress`) | `basics.location` | `address` to `streetAddress`, `postalCode` to `postalCode`, `city` to `addressLocality`, `region` to `addressRegion`, `countryCode` to `addressCountry`. Omitted when no part is set. |
47
+ | `sameAs` | `basics.profiles[].url` | Same order as the export. |
48
+ | `worksFor` | `work[]` with `startDate` and no `endDate` | Current roles only. Entries with no dates are not current. Deduplicated by name, first-seen order. Type `Organization`. |
49
+ | `alumniOf` | `education[].institution` | Type `EducationalOrganization`, deduplicated by name. |
50
+ | `hasCredential` | `certificates[]` | `EducationalOccupationalCredential` with `name`, `url`, `dateCreated` (from `date`) and `recognizedBy` (from `issuer`, only when present). |
51
+ | `award` | `awards[].title` | List of text. |
52
+ | `knowsAbout` | `skills[].name` | List of text. |
53
+ | `knowsLanguage` | `languages[].language` | `Language` objects. |
54
+
55
+ ## Visibility and privacy
56
+
57
+ The block follows the same rules as `resume.json`; see [Visibility and privacy](json-resume-fields.md#visibility-and-privacy) for the table.
58
+
59
+ - `telephone` and `address` appear only when `display_header_contact_info: true`.
60
+ - `email` appears when `display_header_contact_info: true` or `resume_looking_for_work: true`.
61
+ - With `enable_live: true`, the live email and phone values are used, as on the page.
62
+ - `json_resume.privacy.export_contact_info: false` removes email, phone, address and the WhatsApp profile from the block. There is no separate JSON-LD privacy key.
63
+
64
+ `json_ld.enabled` is independent of `json_resume.enabled` and `json_resume.languages`. See [JSON-LD structured data](config.md#13a-json-ld-structured-data).
65
+
66
+ ## Not mapped
67
+
68
+ Volunteering, projects, publications, references, interests, courses, associations and links have no clean `Person` property. They stay in the JSON Resume export and the visible HTML.
69
+
70
+ ## Coexistence with `jekyll-seo-tag`
71
+
72
+ `{% seo %}` always emits its own `WebPage` JSON-LD and the page's only canonical link; it has no option to turn that off. The theme leaves it in place and does not emit a canonical link of its own. The `ProfilePage` `url` and `@id` equal the canonical URL.
73
+
74
+ If `site.author` is set, `jekyll-seo-tag` publishes it as the `author` of its block. Set it to the same name as the CV so the two blocks agree.
75
+
76
+ ## Link to the Person microdata
77
+
78
+ The `.wrapper` element in [`resume.html`](layouts.md#3-resumehtml-resume-every-language) is also a Schema.org `Person` (microdata). It carries `itemid` equal to the JSON-LD `Person.@id`, so parsers can merge the two into one Person. Its `telephone`, `email` and `address` follow the same contact-bar and looking-for-work rules as the JSON-LD block. `json_resume.privacy.export_contact_info` does not apply to the microdata, because it only describes values the page already shows.
79
+
80
+ ## Escaping
81
+
82
+ The script is serialized with `JSON.generate`, and every `<` is replaced with the six characters `\u003c`, which any JSON parser reads as `<`. Resume text such as `</script>` or `<!--<script` therefore cannot end or corrupt the block. If you override `json-ld-resume.html`, keep this escaping.
83
+
84
+ Test coverage: [Testing suites](testing-suites.md).
@@ -19,6 +19,8 @@ collection document, or existing generated page preserves that resource and logs
19
19
  a warning. A collision at the localized route also suppresses its root copy.
20
20
  A root-only collision does not suppress the localized export.
21
21
 
22
+ The same export also feeds the CV page's [JSON-LD block](json-ld-fields.md), so the visibility and privacy rules below apply to both.
23
+
22
24
  Discovery links on CV pages and the `application/json` hosting requirement are covered in [Publish the JSON Resume export](../how-to/publish-json-resume.md).
23
25
 
24
26
  ## Visibility and privacy
@@ -95,10 +97,8 @@ camelCase names on the right belong only to the JSON output.
95
97
  | `references.name`, `reference` | `references[].name`, `reference` |
96
98
 
97
99
  Experience and volunteering remain one record per role; company groups and
98
- newest-first role ordering follow the HTML renderer. Supported social networks
99
- are the ones rendered by the existing social include: GitHub, LinkedIn,
100
- Telegram, Twitter, Medium, Dribbble, Facebook, Instagram, Website, WhatsApp,
101
- Dev.to, Flickr, Pinterest, and YouTube. Their configured keys identify networks
100
+ newest-first role ordering follow the HTML renderer. Every network in `_data/social_networks.yml` except
101
+ `email` is exported. Their configured keys identify networks
102
102
  in the export. Social URLs remain strings; nested `{url, username}` objects are
103
103
  not introduced by this feature.
104
104
 
@@ -68,7 +68,8 @@ The locale schema is in [Locale keys](locale-keys.md); the `languages` schema is
68
68
  - Loads `locale.font_url` when set, else the default Lora and Open Sans stylesheet; neither loads when `disable_google_fonts: true` or `resume_theme: no-custom-fonts`.
69
69
  - Emits `--font-locale` (from `locale.font_family`, when non-empty) and `--line-height-locale` (from `locale.line_height`) in an inline `:root` style.
70
70
  - Links `assets/css/cv-{{ locale.direction }}.css`, so LTR locales get `cv-ltr.css` and RTL locales get `cv-rtl.css`.
71
- - Includes [`hreflang.html`](../../_includes/hreflang.html), `{% seo %}`, and the analytics head include.
71
+ - Includes [`hreflang.html`](../../_includes/hreflang.html), `{% seo %}`, [`json-ld-resume.html`](../../_includes/json-ld-resume.html) (after `{% seo %}`), and the analytics head include.
72
+ - **Person microdata:** the `.wrapper` element is a Schema.org `Person` with `itemid` set to the JSON-LD Person `@id` (`<site url>/<path>/#person`), so parsers merge it with the JSON-LD block. The `itemid` is omitted when no absolute URL is available. Microdata `telephone` and `address` are emitted only when `display_header_contact_info: true`; `email` also when `resume_looking_for_work: true`. This is the same rule as the [JSON Resume export](json-resume-fields.md#visibility-and-privacy).
72
73
  - **Header:** avatar (when `resume_avatar: true`), `lang_cfg.name`, the contact row (when `display_header_contact_info: true`), the header language list (when `display_header_contact_info: true` and `resume_section.lang_header` is set), `lang_cfg.resume_title`, social icons (when `social_links` is set), the `header.yml` intro (when `lang_cfg.header_intro: true`), and the contact button (per `resume_looking_for_work`).
73
74
  - **Contact row:** icon first, then text, in every direction. Phone numbers and emails carry `dir="ltr"`. The date of birth goes through [`date-formatter.html`](../../_includes/date-formatter.html).
74
75
  - **Body:** loops `site.resume_section_order` through [`resume-section.html`](../../_includes/resume-section.html), then the print-only social links section when `resume_print_social_links` is set.
@@ -122,7 +123,7 @@ The order of operations for one resume page:
122
123
 
123
124
  ```text
124
125
  1. Resolve lang, locale, lang_cfg; load resume_data from lang_cfg.data_path
125
- 2. <head>: shared-head, locale font + CSS variables, cv-<direction>.css, hreflang, SEO, analytics
126
+ 2. <head>: shared-head, locale font + CSS variables, cv-<direction>.css, hreflang, SEO, JSON-LD, analytics
126
127
  3. Body start: analytics-body, dark-mode-toggle, language-switcher
127
128
  4. Header: avatar, name, contact row, header languages, title, social icons, intro, contact button
128
129
  5. Sections: for each name in site.resume_section_order
@@ -31,6 +31,7 @@ Every locale file has the same key set. The validator warns when a language's ef
31
31
  | `ui.social_labels.*` | Platform names: the accessible name (`aria-label`, `title`, screen-reader text) of each social icon and the labels of the print-only contact list. |
32
32
  | `error_pages."404"` / `"403"` / `"500"` / `"503"` | `title` and `message` for each HTTP error page. |
33
33
  | `present_values` | Case-insensitive words that mean "ongoing" in `enddate` fields. A match renders `ui.present` instead of a date. |
34
+ | `date_format_mdy` | Pattern for full dates (`style="MDY"`: certifications, courses, publications, date of birth), using `{day}`, `{month}`, `{year}`. Defaults to `{month} {day}, {year}`. |
34
35
  | `months` | The 12 month names, January first. Dates render as `<month> <year>`; a year-only date renders as the year. |
35
36
 
36
37
  Read the shipped files in [`../../_data/locales/`](../../_data/locales/) for the full key list and current values.
@@ -27,6 +27,7 @@ jekyll-theme-resume/
27
27
  │ ├── social-links.html # Social icons (email + 14 platforms)
28
28
  │ ├── print-social-links.html # Print-only social links text list
29
29
  │ ├── hreflang.html # Alternate-language SEO links
30
+ │ ├── json-ld-resume.html # Schema.org JSON-LD script for a CV page
30
31
  │ ├── analytics-head.html # GTM / GA4 head script
31
32
  │ ├── analytics-body.html # GTM noscript body fallback
32
33
  │ └── vendors/svg-icons/ # Bundled Lineicons SVGs (MIT; see ATTRIBUTION.md inside)
@@ -57,13 +58,14 @@ jekyll-theme-resume/
57
58
  │
58
59
  ├── _plugins/
59
60
  │ ├── error_pages_generator.rb # Synthesizes missing HTTP error pages
60
- │ ├── json_resume_generator.rb # Publishes /<lang>/resume.json (and /resume.json for default_lang) via JsonResumeExporter
61
+ │ ├── json_resume_generator.rb # Publishes /<lang>/resume.json (and /resume.json for default_lang) via JsonResumeExporter, and site.json_ld_pages for the JSON-LD include
61
62
  │ ├── resume_pages_generator.rb # Synthesizes missing CV/profile pages per language
62
63
  │ └── resume_validator.rb # Build-time validation (on by default)
63
64
  │
64
65
  ├── lib/
65
66
  │ ├── jekyll-theme-resume.rb # Gem entrypoint
66
67
  │ └── jekyll-theme-resume/
68
+ │ ├── json_ld_builder.rb # Maps the JSON Resume export to a Schema.org ProfilePage + Person
67
69
  │ ├── json_resume_exporter.rb # Maps resume data to a JSON Resume v1.0.0 document
68
70
  │ ├── resume_validator.rb # Validator engine
69
71
  │ ├── template_key_checker.rb # Template checker (repository-only)
@@ -18,13 +18,14 @@ Every test goes through a public interface; see [Tests use public interfaces onl
18
18
 
19
19
  | Suite | Interface under test | Covers |
20
20
  |---|---|---|
21
- | [`test_rendered_site.rb`](../../test/test_rendered_site.rb) | Generated HTML of a fixture site built from the theme's real `_layouts`, `_includes`, `_sass`, `_data`, `assets` | Each of the sections (fields, separators, inactive entries, grouping, RTL `dir="ltr"` isolation); date formats; header, contact bar, avatar, microdata; social and print links; hreflang; profile, default and error layouts (including the URL-language script and `baseurl`); config switches such as `enable_live`, `lang_header`, `baseurl`, analytics, dark mode |
21
+ | [`test_rendered_site.rb`](../../test/test_rendered_site.rb) | Generated HTML of a fixture site built from the theme's real `_layouts`, `_includes`, `_sass`, `_data`, `assets` | Each of the sections (fields, separators, inactive entries, grouping, RTL `dir="ltr"` isolation); date formats; header, contact bar, avatar, microdata (including the contact visibility gate and `itemid`); the JSON-LD block (`json_ld.enabled`, `<` escaping, privacy); social and print links; hreflang; profile, default and error layouts (including the URL-language script and `baseurl`); config switches such as `enable_live`, `lang_header`, `baseurl`, analytics, dark mode |
22
22
  | [`test_language_switcher.rb`](../../test/test_language_switcher.rb) | Generated HTML, six languages | Switcher links, labels, direction, page/site opt-out, error layout |
23
23
  | [`test_resume_validator.rb`](../../test/test_resume_validator.rb) | `ResumeValidator#validate`, `bin/validate-resume`, build plugin | Every schema rule per section, dates, "Present" values, URLs, alias keys, locale merge and parity, config resolution, strict mode, CLI flags |
24
24
  | [`test_template_key_checker.rb`](../../test/test_template_key_checker.rb) | `TemplateKeyChecker#check`, `bin/check-data-keys` | Liquid binding resolution, include boundaries, advisory exit codes |
25
25
  | [`test_error_pages_generator.rb`](../../test/test_error_pages_generator.rb) | Site build | Generated 404/403/500 pages, site overrides, generator registration |
26
26
  | [`test_resume_pages_generator.rb`](../../test/test_resume_pages_generator.rb) | Site build | Generated CV/profile pages, collisions, per-language and global toggles |
27
- | [`test_json_resume_exporter.rb`](../../test/test_json_resume_exporter.rb) | `JsonResumeExporter.export`, site build | JSON Resume mapping, privacy, visibility, schema validation, routes; see [json-resume-fields.md](json-resume-fields.md) |
27
+ | [`test_json_resume_exporter.rb`](../../test/test_json_resume_exporter.rb) | `JsonResumeExporter.export`, site build | JSON Resume mapping, privacy, visibility, schema validation, routes; the generator's `site.json_ld_pages` output and its independence from `json_resume.*`; see [json-resume-fields.md](json-resume-fields.md) |
28
+ | [`test_json_ld_builder.rb`](../../test/test_json_ld_builder.rb) | `JsonLdBuilder.build` and `.script` | Mapping to ProfilePage + Person, dropping empty values, current-role and dedupe rules, `<` escaping; see [json-ld-fields.md](json-ld-fields.md) |
28
29
  | [`test_packaging.rb`](../../test/test_packaging.rb) | Gemspec, shipped `_data` | Gem file list, six-locale key parity, unused locale keys, social icons and labels, demo data parity |
29
30
  | [`test_doc_links.rb`](../../test/test_doc_links.rb) | The tracked Markdown files | Every relative link and `#anchor` in the docs resolves; links in code blocks and external URLs are skipped |
30
31
 
@@ -42,6 +42,15 @@ source "https://rubygems.org"
42
42
  group :jekyll_plugins do
43
43
  gem "jekyll-theme-resume"
44
44
  end
45
+
46
+ # Windows and JRuby do not include zoneinfo files, so bundle tzinfo-data.
47
+ platforms :windows, :jruby do
48
+ gem "tzinfo"
49
+ gem "tzinfo-data"
50
+ end
51
+
52
+ # Faster directory watching on Windows
53
+ gem "wdm", platforms: [:windows]
45
54
  ```
46
55
 
47
56
  The theme must be inside `group :jekyll_plugins`. That group loads the theme's build-time generators and validator. A plain `gem` line outside the group does not load them.
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module JekyllThemeResume
6
+ # Maps a JSON Resume document (JsonResumeExporter output) to a Schema.org ProfilePage + Person.
7
+ # Reusing the export means inactive entries, live contacts, privacy, HTML stripping and URL
8
+ # checks are already applied, so JSON-LD and JSON Resume never disagree.
9
+ module JsonLdBuilder
10
+ module_function
11
+
12
+ def build(resume, lang)
13
+ basics = resume["basics"] || {}
14
+ url = basics["url"]
15
+ compact("@context" => "https://schema.org", "@type" => "ProfilePage", "@id" => url, "url" => url,
16
+ "inLanguage" => lang, "dateModified" => resume.dig("meta", "lastModified"),
17
+ "mainEntity" => person(resume, basics, url))
18
+ end
19
+
20
+ # jsonify/to_json leave "<" raw, so "</script x" or "<!--<script" in resume text could end or
21
+ # corrupt the <script> block. "\u003c" is the same character to any JSON parser.
22
+ def script(graph)
23
+ JSON.generate(graph).gsub("<", "\\u003c")
24
+ end
25
+
26
+ def person(resume, basics, url)
27
+ {
28
+ "@type" => "Person", "@id" => url && "#{url}#person", "name" => basics["name"],
29
+ "jobTitle" => basics["label"], "description" => basics["summary"], "image" => basics["image"],
30
+ "url" => url, "email" => basics["email"], "telephone" => basics["phone"],
31
+ "address" => address(basics["location"]),
32
+ "sameAs" => Array(basics["profiles"]).filter_map { |profile| profile["url"] },
33
+ "worksFor" => organizations(current_roles(resume).map { |role| role["name"] }, "Organization"),
34
+ "alumniOf" => organizations(Array(resume["education"]).map { |item| item["institution"] },
35
+ "EducationalOrganization"),
36
+ "hasCredential" => Array(resume["certificates"]).map { |item| credential(item) },
37
+ "award" => Array(resume["awards"]).filter_map { |item| item["title"] },
38
+ "knowsAbout" => Array(resume["skills"]).filter_map { |item| item["name"] },
39
+ "knowsLanguage" => Array(resume["languages"]).map { |item| { "@type" => "Language", "name" => item["language"] } }
40
+ }
41
+ end
42
+
43
+ # Current roles only: a start date and no end date. Dateless entries are not current.
44
+ def current_roles(resume)
45
+ Array(resume["work"]).select { |role| role["startDate"] && !role["endDate"] }
46
+ end
47
+
48
+ def organizations(names, type)
49
+ names.compact.uniq.map { |name| { "@type" => type, "name" => name } }
50
+ end
51
+
52
+ def credential(item)
53
+ { "@type" => "EducationalOccupationalCredential", "name" => item["name"], "url" => item["url"],
54
+ "dateCreated" => item["date"], "recognizedBy" => { "@type" => "Organization", "name" => item["issuer"] } }
55
+ end
56
+
57
+ def address(location)
58
+ return unless location
59
+
60
+ { "@type" => "PostalAddress", "streetAddress" => location["address"], "postalCode" => location["postalCode"],
61
+ "addressLocality" => location["city"], "addressRegion" => location["region"],
62
+ "addressCountry" => location["countryCode"] }
63
+ end
64
+
65
+ # Drops nil/empty values at every depth; a node left with only "@type" is dropped too.
66
+ def compact(value)
67
+ case value
68
+ when Hash
69
+ result = value.transform_values { |item| compact(item) }.reject { |_key, item| blank?(item) }
70
+ result.keys == ["@type"] ? nil : result
71
+ when Array then value.map { |item| compact(item) }.reject { |item| blank?(item) }
72
+ else value
73
+ end
74
+ end
75
+
76
+ def blank?(value)
77
+ value.nil? || (value.respond_to?(:empty?) && value.empty?)
78
+ end
79
+ end
80
+ end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "json"
4
+ require "yaml"
4
5
  require "date"
5
6
  require "time"
6
7
  require "cgi"
@@ -13,7 +14,7 @@ module JekyllThemeResume
13
14
  # Returns nil when final validation fails; callers must never publish that export.
14
15
  class JsonResumeExporter
15
16
  SCHEMA_URL = "https://raw.githubusercontent.com/jsonresume/resume-schema/v1.0.0/schema.json"
16
- SCHEMA = JSONSchemer.schema(JSON.parse(File.read(File.join(__dir__, "schemas/json_resume_v1.0.0.json"))))
17
+ SCHEMA = JSONSchemer.schema(JSON.parse(File.read(File.join(__dir__, "schemas/json_resume_v1.0.0.json"), encoding: "UTF-8")))
17
18
  SECTIONS = {
18
19
  "experience" => "work", "volunteering" => "volunteer", "education" => "education",
19
20
  "certifications" => "certificates", "recognitions" => "awards", "skills" => "skills",
@@ -37,8 +38,8 @@ module JekyllThemeResume
37
38
  "experience" => %w[highlights], "volunteering" => %w[highlights], "education" => %w[courses],
38
39
  "skills" => %w[keywords], "interests" => %w[keywords], "projects" => %w[highlights keywords]
39
40
  }.freeze
40
- NETWORKS = %w[github linkedin telegram twitter medium dribbble facebook instagram website whatsapp devto flickr pinterest
41
- youtube].freeze
41
+ NETWORKS = (YAML.safe_load_file(File.expand_path("../../_data/social_networks.yml", __dir__)).map { |n| n["key"] } -
42
+ %w[email]).freeze
42
43
  COUNTRY_CODES = %w[
43
44
  AD AE AF AG AI AL AM AO AQ AR AS AT AU AW AX AZ BA BB BD BE BF BG BH BI BJ BL BM BN BO BQ BR BS BT BV BW BY BZ
44
45
  CA CC CD CF CG CH CI CK CL CM CN CO CR CU CV CW CX CY CZ DE DJ DK DM DO DZ EC EE EG EH ER ES ET FI FJ FK FM FO FR
@@ -624,6 +624,14 @@ module JekyllThemeResume
624
624
  validate_date(entry["startdate"], context, "startdate") if entry["startdate"]
625
625
  validate_date_or_present(entry["enddate"], context, "enddate", lang: lang) if entry["enddate"]
626
626
  validate_date_range(entry["startdate"], entry["enddate"], context, lang: lang) if entry["startdate"] && entry["enddate"]
627
+ validate_education_awards(entry["awards"], context) if entry.key?("awards")
628
+ end
629
+
630
+ # The template prints `award.award`, so plain strings would render as empty bullets.
631
+ def validate_education_awards(awards, context)
632
+ return if awards.is_a?(Array) && awards.all? { |award| award.is_a?(Hash) && !award["award"].to_s.strip.empty? }
633
+
634
+ add_error(context, "'awards' must be a list of `- award: text` items")
627
635
  end
628
636
 
629
637
  def validate_certification_entry(entry, context, lang: nil)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jekyll-theme-resume
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.0
4
+ version: 1.3.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Khaldoon Mutahar
@@ -236,6 +236,7 @@ files:
236
236
  - _includes/date-formatter.html
237
237
  - _includes/grouped-item-list.html
238
238
  - _includes/hreflang.html
239
+ - _includes/json-ld-resume.html
239
240
  - _includes/language-switcher.html
240
241
  - _includes/print-social-links.html
241
242
  - _includes/resume-section.html
@@ -243,7 +244,9 @@ files:
243
244
  - _includes/shared-head.html
244
245
  - _includes/social-links.html
245
246
  - _includes/vendors/svg-icons/ATTRIBUTION.md
247
+ - _includes/vendors/svg-icons/behance.svg
246
248
  - _includes/vendors/svg-icons/dev.svg
249
+ - _includes/vendors/svg-icons/discord.svg
247
250
  - _includes/vendors/svg-icons/dribbble-symbol.svg
248
251
  - _includes/vendors/svg-icons/envelope.svg
249
252
  - _includes/vendors/svg-icons/facebook.svg
@@ -314,12 +317,14 @@ files:
314
317
  - docs/how-to/switch-resume-versions.md
315
318
  - docs/how-to/troubleshoot-builds.md
316
319
  - docs/how-to/validate-in-ci.md
320
+ - docs/how-to/validate-structured-data.md
317
321
  - docs/how-to/verify-accessibility.md
318
322
  - docs/reference/accessibility-coverage.md
319
323
  - docs/reference/config.md
320
324
  - docs/reference/data-schemas.md
321
325
  - docs/reference/glossary.md
322
326
  - docs/reference/includes.md
327
+ - docs/reference/json-ld-fields.md
323
328
  - docs/reference/json-resume-fields.md
324
329
  - docs/reference/layouts.md
325
330
  - docs/reference/locale-keys.md
@@ -329,18 +334,19 @@ files:
329
334
  - docs/reference/validator-cli.md
330
335
  - docs/tutorials/getting-started.md
331
336
  - lib/jekyll-theme-resume.rb
337
+ - lib/jekyll-theme-resume/json_ld_builder.rb
332
338
  - lib/jekyll-theme-resume/json_resume_exporter.rb
333
339
  - lib/jekyll-theme-resume/resume_validator.rb
334
340
  - lib/jekyll-theme-resume/schemas/LICENSE.md
335
341
  - lib/jekyll-theme-resume/schemas/json_resume_v1.0.0.json
336
- homepage: https://www.mutahr.me/jekyll-theme-resume
342
+ homepage: https://www.mutahar.me/jekyll-theme-resume
337
343
  licenses:
338
344
  - MIT
339
345
  metadata:
340
346
  bug_tracker_uri: https://github.com/kmutahar/jekyll-theme-resume/issues
341
347
  changelog_uri: https://github.com/kmutahar/jekyll-theme-resume/blob/master/CHANGELOG.md
342
348
  documentation_uri: https://github.com/kmutahar/jekyll-theme-resume#readme
343
- homepage_uri: https://www.mutahr.me/jekyll-theme-resume
349
+ homepage_uri: https://www.mutahar.me/jekyll-theme-resume
344
350
  source_code_uri: https://github.com/kmutahar/jekyll-theme-resume/
345
351
  allowed_push_host: https://rubygems.org
346
352
  post_install_message: |