@opnpress/opnpress-cli 0.2.3 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{build.js → cli/buildSite.js} +4 -4
- package/dist/{init.js → cli/initProject.js} +2 -2
- package/dist/{cli.js → cli/main.js} +2 -2
- package/dist/integrations/booking-calendar.js +23 -0
- package/dist/integrations/company-info.js +24 -0
- package/dist/integrations/contact-form.js +35 -0
- package/dist/integrations/contact-links.js +87 -0
- package/dist/integrations/index.js +18 -0
- package/dist/integrations/maps.js +23 -0
- package/dist/integrations/shareable-links.js +58 -0
- package/dist/integrations/socials-links.js +198 -0
- package/dist/integrations/video.js +74 -0
- package/dist/{linkEmbedding.js → renderer/aioHelper.js} +1 -16
- package/dist/renderer/footerHtmlBuilder.js +11 -0
- package/dist/renderer/headerHtmlBuilder.js +20 -0
- package/dist/renderer/mdBodyHtmlBuilder.js +117 -0
- package/dist/renderer/pageGenerator.js +204 -0
- package/dist/renderer/pageHelpers.js +132 -0
- package/dist/renderer/rawBodyHtmlBuilder.js +3 -0
- package/dist/{jsEmbedding.js → renderer/scriptBuilder.js} +14 -5
- package/dist/renderer/themeBuilder.js +723 -0
- package/dist/rendering/contact.js +149 -0
- package/dist/rendering/shared.js +75 -0
- package/dist/shortcodes/cardrow.js +85 -0
- package/dist/shortcodes/contact-card.js +24 -0
- package/dist/shortcodes/index.js +14 -0
- package/dist/shortcodes/js.js +7 -0
- package/dist/shortcodes/mailto.js +31 -0
- package/dist/shortcodes/pagelist.js +82 -0
- package/dist/shortcodes/tel.js +24 -0
- package/package.json +6 -6
- package/dist/render.js +0 -2041
- /package/dist/{server.js → cli/server.js} +0 -0
package/dist/render.js
DELETED
|
@@ -1,2041 +0,0 @@
|
|
|
1
|
-
import { promises as fs } from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { unified } from 'unified';
|
|
4
|
-
import remarkParse from 'remark-parse';
|
|
5
|
-
import remarkGfm from 'remark-gfm';
|
|
6
|
-
import remarkRehype from 'remark-rehype';
|
|
7
|
-
import rehypeRaw from 'rehype-raw';
|
|
8
|
-
import rehypeSlug from 'rehype-slug';
|
|
9
|
-
import rehypeAutolinkHeadings from 'rehype-autolink-headings';
|
|
10
|
-
import rehypeStringify from 'rehype-stringify';
|
|
11
|
-
import YAML from 'yaml';
|
|
12
|
-
import { escapeHtml, isFullHtmlDocument } from './utils.js';
|
|
13
|
-
import { buildDiscoveryBodyMarkup, buildDiscoveryFooterMarkup, buildDiscoveryHeadMarkup, buildDiscoveryHeaderMarkup, buildSystemReminderMarkup } from './linkEmbedding.js';
|
|
14
|
-
import { buildCustomScriptTags, buildInlineScriptShortcodeMarkup } from './jsEmbedding.js';
|
|
15
|
-
import { buildPageTitle } from './content.js';
|
|
16
|
-
export async function renderMarkdown(markdown) {
|
|
17
|
-
const file = await unified()
|
|
18
|
-
.use(remarkParse)
|
|
19
|
-
.use(remarkGfm)
|
|
20
|
-
.use(remarkRehype, { allowDangerousHtml: true })
|
|
21
|
-
.use(rehypeRaw)
|
|
22
|
-
.use(rehypeSlug)
|
|
23
|
-
.use(rehypeAutolinkHeadings, { behavior: 'append' })
|
|
24
|
-
.use(rehypeStringify, { allowDangerousHtml: true })
|
|
25
|
-
.process(markdown);
|
|
26
|
-
return String(file);
|
|
27
|
-
}
|
|
28
|
-
const CARD_PRESETS = {
|
|
29
|
-
pages: {
|
|
30
|
-
title: 'Pages',
|
|
31
|
-
href: '/about/',
|
|
32
|
-
body: 'Create markdown pages with YAML frontmatter.'
|
|
33
|
-
},
|
|
34
|
-
integrations: {
|
|
35
|
-
title: 'Integrations',
|
|
36
|
-
href: '/integrations/',
|
|
37
|
-
body: 'Render semantic blocks for forms, social links, maps, video, and calendars.'
|
|
38
|
-
},
|
|
39
|
-
services: {
|
|
40
|
-
title: 'Services',
|
|
41
|
-
href: '/services/',
|
|
42
|
-
body: 'Create service pages with the same markdown-first workflow.'
|
|
43
|
-
}
|
|
44
|
-
};
|
|
45
|
-
const SOCIAL_PROVIDER_ORDER = [
|
|
46
|
-
'twitter',
|
|
47
|
-
'x',
|
|
48
|
-
'github',
|
|
49
|
-
'facebook',
|
|
50
|
-
'instagram',
|
|
51
|
-
'linkedin',
|
|
52
|
-
'youtube',
|
|
53
|
-
'tiktok',
|
|
54
|
-
'threads',
|
|
55
|
-
'mastodon',
|
|
56
|
-
'bluesky',
|
|
57
|
-
'website',
|
|
58
|
-
'email'
|
|
59
|
-
];
|
|
60
|
-
function socialProviderLabel(provider) {
|
|
61
|
-
switch (provider) {
|
|
62
|
-
case 'x':
|
|
63
|
-
return 'X';
|
|
64
|
-
case 'youtube':
|
|
65
|
-
return 'YouTube';
|
|
66
|
-
case 'github':
|
|
67
|
-
return 'GitHub';
|
|
68
|
-
case 'linkedin':
|
|
69
|
-
return 'LinkedIn';
|
|
70
|
-
case 'mastodon':
|
|
71
|
-
return 'Mastodon';
|
|
72
|
-
case 'bluesky':
|
|
73
|
-
return 'Bluesky';
|
|
74
|
-
case 'facebook':
|
|
75
|
-
return 'Facebook';
|
|
76
|
-
case 'instagram':
|
|
77
|
-
return 'Instagram';
|
|
78
|
-
case 'tiktok':
|
|
79
|
-
return 'TikTok';
|
|
80
|
-
case 'threads':
|
|
81
|
-
return 'Threads';
|
|
82
|
-
case 'website':
|
|
83
|
-
return 'Website';
|
|
84
|
-
case 'email':
|
|
85
|
-
return 'Email';
|
|
86
|
-
default:
|
|
87
|
-
return 'Twitter';
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
function normalizeSocialInput(value) {
|
|
91
|
-
const trimmed = value.trim();
|
|
92
|
-
if (!trimmed) {
|
|
93
|
-
return '';
|
|
94
|
-
}
|
|
95
|
-
if (isExternalUrl(trimmed) || trimmed.startsWith('mailto:')) {
|
|
96
|
-
return trimmed;
|
|
97
|
-
}
|
|
98
|
-
return trimmed.replace(/^@+/, '');
|
|
99
|
-
}
|
|
100
|
-
function socialMetaHandle(value) {
|
|
101
|
-
if (!value || !value.trim()) {
|
|
102
|
-
return undefined;
|
|
103
|
-
}
|
|
104
|
-
const trimmed = value.trim();
|
|
105
|
-
if (trimmed.startsWith('@')) {
|
|
106
|
-
return trimmed;
|
|
107
|
-
}
|
|
108
|
-
if (isExternalUrl(trimmed)) {
|
|
109
|
-
try {
|
|
110
|
-
const url = new URL(trimmed);
|
|
111
|
-
const segments = url.pathname.split('/').filter(Boolean);
|
|
112
|
-
const lastSegment = segments[segments.length - 1];
|
|
113
|
-
return lastSegment ? `@${lastSegment.replace(/^@+/, '')}` : trimmed;
|
|
114
|
-
}
|
|
115
|
-
catch {
|
|
116
|
-
return trimmed;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
return `@${trimmed.replace(/^@+/, '')}`;
|
|
120
|
-
}
|
|
121
|
-
function socialHref(provider, value) {
|
|
122
|
-
const normalized = normalizeSocialInput(value);
|
|
123
|
-
if (!normalized) {
|
|
124
|
-
return '';
|
|
125
|
-
}
|
|
126
|
-
if (isExternalUrl(normalized) || normalized.startsWith('mailto:')) {
|
|
127
|
-
return normalized;
|
|
128
|
-
}
|
|
129
|
-
const handle = normalized.replace(/^@+/, '');
|
|
130
|
-
switch (provider) {
|
|
131
|
-
case 'twitter':
|
|
132
|
-
case 'x':
|
|
133
|
-
return `https://x.com/${handle}`;
|
|
134
|
-
case 'github':
|
|
135
|
-
return `https://github.com/${handle}`;
|
|
136
|
-
case 'facebook':
|
|
137
|
-
return `https://facebook.com/${handle}`;
|
|
138
|
-
case 'instagram':
|
|
139
|
-
return `https://instagram.com/${handle}`;
|
|
140
|
-
case 'linkedin':
|
|
141
|
-
return `https://www.linkedin.com/in/${handle}`;
|
|
142
|
-
case 'youtube':
|
|
143
|
-
return `https://www.youtube.com/@${handle}`;
|
|
144
|
-
case 'tiktok':
|
|
145
|
-
return `https://www.tiktok.com/@${handle}`;
|
|
146
|
-
case 'threads':
|
|
147
|
-
return `https://www.threads.net/@${handle}`;
|
|
148
|
-
case 'mastodon':
|
|
149
|
-
if (handle.includes('@')) {
|
|
150
|
-
const [user, instance] = handle.split('@').filter(Boolean);
|
|
151
|
-
if (user && instance) {
|
|
152
|
-
return `https://${instance}/@${user}`;
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
return `https://mastodon.social/@${handle}`;
|
|
156
|
-
case 'bluesky':
|
|
157
|
-
return `https://bsky.app/profile/${handle}`;
|
|
158
|
-
case 'website':
|
|
159
|
-
return `https://${handle}`;
|
|
160
|
-
case 'email':
|
|
161
|
-
return `mailto:${handle}`;
|
|
162
|
-
default:
|
|
163
|
-
return normalized;
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
function socialIconSlug(provider) {
|
|
167
|
-
switch (provider) {
|
|
168
|
-
case 'twitter':
|
|
169
|
-
return 'twitter';
|
|
170
|
-
case 'x':
|
|
171
|
-
return 'x';
|
|
172
|
-
case 'github':
|
|
173
|
-
return 'github';
|
|
174
|
-
case 'facebook':
|
|
175
|
-
return 'facebook';
|
|
176
|
-
case 'instagram':
|
|
177
|
-
return 'instagram';
|
|
178
|
-
case 'linkedin':
|
|
179
|
-
return 'linkedin';
|
|
180
|
-
case 'youtube':
|
|
181
|
-
return 'youtube';
|
|
182
|
-
case 'tiktok':
|
|
183
|
-
return 'tiktok';
|
|
184
|
-
case 'threads':
|
|
185
|
-
return 'threads';
|
|
186
|
-
case 'mastodon':
|
|
187
|
-
return 'mastodon';
|
|
188
|
-
case 'bluesky':
|
|
189
|
-
return 'bluesky';
|
|
190
|
-
default:
|
|
191
|
-
return null;
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
function socialIconMarkup(provider) {
|
|
195
|
-
const slug = socialIconSlug(provider);
|
|
196
|
-
if (slug) {
|
|
197
|
-
const src = `https://cdn.simpleicons.org/${slug}?viewbox=auto&size=20`;
|
|
198
|
-
return `<img class="social-icon" src="${escapeHtml(src)}" alt="" aria-hidden="true" loading="lazy" decoding="async" />`;
|
|
199
|
-
}
|
|
200
|
-
switch (provider) {
|
|
201
|
-
case 'website':
|
|
202
|
-
return `<svg class="social-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path fill="currentColor" d="M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20Zm7.9 9h-3.17a15.7 15.7 0 0 0-1.16-5.2A8.02 8.02 0 0 1 19.9 11Zm-1.8 2a8.03 8.03 0 0 1-4.33 4.2c.44-1.3.73-2.75.84-4.2h3.5ZM12 20c-.82 0-2.22-2.05-2.8-6h5.6c-.58 3.95-1.98 6-2.8 6Zm-2.8-8c.11-1.45.4-2.9.84-4.2A8.03 8.03 0 0 0 5.7 12h3.5ZM12 4c.82 0 2.22 2.05 2.8 6H9.2c.58-3.95 1.98-6 2.8-6Zm-3.77 1.8A15.7 15.7 0 0 0 7.07 11H3.9a8.02 8.02 0 0 1 4.33-5.2ZM3.9 13h3.17a15.7 15.7 0 0 0 1.16 5.2A8.02 8.02 0 0 1 3.9 13Zm12.77 5.2c.44-1.3.73-2.75.84-4.2h3.17a8.02 8.02 0 0 1-4.01 4.2Z"/></svg>`;
|
|
203
|
-
case 'email':
|
|
204
|
-
return `<svg class="social-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path fill="currentColor" d="M20 4H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2Zm0 4.236-8 5-8-5V6l8 5 8-5v2.236Z"/></svg>`;
|
|
205
|
-
default:
|
|
206
|
-
return '';
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
function socialEntriesFromConfig(socials, providers) {
|
|
210
|
-
const requestedProviders = providers?.length
|
|
211
|
-
? providers.map((provider) => provider.trim().toLowerCase()).filter(Boolean)
|
|
212
|
-
: SOCIAL_PROVIDER_ORDER;
|
|
213
|
-
const providerSet = new Set(requestedProviders);
|
|
214
|
-
const seenHrefs = new Set();
|
|
215
|
-
return SOCIAL_PROVIDER_ORDER.filter((provider) => providerSet.has(provider)).flatMap((provider) => {
|
|
216
|
-
const value = provider === 'twitter' || provider === 'x'
|
|
217
|
-
? socials.twitter ?? socials.x
|
|
218
|
-
: socials[provider];
|
|
219
|
-
if (!value || !value.trim()) {
|
|
220
|
-
return [];
|
|
221
|
-
}
|
|
222
|
-
const href = socialHref(provider, value);
|
|
223
|
-
if (!href) {
|
|
224
|
-
return [];
|
|
225
|
-
}
|
|
226
|
-
if (seenHrefs.has(href)) {
|
|
227
|
-
return [];
|
|
228
|
-
}
|
|
229
|
-
seenHrefs.add(href);
|
|
230
|
-
return [
|
|
231
|
-
{
|
|
232
|
-
provider,
|
|
233
|
-
label: socialProviderLabel(provider),
|
|
234
|
-
href
|
|
235
|
-
}
|
|
236
|
-
];
|
|
237
|
-
});
|
|
238
|
-
}
|
|
239
|
-
function normalizeContactHref(value, kind) {
|
|
240
|
-
const trimmed = value.trim();
|
|
241
|
-
if (!trimmed) {
|
|
242
|
-
return '';
|
|
243
|
-
}
|
|
244
|
-
if (isExternalUrl(trimmed) || trimmed.startsWith('mailto:') || trimmed.startsWith('tel:')) {
|
|
245
|
-
return trimmed;
|
|
246
|
-
}
|
|
247
|
-
switch (kind) {
|
|
248
|
-
case 'email':
|
|
249
|
-
return `mailto:${trimmed.replace(/^mailto:/i, '')}`;
|
|
250
|
-
case 'phone':
|
|
251
|
-
return `tel:${trimmed.replace(/^tel:/i, '').replace(/[^\d+*#.,]/g, '')}`;
|
|
252
|
-
case 'website':
|
|
253
|
-
if (trimmed.startsWith('/') || trimmed.startsWith('.') || trimmed.startsWith('#')) {
|
|
254
|
-
return trimmed;
|
|
255
|
-
}
|
|
256
|
-
return isExternalUrl(trimmed) ? trimmed : `https://${trimmed}`;
|
|
257
|
-
default:
|
|
258
|
-
return trimmed;
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
function contactWebsiteDisplayLabel(href) {
|
|
262
|
-
return href.startsWith('http://') || href.startsWith('https://') ? href : 'Goto Page';
|
|
263
|
-
}
|
|
264
|
-
function contactLinkIconMarkup(kind) {
|
|
265
|
-
switch (kind) {
|
|
266
|
-
case 'email':
|
|
267
|
-
return `<svg class="contact-link-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path fill="currentColor" d="M20 4H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2Zm0 4.236-8 5-8-5V6l8 5 8-5v2.236Z"/></svg>`;
|
|
268
|
-
case 'phone':
|
|
269
|
-
return `<svg class="contact-link-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path fill="currentColor" d="M6.62 10.79a15.5 15.5 0 0 0 6.59 6.59l2.2-2.2a1 1 0 0 1 1.02-.24c1.12.37 2.33.57 3.57.57a1 1 0 0 1 1 1V20a1 1 0 0 1-1 1C10.31 21 3 13.69 3 4a1 1 0 0 1 1-1h2.49a1 1 0 0 1 1 1c0 1.24.2 2.45.57 3.57a1 1 0 0 1-.24 1.02l-2.2 2.2Z"/></svg>`;
|
|
270
|
-
case 'website':
|
|
271
|
-
return `<svg class="contact-link-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path fill="currentColor" d="M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20Zm7.9 9h-3.17a15.7 15.7 0 0 0-1.16-5.2A8.02 8.02 0 0 1 19.9 11Zm-1.8 2a8.03 8.03 0 0 1-4.33 4.2c.44-1.3.73-2.75.84-4.2h3.5ZM12 20c-.82 0-2.22-2.05-2.8-6h5.6c-.58 3.95-1.98 6-2.8 6Zm-2.8-8c.11-1.45.4-2.9.84-4.2A8.03 8.03 0 0 0 5.7 12h3.5ZM12 4c.82 0 2.22 2.05 2.8 6H9.2c.58-3.95 1.98-6 2.8-6Zm-3.77 1.8A15.7 15.7 0 0 0 7.07 11H3.9a8.02 8.02 0 0 1 4.33-5.2ZM3.9 13h3.17a15.7 15.7 0 0 0 1.16 5.2A8.02 8.02 0 0 1 3.9 13Zm12.77 5.2c.44-1.3.73-2.75.84-4.2h3.17a8.02 8.02 0 0 1-4.01 4.2Z"/></svg>`;
|
|
272
|
-
case 'map':
|
|
273
|
-
return `<svg class="contact-link-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path fill="currentColor" d="M12 2C8.14 2 5 5.14 5 9c0 4.86 7 13 7 13s7-8.14 7-13c0-3.86-3.14-7-7-7Zm0 9.5A2.5 2.5 0 1 1 12 6a2.5 2.5 0 0 1 0 5.5Z"/></svg>`;
|
|
274
|
-
case 'address':
|
|
275
|
-
return `<svg class="contact-link-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path fill="currentColor" d="M12 2 3 7v10l9 5 9-5V7l-9-5Zm0 2.3L18 7l-6 3.7L6 7l6-2.7ZM5 8.6l6 3.7v7.6l-6-3.3V8.6Zm14 0v7.9l-6 3.2v-7.6l6-3.5Z"/></svg>`;
|
|
276
|
-
case 'hours':
|
|
277
|
-
return `<svg class="contact-link-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path fill="currentColor" d="M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20Zm1 10.41V7h-2v6l5 3 .99-1.63L13 12.41Z"/></svg>`;
|
|
278
|
-
default:
|
|
279
|
-
return '';
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
function buildContactLinkItems(data) {
|
|
283
|
-
const items = [];
|
|
284
|
-
const email = data.email?.trim();
|
|
285
|
-
const phone = data.phone?.trim();
|
|
286
|
-
const website = data.website?.trim();
|
|
287
|
-
const mapUrl = data.mapUrl?.trim();
|
|
288
|
-
const address = data.address?.trim();
|
|
289
|
-
const hours = data.hours?.trim();
|
|
290
|
-
if (email) {
|
|
291
|
-
items.push({ kind: 'email', label: 'Email', href: normalizeContactHref(email, 'email') });
|
|
292
|
-
}
|
|
293
|
-
if (phone) {
|
|
294
|
-
items.push({ kind: 'phone', label: 'Phone', href: normalizeContactHref(phone, 'phone') });
|
|
295
|
-
}
|
|
296
|
-
if (website) {
|
|
297
|
-
items.push({ kind: 'website', label: 'Website', href: normalizeContactHref(website, 'website') });
|
|
298
|
-
}
|
|
299
|
-
if (mapUrl) {
|
|
300
|
-
items.push({ kind: 'map', label: 'Map', href: normalizeContactHref(mapUrl, 'website') });
|
|
301
|
-
}
|
|
302
|
-
if (address) {
|
|
303
|
-
items.push({ kind: 'address', label: 'Address', value: address });
|
|
304
|
-
}
|
|
305
|
-
if (hours) {
|
|
306
|
-
items.push({ kind: 'hours', label: 'Hours', value: hours });
|
|
307
|
-
}
|
|
308
|
-
return items;
|
|
309
|
-
}
|
|
310
|
-
function renderContactLinksMarkup(items, options = {}) {
|
|
311
|
-
const showIcons = options.showIcons !== false;
|
|
312
|
-
const showLabels = options.showLabels !== false;
|
|
313
|
-
const variant = options.variant === 'stacked' ? 'stacked' : 'inline';
|
|
314
|
-
const content = items
|
|
315
|
-
.map((item) => {
|
|
316
|
-
const icon = showIcons ? contactLinkIconMarkup(item.kind) : '';
|
|
317
|
-
if (item.href) {
|
|
318
|
-
return `
|
|
319
|
-
<a class="share-link contact-link contact-link-${item.kind}" href="${escapeHtml(item.href)}" rel="noopener noreferrer" target="${item.href.startsWith('http') ? '_blank' : '_self'}"${showLabels ? '' : ` aria-label="${escapeHtml(item.label)}"`}>
|
|
320
|
-
${icon ? `<span class="contact-link-icon-wrap">${icon}</span>` : ''}
|
|
321
|
-
${showLabels ? `<span class="contact-link-label">${escapeHtml(item.label)}${item.value ? `: ${escapeHtml(item.value)}` : ''}</span>` : ''}
|
|
322
|
-
</a>
|
|
323
|
-
`;
|
|
324
|
-
}
|
|
325
|
-
return `
|
|
326
|
-
<span class="share-link contact-link contact-link-${item.kind}">
|
|
327
|
-
${icon ? `<span class="contact-link-icon-wrap">${icon}</span>` : ''}
|
|
328
|
-
${showLabels ? `<span class="contact-link-label">${escapeHtml(item.label)}${item.value ? `: ${escapeHtml(item.value)}` : ''}</span>` : `<span class="contact-link-label">${escapeHtml(item.value ?? item.label)}</span>`}
|
|
329
|
-
</span>
|
|
330
|
-
`;
|
|
331
|
-
})
|
|
332
|
-
.join('');
|
|
333
|
-
return `<div class="integration-links contact-links contact-links-${variant}">${content}</div>`;
|
|
334
|
-
}
|
|
335
|
-
function buildMapEmbedUrl(mapUrl, address) {
|
|
336
|
-
const rawMapUrl = mapUrl?.trim() ?? '';
|
|
337
|
-
const rawAddress = address?.trim() ?? '';
|
|
338
|
-
const source = rawMapUrl || rawAddress;
|
|
339
|
-
if (!source) {
|
|
340
|
-
return '';
|
|
341
|
-
}
|
|
342
|
-
if (isExternalUrl(source) && /\/embed\b/i.test(source)) {
|
|
343
|
-
return source;
|
|
344
|
-
}
|
|
345
|
-
if (isExternalUrl(source)) {
|
|
346
|
-
try {
|
|
347
|
-
const parsed = new URL(source);
|
|
348
|
-
const query = parsed.searchParams.get('q') ??
|
|
349
|
-
parsed.searchParams.get('query') ??
|
|
350
|
-
parsed.searchParams.get('address') ??
|
|
351
|
-
rawAddress ??
|
|
352
|
-
parsed.toString();
|
|
353
|
-
return `https://www.google.com/maps?q=${encodeURIComponent(query)}&output=embed`;
|
|
354
|
-
}
|
|
355
|
-
catch {
|
|
356
|
-
return `https://www.google.com/maps?q=${encodeURIComponent(source)}&output=embed`;
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
return `https://www.google.com/maps?q=${encodeURIComponent(source)}&output=embed`;
|
|
360
|
-
}
|
|
361
|
-
function renderContactCardMarkup(params) {
|
|
362
|
-
const { title, description, sectionStyle, currentRoute, className, info } = params;
|
|
363
|
-
const name = typeof info.name === 'string' ? info.name : '';
|
|
364
|
-
const tagline = typeof info.tagline === 'string' ? info.tagline : '';
|
|
365
|
-
const logo = typeof info.logo === 'string' ? info.logo : '';
|
|
366
|
-
const logoUrl = logo ? (isExternalUrl(logo) || logo.startsWith('data:') ? logo : hrefForAsset(currentRoute, logo)) : '';
|
|
367
|
-
const mapUrl = info.mapUrl?.trim() ?? '';
|
|
368
|
-
const address = info.address?.trim() ?? '';
|
|
369
|
-
const hours = info.hours?.trim() ?? '';
|
|
370
|
-
const website = info.website?.trim() ?? '';
|
|
371
|
-
const email = info.email?.trim() ?? '';
|
|
372
|
-
const phone = info.phone?.trim() ?? '';
|
|
373
|
-
const mapHref = mapUrl ? normalizeContactHref(mapUrl, 'website') : '';
|
|
374
|
-
const mapEmbedUrl = buildMapEmbedUrl(mapUrl, address);
|
|
375
|
-
const websiteHref = website ? normalizeContactHref(website, 'website') : '';
|
|
376
|
-
const emailHref = email ? normalizeContactHref(email, 'email') : '';
|
|
377
|
-
const phoneHref = phone ? normalizeContactHref(phone, 'phone') : '';
|
|
378
|
-
const websiteLabel = websiteHref ? contactWebsiteDisplayLabel(websiteHref) : '';
|
|
379
|
-
const mapPane = mapEmbedUrl ? `
|
|
380
|
-
<div class="${className}-map-pane">
|
|
381
|
-
<div class="embed-frame embed-frame--map contact-card-map-frame">
|
|
382
|
-
<iframe src="${escapeHtml(mapEmbedUrl)}" title="${escapeHtml(name || address || 'Map')}" loading="lazy" referrerpolicy="no-referrer-when-downgrade" allowfullscreen></iframe>
|
|
383
|
-
</div>
|
|
384
|
-
</div>
|
|
385
|
-
` : '';
|
|
386
|
-
return `
|
|
387
|
-
<section class="card ${className}"${sectionStyle}>
|
|
388
|
-
${title ? `<p class="eyebrow">${escapeHtml(title)}</p>` : ''}
|
|
389
|
-
<div class="${className}-layout ${mapPane ? `${className}-layout-with-map` : `${className}-layout-no-map`}">
|
|
390
|
-
<div class="${className}-main">
|
|
391
|
-
<div class="${className}-header">
|
|
392
|
-
${logoUrl ? `<img class="${className}-logo" src="${escapeHtml(logoUrl)}" alt="" aria-hidden="true" loading="lazy" decoding="async" />` : ''}
|
|
393
|
-
${name ? (websiteHref
|
|
394
|
-
? `<h2 class="${className}-name"><a class="${className}-name-link" href="${escapeHtml(websiteHref)}" rel="noopener noreferrer" target="${websiteHref.startsWith('http') ? '_blank' : '_self'}">${escapeHtml(name)}</a></h2>`
|
|
395
|
-
: `<h2 class="${className}-name">${escapeHtml(name)}</h2>`) : ''}
|
|
396
|
-
</div>
|
|
397
|
-
${tagline ? `<p class="${className}-tagline">${escapeHtml(tagline)}</p>` : ''}
|
|
398
|
-
${description ? `<p class="section-description">${escapeHtml(description)}</p>` : ''}
|
|
399
|
-
${mapHref ? `
|
|
400
|
-
<div class="${className}-row ${className}-map-row">
|
|
401
|
-
<a class="contact-card-map-link" href="${escapeHtml(mapHref)}" rel="noopener noreferrer" target="${mapHref.startsWith('http') ? '_blank' : '_self'}">
|
|
402
|
-
<span class="contact-card-map-icon" aria-hidden="true">${contactLinkIconMarkup('map')}</span>
|
|
403
|
-
<span class="contact-card-map-text">${escapeHtml(address || mapUrl)}</span>
|
|
404
|
-
</a>
|
|
405
|
-
</div>
|
|
406
|
-
` : ''}
|
|
407
|
-
${!mapHref && address ? `<div class="${className}-row ${className}-address-row"><span class="contact-card-text">${escapeHtml(address)}</span></div>` : ''}
|
|
408
|
-
${hours ? `<div class="${className}-row ${className}-hours-row"><span class="contact-card-text">${escapeHtml(hours)}</span></div>` : ''}
|
|
409
|
-
${websiteHref ? `<div class="${className}-row ${className}-website-row"><a class="contact-card-link" href="${escapeHtml(websiteHref)}" rel="noopener noreferrer" target="${websiteHref.startsWith('http') ? '_blank' : '_self'}">${escapeHtml(websiteLabel)}</a></div>` : ''}
|
|
410
|
-
${(emailHref || phoneHref) ? `
|
|
411
|
-
<div class="${className}-row ${className}-contact-row">
|
|
412
|
-
${emailHref ? `<a class="contact-card-link" href="${escapeHtml(emailHref)}">${escapeHtml(email)}</a>` : ''}
|
|
413
|
-
${emailHref && phoneHref ? `<span class="contact-card-separator" aria-hidden="true">|</span>` : ''}
|
|
414
|
-
${phoneHref ? `<a class="contact-card-link" href="${escapeHtml(phoneHref)}">${escapeHtml(phone)}</a>` : ''}
|
|
415
|
-
</div>
|
|
416
|
-
` : ''}
|
|
417
|
-
</div>
|
|
418
|
-
${mapPane}
|
|
419
|
-
</div>
|
|
420
|
-
</section>
|
|
421
|
-
`;
|
|
422
|
-
}
|
|
423
|
-
function renderContactCardShortcode(config, currentRoute) {
|
|
424
|
-
const title = typeof config.title === 'string' ? config.title : '';
|
|
425
|
-
const description = typeof config.description === 'string' ? config.description : '';
|
|
426
|
-
return renderContactCardMarkup({
|
|
427
|
-
title,
|
|
428
|
-
description,
|
|
429
|
-
sectionStyle: sectionStyleFromConfig(config, currentRoute),
|
|
430
|
-
currentRoute,
|
|
431
|
-
className: 'contact-card',
|
|
432
|
-
info: {
|
|
433
|
-
name: typeof config.name === 'string' ? config.name : '',
|
|
434
|
-
tagline: typeof config.tagline === 'string' ? config.tagline : '',
|
|
435
|
-
logo: typeof config.logo === 'string' ? config.logo : '',
|
|
436
|
-
address: typeof config.address === 'string' ? config.address : '',
|
|
437
|
-
mapUrl: typeof config.mapUrl === 'string' ? config.mapUrl : '',
|
|
438
|
-
phone: typeof config.phone === 'string' ? config.phone : '',
|
|
439
|
-
email: typeof config.email === 'string' ? config.email : '',
|
|
440
|
-
hours: typeof config.hours === 'string' ? config.hours : '',
|
|
441
|
-
website: typeof config.website === 'string' ? config.website : ''
|
|
442
|
-
}
|
|
443
|
-
});
|
|
444
|
-
}
|
|
445
|
-
function renderContactLinksBlock(config, currentRoute, contactLinks) {
|
|
446
|
-
const title = typeof config.title === 'string' ? config.title : '';
|
|
447
|
-
const description = typeof config.description === 'string' ? config.description : '';
|
|
448
|
-
const variant = typeof config.variant === 'string' ? config.variant : 'inline';
|
|
449
|
-
const showIcons = typeof config.showIcons === 'boolean' ? config.showIcons : contactLinks.showIcons;
|
|
450
|
-
const showLabels = typeof config.showLabels === 'boolean' ? config.showLabels : contactLinks.showLabels;
|
|
451
|
-
const items = buildContactLinkItems(contactLinks);
|
|
452
|
-
return `
|
|
453
|
-
<section class="card integration integration-contact-links"${sectionStyleFromConfig(config, currentRoute)}>
|
|
454
|
-
${title ? `<h2>${escapeHtml(title)}</h2>` : ''}
|
|
455
|
-
${description ? `<p>${escapeHtml(description)}</p>` : ''}
|
|
456
|
-
${items.length ? renderContactLinksMarkup(items, { variant, showIcons, showLabels }) : '<p class="empty-state">No contact links configured.</p>'}
|
|
457
|
-
</section>
|
|
458
|
-
`;
|
|
459
|
-
}
|
|
460
|
-
function renderCompanyInfoBlock(config, currentRoute, companyInfo) {
|
|
461
|
-
const title = typeof config.title === 'string' ? config.title : '';
|
|
462
|
-
const description = typeof config.description === 'string' ? config.description : '';
|
|
463
|
-
return renderContactCardMarkup({
|
|
464
|
-
title,
|
|
465
|
-
description,
|
|
466
|
-
sectionStyle: sectionStyleFromConfig(config, currentRoute),
|
|
467
|
-
currentRoute,
|
|
468
|
-
className: 'company-info',
|
|
469
|
-
info: {
|
|
470
|
-
name: companyInfo.name ?? '',
|
|
471
|
-
tagline: companyInfo.tagline ?? '',
|
|
472
|
-
logo: companyInfo.logo ?? '',
|
|
473
|
-
address: companyInfo.address ?? '',
|
|
474
|
-
mapUrl: companyInfo.mapUrl ?? '',
|
|
475
|
-
phone: companyInfo.phone ?? '',
|
|
476
|
-
email: companyInfo.email ?? '',
|
|
477
|
-
hours: companyInfo.hours ?? '',
|
|
478
|
-
website: companyInfo.website ?? ''
|
|
479
|
-
}
|
|
480
|
-
});
|
|
481
|
-
}
|
|
482
|
-
function renderSingleLinkShortcode(params) {
|
|
483
|
-
const { title, description, label, href, sectionStyle, className, iconLabel, showIcon = true } = params;
|
|
484
|
-
const icon = showIcon && iconLabel ? contactLinkIconMarkup(iconLabel) : '';
|
|
485
|
-
return `
|
|
486
|
-
<section class="card ${className}"${sectionStyle}>
|
|
487
|
-
${title ? `<h2>${escapeHtml(title)}</h2>` : ''}
|
|
488
|
-
${description ? `<p>${escapeHtml(description)}</p>` : ''}
|
|
489
|
-
<a class="share-link ${className}-link" href="${escapeHtml(href)}" rel="noopener noreferrer"${href.startsWith('http') ? ' target="_blank"' : ''}>
|
|
490
|
-
${icon ? `<span class="contact-link-icon-wrap">${icon}</span>` : ''}
|
|
491
|
-
<span class="${className}-label">${escapeHtml(label)}</span>
|
|
492
|
-
</a>
|
|
493
|
-
</section>
|
|
494
|
-
`;
|
|
495
|
-
}
|
|
496
|
-
function buildVideoEmbedUrl(provider, value) {
|
|
497
|
-
const raw = value?.trim() ?? '';
|
|
498
|
-
if (!raw) {
|
|
499
|
-
return '';
|
|
500
|
-
}
|
|
501
|
-
if (isExternalUrl(raw) && /\/embed\//i.test(raw)) {
|
|
502
|
-
return raw;
|
|
503
|
-
}
|
|
504
|
-
const normalizedProvider = (provider ?? 'youtube').trim().toLowerCase();
|
|
505
|
-
const trimmed = raw.replace(/^https?:\/\//i, '');
|
|
506
|
-
if (isExternalUrl(raw)) {
|
|
507
|
-
if (/\/embed\//i.test(raw)) {
|
|
508
|
-
return raw;
|
|
509
|
-
}
|
|
510
|
-
const parsed = new URL(raw);
|
|
511
|
-
const pathParts = parsed.pathname.split('/').filter(Boolean);
|
|
512
|
-
const lastPart = pathParts[pathParts.length - 1] ?? '';
|
|
513
|
-
if (normalizedProvider === 'youtube' || normalizedProvider === 'x') {
|
|
514
|
-
const videoId = parsed.searchParams.get('v') ??
|
|
515
|
-
parsed.searchParams.get('video_id') ??
|
|
516
|
-
lastPart.replace(/^@/, '');
|
|
517
|
-
return videoId ? `https://www.youtube.com/embed/${videoId}` : raw;
|
|
518
|
-
}
|
|
519
|
-
if (normalizedProvider === 'vimeo') {
|
|
520
|
-
const id = pathParts.find((part) => /^\d+$/.test(part)) ?? lastPart;
|
|
521
|
-
return id ? `https://player.vimeo.com/video/${id}` : raw;
|
|
522
|
-
}
|
|
523
|
-
if (normalizedProvider === 'loom') {
|
|
524
|
-
const slug = lastPart || parsed.pathname.replace(/^\//, '');
|
|
525
|
-
return slug ? `https://www.loom.com/embed/${slug}` : raw;
|
|
526
|
-
}
|
|
527
|
-
return raw;
|
|
528
|
-
}
|
|
529
|
-
const clean = trimmed.replace(/^@+/, '');
|
|
530
|
-
switch (normalizedProvider) {
|
|
531
|
-
case 'youtube':
|
|
532
|
-
return `https://www.youtube.com/embed/${clean}`;
|
|
533
|
-
case 'vimeo':
|
|
534
|
-
return `https://player.vimeo.com/video/${clean}`;
|
|
535
|
-
case 'loom':
|
|
536
|
-
return `https://www.loom.com/embed/${clean}`;
|
|
537
|
-
default:
|
|
538
|
-
return clean;
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
function renderNavList(items) {
|
|
542
|
-
if (!items.length) {
|
|
543
|
-
return '';
|
|
544
|
-
}
|
|
545
|
-
return items.map((item) => `<a class="nav-link" href="${escapeHtml(item.url)}">${escapeHtml(item.title)}</a>`).join('');
|
|
546
|
-
}
|
|
547
|
-
function isExternalUrl(url) {
|
|
548
|
-
return /^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(url) || url.startsWith('//');
|
|
549
|
-
}
|
|
550
|
-
function normalizeRoutePath(routePath) {
|
|
551
|
-
if (routePath === '/') {
|
|
552
|
-
return '/';
|
|
553
|
-
}
|
|
554
|
-
return routePath.endsWith('/') ? routePath : `${routePath}/`;
|
|
555
|
-
}
|
|
556
|
-
function hrefForRoute(currentRoute, targetRoute) {
|
|
557
|
-
if (isExternalUrl(targetRoute)) {
|
|
558
|
-
return targetRoute;
|
|
559
|
-
}
|
|
560
|
-
const current = normalizeRoutePath(currentRoute);
|
|
561
|
-
const target = normalizeRoutePath(targetRoute);
|
|
562
|
-
const fromDir = current === '/' ? '' : current.replace(/^\//, '');
|
|
563
|
-
const toDir = target === '/' ? '' : target.replace(/^\//, '');
|
|
564
|
-
const relative = path.posix.relative(fromDir, toDir);
|
|
565
|
-
if (!relative) {
|
|
566
|
-
return './';
|
|
567
|
-
}
|
|
568
|
-
return `${relative}/`;
|
|
569
|
-
}
|
|
570
|
-
function hrefForAsset(currentRoute, assetPath) {
|
|
571
|
-
if (isExternalUrl(assetPath)) {
|
|
572
|
-
return assetPath;
|
|
573
|
-
}
|
|
574
|
-
const current = normalizeRoutePath(currentRoute);
|
|
575
|
-
const fromDir = current === '/' ? '' : current.replace(/^\//, '');
|
|
576
|
-
const toPath = assetPath.startsWith('/') ? assetPath.replace(/^\//, '') : assetPath;
|
|
577
|
-
const relative = path.posix.relative(fromDir, toPath);
|
|
578
|
-
return relative || path.posix.basename(toPath);
|
|
579
|
-
}
|
|
580
|
-
function hrefForSourceMirror(currentRoute, targetRoute, fileName = 'source.md') {
|
|
581
|
-
const current = normalizeRoutePath(currentRoute);
|
|
582
|
-
const target = normalizeRoutePath(targetRoute);
|
|
583
|
-
const fromDir = current === '/' ? '' : current.replace(/^\//, '');
|
|
584
|
-
const toDir = target === '/' ? '' : target.replace(/^\//, '');
|
|
585
|
-
const relative = path.posix.relative(fromDir, path.posix.join(toDir, fileName));
|
|
586
|
-
return relative || fileName;
|
|
587
|
-
}
|
|
588
|
-
function serializeJsonLd(value) {
|
|
589
|
-
return JSON.stringify(value).replace(/</g, '\\u003c');
|
|
590
|
-
}
|
|
591
|
-
function normalizeDateValue(value) {
|
|
592
|
-
if (value instanceof Date) {
|
|
593
|
-
return value.toISOString();
|
|
594
|
-
}
|
|
595
|
-
if (typeof value === 'string' && value.trim()) {
|
|
596
|
-
return value;
|
|
597
|
-
}
|
|
598
|
-
return undefined;
|
|
599
|
-
}
|
|
600
|
-
function stripHtml(input) {
|
|
601
|
-
return input
|
|
602
|
-
.replace(/<script[\s\S]*?<\/script>/gi, ' ')
|
|
603
|
-
.replace(/<style[\s\S]*?<\/style>/gi, ' ')
|
|
604
|
-
.replace(/<[^>]+>/g, ' ')
|
|
605
|
-
.replace(/\s+/g, ' ')
|
|
606
|
-
.trim();
|
|
607
|
-
}
|
|
608
|
-
function extractHeadings(html) {
|
|
609
|
-
const headings = [];
|
|
610
|
-
const pattern = /<h([1-6])[^>]*>([\s\S]*?)<\/h\1>/gi;
|
|
611
|
-
let match;
|
|
612
|
-
while ((match = pattern.exec(html))) {
|
|
613
|
-
headings.push(stripHtml(match[2]));
|
|
614
|
-
}
|
|
615
|
-
return headings;
|
|
616
|
-
}
|
|
617
|
-
function getSiteBaseUrl(domain) {
|
|
618
|
-
if (/^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(domain)) {
|
|
619
|
-
return domain.endsWith('/') ? domain : `${domain}/`;
|
|
620
|
-
}
|
|
621
|
-
return `https://${domain.replace(/\/+$/, '')}/`;
|
|
622
|
-
}
|
|
623
|
-
function canonicalUrlForRoute(domain, routePath) {
|
|
624
|
-
const baseUrl = getSiteBaseUrl(domain);
|
|
625
|
-
return new URL(routePath === '/' ? '/' : routePath, baseUrl).toString();
|
|
626
|
-
}
|
|
627
|
-
function extractExcerpt(text, maxLength = 180) {
|
|
628
|
-
const stripped = stripHtml(text);
|
|
629
|
-
if (stripped.length <= maxLength) {
|
|
630
|
-
return stripped;
|
|
631
|
-
}
|
|
632
|
-
return `${stripped.slice(0, maxLength).trimEnd()}…`;
|
|
633
|
-
}
|
|
634
|
-
function wordCount(text) {
|
|
635
|
-
const stripped = stripHtml(text);
|
|
636
|
-
if (!stripped) {
|
|
637
|
-
return 0;
|
|
638
|
-
}
|
|
639
|
-
return stripped.split(/\s+/).filter(Boolean).length;
|
|
640
|
-
}
|
|
641
|
-
function pageClass() {
|
|
642
|
-
return 'page page-default';
|
|
643
|
-
}
|
|
644
|
-
function normalizeShortcodeText(value) {
|
|
645
|
-
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
646
|
-
}
|
|
647
|
-
function analyticsSnippet(analyticsId) {
|
|
648
|
-
const escapedId = escapeHtml(analyticsId);
|
|
649
|
-
return `
|
|
650
|
-
<script async src="https://www.googletagmanager.com/gtag/js?id=${escapedId}"></script>
|
|
651
|
-
<script>
|
|
652
|
-
window.dataLayer = window.dataLayer || [];
|
|
653
|
-
function gtag(){dataLayer.push(arguments);}
|
|
654
|
-
gtag('js', new Date());
|
|
655
|
-
gtag('config', '${escapedId}');
|
|
656
|
-
</script>
|
|
657
|
-
`;
|
|
658
|
-
}
|
|
659
|
-
function parseShortcodeConfig(body) {
|
|
660
|
-
const trimmed = body.trim();
|
|
661
|
-
if (!trimmed) {
|
|
662
|
-
return {};
|
|
663
|
-
}
|
|
664
|
-
try {
|
|
665
|
-
const parsed = YAML.parse(trimmed);
|
|
666
|
-
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
667
|
-
return parsed;
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
catch {
|
|
671
|
-
// Fall through to an empty config.
|
|
672
|
-
}
|
|
673
|
-
return {};
|
|
674
|
-
}
|
|
675
|
-
function sectionStyleFromConfig(config, currentRoute) {
|
|
676
|
-
const parts = [];
|
|
677
|
-
const backgroundImage = normalizeShortcodeText(config.backgroundImage);
|
|
678
|
-
const backgroundPosition = normalizeShortcodeText(config.backgroundPosition);
|
|
679
|
-
const backgroundSize = normalizeShortcodeText(config.backgroundSize);
|
|
680
|
-
const backgroundRepeat = normalizeShortcodeText(config.backgroundRepeat);
|
|
681
|
-
const backgroundColor = normalizeShortcodeText(config.backgroundColor);
|
|
682
|
-
const color = normalizeShortcodeText(config.color);
|
|
683
|
-
const minHeight = normalizeShortcodeText(config.minHeight);
|
|
684
|
-
if (backgroundImage) {
|
|
685
|
-
const resolved = isExternalUrl(backgroundImage) || backgroundImage.startsWith('data:')
|
|
686
|
-
? backgroundImage
|
|
687
|
-
: hrefForAsset(currentRoute, backgroundImage);
|
|
688
|
-
parts.push(`background-image:url("${escapeHtml(resolved)}")`);
|
|
689
|
-
parts.push('background-position:center');
|
|
690
|
-
parts.push('background-size:cover');
|
|
691
|
-
parts.push('background-repeat:no-repeat');
|
|
692
|
-
}
|
|
693
|
-
if (backgroundPosition) {
|
|
694
|
-
parts.push(`background-position:${escapeHtml(backgroundPosition)}`);
|
|
695
|
-
}
|
|
696
|
-
if (backgroundSize) {
|
|
697
|
-
parts.push(`background-size:${escapeHtml(backgroundSize)}`);
|
|
698
|
-
}
|
|
699
|
-
if (backgroundRepeat) {
|
|
700
|
-
parts.push(`background-repeat:${escapeHtml(backgroundRepeat)}`);
|
|
701
|
-
}
|
|
702
|
-
if (backgroundColor) {
|
|
703
|
-
parts.push(`background-color:${escapeHtml(backgroundColor)}`);
|
|
704
|
-
}
|
|
705
|
-
if (color) {
|
|
706
|
-
parts.push(`color:${escapeHtml(color)}`);
|
|
707
|
-
}
|
|
708
|
-
if (minHeight) {
|
|
709
|
-
parts.push(`min-height:${escapeHtml(minHeight)}`);
|
|
710
|
-
}
|
|
711
|
-
return parts.length ? ` style="${parts.join(';')}"` : '';
|
|
712
|
-
}
|
|
713
|
-
function normalizeRoutePrefix(folder) {
|
|
714
|
-
if (!folder) {
|
|
715
|
-
return null;
|
|
716
|
-
}
|
|
717
|
-
const cleaned = folder.trim().replace(/^\/+|\/+$/g, '');
|
|
718
|
-
if (!cleaned) {
|
|
719
|
-
return null;
|
|
720
|
-
}
|
|
721
|
-
return `/${cleaned}/`;
|
|
722
|
-
}
|
|
723
|
-
function normalizeDateValueFromSource(source) {
|
|
724
|
-
const value = source.frontmatter.date ?? source.frontmatter.updated ?? '';
|
|
725
|
-
if (value instanceof Date) {
|
|
726
|
-
return value.toISOString();
|
|
727
|
-
}
|
|
728
|
-
if (typeof value === 'string') {
|
|
729
|
-
return value;
|
|
730
|
-
}
|
|
731
|
-
return '';
|
|
732
|
-
}
|
|
733
|
-
function sortSourcesByDate(sources, order) {
|
|
734
|
-
const sorted = [...sources].sort((left, right) => {
|
|
735
|
-
const leftValue = normalizeDateValueFromSource(left);
|
|
736
|
-
const rightValue = normalizeDateValueFromSource(right);
|
|
737
|
-
return leftValue.localeCompare(rightValue);
|
|
738
|
-
});
|
|
739
|
-
return order === 'desc' ? sorted.reverse() : sorted;
|
|
740
|
-
}
|
|
741
|
-
function renderCardRowShortcode(config, currentRoute) {
|
|
742
|
-
const rawCards = Array.isArray(config.cards) && config.cards.length ? config.cards : ['pages', 'integrations', 'services'];
|
|
743
|
-
const cards = rawCards.map((entry) => {
|
|
744
|
-
if (typeof entry === 'string') {
|
|
745
|
-
return CARD_PRESETS[entry] ?? {
|
|
746
|
-
title: entry,
|
|
747
|
-
href: '#',
|
|
748
|
-
body: ''
|
|
749
|
-
};
|
|
750
|
-
}
|
|
751
|
-
if (entry && typeof entry === 'object') {
|
|
752
|
-
const card = entry;
|
|
753
|
-
const presetName = typeof card.preset === 'string' ? card.preset : undefined;
|
|
754
|
-
const preset = presetName ? CARD_PRESETS[presetName] : undefined;
|
|
755
|
-
return {
|
|
756
|
-
title: typeof card.title === 'string' ? card.title : preset?.title ?? 'Card',
|
|
757
|
-
href: typeof card.href === 'string' ? card.href : preset?.href ?? '#',
|
|
758
|
-
body: typeof card.body === 'string' ? card.body : preset?.body ?? ''
|
|
759
|
-
};
|
|
760
|
-
}
|
|
761
|
-
return {
|
|
762
|
-
title: 'Card',
|
|
763
|
-
href: '#',
|
|
764
|
-
body: ''
|
|
765
|
-
};
|
|
766
|
-
});
|
|
767
|
-
const title = typeof config.title === 'string' ? config.title : '';
|
|
768
|
-
const description = typeof config.description === 'string' ? config.description : '';
|
|
769
|
-
const sectionStyle = sectionStyleFromConfig(config, currentRoute);
|
|
770
|
-
const currentRoutePath = normalizeRoutePath(currentRoute);
|
|
771
|
-
return `
|
|
772
|
-
<section class="cardrow-section"${sectionStyle}>
|
|
773
|
-
${title ? `<h2 class="section-title">${escapeHtml(title)}</h2>` : ''}
|
|
774
|
-
${description ? `<p class="section-description">${escapeHtml(description)}</p>` : ''}
|
|
775
|
-
<div class="grid cardrow">
|
|
776
|
-
${cards
|
|
777
|
-
.map((card) => {
|
|
778
|
-
const isExplicitSourceLink = card.href.endsWith('/source.md') || card.href.endsWith('/source.html');
|
|
779
|
-
const targetRoute = card.href.startsWith('http') || card.href.startsWith('#')
|
|
780
|
-
? card.href
|
|
781
|
-
: isExplicitSourceLink
|
|
782
|
-
? path.posix.join(currentRoutePath, card.href).replace(/\/source\.(md|html)$/i, '')
|
|
783
|
-
: path.posix.join(currentRoutePath, card.href);
|
|
784
|
-
const resolvedHref = card.href.startsWith('http') || card.href.startsWith('#')
|
|
785
|
-
? targetRoute
|
|
786
|
-
: isExplicitSourceLink
|
|
787
|
-
? hrefForSourceMirror(currentRoute, targetRoute)
|
|
788
|
-
: hrefForRoute(currentRoute, targetRoute);
|
|
789
|
-
return `
|
|
790
|
-
<article class="card">
|
|
791
|
-
<h3><a href="${escapeHtml(resolvedHref)}">${escapeHtml(card.title)}</a></h3>
|
|
792
|
-
${card.body ? `<p>${escapeHtml(card.body)}</p>` : ''}
|
|
793
|
-
</article>
|
|
794
|
-
`;
|
|
795
|
-
})
|
|
796
|
-
.join('')}
|
|
797
|
-
</div>
|
|
798
|
-
</section>
|
|
799
|
-
`;
|
|
800
|
-
}
|
|
801
|
-
function renderPageListShortcode(config, source, allSources) {
|
|
802
|
-
const folderPrefix = normalizeRoutePrefix(typeof config.folder === 'string' ? config.folder : 'blog');
|
|
803
|
-
const count = typeof config.count === 'number' && Number.isFinite(config.count) ? Math.max(0, Math.trunc(config.count)) : 10;
|
|
804
|
-
const order = config.order === 'asc' ? 'asc' : 'desc';
|
|
805
|
-
const title = typeof config.title === 'string' ? config.title : '';
|
|
806
|
-
const description = typeof config.description === 'string' ? config.description : '';
|
|
807
|
-
let pages = allSources.filter((item) => item.kind === 'markdown');
|
|
808
|
-
if (folderPrefix) {
|
|
809
|
-
pages = pages.filter((item) => item.routePath.startsWith(folderPrefix) && item.routePath !== folderPrefix);
|
|
810
|
-
}
|
|
811
|
-
pages = sortSourcesByDate(pages, order);
|
|
812
|
-
if (count > 0) {
|
|
813
|
-
pages = pages.slice(0, count);
|
|
814
|
-
}
|
|
815
|
-
if (!pages.length) {
|
|
816
|
-
return `
|
|
817
|
-
<section class="page-list-section">
|
|
818
|
-
${title ? `<h2 class="section-title">${escapeHtml(title)}</h2>` : ''}
|
|
819
|
-
${description ? `<p class="section-description">${escapeHtml(description)}</p>` : ''}
|
|
820
|
-
<p class="empty-state">No matching pages found.</p>
|
|
821
|
-
</section>
|
|
822
|
-
`;
|
|
823
|
-
}
|
|
824
|
-
return `
|
|
825
|
-
<section class="page-list-section">
|
|
826
|
-
${title ? `<h2 class="section-title">${escapeHtml(title)}</h2>` : ''}
|
|
827
|
-
${description ? `<p class="section-description">${escapeHtml(description)}</p>` : ''}
|
|
828
|
-
<div class="post-list">
|
|
829
|
-
${pages
|
|
830
|
-
.map((page) => {
|
|
831
|
-
const pageTitle = buildPageTitle(page, '');
|
|
832
|
-
const pageDescription = page.frontmatter.description ?? '';
|
|
833
|
-
const dateValue = normalizeDateValueFromSource(page) || 'Page';
|
|
834
|
-
return `
|
|
835
|
-
<article class="card post-card">
|
|
836
|
-
<p class="eyebrow">${escapeHtml(dateValue)}</p>
|
|
837
|
-
<h3><a href="${escapeHtml(hrefForRoute(source.routePath, page.routePath))}">${escapeHtml(pageTitle)}</a></h3>
|
|
838
|
-
${pageDescription ? `<p>${escapeHtml(pageDescription)}</p>` : ''}
|
|
839
|
-
</article>
|
|
840
|
-
`;
|
|
841
|
-
})
|
|
842
|
-
.join('')}
|
|
843
|
-
</div>
|
|
844
|
-
</section>
|
|
845
|
-
`;
|
|
846
|
-
}
|
|
847
|
-
function extractShortcodeText(body) {
|
|
848
|
-
const lines = body
|
|
849
|
-
.split('\n')
|
|
850
|
-
.map((line) => line.trim())
|
|
851
|
-
.filter((line) => line.length > 0);
|
|
852
|
-
if (!lines.length) {
|
|
853
|
-
return { title: '', description: '' };
|
|
854
|
-
}
|
|
855
|
-
const [title, ...rest] = lines;
|
|
856
|
-
return {
|
|
857
|
-
title,
|
|
858
|
-
description: rest.join(' ')
|
|
859
|
-
};
|
|
860
|
-
}
|
|
861
|
-
async function renderShortcodeBlock(params) {
|
|
862
|
-
const { blockName, body, site, title, canonicalUrl, source, allSources } = params;
|
|
863
|
-
const shortcodeConfig = parseShortcodeConfig(body);
|
|
864
|
-
const textParts = extractShortcodeText(body);
|
|
865
|
-
const shortcodeTitle = typeof shortcodeConfig.title === 'string' ? shortcodeConfig.title : textParts.title;
|
|
866
|
-
const shortcodeDescription = typeof shortcodeConfig.description === 'string' ? shortcodeConfig.description : textParts.description;
|
|
867
|
-
const sectionStyle = sectionStyleFromConfig(shortcodeConfig, source.routePath);
|
|
868
|
-
switch (blockName) {
|
|
869
|
-
case 'cardrow':
|
|
870
|
-
return renderCardRowShortcode(shortcodeConfig, source.routePath);
|
|
871
|
-
case 'pagelist':
|
|
872
|
-
return renderPageListShortcode(shortcodeConfig, source, allSources);
|
|
873
|
-
case 'contact-card':
|
|
874
|
-
return renderContactCardShortcode(shortcodeConfig, source.routePath);
|
|
875
|
-
case 'contact-links':
|
|
876
|
-
return renderContactLinksBlock(shortcodeConfig, source.routePath, site.integrations.contactLinks);
|
|
877
|
-
case 'company-info':
|
|
878
|
-
return renderCompanyInfoBlock(shortcodeConfig, source.routePath, site.integrations.companyInfo);
|
|
879
|
-
case 'mailto': {
|
|
880
|
-
const email = typeof shortcodeConfig.email === 'string' ? shortcodeConfig.email : '';
|
|
881
|
-
if (!email) {
|
|
882
|
-
throw new Error('mailto block requires an email parameter in the shortcode body');
|
|
883
|
-
}
|
|
884
|
-
const subject = typeof shortcodeConfig.subject === 'string' ? shortcodeConfig.subject : '';
|
|
885
|
-
const bodyText = typeof shortcodeConfig.body === 'string' ? shortcodeConfig.body : '';
|
|
886
|
-
const label = typeof shortcodeConfig.label === 'string' ? shortcodeConfig.label : email;
|
|
887
|
-
const query = new URLSearchParams();
|
|
888
|
-
if (subject) {
|
|
889
|
-
query.set('subject', subject);
|
|
890
|
-
}
|
|
891
|
-
if (bodyText) {
|
|
892
|
-
query.set('body', bodyText);
|
|
893
|
-
}
|
|
894
|
-
const href = `mailto:${email}${query.toString() ? `?${query.toString()}` : ''}`;
|
|
895
|
-
return renderSingleLinkShortcode({
|
|
896
|
-
title: typeof shortcodeConfig.title === 'string' ? shortcodeConfig.title : '',
|
|
897
|
-
description: typeof shortcodeConfig.description === 'string' ? shortcodeConfig.description : '',
|
|
898
|
-
label,
|
|
899
|
-
href,
|
|
900
|
-
sectionStyle: sectionStyleFromConfig(shortcodeConfig, source.routePath),
|
|
901
|
-
className: 'shortcode-mailto',
|
|
902
|
-
iconLabel: 'email',
|
|
903
|
-
showIcon: typeof shortcodeConfig.showIcon === 'boolean' ? shortcodeConfig.showIcon : true
|
|
904
|
-
});
|
|
905
|
-
}
|
|
906
|
-
case 'tel': {
|
|
907
|
-
const phone = typeof shortcodeConfig.phone === 'string' ? shortcodeConfig.phone : '';
|
|
908
|
-
if (!phone) {
|
|
909
|
-
throw new Error('tel block requires a phone parameter in the shortcode body');
|
|
910
|
-
}
|
|
911
|
-
const label = typeof shortcodeConfig.label === 'string' ? shortcodeConfig.label : phone;
|
|
912
|
-
const href = normalizeContactHref(phone, 'phone');
|
|
913
|
-
return renderSingleLinkShortcode({
|
|
914
|
-
title: typeof shortcodeConfig.title === 'string' ? shortcodeConfig.title : '',
|
|
915
|
-
description: typeof shortcodeConfig.description === 'string' ? shortcodeConfig.description : '',
|
|
916
|
-
label,
|
|
917
|
-
href,
|
|
918
|
-
sectionStyle: sectionStyleFromConfig(shortcodeConfig, source.routePath),
|
|
919
|
-
className: 'shortcode-tel',
|
|
920
|
-
iconLabel: 'phone',
|
|
921
|
-
showIcon: typeof shortcodeConfig.showIcon === 'boolean' ? shortcodeConfig.showIcon : true
|
|
922
|
-
});
|
|
923
|
-
}
|
|
924
|
-
case 'contact-form': {
|
|
925
|
-
const integrationConfig = site.integrations.contactForm;
|
|
926
|
-
if (!integrationConfig.action) {
|
|
927
|
-
throw new Error('contact-form block requires integrations.contactForm.action to be set in site.config.yaml');
|
|
928
|
-
}
|
|
929
|
-
return `
|
|
930
|
-
<section class="card integration integration-contact-form"${sectionStyle}>
|
|
931
|
-
${shortcodeTitle ? `<h2>${escapeHtml(shortcodeTitle)}</h2>` : ''}
|
|
932
|
-
${shortcodeDescription ? `<p>${escapeHtml(shortcodeDescription)}</p>` : ''}
|
|
933
|
-
<form class="integration-form" action="${escapeHtml(integrationConfig.action)}" method="post">
|
|
934
|
-
<label>
|
|
935
|
-
<span>Name</span>
|
|
936
|
-
<input name="name" type="text" autocomplete="name" required />
|
|
937
|
-
</label>
|
|
938
|
-
<label>
|
|
939
|
-
<span>Email</span>
|
|
940
|
-
<input name="email" type="email" autocomplete="email" required />
|
|
941
|
-
</label>
|
|
942
|
-
<label>
|
|
943
|
-
<span>Message</span>
|
|
944
|
-
<textarea name="message" rows="5" required></textarea>
|
|
945
|
-
</label>
|
|
946
|
-
<button type="submit">${escapeHtml(integrationConfig.buttonLabel)}</button>
|
|
947
|
-
</form>
|
|
948
|
-
</section>
|
|
949
|
-
`;
|
|
950
|
-
}
|
|
951
|
-
case 'shareable-links': {
|
|
952
|
-
if (!site.integrations.shareableLinks.enabled) {
|
|
953
|
-
return '';
|
|
954
|
-
}
|
|
955
|
-
const encodedTitle = encodeURIComponent(title);
|
|
956
|
-
const encodedUrl = encodeURIComponent(canonicalUrl);
|
|
957
|
-
const requestedTargets = Array.isArray(shortcodeConfig.targets) && shortcodeConfig.targets.length
|
|
958
|
-
? shortcodeConfig.targets
|
|
959
|
-
: ['copy', 'x', 'linkedin', 'facebook', 'email'];
|
|
960
|
-
const targets = requestedTargets
|
|
961
|
-
.filter((target) => typeof target === 'string')
|
|
962
|
-
.map((target) => {
|
|
963
|
-
switch (target) {
|
|
964
|
-
case 'copy':
|
|
965
|
-
return { label: 'Copy link', href: canonicalUrl };
|
|
966
|
-
case 'x':
|
|
967
|
-
return { label: 'Share on X', href: `https://twitter.com/intent/tweet?text=${encodedTitle}&url=${encodedUrl}` };
|
|
968
|
-
case 'linkedin':
|
|
969
|
-
return { label: 'Share on LinkedIn', href: `https://www.linkedin.com/sharing/share-offsite/?url=${encodedUrl}` };
|
|
970
|
-
case 'facebook':
|
|
971
|
-
return { label: 'Share on Facebook', href: `https://www.facebook.com/sharer/sharer.php?u=${encodedUrl}` };
|
|
972
|
-
case 'email':
|
|
973
|
-
return { label: 'Email', href: `mailto:?subject=${encodedTitle}&body=${encodedUrl}` };
|
|
974
|
-
default:
|
|
975
|
-
return null;
|
|
976
|
-
}
|
|
977
|
-
})
|
|
978
|
-
.filter((target) => Boolean(target));
|
|
979
|
-
const fallbackTargets = [
|
|
980
|
-
{ label: 'Copy link', href: canonicalUrl },
|
|
981
|
-
{ label: 'Share on X', href: `https://twitter.com/intent/tweet?text=${encodedTitle}&url=${encodedUrl}` },
|
|
982
|
-
{ label: 'Share on LinkedIn', href: `https://www.linkedin.com/sharing/share-offsite/?url=${encodedUrl}` },
|
|
983
|
-
{ label: 'Share on Facebook', href: `https://www.facebook.com/sharer/sharer.php?u=${encodedUrl}` },
|
|
984
|
-
{ label: 'Email', href: `mailto:?subject=${encodedTitle}&body=${encodedUrl}` }
|
|
985
|
-
];
|
|
986
|
-
const finalTargets = targets.length ? targets : fallbackTargets;
|
|
987
|
-
return `
|
|
988
|
-
<section class="card integration integration-shareable-links"${sectionStyle}>
|
|
989
|
-
${shortcodeTitle ? `<h2>${escapeHtml(shortcodeTitle)}</h2>` : ''}
|
|
990
|
-
${shortcodeDescription ? `<p>${escapeHtml(shortcodeDescription)}</p>` : ''}
|
|
991
|
-
<div class="integration-links">
|
|
992
|
-
${finalTargets
|
|
993
|
-
.map((target) => `
|
|
994
|
-
<a class="share-link" href="${escapeHtml(target.href)}" rel="noopener noreferrer" target="${target.href.startsWith('http') ? '_blank' : '_self'}">
|
|
995
|
-
${escapeHtml(target.label)}
|
|
996
|
-
</a>
|
|
997
|
-
`)
|
|
998
|
-
.join('')}
|
|
999
|
-
</div>
|
|
1000
|
-
</section>
|
|
1001
|
-
`;
|
|
1002
|
-
}
|
|
1003
|
-
case 'socials-links': {
|
|
1004
|
-
const providers = Array.isArray(shortcodeConfig.providers)
|
|
1005
|
-
? shortcodeConfig.providers.filter((provider) => typeof provider === 'string')
|
|
1006
|
-
: undefined;
|
|
1007
|
-
const links = socialEntriesFromConfig(site.socials, providers);
|
|
1008
|
-
const showLabels = typeof shortcodeConfig.showLabels === 'boolean' ? shortcodeConfig.showLabels : true;
|
|
1009
|
-
return `
|
|
1010
|
-
<section class="card integration integration-socials-links"${sectionStyle}>
|
|
1011
|
-
${shortcodeTitle ? `<h2>${escapeHtml(shortcodeTitle)}</h2>` : ''}
|
|
1012
|
-
${shortcodeDescription ? `<p>${escapeHtml(shortcodeDescription)}</p>` : ''}
|
|
1013
|
-
<div class="integration-links">
|
|
1014
|
-
${links
|
|
1015
|
-
.map((link) => {
|
|
1016
|
-
const icon = socialIconMarkup(link.provider);
|
|
1017
|
-
return `
|
|
1018
|
-
<a class="share-link social-link" href="${escapeHtml(link.href)}" rel="noopener noreferrer" target="_blank"${showLabels ? '' : ` aria-label="${escapeHtml(link.label)}"`}>
|
|
1019
|
-
${icon ? `<span class="social-link-icon">${icon}</span>` : ''}
|
|
1020
|
-
${showLabels ? `<span class="social-link-label">${escapeHtml(link.label)}</span>` : ''}
|
|
1021
|
-
</a>
|
|
1022
|
-
`;
|
|
1023
|
-
})
|
|
1024
|
-
.join('')}
|
|
1025
|
-
</div>
|
|
1026
|
-
</section>
|
|
1027
|
-
`;
|
|
1028
|
-
}
|
|
1029
|
-
case 'booking-calendar': {
|
|
1030
|
-
const integrationConfig = site.integrations.bookingCalendar;
|
|
1031
|
-
if (!integrationConfig.embedUrl) {
|
|
1032
|
-
throw new Error('booking-calendar block requires integrations.bookingCalendar.embedUrl to be set in site.config.yaml');
|
|
1033
|
-
}
|
|
1034
|
-
return `
|
|
1035
|
-
<section class="card integration integration-booking-calendar"${sectionStyle}>
|
|
1036
|
-
${shortcodeTitle ? `<h2>${escapeHtml(shortcodeTitle)}</h2>` : ''}
|
|
1037
|
-
${shortcodeDescription ? `<p>${escapeHtml(shortcodeDescription)}</p>` : ''}
|
|
1038
|
-
<div class="embed-frame">
|
|
1039
|
-
<iframe src="${escapeHtml(integrationConfig.embedUrl)}" title="${escapeHtml(integrationConfig.buttonLabel)}" loading="lazy" referrerpolicy="no-referrer-when-downgrade" allowfullscreen></iframe>
|
|
1040
|
-
</div>
|
|
1041
|
-
</section>
|
|
1042
|
-
`;
|
|
1043
|
-
}
|
|
1044
|
-
case 'maps': {
|
|
1045
|
-
const integrationConfig = site.integrations.maps;
|
|
1046
|
-
if (!integrationConfig.embedUrl) {
|
|
1047
|
-
throw new Error('maps block requires integrations.maps.embedUrl to be set in site.config.yaml');
|
|
1048
|
-
}
|
|
1049
|
-
return `
|
|
1050
|
-
<section class="card integration integration-maps"${sectionStyle}>
|
|
1051
|
-
${shortcodeTitle ? `<h2>${escapeHtml(shortcodeTitle)}</h2>` : ''}
|
|
1052
|
-
${shortcodeDescription ? `<p>${escapeHtml(shortcodeDescription)}</p>` : ''}
|
|
1053
|
-
<div class="embed-frame embed-frame--map">
|
|
1054
|
-
<iframe src="${escapeHtml(integrationConfig.embedUrl)}" title="${escapeHtml(shortcodeTitle || 'Map')}" loading="lazy" referrerpolicy="no-referrer-when-downgrade" allowfullscreen></iframe>
|
|
1055
|
-
</div>
|
|
1056
|
-
</section>
|
|
1057
|
-
`;
|
|
1058
|
-
}
|
|
1059
|
-
case 'video': {
|
|
1060
|
-
const videoProvider = typeof shortcodeConfig.provider === 'string' ? shortcodeConfig.provider : 'youtube';
|
|
1061
|
-
const videoUrl = typeof shortcodeConfig.url === 'string'
|
|
1062
|
-
? shortcodeConfig.url
|
|
1063
|
-
: typeof shortcodeConfig.embedUrl === 'string'
|
|
1064
|
-
? shortcodeConfig.embedUrl
|
|
1065
|
-
: '';
|
|
1066
|
-
const embedUrl = buildVideoEmbedUrl(videoProvider, videoUrl);
|
|
1067
|
-
if (!embedUrl) {
|
|
1068
|
-
throw new Error('video block requires provider and url parameters in the shortcode body');
|
|
1069
|
-
}
|
|
1070
|
-
return `
|
|
1071
|
-
<section class="card integration integration-video"${sectionStyle}>
|
|
1072
|
-
${shortcodeTitle ? `<h2>${escapeHtml(shortcodeTitle)}</h2>` : ''}
|
|
1073
|
-
${shortcodeDescription ? `<p>${escapeHtml(shortcodeDescription)}</p>` : ''}
|
|
1074
|
-
<div class="embed-frame">
|
|
1075
|
-
<iframe src="${escapeHtml(embedUrl)}" title="${escapeHtml(shortcodeTitle || 'Video')}" loading="lazy" referrerpolicy="no-referrer-when-downgrade" allowfullscreen></iframe>
|
|
1076
|
-
</div>
|
|
1077
|
-
</section>
|
|
1078
|
-
`;
|
|
1079
|
-
}
|
|
1080
|
-
case 'js':
|
|
1081
|
-
return buildInlineScriptShortcodeMarkup(body);
|
|
1082
|
-
default:
|
|
1083
|
-
return `
|
|
1084
|
-
<section class="card integration integration-unknown">
|
|
1085
|
-
<h2>${escapeHtml(shortcodeTitle || blockName)}</h2>
|
|
1086
|
-
<p class="empty-state">Unknown integration block: ${escapeHtml(blockName)}</p>
|
|
1087
|
-
</section>
|
|
1088
|
-
`;
|
|
1089
|
-
}
|
|
1090
|
-
}
|
|
1091
|
-
async function expandShortcodes(input, params) {
|
|
1092
|
-
const blockPattern = /:::([a-z-]+)\n([\s\S]*?)\n:::/g;
|
|
1093
|
-
let result = '';
|
|
1094
|
-
let lastIndex = 0;
|
|
1095
|
-
let match;
|
|
1096
|
-
const blocks = [];
|
|
1097
|
-
while ((match = blockPattern.exec(input))) {
|
|
1098
|
-
result += input.slice(lastIndex, match.index);
|
|
1099
|
-
const html = await renderShortcodeBlock({
|
|
1100
|
-
blockName: match[1],
|
|
1101
|
-
body: match[2],
|
|
1102
|
-
site: params.site,
|
|
1103
|
-
title: params.title,
|
|
1104
|
-
canonicalUrl: params.canonicalUrl,
|
|
1105
|
-
source: params.source,
|
|
1106
|
-
allSources: params.allSources
|
|
1107
|
-
});
|
|
1108
|
-
const token = `opnpress-block-${blocks.length + 1}`;
|
|
1109
|
-
blocks.push({ token, html });
|
|
1110
|
-
result += `\n\n<div data-opnpress-block="${token}"></div>\n\n`;
|
|
1111
|
-
lastIndex = match.index + match[0].length;
|
|
1112
|
-
}
|
|
1113
|
-
result += input.slice(lastIndex);
|
|
1114
|
-
return { source: result, blocks };
|
|
1115
|
-
}
|
|
1116
|
-
function injectSemanticBlocks(html, blocks) {
|
|
1117
|
-
let result = html;
|
|
1118
|
-
for (const block of blocks) {
|
|
1119
|
-
const placeholder = `<div data-opnpress-block="${block.token}"></div>`;
|
|
1120
|
-
result = result.replace(placeholder, block.html);
|
|
1121
|
-
}
|
|
1122
|
-
return result;
|
|
1123
|
-
}
|
|
1124
|
-
function buildThemeCss(theme) {
|
|
1125
|
-
const vars = [
|
|
1126
|
-
`--color-primary: ${theme.colors.primary};`,
|
|
1127
|
-
`--color-accent: ${theme.colors.accent};`,
|
|
1128
|
-
`--color-background: ${theme.colors.background};`,
|
|
1129
|
-
`--color-surface: ${theme.colors.surface};`,
|
|
1130
|
-
`--color-text: ${theme.colors.text};`,
|
|
1131
|
-
`--color-muted: ${theme.colors.muted};`,
|
|
1132
|
-
`--color-border: ${theme.colors.border};`,
|
|
1133
|
-
`--font-heading: ${theme.fonts.heading};`,
|
|
1134
|
-
`--font-body: ${theme.fonts.body};`,
|
|
1135
|
-
`--font-mono: ${theme.fonts.mono};`,
|
|
1136
|
-
`--content-width: ${theme.sizing.contentWidth};`,
|
|
1137
|
-
`--radius: ${theme.sizing.radius};`,
|
|
1138
|
-
`--spacing: ${theme.sizing.spacing};`
|
|
1139
|
-
].join('\n');
|
|
1140
|
-
return `
|
|
1141
|
-
:root {
|
|
1142
|
-
${vars}
|
|
1143
|
-
}
|
|
1144
|
-
|
|
1145
|
-
* { box-sizing: border-box; }
|
|
1146
|
-
|
|
1147
|
-
html, body {
|
|
1148
|
-
margin: 0;
|
|
1149
|
-
padding: 0;
|
|
1150
|
-
min-height: 100%;
|
|
1151
|
-
}
|
|
1152
|
-
|
|
1153
|
-
body {
|
|
1154
|
-
font-family: var(--font-body);
|
|
1155
|
-
color: var(--color-text);
|
|
1156
|
-
background: ${theme.backgrounds.page};
|
|
1157
|
-
line-height: 1.6;
|
|
1158
|
-
}
|
|
1159
|
-
|
|
1160
|
-
a {
|
|
1161
|
-
color: var(--color-primary);
|
|
1162
|
-
text-decoration-thickness: 0.08em;
|
|
1163
|
-
text-underline-offset: 0.16em;
|
|
1164
|
-
}
|
|
1165
|
-
|
|
1166
|
-
img {
|
|
1167
|
-
max-width: 100%;
|
|
1168
|
-
height: auto;
|
|
1169
|
-
}
|
|
1170
|
-
|
|
1171
|
-
.site-shell {
|
|
1172
|
-
width: min(var(--content-width), calc(100vw - 2rem));
|
|
1173
|
-
margin: 0 auto;
|
|
1174
|
-
padding: 1.5rem 0 4rem;
|
|
1175
|
-
}
|
|
1176
|
-
|
|
1177
|
-
.site-header {
|
|
1178
|
-
display: flex;
|
|
1179
|
-
align-items: center;
|
|
1180
|
-
justify-content: space-between;
|
|
1181
|
-
gap: 1rem;
|
|
1182
|
-
margin-bottom: 2rem;
|
|
1183
|
-
padding: 1rem 0;
|
|
1184
|
-
border-bottom: 1px solid var(--color-border);
|
|
1185
|
-
}
|
|
1186
|
-
|
|
1187
|
-
.site-brand {
|
|
1188
|
-
display: inline-flex;
|
|
1189
|
-
align-items: center;
|
|
1190
|
-
gap: 0.65rem;
|
|
1191
|
-
font-family: var(--font-heading);
|
|
1192
|
-
font-size: 1.35rem;
|
|
1193
|
-
font-weight: 700;
|
|
1194
|
-
color: var(--color-text);
|
|
1195
|
-
text-decoration: none;
|
|
1196
|
-
}
|
|
1197
|
-
|
|
1198
|
-
.site-logo {
|
|
1199
|
-
width: 2.75rem;
|
|
1200
|
-
height: 2.75rem;
|
|
1201
|
-
object-fit: contain;
|
|
1202
|
-
display: block;
|
|
1203
|
-
}
|
|
1204
|
-
|
|
1205
|
-
.site-nav,
|
|
1206
|
-
.site-footer-nav {
|
|
1207
|
-
display: flex;
|
|
1208
|
-
flex-wrap: wrap;
|
|
1209
|
-
gap: 0.85rem 1.25rem;
|
|
1210
|
-
align-items: center;
|
|
1211
|
-
}
|
|
1212
|
-
|
|
1213
|
-
.site-discovery-nav {
|
|
1214
|
-
display: flex;
|
|
1215
|
-
flex-wrap: wrap;
|
|
1216
|
-
gap: 0.45rem 0.85rem;
|
|
1217
|
-
align-items: center;
|
|
1218
|
-
font-size: 0.84rem;
|
|
1219
|
-
line-height: 1.25;
|
|
1220
|
-
}
|
|
1221
|
-
|
|
1222
|
-
.site-discovery-nav-header {
|
|
1223
|
-
margin-top: -0.2rem;
|
|
1224
|
-
}
|
|
1225
|
-
|
|
1226
|
-
.site-discovery-nav-footer {
|
|
1227
|
-
justify-content: flex-end;
|
|
1228
|
-
}
|
|
1229
|
-
|
|
1230
|
-
.page-discovery {
|
|
1231
|
-
display: grid;
|
|
1232
|
-
gap: 0.45rem;
|
|
1233
|
-
margin-top: 1.75rem;
|
|
1234
|
-
padding-top: 1rem;
|
|
1235
|
-
border-top: 1px solid var(--color-border);
|
|
1236
|
-
}
|
|
1237
|
-
|
|
1238
|
-
.page-discovery-label {
|
|
1239
|
-
margin: 0;
|
|
1240
|
-
color: var(--color-muted);
|
|
1241
|
-
font-size: 0.82rem;
|
|
1242
|
-
text-transform: uppercase;
|
|
1243
|
-
letter-spacing: 0.12em;
|
|
1244
|
-
}
|
|
1245
|
-
|
|
1246
|
-
.page-discovery-links {
|
|
1247
|
-
display: flex;
|
|
1248
|
-
flex-wrap: wrap;
|
|
1249
|
-
gap: 0.45rem 0.85rem;
|
|
1250
|
-
align-items: center;
|
|
1251
|
-
}
|
|
1252
|
-
|
|
1253
|
-
.nav-link {
|
|
1254
|
-
color: var(--color-muted);
|
|
1255
|
-
text-decoration: none;
|
|
1256
|
-
font-size: 0.95rem;
|
|
1257
|
-
}
|
|
1258
|
-
|
|
1259
|
-
.discovery-link {
|
|
1260
|
-
color: var(--color-muted);
|
|
1261
|
-
text-decoration: underline;
|
|
1262
|
-
text-decoration-thickness: 0.08em;
|
|
1263
|
-
text-underline-offset: 0.16em;
|
|
1264
|
-
}
|
|
1265
|
-
|
|
1266
|
-
.discovery-link:hover {
|
|
1267
|
-
color: var(--color-primary);
|
|
1268
|
-
}
|
|
1269
|
-
|
|
1270
|
-
.nav-link:hover {
|
|
1271
|
-
color: var(--color-primary);
|
|
1272
|
-
}
|
|
1273
|
-
|
|
1274
|
-
.page-hero {
|
|
1275
|
-
display: grid;
|
|
1276
|
-
gap: 1rem;
|
|
1277
|
-
padding: 2rem;
|
|
1278
|
-
margin-bottom: 2rem;
|
|
1279
|
-
border: 1px solid var(--color-border);
|
|
1280
|
-
border-radius: var(--radius);
|
|
1281
|
-
background: color-mix(in srgb, var(--color-surface) 92%, white 8%);
|
|
1282
|
-
box-shadow: 0 10px 30px rgba(20, 33, 47, 0.05);
|
|
1283
|
-
}
|
|
1284
|
-
|
|
1285
|
-
.page-hero h1,
|
|
1286
|
-
.page-content h1,
|
|
1287
|
-
.card h3 {
|
|
1288
|
-
font-family: var(--font-heading);
|
|
1289
|
-
line-height: 1.1;
|
|
1290
|
-
margin: 0;
|
|
1291
|
-
}
|
|
1292
|
-
|
|
1293
|
-
.eyebrow {
|
|
1294
|
-
margin: 0;
|
|
1295
|
-
text-transform: uppercase;
|
|
1296
|
-
letter-spacing: 0.12em;
|
|
1297
|
-
font-size: 0.77rem;
|
|
1298
|
-
color: var(--color-muted);
|
|
1299
|
-
}
|
|
1300
|
-
|
|
1301
|
-
.page-content {
|
|
1302
|
-
display: grid;
|
|
1303
|
-
gap: 0.85rem;
|
|
1304
|
-
}
|
|
1305
|
-
|
|
1306
|
-
.page-content > * {
|
|
1307
|
-
margin: 0;
|
|
1308
|
-
}
|
|
1309
|
-
|
|
1310
|
-
.page-content h2,
|
|
1311
|
-
.page-content h3,
|
|
1312
|
-
.page-content h4 {
|
|
1313
|
-
font-family: var(--font-heading);
|
|
1314
|
-
line-height: 1.2;
|
|
1315
|
-
}
|
|
1316
|
-
|
|
1317
|
-
.page-content h2 {
|
|
1318
|
-
margin-top: 1rem;
|
|
1319
|
-
}
|
|
1320
|
-
|
|
1321
|
-
.page-content h3 {
|
|
1322
|
-
margin-top: 0.85rem;
|
|
1323
|
-
}
|
|
1324
|
-
|
|
1325
|
-
.page-content h4 {
|
|
1326
|
-
margin-top: 0.7rem;
|
|
1327
|
-
}
|
|
1328
|
-
|
|
1329
|
-
.page-content p,
|
|
1330
|
-
.page-content ul,
|
|
1331
|
-
.page-content ol,
|
|
1332
|
-
.page-content blockquote,
|
|
1333
|
-
.page-content pre,
|
|
1334
|
-
.page-content table {
|
|
1335
|
-
margin: 0;
|
|
1336
|
-
}
|
|
1337
|
-
|
|
1338
|
-
.page-content ul,
|
|
1339
|
-
.page-content ol {
|
|
1340
|
-
padding-left: 1.35rem;
|
|
1341
|
-
}
|
|
1342
|
-
|
|
1343
|
-
.page-content li + li {
|
|
1344
|
-
margin-top: 0.25rem;
|
|
1345
|
-
}
|
|
1346
|
-
|
|
1347
|
-
.grid {
|
|
1348
|
-
display: grid;
|
|
1349
|
-
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
|
1350
|
-
gap: 1rem;
|
|
1351
|
-
}
|
|
1352
|
-
|
|
1353
|
-
.card {
|
|
1354
|
-
padding: 1.15rem;
|
|
1355
|
-
border: 1px solid var(--color-border);
|
|
1356
|
-
border-radius: calc(var(--radius) - 8px);
|
|
1357
|
-
background: var(--color-surface);
|
|
1358
|
-
}
|
|
1359
|
-
|
|
1360
|
-
.contact-card {
|
|
1361
|
-
display: grid;
|
|
1362
|
-
gap: 1rem;
|
|
1363
|
-
}
|
|
1364
|
-
|
|
1365
|
-
.contact-card-layout-with-map {
|
|
1366
|
-
display: grid;
|
|
1367
|
-
grid-template-columns: minmax(0, 1.2fr) minmax(260px, 0.8fr);
|
|
1368
|
-
gap: 1rem;
|
|
1369
|
-
align-items: start;
|
|
1370
|
-
}
|
|
1371
|
-
|
|
1372
|
-
.contact-card-layout-no-map {
|
|
1373
|
-
display: block;
|
|
1374
|
-
}
|
|
1375
|
-
|
|
1376
|
-
.contact-card-main {
|
|
1377
|
-
display: grid;
|
|
1378
|
-
gap: 0.7rem;
|
|
1379
|
-
}
|
|
1380
|
-
|
|
1381
|
-
.contact-card-header {
|
|
1382
|
-
display: flex;
|
|
1383
|
-
align-items: center;
|
|
1384
|
-
gap: 0.85rem;
|
|
1385
|
-
}
|
|
1386
|
-
|
|
1387
|
-
.contact-card-logo {
|
|
1388
|
-
width: 3rem;
|
|
1389
|
-
height: 3rem;
|
|
1390
|
-
object-fit: contain;
|
|
1391
|
-
flex: 0 0 auto;
|
|
1392
|
-
}
|
|
1393
|
-
|
|
1394
|
-
.contact-card-name {
|
|
1395
|
-
margin: 0;
|
|
1396
|
-
font-family: var(--font-heading);
|
|
1397
|
-
line-height: 1.1;
|
|
1398
|
-
}
|
|
1399
|
-
|
|
1400
|
-
.contact-card-name-link {
|
|
1401
|
-
color: var(--color-primary);
|
|
1402
|
-
text-decoration: underline;
|
|
1403
|
-
text-decoration-thickness: 0.08em;
|
|
1404
|
-
text-underline-offset: 0.16em;
|
|
1405
|
-
}
|
|
1406
|
-
|
|
1407
|
-
.contact-card-name-link:hover {
|
|
1408
|
-
text-decoration: underline;
|
|
1409
|
-
}
|
|
1410
|
-
|
|
1411
|
-
.contact-card-tagline,
|
|
1412
|
-
.contact-card-text {
|
|
1413
|
-
margin: 0;
|
|
1414
|
-
}
|
|
1415
|
-
|
|
1416
|
-
.contact-card-tagline {
|
|
1417
|
-
margin-top: -0.4rem;
|
|
1418
|
-
font-size: 0.92rem;
|
|
1419
|
-
color: var(--color-muted);
|
|
1420
|
-
}
|
|
1421
|
-
|
|
1422
|
-
.contact-card-row {
|
|
1423
|
-
display: flex;
|
|
1424
|
-
align-items: center;
|
|
1425
|
-
gap: 0.6rem;
|
|
1426
|
-
flex-wrap: wrap;
|
|
1427
|
-
min-width: 0;
|
|
1428
|
-
}
|
|
1429
|
-
|
|
1430
|
-
.contact-card-map-link,
|
|
1431
|
-
.contact-card-link {
|
|
1432
|
-
display: inline-flex;
|
|
1433
|
-
align-items: center;
|
|
1434
|
-
gap: 0.5rem;
|
|
1435
|
-
color: var(--color-primary);
|
|
1436
|
-
text-decoration: underline;
|
|
1437
|
-
text-decoration-thickness: 0.08em;
|
|
1438
|
-
text-underline-offset: 0.16em;
|
|
1439
|
-
line-height: 1.3;
|
|
1440
|
-
max-width: 100%;
|
|
1441
|
-
min-width: 0;
|
|
1442
|
-
overflow-wrap: anywhere;
|
|
1443
|
-
}
|
|
1444
|
-
|
|
1445
|
-
.contact-card-map-link:hover,
|
|
1446
|
-
.contact-card-link:hover {
|
|
1447
|
-
text-decoration: underline;
|
|
1448
|
-
}
|
|
1449
|
-
|
|
1450
|
-
.contact-card-map-icon {
|
|
1451
|
-
display: inline-flex;
|
|
1452
|
-
align-items: center;
|
|
1453
|
-
justify-content: center;
|
|
1454
|
-
width: 1rem;
|
|
1455
|
-
height: 1rem;
|
|
1456
|
-
flex: 0 0 auto;
|
|
1457
|
-
color: var(--color-primary);
|
|
1458
|
-
}
|
|
1459
|
-
|
|
1460
|
-
.contact-card-separator {
|
|
1461
|
-
color: var(--color-muted);
|
|
1462
|
-
}
|
|
1463
|
-
|
|
1464
|
-
.contact-card-map-pane {
|
|
1465
|
-
display: flex;
|
|
1466
|
-
flex-direction: column;
|
|
1467
|
-
align-self: start;
|
|
1468
|
-
}
|
|
1469
|
-
|
|
1470
|
-
.contact-card-map-frame {
|
|
1471
|
-
width: 100%;
|
|
1472
|
-
height: 220px;
|
|
1473
|
-
min-height: 220px;
|
|
1474
|
-
flex: 0 0 auto;
|
|
1475
|
-
min-width: 0;
|
|
1476
|
-
}
|
|
1477
|
-
|
|
1478
|
-
.contact-card-map-frame iframe {
|
|
1479
|
-
width: 100%;
|
|
1480
|
-
height: 220px;
|
|
1481
|
-
min-height: 220px;
|
|
1482
|
-
}
|
|
1483
|
-
|
|
1484
|
-
.company-info {
|
|
1485
|
-
display: grid;
|
|
1486
|
-
gap: 0.85rem;
|
|
1487
|
-
}
|
|
1488
|
-
|
|
1489
|
-
.company-info-logo {
|
|
1490
|
-
width: 3rem;
|
|
1491
|
-
height: 3rem;
|
|
1492
|
-
object-fit: contain;
|
|
1493
|
-
}
|
|
1494
|
-
|
|
1495
|
-
.company-info-name {
|
|
1496
|
-
margin: 0;
|
|
1497
|
-
font-family: var(--font-heading);
|
|
1498
|
-
line-height: 1.1;
|
|
1499
|
-
}
|
|
1500
|
-
|
|
1501
|
-
.company-info-tagline {
|
|
1502
|
-
margin: 0;
|
|
1503
|
-
color: var(--color-muted);
|
|
1504
|
-
}
|
|
1505
|
-
|
|
1506
|
-
.contact-links {
|
|
1507
|
-
display: grid;
|
|
1508
|
-
gap: 0.75rem;
|
|
1509
|
-
}
|
|
1510
|
-
|
|
1511
|
-
.contact-links-inline .integration-links {
|
|
1512
|
-
flex-direction: row;
|
|
1513
|
-
}
|
|
1514
|
-
|
|
1515
|
-
.contact-links-stacked .integration-links {
|
|
1516
|
-
flex-direction: column;
|
|
1517
|
-
align-items: flex-start;
|
|
1518
|
-
}
|
|
1519
|
-
|
|
1520
|
-
.contact-link {
|
|
1521
|
-
justify-content: flex-start;
|
|
1522
|
-
text-align: left;
|
|
1523
|
-
}
|
|
1524
|
-
|
|
1525
|
-
.contact-link-icon-wrap {
|
|
1526
|
-
display: inline-flex;
|
|
1527
|
-
align-items: center;
|
|
1528
|
-
justify-content: center;
|
|
1529
|
-
width: 1.1rem;
|
|
1530
|
-
height: 1.1rem;
|
|
1531
|
-
flex: 0 0 auto;
|
|
1532
|
-
}
|
|
1533
|
-
|
|
1534
|
-
.contact-link-icon {
|
|
1535
|
-
width: 1.1rem;
|
|
1536
|
-
height: 1.1rem;
|
|
1537
|
-
display: block;
|
|
1538
|
-
color: currentColor;
|
|
1539
|
-
}
|
|
1540
|
-
|
|
1541
|
-
.contact-link-label {
|
|
1542
|
-
line-height: 1.25;
|
|
1543
|
-
}
|
|
1544
|
-
|
|
1545
|
-
.integration {
|
|
1546
|
-
display: grid;
|
|
1547
|
-
gap: 1rem;
|
|
1548
|
-
}
|
|
1549
|
-
|
|
1550
|
-
.integration h2 {
|
|
1551
|
-
margin: 0;
|
|
1552
|
-
font-family: var(--font-heading);
|
|
1553
|
-
}
|
|
1554
|
-
|
|
1555
|
-
.integration-form {
|
|
1556
|
-
display: grid;
|
|
1557
|
-
gap: 1rem;
|
|
1558
|
-
}
|
|
1559
|
-
|
|
1560
|
-
.integration-form label {
|
|
1561
|
-
display: grid;
|
|
1562
|
-
gap: 0.35rem;
|
|
1563
|
-
font-size: 0.95rem;
|
|
1564
|
-
}
|
|
1565
|
-
|
|
1566
|
-
.integration-form input,
|
|
1567
|
-
.integration-form textarea {
|
|
1568
|
-
width: 100%;
|
|
1569
|
-
border: 1px solid var(--color-border);
|
|
1570
|
-
border-radius: 14px;
|
|
1571
|
-
padding: 0.85rem 0.95rem;
|
|
1572
|
-
background: var(--color-surface);
|
|
1573
|
-
font: inherit;
|
|
1574
|
-
color: inherit;
|
|
1575
|
-
}
|
|
1576
|
-
|
|
1577
|
-
.integration-form button {
|
|
1578
|
-
justify-self: start;
|
|
1579
|
-
border: 0;
|
|
1580
|
-
border-radius: 999px;
|
|
1581
|
-
padding: 0.85rem 1.2rem;
|
|
1582
|
-
background: var(--color-primary);
|
|
1583
|
-
color: white;
|
|
1584
|
-
font: inherit;
|
|
1585
|
-
cursor: pointer;
|
|
1586
|
-
}
|
|
1587
|
-
|
|
1588
|
-
.integration-links {
|
|
1589
|
-
display: flex;
|
|
1590
|
-
flex-wrap: wrap;
|
|
1591
|
-
gap: 0.75rem;
|
|
1592
|
-
}
|
|
1593
|
-
|
|
1594
|
-
.share-link {
|
|
1595
|
-
display: inline-flex;
|
|
1596
|
-
align-items: center;
|
|
1597
|
-
justify-content: center;
|
|
1598
|
-
gap: 0.55rem;
|
|
1599
|
-
border: 1px solid var(--color-border);
|
|
1600
|
-
border-radius: 999px;
|
|
1601
|
-
padding: 0.65rem 0.95rem;
|
|
1602
|
-
text-decoration: none;
|
|
1603
|
-
color: var(--color-text);
|
|
1604
|
-
background: color-mix(in srgb, var(--color-surface) 88%, white 12%);
|
|
1605
|
-
max-width: 100%;
|
|
1606
|
-
min-width: 0;
|
|
1607
|
-
overflow-wrap: anywhere;
|
|
1608
|
-
}
|
|
1609
|
-
|
|
1610
|
-
.social-link-icon {
|
|
1611
|
-
display: inline-flex;
|
|
1612
|
-
align-items: center;
|
|
1613
|
-
justify-content: center;
|
|
1614
|
-
width: 1.1rem;
|
|
1615
|
-
height: 1.1rem;
|
|
1616
|
-
flex: 0 0 auto;
|
|
1617
|
-
}
|
|
1618
|
-
|
|
1619
|
-
.social-icon {
|
|
1620
|
-
width: 1.1rem;
|
|
1621
|
-
height: 1.1rem;
|
|
1622
|
-
display: block;
|
|
1623
|
-
color: currentColor;
|
|
1624
|
-
}
|
|
1625
|
-
|
|
1626
|
-
.social-link-label {
|
|
1627
|
-
line-height: 1;
|
|
1628
|
-
}
|
|
1629
|
-
|
|
1630
|
-
.embed-frame {
|
|
1631
|
-
position: relative;
|
|
1632
|
-
width: 100%;
|
|
1633
|
-
min-height: 320px;
|
|
1634
|
-
border-radius: 18px;
|
|
1635
|
-
overflow: hidden;
|
|
1636
|
-
border: 1px solid var(--color-border);
|
|
1637
|
-
}
|
|
1638
|
-
|
|
1639
|
-
.embed-frame iframe {
|
|
1640
|
-
width: 100%;
|
|
1641
|
-
height: 100%;
|
|
1642
|
-
min-height: 320px;
|
|
1643
|
-
border: 0;
|
|
1644
|
-
}
|
|
1645
|
-
|
|
1646
|
-
.embed-frame--map,
|
|
1647
|
-
.contact-card-map-frame {
|
|
1648
|
-
height: 220px;
|
|
1649
|
-
min-height: 220px;
|
|
1650
|
-
}
|
|
1651
|
-
|
|
1652
|
-
.embed-frame--map iframe,
|
|
1653
|
-
.contact-card-map-frame iframe {
|
|
1654
|
-
height: 220px;
|
|
1655
|
-
min-height: 220px;
|
|
1656
|
-
}
|
|
1657
|
-
|
|
1658
|
-
.empty-state {
|
|
1659
|
-
color: var(--color-muted);
|
|
1660
|
-
font-style: italic;
|
|
1661
|
-
}
|
|
1662
|
-
|
|
1663
|
-
.post-list {
|
|
1664
|
-
display: grid;
|
|
1665
|
-
gap: 1rem;
|
|
1666
|
-
}
|
|
1667
|
-
|
|
1668
|
-
.site-footer {
|
|
1669
|
-
display: flex;
|
|
1670
|
-
justify-content: space-between;
|
|
1671
|
-
gap: 1rem;
|
|
1672
|
-
flex-wrap: wrap;
|
|
1673
|
-
margin-top: 3rem;
|
|
1674
|
-
padding-top: 1.5rem;
|
|
1675
|
-
border-top: 1px solid var(--color-border);
|
|
1676
|
-
color: var(--color-muted);
|
|
1677
|
-
font-size: 0.95rem;
|
|
1678
|
-
}
|
|
1679
|
-
|
|
1680
|
-
@media (max-width: 720px) {
|
|
1681
|
-
.site-shell {
|
|
1682
|
-
width: min(100vw - 1rem, var(--content-width));
|
|
1683
|
-
padding: 1rem 0 2.5rem;
|
|
1684
|
-
}
|
|
1685
|
-
|
|
1686
|
-
.site-header {
|
|
1687
|
-
flex-direction: column;
|
|
1688
|
-
align-items: flex-start;
|
|
1689
|
-
gap: 0.65rem;
|
|
1690
|
-
margin-bottom: 1.25rem;
|
|
1691
|
-
}
|
|
1692
|
-
|
|
1693
|
-
.site-nav,
|
|
1694
|
-
.site-footer-nav {
|
|
1695
|
-
gap: 0.5rem 0.75rem;
|
|
1696
|
-
width: 100%;
|
|
1697
|
-
flex-wrap: nowrap;
|
|
1698
|
-
overflow-x: auto;
|
|
1699
|
-
-webkit-overflow-scrolling: touch;
|
|
1700
|
-
scrollbar-width: none;
|
|
1701
|
-
padding-bottom: 0.25rem;
|
|
1702
|
-
}
|
|
1703
|
-
|
|
1704
|
-
.site-nav::-webkit-scrollbar,
|
|
1705
|
-
.site-footer-nav::-webkit-scrollbar {
|
|
1706
|
-
display: none;
|
|
1707
|
-
}
|
|
1708
|
-
|
|
1709
|
-
.site-discovery-nav {
|
|
1710
|
-
width: 100%;
|
|
1711
|
-
flex-wrap: wrap;
|
|
1712
|
-
justify-content: flex-start;
|
|
1713
|
-
}
|
|
1714
|
-
|
|
1715
|
-
.site-discovery-nav-footer {
|
|
1716
|
-
justify-content: flex-start;
|
|
1717
|
-
}
|
|
1718
|
-
|
|
1719
|
-
.page-discovery {
|
|
1720
|
-
margin-top: 1.25rem;
|
|
1721
|
-
padding-top: 0.85rem;
|
|
1722
|
-
}
|
|
1723
|
-
|
|
1724
|
-
.site-brand {
|
|
1725
|
-
font-size: 1.15rem;
|
|
1726
|
-
}
|
|
1727
|
-
|
|
1728
|
-
.site-logo {
|
|
1729
|
-
width: 2.25rem;
|
|
1730
|
-
height: 2.25rem;
|
|
1731
|
-
}
|
|
1732
|
-
|
|
1733
|
-
.page-hero,
|
|
1734
|
-
.card {
|
|
1735
|
-
padding: 0.95rem;
|
|
1736
|
-
}
|
|
1737
|
-
|
|
1738
|
-
.page-hero h1,
|
|
1739
|
-
.page-content h1 {
|
|
1740
|
-
font-size: clamp(2.35rem, 9vw, 3rem);
|
|
1741
|
-
}
|
|
1742
|
-
|
|
1743
|
-
.page-content h2 {
|
|
1744
|
-
font-size: clamp(1.7rem, 6.4vw, 2.1rem);
|
|
1745
|
-
}
|
|
1746
|
-
|
|
1747
|
-
.page-content h3 {
|
|
1748
|
-
font-size: clamp(1.35rem, 5.5vw, 1.6rem);
|
|
1749
|
-
}
|
|
1750
|
-
|
|
1751
|
-
.page-content h4 {
|
|
1752
|
-
font-size: 1.05rem;
|
|
1753
|
-
}
|
|
1754
|
-
|
|
1755
|
-
.page-content {
|
|
1756
|
-
gap: 0.7rem;
|
|
1757
|
-
}
|
|
1758
|
-
|
|
1759
|
-
.page-content h2 {
|
|
1760
|
-
margin-top: 0.85rem;
|
|
1761
|
-
}
|
|
1762
|
-
|
|
1763
|
-
.page-content h3 {
|
|
1764
|
-
margin-top: 0.7rem;
|
|
1765
|
-
}
|
|
1766
|
-
|
|
1767
|
-
.page-content h4 {
|
|
1768
|
-
margin-top: 0.6rem;
|
|
1769
|
-
}
|
|
1770
|
-
|
|
1771
|
-
.integration-links {
|
|
1772
|
-
flex-direction: column;
|
|
1773
|
-
align-items: stretch;
|
|
1774
|
-
}
|
|
1775
|
-
|
|
1776
|
-
.share-link {
|
|
1777
|
-
width: 100%;
|
|
1778
|
-
justify-content: flex-start;
|
|
1779
|
-
}
|
|
1780
|
-
|
|
1781
|
-
.contact-card-map-frame,
|
|
1782
|
-
.embed-frame--map {
|
|
1783
|
-
height: 200px;
|
|
1784
|
-
min-height: 200px;
|
|
1785
|
-
}
|
|
1786
|
-
|
|
1787
|
-
.contact-card-map-frame iframe,
|
|
1788
|
-
.embed-frame--map iframe {
|
|
1789
|
-
height: 200px;
|
|
1790
|
-
min-height: 200px;
|
|
1791
|
-
}
|
|
1792
|
-
|
|
1793
|
-
.contact-card-layout-with-map {
|
|
1794
|
-
grid-template-columns: 1fr;
|
|
1795
|
-
}
|
|
1796
|
-
|
|
1797
|
-
.contact-card-header {
|
|
1798
|
-
align-items: flex-start;
|
|
1799
|
-
}
|
|
1800
|
-
|
|
1801
|
-
.contact-card-row {
|
|
1802
|
-
gap: 0.45rem;
|
|
1803
|
-
}
|
|
1804
|
-
|
|
1805
|
-
.contact-card-map-link,
|
|
1806
|
-
.contact-card-link {
|
|
1807
|
-
width: 100%;
|
|
1808
|
-
}
|
|
1809
|
-
|
|
1810
|
-
.contact-card-map-pane,
|
|
1811
|
-
.company-info-map-pane {
|
|
1812
|
-
display: none;
|
|
1813
|
-
}
|
|
1814
|
-
|
|
1815
|
-
.embed-frame {
|
|
1816
|
-
min-height: 240px;
|
|
1817
|
-
}
|
|
1818
|
-
|
|
1819
|
-
.embed-frame iframe {
|
|
1820
|
-
min-height: 240px;
|
|
1821
|
-
}
|
|
1822
|
-
|
|
1823
|
-
.embed-frame--map {
|
|
1824
|
-
height: 180px;
|
|
1825
|
-
min-height: 180px;
|
|
1826
|
-
}
|
|
1827
|
-
|
|
1828
|
-
.embed-frame--map iframe {
|
|
1829
|
-
height: 180px;
|
|
1830
|
-
min-height: 180px;
|
|
1831
|
-
}
|
|
1832
|
-
|
|
1833
|
-
.site-footer {
|
|
1834
|
-
flex-direction: column;
|
|
1835
|
-
align-items: flex-start;
|
|
1836
|
-
margin-top: 2rem;
|
|
1837
|
-
padding-top: 1.25rem;
|
|
1838
|
-
}
|
|
1839
|
-
|
|
1840
|
-
.site-footer-nav {
|
|
1841
|
-
flex-direction: column;
|
|
1842
|
-
align-items: flex-start;
|
|
1843
|
-
}
|
|
1844
|
-
}
|
|
1845
|
-
`;
|
|
1846
|
-
}
|
|
1847
|
-
export async function buildPageArtifact(params) {
|
|
1848
|
-
const { source, site, theme, navigation, allPublishedSources, headerLogoUrl, customScriptUrls = [] } = params;
|
|
1849
|
-
const title = buildPageTitle(source, site.site.name);
|
|
1850
|
-
const description = source.frontmatter.description ?? site.site.description ?? '';
|
|
1851
|
-
const canonicalUrl = source.frontmatter.canonical
|
|
1852
|
-
? source.frontmatter.canonical
|
|
1853
|
-
: canonicalUrlForRoute(site.site.domain, source.routePath);
|
|
1854
|
-
const pageUrl = canonicalUrlForRoute(site.site.domain, source.routePath);
|
|
1855
|
-
const sourceKind = source.kind;
|
|
1856
|
-
let bodyHtml;
|
|
1857
|
-
if (sourceKind === 'markdown') {
|
|
1858
|
-
const semanticResult = await expandShortcodes(source.body, {
|
|
1859
|
-
site,
|
|
1860
|
-
title,
|
|
1861
|
-
canonicalUrl,
|
|
1862
|
-
source,
|
|
1863
|
-
allSources: allPublishedSources
|
|
1864
|
-
});
|
|
1865
|
-
bodyHtml = injectSemanticBlocks(await renderMarkdown(semanticResult.source), semanticResult.blocks);
|
|
1866
|
-
}
|
|
1867
|
-
else {
|
|
1868
|
-
bodyHtml = source.body;
|
|
1869
|
-
}
|
|
1870
|
-
const content = `<section class="page-content">${bodyHtml}</section>`;
|
|
1871
|
-
const navigationHtml = renderNavList(navigation.main.map((item) => ({
|
|
1872
|
-
...item,
|
|
1873
|
-
url: hrefForRoute(source.routePath, item.url)
|
|
1874
|
-
})));
|
|
1875
|
-
const footerHtml = renderNavList(navigation.footer.map((item) => ({
|
|
1876
|
-
...item,
|
|
1877
|
-
url: hrefForRoute(source.routePath, item.url)
|
|
1878
|
-
})));
|
|
1879
|
-
const customScriptTags = buildCustomScriptTags(customScriptUrls.map((scriptUrl) => hrefForAsset(source.routePath, scriptUrl)));
|
|
1880
|
-
const isRawHtml = source.kind === 'html' && isFullHtmlDocument(source.body);
|
|
1881
|
-
const ogImage = site.seo.defaultImage
|
|
1882
|
-
? canonicalUrlForRoute(site.site.domain, site.seo.defaultImage)
|
|
1883
|
-
: undefined;
|
|
1884
|
-
const renderedBodyText = stripHtml(bodyHtml);
|
|
1885
|
-
const headings = extractHeadings(bodyHtml);
|
|
1886
|
-
const excerpt = description || extractExcerpt(renderedBodyText);
|
|
1887
|
-
const tags = source.frontmatter.tags ?? [];
|
|
1888
|
-
const categories = source.frontmatter.categories ?? [];
|
|
1889
|
-
const date = normalizeDateValue(source.frontmatter.date);
|
|
1890
|
-
const updated = normalizeDateValue(source.frontmatter.updated);
|
|
1891
|
-
const jsonLd = {
|
|
1892
|
-
'@context': 'https://schema.org',
|
|
1893
|
-
'@type': 'WebPage',
|
|
1894
|
-
name: title,
|
|
1895
|
-
description,
|
|
1896
|
-
url: canonicalUrl
|
|
1897
|
-
};
|
|
1898
|
-
const analyticsId = site.integrations.analytics.id ?? site.integrations.analytics.measurementId;
|
|
1899
|
-
const analytics = analyticsId
|
|
1900
|
-
? analyticsSnippet(analyticsId)
|
|
1901
|
-
: '';
|
|
1902
|
-
const poweredBy = site.branding.poweredByOpnPress
|
|
1903
|
-
? `<a class="nav-link" href="https://opnpress.com" rel="noopener noreferrer">Built with OpnPress</a>`
|
|
1904
|
-
: '';
|
|
1905
|
-
const logoUrl = headerLogoUrl ?? (site.site.logo ? hrefForAsset(source.routePath, site.site.logo) : '');
|
|
1906
|
-
const faviconAsset = site.site.favicon ?? site.site.logo;
|
|
1907
|
-
const faviconUrl = faviconAsset ? hrefForAsset(source.routePath, faviconAsset) : '';
|
|
1908
|
-
const twitterSiteHandle = socialMetaHandle(site.socials.twitter ?? site.socials.x);
|
|
1909
|
-
const pageJson = {
|
|
1910
|
-
generator: 'OpnPress',
|
|
1911
|
-
title,
|
|
1912
|
-
description,
|
|
1913
|
-
routePath: source.routePath,
|
|
1914
|
-
url: pageUrl,
|
|
1915
|
-
canonicalUrl,
|
|
1916
|
-
section: source.section,
|
|
1917
|
-
kind: source.kind,
|
|
1918
|
-
published: true,
|
|
1919
|
-
draft: false,
|
|
1920
|
-
date,
|
|
1921
|
-
updated,
|
|
1922
|
-
image: source.frontmatter.image,
|
|
1923
|
-
tags,
|
|
1924
|
-
categories,
|
|
1925
|
-
headings,
|
|
1926
|
-
entities: [...tags, ...categories],
|
|
1927
|
-
wordCount: wordCount(bodyHtml),
|
|
1928
|
-
excerpt
|
|
1929
|
-
};
|
|
1930
|
-
const sourceMirrorUrl = source.kind === 'markdown'
|
|
1931
|
-
? new URL('source.md', canonicalUrl).toString()
|
|
1932
|
-
: new URL('source.html', canonicalUrl).toString();
|
|
1933
|
-
const sourceMirrorRelativeUrl = source.kind === 'markdown'
|
|
1934
|
-
? hrefForSourceMirror(source.routePath, source.routePath)
|
|
1935
|
-
: hrefForSourceMirror(source.routePath, source.routePath, 'source.html');
|
|
1936
|
-
const llmsRelativeUrl = hrefForAsset(source.routePath, '/llms.txt');
|
|
1937
|
-
const fullSiteContentRelativeUrl = hrefForAsset(source.routePath, '/fullSiteContent.md');
|
|
1938
|
-
const contentSource = source.kind === 'markdown' ? 'markdown' : 'html';
|
|
1939
|
-
const discoveryTargets = {
|
|
1940
|
-
llmsRelativeUrl,
|
|
1941
|
-
fullSiteContentRelativeUrl,
|
|
1942
|
-
sourceMirrorRelativeUrl
|
|
1943
|
-
};
|
|
1944
|
-
const discoveryHead = buildDiscoveryHeadMarkup({
|
|
1945
|
-
llmsRelativeUrl,
|
|
1946
|
-
fullSiteContentRelativeUrl,
|
|
1947
|
-
contentSource,
|
|
1948
|
-
sourceMirrorRelativeUrl
|
|
1949
|
-
});
|
|
1950
|
-
const discoveryHeaderNav = buildDiscoveryHeaderMarkup(discoveryTargets);
|
|
1951
|
-
const discoveryFooterNav = buildDiscoveryFooterMarkup(discoveryTargets);
|
|
1952
|
-
const discoveryBodyMarkup = buildDiscoveryBodyMarkup(discoveryTargets);
|
|
1953
|
-
if (isRawHtml) {
|
|
1954
|
-
const bodyDiscoveryMarkup = `${customScriptTags}\n${discoveryBodyMarkup}\n${buildSystemReminderMarkup(fullSiteContentRelativeUrl)}`;
|
|
1955
|
-
const wrappedHtml = source.body
|
|
1956
|
-
.replace(/<\/head>/i, `${discoveryHead}\n</head>`)
|
|
1957
|
-
.replace(/<\/body>/i, `${bodyDiscoveryMarkup}\n</body>`);
|
|
1958
|
-
return {
|
|
1959
|
-
html: isFullHtmlDocument(source.body) ? wrappedHtml : source.body,
|
|
1960
|
-
pageJson,
|
|
1961
|
-
sourceMirrorUrl,
|
|
1962
|
-
sourceMirror: {
|
|
1963
|
-
path: 'source.html',
|
|
1964
|
-
content: source.body
|
|
1965
|
-
}
|
|
1966
|
-
};
|
|
1967
|
-
}
|
|
1968
|
-
return {
|
|
1969
|
-
html: `
|
|
1970
|
-
<!doctype html>
|
|
1971
|
-
<html lang="${escapeHtml(site.site.language)}">
|
|
1972
|
-
<head>
|
|
1973
|
-
<!-- opnpress:generator=OpnPress -->
|
|
1974
|
-
<meta charset="utf-8" />
|
|
1975
|
-
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
1976
|
-
${discoveryHead}
|
|
1977
|
-
${faviconUrl ? `<link rel="icon" href="${escapeHtml(faviconUrl)}" type="image/png" />` : ''}
|
|
1978
|
-
${faviconUrl ? `<link rel="apple-touch-icon" href="${escapeHtml(faviconUrl)}" />` : ''}
|
|
1979
|
-
<title>${escapeHtml(title)}</title>
|
|
1980
|
-
${description ? `<meta name="description" content="${escapeHtml(description)}" />` : ''}
|
|
1981
|
-
<link rel="canonical" href="${escapeHtml(canonicalUrl)}" />
|
|
1982
|
-
<meta property="og:type" content="article" />
|
|
1983
|
-
<meta property="og:title" content="${escapeHtml(title)}" />
|
|
1984
|
-
${description ? `<meta property="og:description" content="${escapeHtml(description)}" />` : ''}
|
|
1985
|
-
<meta property="og:url" content="${escapeHtml(canonicalUrl)}" />
|
|
1986
|
-
${ogImage ? `<meta property="og:image" content="${escapeHtml(ogImage)}" />` : ''}
|
|
1987
|
-
<meta name="twitter:card" content="${ogImage ? 'summary_large_image' : 'summary'}" />
|
|
1988
|
-
<meta name="twitter:title" content="${escapeHtml(title)}" />
|
|
1989
|
-
${description ? `<meta name="twitter:description" content="${escapeHtml(description)}" />` : ''}
|
|
1990
|
-
${twitterSiteHandle ? `<meta name="twitter:site" content="${escapeHtml(twitterSiteHandle)}" />` : ''}
|
|
1991
|
-
${ogImage ? `<meta name="twitter:image" content="${escapeHtml(ogImage)}" />` : ''}
|
|
1992
|
-
<script type="application/ld+json">${serializeJsonLd(jsonLd)}</script>
|
|
1993
|
-
${analytics}
|
|
1994
|
-
${customScriptTags}
|
|
1995
|
-
<style>${buildThemeCss(theme)}</style>
|
|
1996
|
-
</head>
|
|
1997
|
-
<body>
|
|
1998
|
-
<div class="site-shell">
|
|
1999
|
-
<header class="site-header">
|
|
2000
|
-
<a class="site-brand" href="${escapeHtml(hrefForRoute(source.routePath, '/'))}">
|
|
2001
|
-
${logoUrl ? `<img class="site-logo" src="${escapeHtml(logoUrl)}" alt="" aria-hidden="true" />` : ''}
|
|
2002
|
-
<span>${escapeHtml(site.site.name)}</span>
|
|
2003
|
-
</a>
|
|
2004
|
-
<nav class="site-nav" aria-label="Main navigation">${navigationHtml}</nav>
|
|
2005
|
-
${discoveryHeaderNav}
|
|
2006
|
-
</header>
|
|
2007
|
-
<main class="${pageClass()}">
|
|
2008
|
-
${content}
|
|
2009
|
-
${discoveryBodyMarkup}
|
|
2010
|
-
</main>
|
|
2011
|
-
${buildSystemReminderMarkup(fullSiteContentRelativeUrl)}
|
|
2012
|
-
<footer class="site-footer">
|
|
2013
|
-
<div>${escapeHtml(site.site.description)}</div>
|
|
2014
|
-
<nav class="site-footer-nav" aria-label="Footer navigation">${poweredBy}${footerHtml}</nav>
|
|
2015
|
-
${discoveryFooterNav}
|
|
2016
|
-
</footer>
|
|
2017
|
-
</div>
|
|
2018
|
-
</body>
|
|
2019
|
-
</html>
|
|
2020
|
-
`,
|
|
2021
|
-
pageJson,
|
|
2022
|
-
sourceMirrorUrl,
|
|
2023
|
-
sourceMirror: source.kind === 'markdown'
|
|
2024
|
-
? {
|
|
2025
|
-
path: 'source.md',
|
|
2026
|
-
content: `---\n${YAML.stringify(source.frontmatter).trim()}\n---\n\n${source.body}\n`
|
|
2027
|
-
}
|
|
2028
|
-
: {
|
|
2029
|
-
path: 'source.html',
|
|
2030
|
-
content: source.body
|
|
2031
|
-
}
|
|
2032
|
-
};
|
|
2033
|
-
}
|
|
2034
|
-
export async function renderPageDocument(params) {
|
|
2035
|
-
const artifact = await buildPageArtifact(params);
|
|
2036
|
-
return artifact.html;
|
|
2037
|
-
}
|
|
2038
|
-
export async function writeRenderedPage(outputPath, html) {
|
|
2039
|
-
await fs.mkdir(path.dirname(outputPath), { recursive: true });
|
|
2040
|
-
await fs.writeFile(outputPath, html, 'utf8');
|
|
2041
|
-
}
|