@takuhon/api 1.1.0 → 1.4.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.
package/dist/index.js CHANGED
@@ -54,7 +54,7 @@ import {
54
54
  import { Hono } from "hono";
55
55
 
56
56
  // src/html/build-html.ts
57
- import { generateJsonLd, renderActivitySvg } from "@takuhon/core";
57
+ import { generateJsonLd, renderActivitySvg, SECTION_KEYS } from "@takuhon/core";
58
58
 
59
59
  // src/html/html-helpers.ts
60
60
  import { formatDate, getPresentLabel } from "@takuhon/core";
@@ -74,10 +74,10 @@ function presentLabel(locale) {
74
74
  return escapeHtml(getPresentLabel(locale));
75
75
  }
76
76
  function dateRange(start, opts) {
77
- const { end, isCurrent, locale } = opts;
77
+ const { end, isCurrent, locale, separator = " \u2013 " } = opts;
78
78
  const left = start ? timeTag(start, locale) : "";
79
79
  const right = isCurrent === true || end === null ? presentLabel(locale) : end ? timeTag(end, locale) : "";
80
- if (left && right) return `${left} \u2013 ${right}`;
80
+ if (left && right) return `${left}${separator}${right}`;
81
81
  return left || right;
82
82
  }
83
83
  function nonEmpty(values, separator) {
@@ -85,55 +85,477 @@ function nonEmpty(values, separator) {
85
85
  return joined.length > 0 ? joined : void 0;
86
86
  }
87
87
 
88
+ // src/html/brand-icons.ts
89
+ var GLYPHS = {
90
+ github: {
91
+ viewBox: "0 0 24 24",
92
+ path: "M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"
93
+ },
94
+ gitlab: {
95
+ viewBox: "0 0 24 24",
96
+ path: "m23.6004 9.5927-.0337-.0862L20.3.9814a.851.851 0 0 0-.3362-.405.8748.8748 0 0 0-.9997.0539.8748.8748 0 0 0-.29.4399l-2.2055 6.748H7.5375l-2.2057-6.748a.8573.8573 0 0 0-.29-.4412.8748.8748 0 0 0-.9997-.0537.8585.8585 0 0 0-.3362.4049L.4332 9.5015l-.0325.0862a6.0657 6.0657 0 0 0 2.0119 7.0105l.0113.0087.03.0213 4.976 3.7264 2.462 1.8633 1.4995 1.1321a1.0085 1.0085 0 0 0 1.2197 0l1.4995-1.1321 2.4619-1.8633 5.006-3.7489.0125-.01a6.0682 6.0682 0 0 0 2.0094-7.003z"
97
+ },
98
+ linkedin: {
99
+ viewBox: "0 0 16 16",
100
+ path: "M0 1.146C0 .513.526 0 1.175 0h13.65C15.474 0 16 .513 16 1.146v13.708c0 .633-.526 1.146-1.175 1.146H1.175C.526 16 0 15.487 0 14.854zm4.943 12.248V6.169H2.542v7.225zm-1.2-8.212c.837 0 1.358-.554 1.358-1.248-.015-.709-.52-1.248-1.342-1.248S2.4 3.226 2.4 3.934c0 .694.521 1.248 1.327 1.248zm4.908 8.212V9.359c0-.216.016-.432.08-.586.173-.431.568-.878 1.232-.878.869 0 1.216.662 1.216 1.634v3.865h2.401V9.25c0-2.22-1.184-3.252-2.764-3.252-1.274 0-1.845.7-2.165 1.193v.025h-.016l.016-.025V6.169h-2.4c.03.678 0 7.225 0 7.225z"
101
+ },
102
+ x: {
103
+ viewBox: "0 0 24 24",
104
+ path: "M14.234 10.162 22.977 0h-2.072l-7.591 8.824L7.251 0H.258l9.168 13.343L.258 24H2.33l8.016-9.318L16.749 24h6.993zm-2.837 3.299-.929-1.329L3.076 1.56h3.182l5.965 8.532.929 1.329 7.754 11.09h-3.182z"
105
+ },
106
+ mastodon: {
107
+ viewBox: "0 0 24 24",
108
+ path: "M23.268 5.313c-.35-2.578-2.617-4.61-5.304-5.004C17.51.242 15.792 0 11.813 0h-.03c-3.98 0-4.835.242-5.288.309C3.882.692 1.496 2.518.917 5.127.64 6.412.61 7.837.661 9.143c.074 1.874.088 3.745.26 5.611.118 1.24.325 2.47.62 3.68.55 2.237 2.777 4.098 4.96 4.857 2.336.792 4.849.923 7.256.38.265-.061.527-.132.786-.213.585-.184 1.27-.39 1.774-.753a.057.057 0 0 0 .023-.043v-1.809a.052.052 0 0 0-.02-.041.053.053 0 0 0-.046-.01 20.282 20.282 0 0 1-4.709.545c-2.73 0-3.463-1.284-3.674-1.818a5.593 5.593 0 0 1-.319-1.433.053.053 0 0 1 .066-.054c1.517.363 3.072.546 4.632.546.376 0 .75 0 1.125-.01 1.57-.044 3.224-.124 4.768-.422.038-.008.077-.015.11-.024 2.435-.464 4.753-1.92 4.989-5.604.008-.145.03-1.52.03-1.67.002-.512.167-3.63-.024-5.545zm-3.748 9.195h-2.561V8.29c0-1.309-.55-1.976-1.67-1.976-1.23 0-1.846.79-1.846 2.35v3.403h-2.546V8.663c0-1.56-.617-2.35-1.848-2.35-1.112 0-1.668.668-1.67 1.977v6.218H4.822V8.102c0-1.31.337-2.35 1.011-3.12.696-.77 1.608-1.164 2.74-1.164 1.311 0 2.302.5 2.962 1.498l.638 1.06.638-1.06c.66-.999 1.65-1.498 2.96-1.498 1.13 0 2.043.395 2.74 1.164.675.77 1.012 1.81 1.012 3.12z"
109
+ },
110
+ bluesky: {
111
+ viewBox: "0 0 24 24",
112
+ path: "M5.202 2.857C7.954 4.922 10.913 9.11 12 11.358c1.087-2.247 4.046-6.436 6.798-8.501C20.783 1.366 24 .213 24 3.883c0 .732-.42 6.156-.667 7.037-.856 3.061-3.978 3.842-6.755 3.37 4.854.826 6.089 3.562 3.422 6.299-5.065 5.196-7.28-1.304-7.847-2.97-.104-.305-.152-.448-.153-.327 0-.121-.05.022-.153.327-.568 1.666-2.782 8.166-7.847 2.97-2.667-2.737-1.432-5.473 3.422-6.3-2.777.473-5.899-.308-6.755-3.369C.42 10.04 0 4.615 0 3.883c0-3.67 3.217-2.517 5.202-1.026"
113
+ },
114
+ instagram: {
115
+ viewBox: "0 0 24 24",
116
+ path: "M7.0301.084c-1.2768.0602-2.1487.264-2.911.5634-.7888.3075-1.4575.72-2.1228 1.3877-.6652.6677-1.075 1.3368-1.3802 2.127-.2954.7638-.4956 1.6365-.552 2.914-.0564 1.2775-.0689 1.6882-.0626 4.947.0062 3.2586.0206 3.6671.0825 4.9473.061 1.2765.264 2.1482.5635 2.9107.308.7889.72 1.4573 1.388 2.1228.6679.6655 1.3365 1.0743 2.1285 1.38.7632.295 1.6361.4961 2.9134.552 1.2773.056 1.6884.069 4.9462.0627 3.2578-.0062 3.668-.0207 4.9478-.0814 1.28-.0607 2.147-.2652 2.9098-.5633.7889-.3086 1.4578-.72 2.1228-1.3881.665-.6682 1.0745-1.3378 1.3795-2.1284.2957-.7632.4966-1.636.552-2.9124.056-1.2809.0692-1.6898.063-4.948-.0063-3.2583-.021-3.6668-.0817-4.9465-.0607-1.2797-.264-2.1487-.5633-2.9117-.3084-.7889-.72-1.4568-1.3876-2.1228C21.2982 1.33 20.628.9208 19.8378.6165 19.074.321 18.2017.1197 16.9244.0645 15.6471.0093 15.236-.005 11.977.0014 8.718.0076 8.31.0215 7.0301.0839m.1402 21.6932c-1.17-.0509-1.8053-.2453-2.2287-.408-.5606-.216-.96-.4771-1.3819-.895-.422-.4178-.6811-.8186-.9-1.378-.1644-.4234-.3624-1.058-.4171-2.228-.0595-1.2645-.072-1.6442-.079-4.848-.007-3.2037.0053-3.583.0607-4.848.05-1.169.2456-1.805.408-2.2282.216-.5613.4762-.96.895-1.3816.4188-.4217.8184-.6814 1.3783-.9003.423-.1651 1.0575-.3614 2.227-.4171 1.2655-.06 1.6447-.072 4.848-.079 3.2033-.007 3.5835.005 4.8495.0608 1.169.0508 1.8053.2445 2.228.408.5608.216.96.4754 1.3816.895.4217.4194.6816.8176.9005 1.3787.1653.4217.3617 1.056.4169 2.2263.0602 1.2655.0739 1.645.0796 4.848.0058 3.203-.0055 3.5834-.061 4.848-.051 1.17-.245 1.8055-.408 2.2294-.216.5604-.4763.96-.8954 1.3814-.419.4215-.8181.6811-1.3783.9-.4224.1649-1.0577.3617-2.2262.4174-1.2656.0595-1.6448.072-4.8493.079-3.2045.007-3.5825-.006-4.848-.0608M16.953 5.5864A1.44 1.44 0 1 0 18.39 4.144a1.44 1.44 0 0 0-1.437 1.4424M5.8385 12.012c.0067 3.4032 2.7706 6.1557 6.173 6.1493 3.4026-.0065 6.157-2.7701 6.1506-6.1733-.0065-3.4032-2.771-6.1565-6.174-6.1498-3.403.0067-6.156 2.771-6.1496 6.1738M8 12.0077a4 4 0 1 1 4.008 3.9921A3.9996 3.9996 0 0 1 8 12.0077"
117
+ },
118
+ youtube: {
119
+ viewBox: "0 0 24 24",
120
+ path: "M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"
121
+ },
122
+ threads: {
123
+ viewBox: "0 0 24 24",
124
+ path: "M12.186 24h-.007c-3.581-.024-6.334-1.205-8.184-3.509C2.35 18.44 1.5 15.586 1.472 12.01v-.017c.03-3.579.879-6.43 2.525-8.482C5.845 1.205 8.6.024 12.18 0h.014c2.746.02 5.043.725 6.826 2.098 1.677 1.29 2.858 3.13 3.509 5.467l-2.04.569c-1.104-3.96-3.898-5.984-8.304-6.015-2.91.022-5.11.936-6.54 2.717C4.307 6.504 3.616 8.914 3.589 12c.027 3.086.718 5.496 2.057 7.164 1.43 1.783 3.631 2.698 6.54 2.717 2.623-.02 4.358-.631 5.8-2.045 1.647-1.613 1.618-3.593 1.09-4.798-.31-.71-.873-1.3-1.634-1.75-.192 1.352-.622 2.446-1.284 3.272-.886 1.102-2.14 1.704-3.73 1.79-1.202.065-2.361-.218-3.259-.801-1.063-.689-1.685-1.74-1.752-2.964-.065-1.19.408-2.285 1.33-3.082.88-.76 2.119-1.207 3.583-1.291a13.853 13.853 0 0 1 3.02.142c-.126-.742-.375-1.332-.75-1.757-.513-.586-1.308-.883-2.359-.89h-.029c-.844 0-1.992.232-2.721 1.32L7.734 7.847c.98-1.454 2.568-2.256 4.478-2.256h.044c3.194.02 5.097 1.975 5.287 5.388.108.046.216.094.321.142 1.49.7 2.58 1.761 3.154 3.07.797 1.82.871 4.79-1.548 7.158-1.85 1.81-4.094 2.628-7.277 2.65Zm1.003-11.69c-.242 0-.487.007-.739.021-1.836.103-2.98.946-2.916 2.143.067 1.256 1.452 1.839 2.784 1.767 1.224-.065 2.818-.543 3.086-3.71a10.5 10.5 0 0 0-2.215-.221z"
125
+ },
126
+ facebook: {
127
+ viewBox: "0 0 24 24",
128
+ path: "M9.101 23.691v-7.98H6.627v-3.667h2.474v-1.58c0-4.085 1.848-5.978 5.858-5.978.401 0 .955.042 1.468.103a8.68 8.68 0 0 1 1.141.195v3.325a8.623 8.623 0 0 0-.653-.036 26.805 26.805 0 0 0-.733-.009c-.707 0-1.259.096-1.675.309a1.686 1.686 0 0 0-.679.622c-.258.42-.374.995-.374 1.752v1.297h3.919l-.386 2.103-.287 1.564h-3.246v8.245C19.396 23.238 24 18.179 24 12.044c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.628 3.874 10.35 9.101 11.647Z"
129
+ },
130
+ rss: {
131
+ viewBox: "0 0 24 24",
132
+ path: "M19.199 24C19.199 13.467 10.533 4.8 0 4.8V0c13.165 0 24 10.835 24 24h-4.801zM3.291 17.415c1.814 0 3.293 1.479 3.293 3.295 0 1.813-1.485 3.29-3.301 3.29C1.47 24 0 22.526 0 20.71s1.475-3.294 3.291-3.295zM15.909 24h-4.665c0-6.169-5.075-11.245-11.244-11.245V8.09c8.727 0 15.909 7.184 15.909 15.91z"
133
+ },
134
+ npm: {
135
+ viewBox: "0 0 24 24",
136
+ path: "M1.763 0C.786 0 0 .786 0 1.763v20.474C0 23.214.786 24 1.763 24h20.474c.977 0 1.763-.786 1.763-1.763V1.763C24 .786 23.214 0 22.237 0zM5.13 5.323l13.837.019-.009 13.836h-3.464l.01-10.382h-3.456L12.04 19.17H5.113z"
137
+ },
138
+ linktree: {
139
+ viewBox: "0 0 24 24",
140
+ path: "m13.73635 5.85251 4.00467-4.11665 2.3248 2.3808-4.20064 4.00466h5.9085v3.30473h-5.9365l4.22865 4.10766-2.3248 2.3338L12.0005 12.099l-5.74052 5.76852-2.3248-2.3248 4.22864-4.10766h-5.9375V8.12132h5.9085L3.93417 4.11666l2.3248-2.3808 4.00468 4.11665V0h3.4727zm-3.4727 10.30614h3.4727V24h-3.4727z"
141
+ },
142
+ wakatime: {
143
+ viewBox: "0 0 24 24",
144
+ path: "M12 0C5.373 0 0 5.373 0 12s5.373 12 12 12 12-5.373 12-12S18.627 0 12 0zm0 2.824a9.176 9.176 0 1 1 0 18.352 9.176 9.176 0 0 1 0-18.352zm5.097 5.058c-.327 0-.61.19-.764.45-1.025 1.463-2.21 3.162-3.288 4.706l-.387-.636a.897.897 0 0 0-.759-.439.901.901 0 0 0-.788.492l-.357.581-1.992-2.943a.897.897 0 0 0-.761-.446c-.514 0-.903.452-.903.96a1 1 0 0 0 .207.61l2.719 3.96c.152.272.44.47.776.47a.91.91 0 0 0 .787-.483c.046-.071.23-.368.314-.504l.324.52c-.035-.047.076.113.087.13.024.031.054.059.078.085.019.019.04.036.058.052.036.033.08.056.115.08.025.016.052.028.076.04.029.015.06.024.088.035.058.025.122.027.18.04.031.004.064.003.092.005.29 0 .546-.149.707-.36 1.4-2 2.842-4.055 4.099-5.849A.995.995 0 0 0 18 8.842c0-.508-.389-.96-.903-.96"
145
+ },
146
+ wordpress: {
147
+ viewBox: "0 0 24 24",
148
+ path: "M21.469 6.825c.84 1.537 1.318 3.3 1.318 5.175 0 3.979-2.156 7.456-5.363 9.325l3.295-9.527c.615-1.54.82-2.771.82-3.864 0-.405-.026-.78-.07-1.11m-7.981.105c.647-.03 1.232-.105 1.232-.105.582-.075.514-.93-.067-.899 0 0-1.755.135-2.88.135-1.064 0-2.85-.15-2.85-.15-.585-.03-.661.855-.075.885 0 0 .54.061 1.125.09l1.68 4.605-2.37 7.08L5.354 6.9c.649-.03 1.234-.1 1.234-.1.585-.075.516-.93-.065-.896 0 0-1.746.138-2.874.138-.2 0-.438-.008-.69-.015C4.911 3.15 8.235 1.215 12 1.215c2.809 0 5.365 1.072 7.286 2.833-.046-.003-.091-.009-.141-.009-1.06 0-1.812.923-1.812 1.914 0 .89.513 1.643 1.06 2.531.411.72.89 1.643.89 2.977 0 .915-.354 1.994-.821 3.479l-1.075 3.585-3.9-11.61.001.014zM12 22.784c-1.059 0-2.081-.153-3.048-.437l3.237-9.406 3.315 9.087c.024.053.05.101.078.149-1.12.393-2.325.609-3.582.609M1.211 12c0-1.564.336-3.05.935-4.39L7.29 21.709C3.694 19.96 1.212 16.271 1.211 12M12 0C5.385 0 0 5.385 0 12s5.385 12 12 12 12-5.385 12-12S18.615 0 12 0"
149
+ }
150
+ };
151
+ var TYPE_GLYPHS = {
152
+ github: "github",
153
+ gitlab: "gitlab",
154
+ linkedin: "linkedin",
155
+ x: "x",
156
+ mastodon: "mastodon",
157
+ bluesky: "bluesky",
158
+ instagram: "instagram",
159
+ youtube: "youtube",
160
+ threads: "threads",
161
+ facebook: "facebook",
162
+ rss: "rss",
163
+ blog: "rss"
164
+ };
165
+ var HOST_GLYPHS = {
166
+ "github.com": "github",
167
+ "gitlab.com": "gitlab",
168
+ "linkedin.com": "linkedin",
169
+ "x.com": "x",
170
+ "twitter.com": "x",
171
+ "instagram.com": "instagram",
172
+ "youtube.com": "youtube",
173
+ "threads.net": "threads",
174
+ "facebook.com": "facebook",
175
+ "bsky.app": "bluesky",
176
+ "npmjs.com": "npm",
177
+ "linktr.ee": "linktree",
178
+ "wakatime.com": "wakatime",
179
+ "wordpress.org": "wordpress"
180
+ };
181
+ function hostGlyph(url) {
182
+ let host;
183
+ try {
184
+ host = new URL(url, "https://takuhon.invalid").hostname.toLowerCase();
185
+ } catch {
186
+ return null;
187
+ }
188
+ for (const [domain, glyph] of Object.entries(HOST_GLYPHS)) {
189
+ if (host === domain || host.endsWith(`.${domain}`)) return glyph;
190
+ }
191
+ return null;
192
+ }
193
+ function renderGlyph(name) {
194
+ const icon = GLYPHS[name];
195
+ if (!icon) return "";
196
+ return `<svg class="brand-icon" viewBox="${icon.viewBox}" width="18" height="18" fill="currentColor" aria-hidden="true" focusable="false"><path d="${escapeHtml(icon.path)}"/></svg>`;
197
+ }
198
+ function brandIconForLink(link) {
199
+ const glyph = link.type === "custom" ? hostGlyph(link.url) : TYPE_GLYPHS[link.type];
200
+ return glyph ? renderGlyph(glyph) : "";
201
+ }
202
+ function brandIconForPlatform(platform, url) {
203
+ const key = platform.trim().toLowerCase();
204
+ const named = key === "blog" ? "rss" : Object.hasOwn(GLYPHS, key) ? key : null;
205
+ const glyph = named ?? hostGlyph(url);
206
+ return glyph ? renderGlyph(glyph) : "";
207
+ }
208
+ function brandIconForHost(url) {
209
+ const glyph = hostGlyph(url);
210
+ return glyph ? renderGlyph(glyph) : "";
211
+ }
212
+
88
213
  // src/html/build-html.ts
214
+ var LABELS_EN = {
215
+ about: "About",
216
+ careers: "Experience",
217
+ projects: "Projects",
218
+ volunteering: "Volunteering",
219
+ skills: "Skills",
220
+ activity: "Developer activity",
221
+ education: "Education",
222
+ certifications: "Certifications",
223
+ publications: "Publications",
224
+ honors: "Honors & awards",
225
+ memberships: "Memberships",
226
+ courses: "Courses",
227
+ patents: "Patents",
228
+ testScores: "Test scores",
229
+ languages: "Languages",
230
+ recommendations: "Recommendations",
231
+ highlights: "Selected posts",
232
+ contact: "Contact",
233
+ skipLink: "Skip to main content",
234
+ localeNav: "Language",
235
+ featuredLinks: "Featured links",
236
+ otherLinks: "Links",
237
+ poweredBy: "Powered by"
238
+ };
239
+ var LABELS_JA = {
240
+ about: "\u81EA\u5DF1\u7D39\u4ECB",
241
+ careers: "\u7D4C\u6B74",
242
+ projects: "\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8",
243
+ volunteering: "\u30DC\u30E9\u30F3\u30C6\u30A3\u30A2",
244
+ skills: "\u30B9\u30AD\u30EB",
245
+ activity: "\u958B\u767A\u30A2\u30AF\u30C6\u30A3\u30D3\u30C6\u30A3",
246
+ education: "\u5B66\u6B74",
247
+ certifications: "\u8CC7\u683C\u30FB\u8A8D\u5B9A",
248
+ publications: "\u51FA\u7248\u7269",
249
+ honors: "\u53D7\u8CDE\u30FB\u6804\u8A89",
250
+ memberships: "\u6240\u5C5E",
251
+ courses: "\u8B1B\u5EA7",
252
+ patents: "\u7279\u8A31",
253
+ testScores: "\u30C6\u30B9\u30C8\u30B9\u30B3\u30A2",
254
+ languages: "\u8A00\u8A9E",
255
+ recommendations: "\u63A8\u85A6",
256
+ highlights: "\u30D4\u30C3\u30AF\u30A2\u30C3\u30D7\u6295\u7A3F",
257
+ contact: "\u9023\u7D61\u5148",
258
+ skipLink: "\u30E1\u30A4\u30F3\u30B3\u30F3\u30C6\u30F3\u30C4\u3078\u30B9\u30AD\u30C3\u30D7",
259
+ localeNav: "\u8A00\u8A9E",
260
+ featuredLinks: "\u4E3B\u8981\u30EA\u30F3\u30AF",
261
+ otherLinks: "\u305D\u306E\u4ED6\u306E\u30EA\u30F3\u30AF",
262
+ poweredBy: "Powered by"
263
+ };
264
+ function isJapaneseLocale(resolvedLocale) {
265
+ return resolvedLocale.toLowerCase().split(/[-_]/)[0] === "ja";
266
+ }
267
+ function pickLabelPack(resolvedLocale) {
268
+ return isJapaneseLocale(resolvedLocale) ? LABELS_JA : LABELS_EN;
269
+ }
89
270
  function escapeJsonLd(json) {
90
271
  return json.replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026");
91
272
  }
92
- var CSS = `:root{--fg:#1a1a1a;--muted:#666;--accent:#0b5fff;--line:#e5e5e5}
93
- *{box-sizing:border-box}
94
- body{margin:0;color:var(--fg);font:16px/1.6 system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;background:#fff}
95
- main{max-width:42rem;margin:0 auto;padding:2rem 1.25rem}
96
- a{color:var(--accent)}
97
- h1{font-size:1.9rem;margin:.2rem 0}
98
- h2{font-size:1.15rem;margin:2rem 0 .75rem;padding-bottom:.3rem;border-bottom:1px solid var(--line)}
99
- h3{font-size:1rem;margin:0}
100
- header .avatar{width:96px;height:96px;border-radius:50%;object-fit:cover}
101
- .tagline{font-size:1.1rem;color:var(--muted);margin:.2rem 0}
102
- .location{color:var(--muted);margin:.2rem 0}
103
- .bio{margin:.75rem 0}
273
+ var DEFAULT_TOKENS = {
274
+ "--takuhon-color-bg": "#ffffff",
275
+ "--takuhon-color-surface": "#f6f7f9",
276
+ "--takuhon-color-text": "#1f2933",
277
+ "--takuhon-color-text-muted": "#52606d",
278
+ "--takuhon-color-border": "#d8dee7",
279
+ "--takuhon-color-primary": "#2563eb",
280
+ "--takuhon-color-primary-contrast": "#ffffff",
281
+ "--takuhon-color-accent": "#4f46e5",
282
+ "--takuhon-color-heading": "#1e1888",
283
+ "--takuhon-font-family": "system-ui,-apple-system,'Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif"
284
+ };
285
+ var DEFAULT_TOKENS_DARK = {
286
+ "--takuhon-color-bg": "#0f172a",
287
+ "--takuhon-color-surface": "#1e293b",
288
+ "--takuhon-color-text": "#e2e8f0",
289
+ "--takuhon-color-text-muted": "#94a3b8",
290
+ "--takuhon-color-border": "#334155",
291
+ "--takuhon-color-primary": "#60a5fa",
292
+ "--takuhon-color-primary-contrast": "#0f172a",
293
+ "--takuhon-color-accent": "#818cf8",
294
+ "--takuhon-color-heading": "#c7d2fe"
295
+ };
296
+ var INTERNAL_TOKENS = {
297
+ "--takuhon-space-1": "4px",
298
+ "--takuhon-space-2": "8px",
299
+ "--takuhon-space-3": "12px",
300
+ "--takuhon-space-4": "16px",
301
+ "--takuhon-space-5": "24px",
302
+ "--takuhon-space-6": "32px",
303
+ "--takuhon-radius-sm": "6px",
304
+ "--takuhon-radius-md": "12px",
305
+ "--takuhon-radius-full": "9999px",
306
+ "--takuhon-tap-target": "44px",
307
+ "--takuhon-font-size-sm": "14px",
308
+ "--takuhon-font-size-base": "16px",
309
+ "--takuhon-font-size-lg": "18px",
310
+ "--takuhon-font-size-xl": "22px",
311
+ "--takuhon-font-size-2xl": "28px",
312
+ "--takuhon-line-height": "1.7",
313
+ "--takuhon-max-content-width": "720px"
314
+ };
315
+ var COLOR_TOKEN_VARS = {
316
+ bg: "--takuhon-color-bg",
317
+ surface: "--takuhon-color-surface",
318
+ text: "--takuhon-color-text",
319
+ textMuted: "--takuhon-color-text-muted",
320
+ border: "--takuhon-color-border",
321
+ accent: "--takuhon-color-accent",
322
+ primary: "--takuhon-color-primary",
323
+ primaryContrast: "--takuhon-color-primary-contrast",
324
+ heading: "--takuhon-color-heading"
325
+ };
326
+ var SAFE_COLOR = /^(?:#[0-9A-Fa-f]{3,8}|[A-Za-z]+|(?:rgb|rgba|hsl|hsla|hwb|lab|lch|oklab|oklch|color)\([A-Za-z0-9.,%/\s-]*\))$/;
327
+ var SAFE_FONT = /^[A-Za-z0-9\s,'"._-]+$/;
328
+ function safeValue(value, pattern, maxLength) {
329
+ if (typeof value !== "string") return void 0;
330
+ const v = value.trim();
331
+ if (v === "" || v.length > maxLength || !pattern.test(v)) return void 0;
332
+ return v;
333
+ }
334
+ var safeColor = (value) => safeValue(value, SAFE_COLOR, 64);
335
+ var safeFont = (value) => safeValue(value, SAFE_FONT, 256);
336
+ function colorOverrides(colors) {
337
+ if (!colors) return [];
338
+ const out = [];
339
+ for (const key of Object.keys(COLOR_TOKEN_VARS)) {
340
+ const safe = safeColor(colors[key]);
341
+ if (safe !== void 0) out.push([COLOR_TOKEN_VARS[key], safe]);
342
+ }
343
+ return out;
344
+ }
345
+ function rootBlock(pairs) {
346
+ return `:root{${[...pairs].map(([k, v]) => `${k}:${v}`).join(";")}}`;
347
+ }
348
+ function buildTokenCss(appearance) {
349
+ const light = new Map([
350
+ ...Object.entries(INTERNAL_TOKENS),
351
+ ...Object.entries(DEFAULT_TOKENS)
352
+ ]);
353
+ if (appearance) {
354
+ const font = safeFont(appearance.fontFamily);
355
+ if (font !== void 0) light.set("--takuhon-font-family", font);
356
+ for (const [cssVar, value] of colorOverrides(appearance.colors)) {
357
+ light.set(cssVar, value);
358
+ }
359
+ }
360
+ const dark = new Map(Object.entries(DEFAULT_TOKENS_DARK));
361
+ for (const [cssVar, value] of colorOverrides(appearance?.colorsDark)) {
362
+ dark.set(cssVar, value);
363
+ }
364
+ return `${rootBlock(light)}
365
+ @media (prefers-color-scheme:dark){${rootBlock(dark)}}`;
366
+ }
367
+ var CSS = `*{box-sizing:border-box}
368
+ html{font-size:100%}
369
+ body{margin:0;color:var(--takuhon-color-text);background:var(--takuhon-color-bg);font-family:var(--takuhon-font-family);font-size:var(--takuhon-font-size-base);line-height:var(--takuhon-line-height);-webkit-text-size-adjust:100%}
370
+ main{max-width:var(--takuhon-max-content-width);margin:0 auto;padding:var(--takuhon-space-6) var(--takuhon-space-4)}
371
+ a{color:var(--takuhon-color-primary)}
372
+ a:focus-visible{outline:2px solid var(--takuhon-color-accent);outline-offset:2px;border-radius:var(--takuhon-radius-sm)}
373
+ .skip-link{position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden}
374
+ .skip-link:focus{position:fixed;left:var(--takuhon-space-2);top:var(--takuhon-space-2);width:auto;height:auto;padding:var(--takuhon-space-2) var(--takuhon-space-4);background:var(--takuhon-color-primary);color:var(--takuhon-color-primary-contrast);border-radius:var(--takuhon-radius-sm);z-index:100}
375
+ h1{font-size:var(--takuhon-font-size-2xl);font-weight:700;line-height:1.2;margin:0 0 var(--takuhon-space-2)}
376
+ h2{font-size:var(--takuhon-font-size-xl);margin:0 0 var(--takuhon-space-3);padding-bottom:var(--takuhon-space-2);border-bottom:1px solid var(--takuhon-color-border)}
377
+ h3{font-size:var(--takuhon-font-size-lg);font-weight:600;margin:0}
378
+ header{margin-bottom:var(--takuhon-space-6);display:flow-root}
379
+ header .avatar{width:96px;height:96px;border-radius:var(--takuhon-radius-full);object-fit:cover;float:left;margin:0 var(--takuhon-space-3) var(--takuhon-space-3) 0;shape-outside:circle();border:1px solid var(--takuhon-color-border)}
380
+ .tagline{font-size:var(--takuhon-font-size-lg);color:var(--takuhon-color-text-muted);margin:0 0 var(--takuhon-space-2)}
381
+ .location{font-size:var(--takuhon-font-size-sm);color:var(--takuhon-color-text-muted);margin:0}
382
+ section{margin:0 0 var(--takuhon-space-6)}
383
+ .bio-body h3{font-size:var(--takuhon-font-size-lg);font-weight:600;color:var(--takuhon-color-heading);margin:var(--takuhon-space-5) 0 var(--takuhon-space-2)}
384
+ .bio-body h4{font-size:var(--takuhon-font-size-base);font-weight:600;color:var(--takuhon-color-heading);margin:var(--takuhon-space-4) 0 var(--takuhon-space-2)}
385
+ .bio-body p{margin:0 0 var(--takuhon-space-3)}
386
+ .bio-body ul{list-style:disc;margin:0 0 var(--takuhon-space-3);padding-left:var(--takuhon-space-5)}
387
+ .bio-body li{margin:0 0 var(--takuhon-space-1)}
388
+ .bio-body hr{border:0;border-top:1px solid var(--takuhon-color-border);margin:var(--takuhon-space-5) 0}
104
389
  ul{padding:0;margin:0;list-style:none}
105
- .entries>li{margin:0 0 1.1rem}
106
- .sub{margin:.1rem 0;font-weight:600}
107
- .meta{margin:.1rem 0;color:var(--muted);font-size:.9rem}
108
- .links{display:flex;flex-wrap:wrap;gap:.5rem 1rem;margin:.75rem 0}
109
- .skills,.tags{display:flex;flex-wrap:wrap;gap:.4rem}
110
- .skills>li,.tags>li{background:#f2f2f2;border-radius:1rem;padding:.15rem .6rem;font-size:.85rem}
111
- .rec{margin:0 0 1.1rem}
112
- .rec blockquote{margin:0;padding-left:.9rem;border-left:3px solid var(--line)}
113
- .rec figcaption{color:var(--muted);font-size:.9rem;margin-top:.3rem}
114
- nav.locales{display:flex;gap:.75rem;margin-bottom:1rem;font-size:.9rem}
115
- .activity svg{max-width:100%;height:auto}
116
- footer.powered{max-width:42rem;margin:0 auto;padding:1.5rem 1.25rem;color:var(--muted);font-size:.85rem}`;
390
+ .entries>li{margin:0 0 var(--takuhon-space-4)}
391
+ .entries--timeline>li{position:relative;display:flex;flex-direction:column;margin:0 0 0 var(--takuhon-space-2);padding:0 0 var(--takuhon-space-5) var(--takuhon-space-4);border-left:2px solid var(--takuhon-color-border)}
392
+ .entries--timeline>li:last-child{padding-bottom:0}
393
+ .entries--timeline>li::before{content:"";position:absolute;left:-7px;top:6px;width:12px;height:12px;border-radius:var(--takuhon-radius-full);background:var(--takuhon-color-text-muted)}
394
+ .entries--timeline>li.is-current::before{background:var(--takuhon-color-accent)}
395
+ .entries--timeline .meta{order:-1;margin:0 0 var(--takuhon-space-1)}
396
+ .entries--timeline>li>h3{font-size:var(--takuhon-font-size-base);font-weight:600;margin:0}
397
+ .entries--cards{display:grid;gap:var(--takuhon-space-3)}
398
+ .entries--cards>li{margin:0;padding:var(--takuhon-space-4);border:1px solid var(--takuhon-color-border);border-radius:var(--takuhon-radius-md);background:var(--takuhon-color-surface)}
399
+ .entries--cards>li.is-highlighted{border-color:var(--takuhon-color-accent)}
400
+ .entries h3 a{color:inherit;text-decoration:none}
401
+ .entries h3 a:hover{color:var(--takuhon-color-primary);text-decoration:underline}
402
+ .sub{margin:var(--takuhon-space-1) 0 0;color:var(--takuhon-color-text-muted)}
403
+ .sub a{color:inherit;text-decoration:none}
404
+ .sub a:hover{color:var(--takuhon-color-primary);text-decoration:underline}
405
+ .role-badge{margin:var(--takuhon-space-1) 0 0;font-size:var(--takuhon-font-size-sm);font-weight:600;letter-spacing:.02em;color:var(--takuhon-color-accent)}
406
+ .desc{margin:var(--takuhon-space-2) 0 0}
407
+ .entries--timeline .desc{white-space:pre-wrap}
408
+ .external-icon{width:.72em;height:.72em;margin-left:.35em;vertical-align:-.05em;opacity:.55}
409
+ .meta{margin:var(--takuhon-space-1) 0 0;color:var(--takuhon-color-text-muted);font-size:var(--takuhon-font-size-sm)}
410
+ .featured-links,.other-links{margin:0 0 var(--takuhon-space-6)}
411
+ .featured-links>ul,.other-links>ul{list-style:none;padding:0;margin:0;display:grid;gap:var(--takuhon-space-2)}
412
+ .featured-links>ul{grid-template-columns:repeat(auto-fill,minmax(220px,1fr))}
413
+ .featured-links a,.other-links a{display:flex;align-items:center;justify-content:space-between;gap:var(--takuhon-space-2);min-height:var(--takuhon-tap-target);padding:var(--takuhon-space-2) var(--takuhon-space-3);background:var(--takuhon-color-surface);color:var(--takuhon-color-text);text-decoration:none;border:1px solid var(--takuhon-color-border);border-radius:var(--takuhon-radius-md)}
414
+ .featured-links a:hover,.other-links a:hover{border-color:var(--takuhon-color-accent)}
415
+ .link-main{display:inline-flex;align-items:center;gap:var(--takuhon-space-2);min-width:0}
416
+ .link-type{font-size:var(--takuhon-font-size-sm);color:var(--takuhon-color-text-muted)}
417
+ .brand-icon{width:1.15em;height:1.15em;flex:none;opacity:.85}
418
+ .skills,.tags{display:flex;flex-wrap:wrap;gap:var(--takuhon-space-2)}
419
+ .skills>li,.tags>li{background:var(--takuhon-color-surface);border:1px solid var(--takuhon-color-border);border-radius:var(--takuhon-radius-full);padding:var(--takuhon-space-1) var(--takuhon-space-3);font-size:var(--takuhon-font-size-sm)}
420
+ .entries--cards .tags{margin-top:var(--takuhon-space-2)}
421
+ .tags>li{background:var(--takuhon-color-bg);color:var(--takuhon-color-text-muted);padding:2px var(--takuhon-space-2)}
422
+ .skills-groups{display:grid;gap:var(--takuhon-space-4)}
423
+ .skill-group h3{font-size:var(--takuhon-font-size-base);margin:0 0 var(--takuhon-space-2);color:var(--takuhon-color-text-muted);text-transform:uppercase;letter-spacing:.04em}
424
+ .vol-list{display:grid;gap:var(--takuhon-space-2)}
425
+ .vol{padding:var(--takuhon-space-3) 0;border-bottom:1px solid var(--takuhon-color-border)}
426
+ .vol:last-child{border-bottom:0}
427
+ .vol-head{display:flex;flex-wrap:wrap;align-items:baseline;gap:var(--takuhon-space-1) var(--takuhon-space-3)}
428
+ .vol-org{font-size:var(--takuhon-font-size-lg)}
429
+ .vol-org a{color:inherit;text-decoration:none}
430
+ .vol-org a:hover{color:var(--takuhon-color-primary);text-decoration:underline}
431
+ .vol-role{color:var(--takuhon-color-text-muted);font-size:var(--takuhon-font-size-sm)}
432
+ .vol-desc{margin:var(--takuhon-space-1) 0 0;font-size:var(--takuhon-font-size-sm)}
433
+ .vol-secondary{display:inline-flex;align-items:center;gap:var(--takuhon-space-1);margin-top:var(--takuhon-space-2);padding:2px var(--takuhon-space-2);font-size:var(--takuhon-font-size-sm);background:var(--takuhon-color-surface);border:1px solid var(--takuhon-color-border);border-radius:var(--takuhon-radius-full);color:var(--takuhon-color-text-muted);text-decoration:none}
434
+ .vol-secondary:hover{color:var(--takuhon-color-text);border-color:var(--takuhon-color-accent)}
435
+ .vol-secondary .brand-icon{width:1em;height:1em;opacity:.85}
436
+ .rec{margin:0 0 var(--takuhon-space-4)}
437
+ .rec blockquote{margin:0;padding-left:var(--takuhon-space-3);border-left:3px solid var(--takuhon-color-border)}
438
+ .rec figcaption{color:var(--takuhon-color-text-muted);font-size:var(--takuhon-font-size-sm);margin-top:var(--takuhon-space-2)}
439
+ nav.locales{display:flex;justify-content:flex-end;gap:var(--takuhon-space-3);margin-bottom:var(--takuhon-space-4);font-size:var(--takuhon-font-size-sm)}
440
+ nav.locales a,nav.locales [aria-current]{padding:var(--takuhon-space-1) var(--takuhon-space-2);border-radius:var(--takuhon-radius-sm);text-transform:uppercase}
441
+ nav.locales a{color:var(--takuhon-color-primary);text-decoration:none}
442
+ nav.locales a:hover{text-decoration:underline}
443
+ nav.locales [aria-current]{color:var(--takuhon-color-text);font-weight:700;background:var(--takuhon-color-surface)}
444
+ .activity svg{width:100%;height:auto}
445
+ footer.powered{max-width:var(--takuhon-max-content-width);margin:var(--takuhon-space-6) auto 0;padding:var(--takuhon-space-4) var(--takuhon-space-4) var(--takuhon-space-6);border-top:1px solid var(--takuhon-color-border);text-align:center;color:var(--takuhon-color-text-muted);font-size:var(--takuhon-font-size-sm)}
446
+ footer.powered p{margin:0 0 var(--takuhon-space-2)}
447
+ footer.powered p:last-child{margin-bottom:0}
448
+ footer.powered .powered-by a{color:var(--takuhon-color-primary);text-decoration:none}
449
+ footer.powered .powered-by a:hover{text-decoration:underline}
450
+ .highlights .highlights-intro{color:var(--takuhon-color-text-muted);margin:0 0 var(--takuhon-space-3)}
451
+ .highlights-track{display:flex;gap:var(--takuhon-space-3);list-style:none;margin:0;padding:0 0 var(--takuhon-space-2);overflow-x:auto;scroll-snap-type:x mandatory;scroll-padding-left:var(--takuhon-space-1);-webkit-overflow-scrolling:touch;overscroll-behavior-x:contain}
452
+ .highlight-card{position:relative;flex:0 0 85%;scroll-snap-align:start;display:flex;flex-direction:column;background:var(--takuhon-color-surface);border:1px solid var(--takuhon-color-border);border-radius:var(--takuhon-radius-md);overflow:hidden}
453
+ @media (min-width:640px){.highlight-card{flex-basis:calc((100% - var(--takuhon-space-3)) / 2)}}
454
+ @media (min-width:960px){.highlight-card{flex-basis:calc((100% - 2 * var(--takuhon-space-3)) / 3)}}
455
+ .highlight-card:hover{border-color:var(--takuhon-color-accent)}
456
+ .highlight-card:focus-within{outline:2px solid var(--takuhon-color-accent);outline-offset:2px}
457
+ .highlight-thumb{aspect-ratio:1 / 1;background:var(--takuhon-color-bg)}
458
+ .highlight-thumb img{display:block;width:100%;height:100%;object-fit:cover}
459
+ .highlight-body{padding:var(--takuhon-space-3);display:flex;flex-direction:column;gap:var(--takuhon-space-2)}
460
+ .highlight-body>*{margin:0}
461
+ .highlight-badge{display:inline-flex;align-items:center;gap:var(--takuhon-space-1);font-size:var(--takuhon-font-size-sm);color:var(--takuhon-color-text-muted)}
462
+ .highlight-badge .brand-icon{width:1em;height:1em;opacity:.85}
463
+ .highlight-title{font-size:var(--takuhon-font-size-base)}
464
+ .highlight-title a{color:var(--takuhon-color-text);text-decoration:none}
465
+ .highlight-title a:hover{color:var(--takuhon-color-primary)}
466
+ .highlight-title a::after{content:"";position:absolute;inset:0}
467
+ .highlight-title a:focus-visible{outline:none}
468
+ .highlight-date{font-size:var(--takuhon-font-size-sm);color:var(--takuhon-color-text-muted)}
469
+ .highlight-desc{font-size:var(--takuhon-font-size-sm)}
470
+ .highlight-cta{font-size:var(--takuhon-font-size-sm);font-weight:600;color:var(--takuhon-color-primary)}`;
471
+ var EXTERNAL_ICON = '<svg class="external-icon" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>';
472
+ function externalLink(url, innerEscaped) {
473
+ return `<a href="${escapeHtml(url)}" rel="noopener">${innerEscaped}${EXTERNAL_ICON}</a>`;
474
+ }
117
475
  function renderEntry(entry) {
118
- const href = entry.url ? safeUrl(entry.url) : void 0;
119
- const heading = href ? `<a href="${escapeHtml(href)}">${escapeHtml(entry.heading)}</a>` : escapeHtml(entry.heading);
476
+ const headHref = entry.url ? safeUrl(entry.url) : void 0;
477
+ const heading = headHref ? externalLink(headHref, escapeHtml(entry.heading)) : escapeHtml(entry.heading);
120
478
  const parts = [`<h3>${heading}</h3>`];
121
- if (entry.sub) parts.push(`<p class="sub">${escapeHtml(entry.sub)}</p>`);
479
+ if (entry.role) parts.push(`<p class="role-badge">${escapeHtml(entry.role)}</p>`);
480
+ if (entry.sub) {
481
+ const subHref = entry.subUrl ? safeUrl(entry.subUrl) : void 0;
482
+ const sub = subHref ? externalLink(subHref, escapeHtml(entry.sub)) : escapeHtml(entry.sub);
483
+ parts.push(`<p class="sub">${sub}</p>`);
484
+ }
122
485
  if (entry.dates) parts.push(`<p class="meta">${entry.dates}</p>`);
123
- if (entry.body) parts.push(`<p>${escapeHtml(entry.body)}</p>`);
486
+ if (entry.body) parts.push(`<p class="desc">${escapeHtml(entry.body)}</p>`);
124
487
  if (entry.tags && entry.tags.length > 0) {
125
488
  parts.push(
126
489
  `<ul class="tags">${entry.tags.map((t) => `<li>${escapeHtml(t)}</li>`).join("")}</ul>`
127
490
  );
128
491
  }
129
- return `<li>${parts.join("")}</li>`;
492
+ const cls = [entry.current ? "is-current" : "", entry.highlighted ? "is-highlighted" : ""].filter(Boolean).join(" ");
493
+ return `<li${cls ? ` class="${cls}"` : ""}>${parts.join("")}</li>`;
130
494
  }
131
- function entryList(title, entries) {
495
+ function entryList(title, entries, variant) {
132
496
  if (entries.length === 0) return "";
133
- return `<section><h2>${escapeHtml(title)}</h2><ul class="entries">${entries.map(renderEntry).join("")}</ul></section>`;
497
+ const cls = variant ? `entries entries--${variant}` : "entries";
498
+ return `<section><h2>${escapeHtml(title)}</h2><ul class="${cls}">${entries.map(renderEntry).join("")}</ul></section>`;
134
499
  }
135
- function renderHeader(p) {
500
+ function renderInline(text) {
501
+ return escapeHtml(text).replace(/\*\*([^*]+?)\*\*/g, "<strong>$1</strong>");
502
+ }
503
+ function renderMarkdown(input) {
504
+ const lines = input.split("\n");
505
+ const out = [];
506
+ let i = 0;
507
+ while (i < lines.length) {
508
+ const trimmed = (lines[i] ?? "").trim();
509
+ if (trimmed === "") {
510
+ i++;
511
+ continue;
512
+ }
513
+ if (trimmed === "---") {
514
+ out.push("<hr>");
515
+ i++;
516
+ continue;
517
+ }
518
+ if (trimmed.startsWith("### ")) {
519
+ out.push(`<h4>${renderInline(trimmed.slice(4))}</h4>`);
520
+ i++;
521
+ continue;
522
+ }
523
+ if (trimmed.startsWith("## ")) {
524
+ out.push(`<h3>${renderInline(trimmed.slice(3))}</h3>`);
525
+ i++;
526
+ continue;
527
+ }
528
+ if (trimmed.startsWith("- ")) {
529
+ const items = [];
530
+ while (i < lines.length) {
531
+ const cur = (lines[i] ?? "").trim();
532
+ if (!cur.startsWith("- ")) break;
533
+ items.push(`<li>${renderInline(cur.slice(2))}</li>`);
534
+ i++;
535
+ }
536
+ out.push(`<ul>${items.join("")}</ul>`);
537
+ continue;
538
+ }
539
+ const para = [];
540
+ while (i < lines.length) {
541
+ const cur = (lines[i] ?? "").trim();
542
+ if (cur === "" || cur === "---" || cur.startsWith("## ") || cur.startsWith("### ") || cur.startsWith("- ")) {
543
+ break;
544
+ }
545
+ para.push(cur);
546
+ i++;
547
+ }
548
+ if (para.length > 0) out.push(`<p>${renderInline(para.join(" "))}</p>`);
549
+ }
550
+ return out.join("\n");
551
+ }
552
+ function renderBio(bio, heading) {
553
+ if (!bio) return "";
554
+ return `<section class="bio"><h2>${escapeHtml(heading)}</h2><div class="bio-body">${renderMarkdown(bio)}</div></section>`;
555
+ }
556
+ function renderHeader(p, localeNavHtml) {
136
557
  const parts = [];
558
+ if (localeNavHtml) parts.push(localeNavHtml);
137
559
  const avatarSrc = p.avatar?.url ? safeUrl(p.avatar.url) : void 0;
138
560
  if (avatarSrc) {
139
561
  parts.push(
@@ -143,29 +565,114 @@ function renderHeader(p) {
143
565
  parts.push(`<h1>${escapeHtml(p.displayName)}</h1>`);
144
566
  if (p.tagline) parts.push(`<p class="tagline">${escapeHtml(p.tagline)}</p>`);
145
567
  if (p.location?.display) parts.push(`<p class="location">${escapeHtml(p.location.display)}</p>`);
146
- if (p.bio) parts.push(`<p class="bio">${escapeHtml(p.bio)}</p>`);
147
568
  return `<header>${parts.join("")}</header>`;
148
569
  }
149
- function renderLinks(links) {
150
- if (links.length === 0) return "";
151
- const items = links.map((l) => {
152
- const text = escapeHtml(l.label ?? l.url);
153
- const href = safeUrl(l.url);
154
- return href ? `<li><a href="${escapeHtml(href)}">${text}</a></li>` : `<li>${text}</li>`;
155
- }).join("");
156
- return `<nav aria-label="Links"><ul class="links">${items}</ul></nav>`;
570
+ var LINK_TYPE_DISPLAY = {
571
+ website: "Website",
572
+ blog: "Blog",
573
+ github: "GitHub",
574
+ gitlab: "GitLab",
575
+ linkedin: "LinkedIn",
576
+ x: "X (Twitter)",
577
+ mastodon: "Mastodon",
578
+ bluesky: "Bluesky",
579
+ instagram: "Instagram",
580
+ youtube: "YouTube",
581
+ threads: "Threads",
582
+ facebook: "Facebook",
583
+ email: "Email",
584
+ rss: "RSS"
585
+ };
586
+ function linkTypePill(link, label) {
587
+ const display = LINK_TYPE_DISPLAY[link.type];
588
+ if (!display || display === label) return "";
589
+ return `<span class="link-type">${escapeHtml(display)}</span>`;
590
+ }
591
+ function renderLinkItem(link) {
592
+ const label = link.label ?? link.url;
593
+ const main = `<span class="link-main">${brandIconForLink(link)}<span>${escapeHtml(label)}</span></span>`;
594
+ const pill = link.label == null ? linkTypePill(link, label) : "";
595
+ const href = safeUrl(link.url);
596
+ return href ? `<li><a href="${escapeHtml(href)}" rel="me noopener">${main}${pill}</a></li>` : `<li>${main}${pill}</li>`;
597
+ }
598
+ function byOrder(a, b) {
599
+ return (a.order ?? 0) - (b.order ?? 0);
157
600
  }
158
- function renderSkills(skills) {
601
+ function renderFeaturedLinks(links, ariaLabel) {
602
+ const featured = links.filter((l) => l.featured === true).sort(byOrder);
603
+ if (featured.length === 0) return "";
604
+ return `<nav class="featured-links" aria-label="${escapeHtml(ariaLabel)}"><ul>${featured.map(renderLinkItem).join("")}</ul></nav>`;
605
+ }
606
+ function renderOtherLinks(links, heading) {
607
+ const others = links.filter((l) => l.featured !== true).sort(byOrder);
608
+ if (others.length === 0) return "";
609
+ return `<section class="other-links"><h2>${escapeHtml(heading)}</h2><ul>${others.map(renderLinkItem).join("")}</ul></section>`;
610
+ }
611
+ function renderSkills(skills, categories, heading) {
159
612
  if (skills.length === 0) return "";
160
- const items = skills.map((s) => `<li>${escapeHtml(s.label)}</li>`).join("");
161
- return `<section><h2>Skills</h2><ul class="skills">${items}</ul></section>`;
613
+ const h = escapeHtml(heading);
614
+ const chips = (list) => `<ul class="skills">${list.map((s) => `<li>${escapeHtml(s.label)}</li>`).join("")}</ul>`;
615
+ if (!categories || categories.length === 0) {
616
+ return `<section><h2>${h}</h2>${chips(skills)}</section>`;
617
+ }
618
+ const UNCAT = "";
619
+ const buckets = /* @__PURE__ */ new Map();
620
+ for (const s of skills) {
621
+ const key = s.category ?? UNCAT;
622
+ const arr = buckets.get(key) ?? [];
623
+ arr.push(s);
624
+ buckets.set(key, arr);
625
+ }
626
+ const group = (heading2, list) => `<div class="skill-group">${heading2 !== void 0 ? `<h3>${escapeHtml(heading2)}</h3>` : ""}${chips(list)}</div>`;
627
+ const seen = /* @__PURE__ */ new Set();
628
+ const groups = [];
629
+ for (const cat of categories) {
630
+ const list = buckets.get(cat.id);
631
+ if (!list || list.length === 0) continue;
632
+ seen.add(cat.id);
633
+ groups.push(group(cat.label, list));
634
+ }
635
+ for (const [key, list] of buckets) {
636
+ if (key === UNCAT || seen.has(key)) continue;
637
+ groups.push(group(key, list));
638
+ }
639
+ const uncategorized = buckets.get(UNCAT);
640
+ if (uncategorized && uncategorized.length > 0) groups.push(group(void 0, uncategorized));
641
+ return `<section><h2>${h}</h2><div class="skills-groups">${groups.join("")}</div></section>`;
642
+ }
643
+ function hostLabel(url) {
644
+ try {
645
+ const host = new URL(url).hostname.replace(/^www\./, "");
646
+ return host || url;
647
+ } catch {
648
+ return url;
649
+ }
162
650
  }
163
- function renderLanguages(languages) {
651
+ function secondaryLinkPill(link) {
652
+ const label = escapeHtml(link.label ?? hostLabel(link.url));
653
+ const href = safeUrl(link.url);
654
+ const glyph = href ? brandIconForHost(link.url) : "";
655
+ const inner = `${glyph}<span>${label}</span>`;
656
+ return href ? `<a class="vol-secondary" href="${escapeHtml(href)}" rel="noopener">${inner}</a>` : `<span class="vol-secondary">${inner}</span>`;
657
+ }
658
+ function renderVolunteering(items, heading) {
659
+ if (items.length === 0) return "";
660
+ const li = (v) => {
661
+ const orgHref = v.url ? safeUrl(v.url) : void 0;
662
+ const org = orgHref ? externalLink(orgHref, escapeHtml(v.organization)) : escapeHtml(v.organization);
663
+ const role = v.role ? `<span class="vol-role">${escapeHtml(v.role)}</span>` : "";
664
+ const desc = v.description ? `<p class="vol-desc">${escapeHtml(v.description)}</p>` : "";
665
+ const secondary = v.secondaryLink ? secondaryLinkPill(v.secondaryLink) : "";
666
+ return `<li class="vol"><div class="vol-head"><span class="vol-org">${org}</span>${role}</div>${desc}${secondary}</li>`;
667
+ };
668
+ return `<section><h2>${escapeHtml(heading)}</h2><ul class="vol-list">${items.map(li).join("")}</ul></section>`;
669
+ }
670
+ function renderLanguages(languages, heading) {
164
671
  if (languages.length === 0) return "";
165
672
  const items = languages.map((l) => `<li>${escapeHtml(`${l.displayName ?? l.language} \u2014 ${l.proficiency}`)}</li>`).join("");
166
- return `<section><h2>Languages</h2><ul class="entries">${items}</ul></section>`;
673
+ return `<section><h2>${escapeHtml(heading)}</h2><ul class="entries">${items}</ul></section>`;
167
674
  }
168
- function renderRecommendations(recs) {
675
+ function renderRecommendations(recs, heading) {
169
676
  if (recs.length === 0) return "";
170
677
  const items = recs.map((r) => {
171
678
  const authorHref = r.author.url ? safeUrl(r.author.url) : void 0;
@@ -174,9 +681,9 @@ function renderRecommendations(recs) {
174
681
  const rel = r.relationship ? ` (${escapeHtml(r.relationship)})` : "";
175
682
  return `<figure class="rec"><blockquote>${escapeHtml(r.body)}</blockquote><figcaption>\u2014 ${caption}${rel}</figcaption></figure>`;
176
683
  }).join("");
177
- return `<section><h2>Recommendations</h2>${items}</section>`;
684
+ return `<section><h2>${escapeHtml(heading)}</h2>${items}</section>`;
178
685
  }
179
- function renderContact(contact) {
686
+ function renderContact(contact, heading) {
180
687
  const items = [];
181
688
  if (contact.email) {
182
689
  items.push(
@@ -188,191 +695,266 @@ function renderContact(contact) {
188
695
  items.push(`<li><a href="${escapeHtml(formHref)}">Contact form</a></li>`);
189
696
  }
190
697
  if (items.length === 0) return "";
191
- return `<section><h2>Contact</h2><ul class="entries">${items.join("")}</ul></section>`;
698
+ return `<section><h2>${escapeHtml(heading)}</h2><ul class="entries">${items.join("")}</ul></section>`;
699
+ }
700
+ var PLATFORM_DISPLAY = {
701
+ instagram: "Instagram",
702
+ x: "X",
703
+ linkedin: "LinkedIn",
704
+ github: "GitHub",
705
+ gitlab: "GitLab",
706
+ youtube: "YouTube",
707
+ threads: "Threads",
708
+ facebook: "Facebook",
709
+ mastodon: "Mastodon",
710
+ bluesky: "Bluesky",
711
+ zenn: "Zenn",
712
+ qiita: "Qiita",
713
+ note: "note",
714
+ blog: "Blog",
715
+ event: "Event",
716
+ artwork: "Artwork",
717
+ project: "Project"
718
+ };
719
+ function platformDisplay(platform) {
720
+ const label = PLATFORM_DISPLAY[platform.trim().toLowerCase()];
721
+ return typeof label === "string" ? label : platform;
722
+ }
723
+ function highlightBadge(h) {
724
+ return `<span class="highlight-badge">${brandIconForPlatform(h.platform, h.url)}<span>${escapeHtml(platformDisplay(h.platform))}</span></span>`;
725
+ }
726
+ function renderHighlightCard(h, locale) {
727
+ const imgSrc = h.image ? safeUrl(h.image) : void 0;
728
+ const img = imgSrc ? `<div class="highlight-thumb"><img src="${escapeHtml(imgSrc)}" alt="${escapeHtml(h.alt)}" loading="lazy" decoding="async"></div>` : "";
729
+ const href = h.url ? safeUrl(h.url) : void 0;
730
+ const name = platformDisplay(h.platform);
731
+ const cta = isJapaneseLocale(locale) ? `${name}\u3067\u898B\u308B` : `View on ${name}`;
732
+ const title = escapeHtml(h.title);
733
+ const titleEl = href ? `<h3 class="highlight-title"><a href="${escapeHtml(href)}" rel="noopener" aria-label="${escapeHtml(`${h.title} \u2014 ${cta}`)}">${title}</a></h3>` : `<h3 class="highlight-title">${title}</h3>`;
734
+ const date = h.postedAt ? `<p class="highlight-date">${timeTag(h.postedAt, locale)}</p>` : "";
735
+ const desc = h.description ? `<p class="highlight-desc">${escapeHtml(h.description)}</p>` : "";
736
+ const tags = h.tags && h.tags.length > 0 ? `<ul class="tags">${h.tags.map((t) => `<li>${escapeHtml(t)}</li>`).join("")}</ul>` : "";
737
+ const ctaEl = href ? `<span class="highlight-cta" aria-hidden="true">${escapeHtml(cta)} \u2192</span>` : "";
738
+ return `<li class="highlight-card">${img}<div class="highlight-body">${highlightBadge(h)}${titleEl}${date}${desc}${tags}${ctaEl}</div></li>`;
739
+ }
740
+ function renderHighlights(items, heading, intro, locale) {
741
+ if (items.length === 0) return "";
742
+ const introEl = intro ? `<p class="highlights-intro">${escapeHtml(intro)}</p>` : "";
743
+ const cards = items.map((h) => renderHighlightCard(h, locale)).join("");
744
+ return `<section class="highlights"><h2>${escapeHtml(heading)}</h2>${introEl}<ul class="highlights-track" role="list">${cards}</ul></section>`;
192
745
  }
193
- function renderActivity(snapshot) {
746
+ function renderActivity(snapshot, heading) {
194
747
  if (!snapshot) return "";
195
748
  const svg = renderActivitySvg(snapshot);
196
749
  if (svg === "") return "";
197
- return `<section class="activity"><h2>Activity</h2>${svg}</section>`;
750
+ return `<section class="activity"><h2>${escapeHtml(heading)}</h2>${svg}</section>`;
198
751
  }
199
752
  function renderJsonLdScript(data) {
200
753
  const payload = JSON.stringify(generateJsonLd(data));
201
754
  return `<script type="application/ld+json">${escapeJsonLd(payload)}</script>`;
202
755
  }
203
- function renderLocaleNav(localeNav) {
756
+ function renderLocaleNav(localeNav, ariaLabel) {
204
757
  const items = localeNav.map(
205
758
  (l) => l.current ? `<span aria-current="true">${escapeHtml(l.locale)}</span>` : `<a href="${escapeHtml(l.href)}">${escapeHtml(l.locale)}</a>`
206
759
  ).join("");
207
- return `<nav class="locales" aria-label="Language">${items}</nav>`;
760
+ return `<nav class="locales" aria-label="${escapeHtml(ariaLabel)}">${items}</nav>`;
208
761
  }
762
+ var DATE_SEPARATOR_JA = " \u301C ";
763
+ var DATE_SEPARATOR_DEFAULT = " \u2013 ";
209
764
  function renderProfileHtml(input) {
210
765
  const d = input.localized;
211
766
  const p = d.profile;
767
+ const locale = d.resolvedLocale;
212
768
  const description = p.tagline ?? p.bio ?? "";
769
+ const L = {
770
+ ...pickLabelPack(locale),
771
+ ...d.settings.sectionLabels ?? {},
772
+ ...input.labels
773
+ };
774
+ const omit = new Set(input.omitSections ?? []);
775
+ const keep = (key, html) => omit.has(key) ? "" : html;
776
+ const dateSeparator = isJapaneseLocale(locale) ? DATE_SEPARATOR_JA : DATE_SEPARATOR_DEFAULT;
777
+ const dates = (start, opts) => dateRange(start, { ...opts, locale, separator: dateSeparator });
213
778
  const head = [
214
779
  '<meta charset="utf-8">',
215
780
  '<meta name="viewport" content="width=device-width, initial-scale=1">',
216
781
  `<title>${escapeHtml(p.displayName)}</title>`,
217
782
  description ? `<meta name="description" content="${escapeHtml(description.slice(0, 300))}">` : "",
783
+ // Data-derived Open Graph tags (the renderer owns these; asset tags like
784
+ // og:image and PWA/theme-color are the host's, via the `head` slot).
785
+ '<meta property="og:type" content="profile">',
786
+ `<meta property="og:title" content="${escapeHtml(p.displayName)}">`,
787
+ description ? `<meta property="og:description" content="${escapeHtml(description.slice(0, 300))}">` : "",
788
+ input.canonicalUrl ? `<meta property="og:url" content="${escapeHtml(input.canonicalUrl)}">` : "",
218
789
  input.canonicalUrl ? `<link rel="canonical" href="${escapeHtml(input.canonicalUrl)}">` : "",
219
790
  ...input.alternates.map(
220
791
  (a) => `<link rel="alternate" hreflang="${escapeHtml(a.hreflang)}" href="${escapeHtml(a.href)}">`
221
792
  ),
222
793
  input.jsonLd ? renderJsonLdScript(d) : "",
223
794
  input.contact ? '<link rel="stylesheet" href="/contact-widget.css">' : "",
224
- `<style>${CSS}</style>`
795
+ `<style>${buildTokenCss(d.settings.appearance)}
796
+ ${CSS}</style>`,
797
+ // First-party <head> slot (raw, unescaped): OG image tags, manifest link, etc.
798
+ input.slots?.head ?? ""
225
799
  ].filter(Boolean).join("\n ");
226
- const body = [
227
- input.localeNav.length > 1 ? renderLocaleNav(input.localeNav) : "",
228
- renderHeader(p),
229
- renderLinks(d.links),
230
- entryList(
231
- "Experience",
800
+ const sections = {
801
+ about: renderBio(p.bio, L.about),
802
+ careers: entryList(
803
+ L.careers,
232
804
  d.careers.map((c) => ({
233
805
  heading: c.role,
234
806
  sub: c.organization,
235
- dates: dateRange(c.startDate, {
236
- end: c.endDate,
237
- isCurrent: c.isCurrent,
238
- locale: d.resolvedLocale
239
- }),
807
+ subUrl: c.url,
808
+ dates: dates(c.startDate, { end: c.endDate, isCurrent: c.isCurrent }),
240
809
  body: c.description,
241
- url: c.url
242
- }))
810
+ current: c.isCurrent
811
+ })),
812
+ "timeline"
243
813
  ),
244
- entryList(
245
- "Projects",
814
+ projects: entryList(
815
+ L.projects,
246
816
  d.projects.map((x) => ({
247
817
  heading: x.title,
248
- dates: dateRange(x.startDate, { end: x.endDate, locale: d.resolvedLocale }),
249
- body: x.description,
250
818
  url: x.url,
251
- tags: x.tags
252
- }))
819
+ role: x.role,
820
+ dates: dates(x.startDate, { end: x.endDate }),
821
+ body: x.description,
822
+ tags: x.tags,
823
+ highlighted: x.highlighted
824
+ })),
825
+ "cards"
253
826
  ),
254
- renderSkills(d.skills),
255
- renderActivity(input.activitySnapshot),
256
- entryList(
257
- "Education",
827
+ volunteering: renderVolunteering(d.volunteering, L.volunteering),
828
+ skills: renderSkills(d.skills, d.settings.skillCategories, L.skills),
829
+ activity: renderActivity(input.activitySnapshot, L.activity),
830
+ education: entryList(
831
+ L.education,
258
832
  d.education.map((e) => {
259
833
  const degree = nonEmpty([e.degree, e.fieldOfStudy], ", ");
260
834
  return {
261
835
  heading: degree ?? e.institution,
262
836
  sub: degree ? e.institution : void 0,
263
- dates: dateRange(e.startDate, {
264
- end: e.endDate,
265
- isCurrent: e.isCurrent,
266
- locale: d.resolvedLocale
267
- }),
837
+ dates: dates(e.startDate, { end: e.endDate, isCurrent: e.isCurrent }),
268
838
  body: e.description,
269
839
  url: e.url
270
840
  };
271
841
  })
272
842
  ),
273
- entryList(
274
- "Certifications",
843
+ certifications: entryList(
844
+ L.certifications,
275
845
  d.certifications.map((c) => ({
276
846
  heading: c.title,
277
847
  sub: c.issuingOrganization,
278
- dates: dateRange(c.issueDate, { end: c.expirationDate, locale: d.resolvedLocale }),
848
+ dates: dates(c.issueDate, { end: c.expirationDate }),
279
849
  url: c.url
280
850
  }))
281
851
  ),
282
- entryList(
283
- "Publications",
852
+ publications: entryList(
853
+ L.publications,
284
854
  d.publications.map((x) => ({
285
855
  heading: x.title,
286
856
  sub: nonEmpty([x.publisher, x.coAuthors?.join(", ")], " \xB7 "),
287
- dates: dateRange(x.date, { locale: d.resolvedLocale }),
857
+ dates: dates(x.date, {}),
288
858
  body: x.description,
289
859
  url: x.url ?? (x.doi ? `https://doi.org/${x.doi}` : void 0)
290
860
  }))
291
861
  ),
292
- entryList(
293
- "Honors & awards",
862
+ honors: entryList(
863
+ L.honors,
294
864
  d.honors.map((x) => ({
295
865
  heading: x.title,
296
866
  sub: x.issuer,
297
- dates: dateRange(x.date, { locale: d.resolvedLocale }),
867
+ dates: dates(x.date, {}),
298
868
  body: x.description,
299
869
  url: x.url
300
870
  }))
301
871
  ),
302
- entryList(
303
- "Memberships",
872
+ memberships: entryList(
873
+ L.memberships,
304
874
  d.memberships.map((x) => ({
305
875
  heading: x.role ?? x.organization,
306
876
  sub: x.role ? x.organization : void 0,
307
- dates: dateRange(x.startDate, {
308
- end: x.endDate,
309
- isCurrent: x.isCurrent,
310
- locale: d.resolvedLocale
311
- }),
312
- body: x.description,
313
- url: x.url
314
- }))
315
- ),
316
- entryList(
317
- "Volunteering",
318
- d.volunteering.map((x) => ({
319
- heading: x.role,
320
- sub: nonEmpty([x.organization, x.cause], " \xB7 "),
321
- dates: dateRange(x.startDate, {
322
- end: x.endDate,
323
- isCurrent: x.isCurrent,
324
- locale: d.resolvedLocale
325
- }),
877
+ dates: dates(x.startDate, { end: x.endDate, isCurrent: x.isCurrent }),
326
878
  body: x.description,
327
879
  url: x.url
328
880
  }))
329
881
  ),
330
- entryList(
331
- "Courses",
882
+ courses: entryList(
883
+ L.courses,
332
884
  d.courses.map((x) => ({
333
885
  heading: x.title,
334
886
  sub: x.provider,
335
- dates: dateRange(x.completionDate, { locale: d.resolvedLocale }),
887
+ dates: dates(x.completionDate, {}),
336
888
  body: x.description,
337
889
  url: x.certificateUrl
338
890
  }))
339
891
  ),
340
- entryList(
341
- "Patents",
892
+ patents: entryList(
893
+ L.patents,
342
894
  d.patents.map((x) => ({
343
895
  heading: x.title,
344
896
  sub: nonEmpty([x.patentNumber, x.office, x.status, x.coInventors?.join(", ")], " \xB7 "),
345
- dates: dateRange(x.filingDate ?? x.grantDate, { locale: d.resolvedLocale }),
897
+ dates: dates(x.filingDate ?? x.grantDate, {}),
346
898
  body: x.description,
347
899
  url: x.url
348
900
  }))
349
901
  ),
350
- entryList(
351
- "Test scores",
902
+ testScores: entryList(
903
+ L.testScores,
352
904
  d.testScores.map((x) => ({
353
905
  heading: `${x.title}: ${x.score}`,
354
- dates: dateRange(x.date, { locale: d.resolvedLocale }),
906
+ dates: dates(x.date, {}),
355
907
  body: x.description,
356
908
  url: x.url
357
909
  }))
358
910
  ),
359
- renderLanguages(d.languages),
360
- renderRecommendations(d.recommendations),
361
- renderContact(d.contact)
911
+ languages: renderLanguages(d.languages, L.languages),
912
+ recommendations: renderRecommendations(d.recommendations, L.recommendations),
913
+ highlights: renderHighlights(d.highlights, L.highlights, d.settings.highlightsIntro, locale),
914
+ contact: renderContact(d.contact, L.contact)
915
+ };
916
+ const localeNavHtml = input.localeNav.length > 1 ? renderLocaleNav(input.localeNav, L.localeNav) : "";
917
+ const seenKeys = /* @__PURE__ */ new Set();
918
+ const orderedKeys = [];
919
+ for (const key of d.settings.sectionOrder ?? []) {
920
+ if (SECTION_KEYS.includes(key) && !seenKeys.has(key)) {
921
+ seenKeys.add(key);
922
+ orderedKeys.push(key);
923
+ }
924
+ }
925
+ for (const key of SECTION_KEYS) {
926
+ if (!seenKeys.has(key)) orderedKeys.push(key);
927
+ }
928
+ const body = [
929
+ renderHeader(p, localeNavHtml),
930
+ // Featured links at the top; the remaining links are a fixed bottom section.
931
+ renderFeaturedLinks(d.links, L.featuredLinks),
932
+ ...orderedKeys.map((key) => keep(key, sections[key])),
933
+ renderOtherLinks(d.links, L.otherLinks),
934
+ input.slots?.mainEnd ?? ""
362
935
  ].filter(Boolean).join("\n");
363
- const footer = d.settings.showPoweredBy === true ? '<footer class="powered">Powered by takuhon</footer>' : "";
936
+ const spdx = d.meta?.contentLicense?.spdxId;
937
+ const year = typeof input.year === "number" && Number.isFinite(input.year) ? input.year : void 0;
938
+ const licenseText = year !== void 0 ? `\xA9 ${year} ${escapeHtml(p.displayName)} \u2014 ${escapeHtml(spdx ?? "")}` : `\xA9 ${escapeHtml(p.displayName)} \u2014 ${escapeHtml(spdx ?? "")}`;
939
+ const licenseLine = spdx ? `<p class="license">${licenseText}</p>` : "";
940
+ const poweredByLine = d.settings.showPoweredBy !== false ? `<p class="powered-by">${escapeHtml(L.poweredBy)} <a href="https://takuhon.org/" rel="noopener">Takuhon</a></p>` : "";
941
+ const footerInner = `${licenseLine}${poweredByLine}`;
942
+ const footer = footerInner ? `<footer class="powered">${footerInner}</footer>` : "";
364
943
  const contactScript = input.contact ? `<script src="/contact-widget.js" data-site-key="${escapeHtml(input.contact.siteKey)}"` + (input.contact.endpoint ? ` data-endpoint="${escapeHtml(input.contact.endpoint)}"` : "") + " defer></script>\n" : "";
944
+ const skipLink = `<a class="skip-link" href="#main">${escapeHtml(L.skipLink)}</a>
945
+ `;
946
+ const bodyEnd = input.slots?.bodyEnd ?? "";
365
947
  return `<!DOCTYPE html>
366
948
  <html lang="${escapeHtml(d.resolvedLocale)}">
367
949
  <head>
368
950
  ${head}
369
951
  </head>
370
952
  <body>
371
- <main>
953
+ ${skipLink}<main id="main">
372
954
  ${body}
373
955
  </main>
374
956
  ${footer ? `${footer}
375
- ` : ""}${contactScript}</body>
957
+ ` : ""}${contactScript}${bodyEnd}</body>
376
958
  </html>
377
959
  `;
378
960
  }
@@ -497,7 +1079,33 @@ function pathLocaleFromUrl(url) {
497
1079
  // src/public-app.ts
498
1080
  var FALLBACK_VERSION = "bundled-fixture";
499
1081
  var TURNSTILE_ORIGIN = "https://challenges.cloudflare.com";
500
- function buildPublicCsp(contact) {
1082
+ var CSP_SOURCE = /^[!-~]+$/;
1083
+ var CSP_HASH = /^'sha(256|384|512)-[A-Za-z0-9+/]+={0,2}'$/;
1084
+ var CSP_BLANKET = /* @__PURE__ */ new Set(["'unsafe-inline'", "'unsafe-eval'"]);
1085
+ function sanitizeCsp(ext) {
1086
+ const dropped = [];
1087
+ const keep = (tokens, ok) => (tokens ?? []).filter((t) => {
1088
+ if (ok(t)) return true;
1089
+ dropped.push(t);
1090
+ return false;
1091
+ });
1092
+ const source = (t) => CSP_SOURCE.test(t) && !t.includes(";") && !t.includes(",") && !CSP_BLANKET.has(t.toLowerCase());
1093
+ return {
1094
+ clean: {
1095
+ scriptSrc: keep(ext.scriptSrc, source),
1096
+ connectSrc: keep(ext.connectSrc, source),
1097
+ workerSrc: keep(ext.workerSrc, source),
1098
+ scriptHashes: keep(ext.scriptHashes, (t) => CSP_HASH.test(t))
1099
+ },
1100
+ dropped
1101
+ };
1102
+ }
1103
+ function buildPublicCsp(contact, ext) {
1104
+ const turnstile = contact ? [TURNSTILE_ORIGIN] : [];
1105
+ const scriptSrc = ["'self'", ...turnstile, ...ext?.scriptSrc ?? [], ...ext?.scriptHashes ?? []];
1106
+ const connectSrc = ["'self'", ...turnstile, ...ext?.connectSrc ?? []];
1107
+ const frameSrc = [...turnstile];
1108
+ const workerSrc = ext?.workerSrc ?? [];
501
1109
  return [
502
1110
  "default-src 'self'",
503
1111
  // `https:` lets the server-rendered profile page load remote avatar images
@@ -505,10 +1113,11 @@ function buildPublicCsp(contact) {
505
1113
  // already blocks non-http(s) schemes); `data:` covers inline placeholders.
506
1114
  "img-src 'self' https: data:",
507
1115
  "style-src 'self' 'unsafe-inline'",
508
- contact ? `script-src 'self' ${TURNSTILE_ORIGIN}` : "script-src 'self'",
1116
+ `script-src ${scriptSrc.join(" ")}`,
509
1117
  "font-src 'self'",
510
- contact ? `connect-src 'self' ${TURNSTILE_ORIGIN}` : "connect-src 'self'",
511
- ...contact ? [`frame-src ${TURNSTILE_ORIGIN}`] : [],
1118
+ `connect-src ${connectSrc.join(" ")}`,
1119
+ ...frameSrc.length > 0 ? [`frame-src ${frameSrc.join(" ")}`] : [],
1120
+ ...workerSrc.length > 0 ? [`worker-src ${workerSrc.join(" ")}`] : [],
512
1121
  "frame-ancestors 'none'",
513
1122
  "base-uri 'self'",
514
1123
  "form-action 'self'",
@@ -529,6 +1138,19 @@ async function loadProfile(deps) {
529
1138
  }
530
1139
  function createPublicApp(deps) {
531
1140
  const app = new Hono({ getPath: localePrefixGetPath });
1141
+ const cspExt = deps.render?.csp;
1142
+ let pageCsp = PUBLIC_CSP;
1143
+ let pageCspWithContact = PUBLIC_CSP_WITH_CONTACT;
1144
+ if (cspExt) {
1145
+ const { clean, dropped } = sanitizeCsp(cspExt);
1146
+ if (dropped.length > 0) {
1147
+ console.warn(
1148
+ `[takuhon] Ignored ${dropped.length} invalid render.csp token(s): ${dropped.join(", ")}`
1149
+ );
1150
+ }
1151
+ pageCsp = buildPublicCsp(false, clean);
1152
+ pageCspWithContact = buildPublicCsp(true, clean);
1153
+ }
532
1154
  app.use("*", async (c, next) => {
533
1155
  await next();
534
1156
  const h = c.res.headers;
@@ -537,9 +1159,11 @@ function createPublicApp(deps) {
537
1159
  h.set("x-frame-options", "DENY");
538
1160
  h.set("referrer-policy", "strict-origin-when-cross-origin");
539
1161
  h.set("permissions-policy", "camera=(), microphone=(), geolocation=(), interest-cohort=()");
1162
+ const contact = c.get("contactEnabled") === true;
1163
+ const page = c.get("profilePage") === true;
540
1164
  h.set(
541
1165
  "content-security-policy",
542
- c.get("contactEnabled") ? PUBLIC_CSP_WITH_CONTACT : PUBLIC_CSP
1166
+ page ? contact ? pageCspWithContact : pageCsp : contact ? PUBLIC_CSP_WITH_CONTACT : PUBLIC_CSP
543
1167
  );
544
1168
  h.set("access-control-allow-origin", "*");
545
1169
  h.set("access-control-expose-headers", "ETag");
@@ -584,6 +1208,14 @@ function createPublicApp(deps) {
584
1208
  } : void 0;
585
1209
  if (contact) c.set("contactEnabled", true);
586
1210
  const html = renderProfileHtml({
1211
+ // Host composition seams (slots / labels / omitSections). `csp` is a
1212
+ // response-header concern handled above, not a renderer input, so it is
1213
+ // intentionally excluded here.
1214
+ ...deps.render ? {
1215
+ slots: deps.render.slots,
1216
+ labels: deps.render.labels,
1217
+ omitSections: deps.render.omitSections
1218
+ } : {},
587
1219
  localized,
588
1220
  canonicalUrl: `${origin}${localePath(current)}`,
589
1221
  alternates: [
@@ -593,11 +1225,13 @@ function createPublicApp(deps) {
593
1225
  localeNav: locales.map((l) => ({ locale: l, href: localePath(l), current: l === current })),
594
1226
  jsonLd: profile.settings.enableJsonLd !== false,
595
1227
  activitySnapshot: snapshot ?? void 0,
596
- contact
1228
+ contact,
1229
+ year: (/* @__PURE__ */ new Date()).getFullYear()
597
1230
  });
598
1231
  c.header("etag", `"${version}"`);
599
1232
  c.header("cache-control", "public, max-age=300");
600
1233
  c.header("vary", "Accept-Language, Cookie");
1234
+ c.set("profilePage", true);
601
1235
  return c.html(html);
602
1236
  });
603
1237
  app.get("/health", (c) => {
@@ -1552,7 +2186,8 @@ function generateSite(profile, options = {}) {
1552
2186
  alternates,
1553
2187
  localeNav,
1554
2188
  jsonLd,
1555
- activitySnapshot
2189
+ activitySnapshot,
2190
+ year: (/* @__PURE__ */ new Date()).getFullYear()
1556
2191
  });
1557
2192
  pages.push({
1558
2193
  route: isDefault ? "/" : `/${locale}/`,