tfrere HF Staff commited on
Commit
4e4e67f
·
1 Parent(s): 7d0d755
app/src/components/Hero.astro CHANGED
@@ -12,6 +12,7 @@ interface Props {
12
  affiliation?: string; // legacy single affiliation
13
  published?: string;
14
  doi?: string;
 
15
  }
16
 
17
  const {
@@ -23,6 +24,7 @@ const {
23
  affiliation,
24
  published,
25
  doi,
 
26
  } = Astro.props as Props;
27
 
28
  type Author = { name: string; url?: string; affiliationIndices?: number[] };
@@ -184,8 +186,22 @@ const pdfFilename = `${slugify(pdfBase)}.pdf`;
184
  </div>
185
  )} -->
186
  <div class="meta-container-cell meta-container-cell--pdf">
187
- <h3>PDF</h3>
188
- <div id="pdf-download-container">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
189
  <p class="pdf-loading">Checking access...</p>
190
  <p class="pdf-pro-only" style="display: none;">
191
  <a
@@ -196,11 +212,20 @@ const pdfFilename = `${slugify(pdfBase)}.pdf`;
196
  >
197
  Download PDF
198
  </a>
199
- <span class="pro-badge">PRO</span>
200
- </p>
201
- <p class="pdf-locked" style="display: none;">
202
- <span class="locked-message">🔒 PDF available for Pro users only</span>
203
  </p>
 
 
 
 
 
 
 
 
 
 
 
 
 
204
  </div>
205
  </div>
206
  </div>
@@ -209,9 +234,16 @@ const pdfFilename = `${slugify(pdfBase)}.pdf`;
209
  <script>
210
  // PDF access control for Pro users only
211
 
 
 
 
212
  const FALLBACK_TIMEOUT_MS = 3000;
213
  let userPlanChecked = false;
214
 
 
 
 
 
215
  /**
216
  * Check if user has Pro access
217
  * Isolated logic for Pro user verification
@@ -231,15 +263,32 @@ const pdfFilename = `${slugify(pdfBase)}.pdf`;
231
  const loadingEl = document.querySelector(".pdf-loading") as HTMLElement;
232
  const proOnlyEl = document.querySelector(".pdf-pro-only") as HTMLElement;
233
  const lockedEl = document.querySelector(".pdf-locked") as HTMLElement;
 
 
234
 
235
  // Hide loading state
236
  if (loadingEl) loadingEl.style.display = "none";
237
 
238
- // Show appropriate state
 
 
 
 
 
 
 
 
 
239
  if (isPro) {
240
  if (proOnlyEl) proOnlyEl.style.display = "block";
 
 
 
241
  } else {
 
 
242
  if (lockedEl) lockedEl.style.display = "block";
 
243
  }
244
  }
245
 
@@ -257,38 +306,42 @@ const pdfFilename = `${slugify(pdfBase)}.pdf`;
257
 
258
  /**
259
  * Fallback behavior when no parent window responds
260
- * Change this to test different scenarios locally
261
  */
262
  function handleFallback() {
263
- // Default to Pro access in development/standalone mode
264
- handleUserPlan({ user: "pro" });
265
-
266
- // Uncomment to test locked state:
267
- // handleUserPlan({ user: "free" });
268
- // handleUserPlan(null);
269
- }
270
-
271
- // Listen for messages from parent window (Hugging Face Spaces)
272
- window.addEventListener("message", (event) => {
273
- if (event.data.type === "USER_PLAN") {
274
- handleUserPlan(event.data.plan);
275
  }
276
- });
277
 
278
- // Request user plan on page load
279
- if (window.parent && window.parent !== window) {
280
- // We're in an iframe, request user plan
281
- window.parent.postMessage({ type: "USER_PLAN_REQUEST" }, "*");
282
-
283
- // Fallback if no response after timeout
284
- setTimeout(() => {
285
- if (!userPlanChecked) {
286
- handleFallback();
287
- }
288
- }, FALLBACK_TIMEOUT_MS);
289
  } else {
290
- // Not in iframe (local development), use fallback immediately
291
- handleFallback();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
292
  }
293
  </script>
294
 
@@ -403,59 +456,138 @@ const pdfFilename = `${slugify(pdfBase)}.pdf`;
403
  }
404
 
405
  /* PDF access control styles */
 
 
 
 
 
 
 
 
 
 
 
406
  .pdf-loading {
407
  color: var(--muted-color);
408
  font-size: 0.9em;
409
  }
410
 
411
  .pdf-pro-only {
412
- display: flex;
413
- flex-direction: row;
414
- gap: 8px;
 
 
415
  align-items: center;
 
 
 
 
 
 
 
 
 
416
  }
417
 
418
  .pro-badge {
419
  display: inline-block;
420
- transform: skewX(-12deg);
421
- border: 1px solid rgba(229, 231, 235, 0.5);
422
  background: linear-gradient(to bottom right, #f9a8d4, #86efac, #fde047);
423
  color: black;
424
- padding: 4px 10px;
425
- border-radius: 8px;
426
- font-size: 0.875rem;
427
  font-weight: 700;
 
428
  letter-spacing: 0.025em;
429
  text-transform: uppercase;
430
- box-shadow: 0 10px 15px -3px rgba(34, 197, 94, 0.1),
431
- 0 4px 6px -4px rgba(34, 197, 94, 0.1);
432
  }
433
 
434
  /* Dark mode pro badge */
435
  :global(.dark) .pro-badge,
436
  :global([data-theme="dark"]) .pro-badge {
437
  background: linear-gradient(to bottom right, #ec4899, #22c55e, #eab308);
438
- border-color: rgba(255, 255, 255, 0.1);
439
- box-shadow: 0 10px 15px -3px rgba(34, 197, 94, 0.2),
440
- 0 4px 6px -4px rgba(34, 197, 94, 0.2);
441
  }
442
 
443
- .pdf-locked {
444
- display: flex;
 
445
  align-items: center;
 
 
 
 
 
446
  }
447
 
448
- .locked-message {
449
- color: var(--muted-color);
450
- font-size: 0.85em;
451
- display: flex;
452
  align-items: center;
453
- gap: 4px;
454
  }
455
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
456
  @media (max-width: 768px) {
457
- .pdf-pro-only {
458
- justify-content: flex-end;
 
 
459
  }
460
  }
461
  </style>
 
12
  affiliation?: string; // legacy single affiliation
13
  published?: string;
14
  doi?: string;
15
+ pdfProOnly?: boolean; // Gate PDF download to Pro users only
16
  }
17
 
18
  const {
 
24
  affiliation,
25
  published,
26
  doi,
27
+ pdfProOnly = false,
28
  } = Astro.props as Props;
29
 
30
  type Author = { name: string; url?: string; affiliationIndices?: number[] };
 
186
  </div>
187
  )} -->
188
  <div class="meta-container-cell meta-container-cell--pdf">
189
+ <div class="pdf-header-wrapper">
190
+ <h3>PDF</h3>
191
+ <span class="pro-badge-wrapper" style="display: none;">
192
+ <span class="pro-badge-prefix">- you are</span>
193
+ <span class="pro-badge">PRO</span>
194
+ </span>
195
+ <span class="pro-only-label" style="display: none;">
196
+ <span class="pro-only-dash">-</span>
197
+ <span class="pro-only-text">pro only</span>
198
+ <svg class="pro-only-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
199
+ <rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
200
+ <path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
201
+ </svg>
202
+ </span>
203
+ </div>
204
+ <div id="pdf-download-container" data-pdf-pro-only={pdfProOnly.toString()}>
205
  <p class="pdf-loading">Checking access...</p>
206
  <p class="pdf-pro-only" style="display: none;">
207
  <a
 
212
  >
213
  Download PDF
214
  </a>
 
 
 
 
215
  </p>
216
+ <div class="pdf-locked" style="display: none;">
217
+ <a
218
+ class="button button-locked"
219
+ href="https://huggingface.co/subscribe/pro"
220
+ target="_blank"
221
+ rel="noopener noreferrer"
222
+ >
223
+ <svg class="lock-icon" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" role="img" width="1em" height="1em" viewBox="0 0 12 12" fill="none">
224
+ <path d="M6.48 1.26c0 1.55.67 2.58 1.5 3.24.86.68 1.9 1 2.58 1.07v.86A5.3 5.3 0 0 0 7.99 7.5a3.95 3.95 0 0 0-1.51 3.24h-.96c0-1.55-.67-2.58-1.5-3.24a5.3 5.3 0 0 0-2.58-1.07v-.86A5.3 5.3 0 0 0 4.01 4.5a3.95 3.95 0 0 0 1.51-3.24h.96Z" fill="currentColor"></path>
225
+ </svg>
226
+ <span class="locked-title">Subscribe to Pro</span>
227
+ </a>
228
+ </div>
229
  </div>
230
  </div>
231
  </div>
 
234
  <script>
235
  // PDF access control for Pro users only
236
 
237
+ // ⚙️ Configuration for local development
238
+ const LOCAL_IS_PRO = false; // Set to true to test Pro access locally
239
+
240
  const FALLBACK_TIMEOUT_MS = 3000;
241
  let userPlanChecked = false;
242
 
243
+ // Check if PDF Pro gating is enabled
244
+ const pdfContainer = document.querySelector("#pdf-download-container") as HTMLElement;
245
+ const pdfProOnly = pdfContainer?.getAttribute("data-pdf-pro-only") === "true";
246
+
247
  /**
248
  * Check if user has Pro access
249
  * Isolated logic for Pro user verification
 
263
  const loadingEl = document.querySelector(".pdf-loading") as HTMLElement;
264
  const proOnlyEl = document.querySelector(".pdf-pro-only") as HTMLElement;
265
  const lockedEl = document.querySelector(".pdf-locked") as HTMLElement;
266
+ const proOnlyLabel = document.querySelector(".pro-only-label") as HTMLElement;
267
+ const proBadgeWrapper = document.querySelector(".pro-badge-wrapper") as HTMLElement;
268
 
269
  // Hide loading state
270
  if (loadingEl) loadingEl.style.display = "none";
271
 
272
+ // If PDF Pro gating is disabled, just show the download button
273
+ if (!pdfProOnly) {
274
+ if (proOnlyEl) proOnlyEl.style.display = "block";
275
+ if (proOnlyLabel) proOnlyLabel.style.display = "none";
276
+ if (lockedEl) lockedEl.style.display = "none";
277
+ if (proBadgeWrapper) proBadgeWrapper.style.display = "none";
278
+ return;
279
+ }
280
+
281
+ // Show appropriate state based on Pro status
282
  if (isPro) {
283
  if (proOnlyEl) proOnlyEl.style.display = "block";
284
+ if (proOnlyLabel) proOnlyLabel.style.display = "none";
285
+ if (lockedEl) lockedEl.style.display = "none";
286
+ if (proBadgeWrapper) proBadgeWrapper.style.display = "inline-flex";
287
  } else {
288
+ if (proOnlyEl) proOnlyEl.style.display = "none";
289
+ if (proOnlyLabel) proOnlyLabel.style.display = "inline-flex";
290
  if (lockedEl) lockedEl.style.display = "block";
291
+ if (proBadgeWrapper) proBadgeWrapper.style.display = "none";
292
  }
293
  }
294
 
 
306
 
307
  /**
308
  * Fallback behavior when no parent window responds
309
+ * Uses LOCAL_IS_PRO configuration for local development
310
  */
311
  function handleFallback() {
312
+ if (LOCAL_IS_PRO) {
313
+ handleUserPlan({ user: "pro" });
314
+ } else {
315
+ handleUserPlan({ user: "free" });
 
 
 
 
 
 
 
 
316
  }
317
+ }
318
 
319
+ // If PDF Pro gating is disabled, show the download button immediately
320
+ if (!pdfProOnly) {
321
+ updatePdfAccess(true);
 
 
 
 
 
 
 
 
322
  } else {
323
+ // Listen for messages from parent window (Hugging Face Spaces)
324
+ window.addEventListener("message", (event) => {
325
+ if (event.data.type === "USER_PLAN") {
326
+ handleUserPlan(event.data.plan);
327
+ }
328
+ });
329
+
330
+ // Request user plan on page load
331
+ if (window.parent && window.parent !== window) {
332
+ // We're in an iframe, request user plan
333
+ window.parent.postMessage({ type: "USER_PLAN_REQUEST" }, "*");
334
+
335
+ // Fallback if no response after timeout
336
+ setTimeout(() => {
337
+ if (!userPlanChecked) {
338
+ handleFallback();
339
+ }
340
+ }, FALLBACK_TIMEOUT_MS);
341
+ } else {
342
+ // Not in iframe (local development), use fallback immediately
343
+ handleFallback();
344
+ }
345
  }
346
  </script>
347
 
 
456
  }
457
 
458
  /* PDF access control styles */
459
+ .pdf-header-wrapper {
460
+ display: flex;
461
+ align-items: center;
462
+ gap: 6px;
463
+ line-height: 1;
464
+ }
465
+
466
+ .pdf-header-wrapper h3 {
467
+ line-height: 1;
468
+ }
469
+
470
  .pdf-loading {
471
  color: var(--muted-color);
472
  font-size: 0.9em;
473
  }
474
 
475
  .pdf-pro-only {
476
+ margin: 0;
477
+ }
478
+
479
+ .pro-badge-wrapper {
480
+ display: inline-flex;
481
  align-items: center;
482
+ gap: 5px;
483
+ font-style: normal;
484
+ }
485
+
486
+ .pro-badge-prefix {
487
+ font-size: 0.85em;
488
+ opacity: 0.5;
489
+ font-weight: 400;
490
+ font-style: normal;
491
  }
492
 
493
  .pro-badge {
494
  display: inline-block;
495
+ border: 1px solid rgba(0, 0, 0, 0.025);
 
496
  background: linear-gradient(to bottom right, #f9a8d4, #86efac, #fde047);
497
  color: black;
498
+ padding: 1px 5px;
499
+ border-radius: 3px;
500
+ font-size: 0.5rem;
501
  font-weight: 700;
502
+ font-style: normal;
503
  letter-spacing: 0.025em;
504
  text-transform: uppercase;
 
 
505
  }
506
 
507
  /* Dark mode pro badge */
508
  :global(.dark) .pro-badge,
509
  :global([data-theme="dark"]) .pro-badge {
510
  background: linear-gradient(to bottom right, #ec4899, #22c55e, #eab308);
511
+ border-color: rgba(255, 255, 255, 0.15);
 
 
512
  }
513
 
514
+ .pro-only-label {
515
+ display: inline-flex;
516
+ flex-direction: row;
517
  align-items: center;
518
+ gap: 5px;
519
+ font-size: 0.85em;
520
+ opacity: 0.5;
521
+ font-weight: 400;
522
+ line-height: 1;
523
  }
524
 
525
+ .pro-only-dash {
526
+ display: inline-flex;
 
 
527
  align-items: center;
528
+ line-height: 1;
529
  }
530
 
531
+ .pro-only-icon {
532
+ width: 11px;
533
+ height: 11px;
534
+ flex-shrink: 0;
535
+ display: inline-flex;
536
+ align-items: center;
537
+ }
538
+
539
+ .pro-only-text {
540
+ display: inline-flex;
541
+ align-items: center;
542
+ line-height: 1;
543
+ }
544
+
545
+ .pdf-locked {
546
+ display: block;
547
+ }
548
+
549
+ .button-locked {
550
+ display: inline-flex;
551
+ align-items: center;
552
+ gap: 6px;
553
+ background: linear-gradient(135deg,
554
+ var(--primary-color) 0%,
555
+ oklch(from var(--primary-color) calc(l - 0.1) calc(c + 0.05) calc(h - 60)) 100%);
556
+ border-radius: var(--button-radius);
557
+ padding: var(--button-padding-y) var(--button-padding-x);
558
+ font-size: var(--button-font-size);
559
+ line-height: 1;
560
+ color: var(--on-primary);
561
+ position: relative;
562
+ overflow: hidden;
563
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
564
+ font-weight: normal;
565
+ border-color: rgba(0, 0, 0, 0.15);
566
+ }
567
+
568
+ .button-locked:active {
569
+ transform: translateY(0);
570
+ }
571
+
572
+ .lock-icon {
573
+ font-size: 1em;
574
+ flex-shrink: 0;
575
+ position: relative;
576
+ z-index: 1;
577
+ }
578
+
579
+ .locked-title {
580
+ position: relative;
581
+ z-index: 1;
582
+ }
583
+
584
+ /* Dark mode locked button - inherits from light mode variables */
585
+
586
  @media (max-width: 768px) {
587
+ .meta-container-cell--pdf {
588
+ display: flex;
589
+ flex-direction: column;
590
+ align-items: flex-end;
591
  }
592
  }
593
  </style>
app/src/content/article.mdx CHANGED
@@ -6,9 +6,6 @@ authors:
6
  - name: "Thibaud Frere"
7
  url: "https://huggingface.co/tfrere"
8
  affiliations: [1]
9
- - name: "Thibaud Frere2"
10
- url: "https://huggingface.co/tfrere"
11
- affiliations: [1]
12
  affiliations:
13
  - name: "Hugging Face"
14
  url: "https://huggingface.co"
@@ -21,6 +18,7 @@ tags:
21
  - research
22
  - template
23
  tableOfContentsAutoCollapse: true
 
24
  ---
25
 
26
  import Introduction from "./chapters/demo/introduction.mdx";
 
6
  - name: "Thibaud Frere"
7
  url: "https://huggingface.co/tfrere"
8
  affiliations: [1]
 
 
 
9
  affiliations:
10
  - name: "Hugging Face"
11
  url: "https://huggingface.co"
 
18
  - research
19
  - template
20
  tableOfContentsAutoCollapse: true
21
+ pdfProOnly: false
22
  ---
23
 
24
  import Introduction from "./chapters/demo/introduction.mdx";
app/src/content/chapters/demo/components.mdx CHANGED
@@ -343,71 +343,6 @@ import Quote from '../../../components/Quote.astro'
343
  ```
344
  </Accordion>
345
 
346
- ### Iframes
347
-
348
- You can embed external content in your article using **iframes**. For example, **TrackIO**, **Gradio** or even **Github code embeds** can be used this way.
349
-
350
- <small className="muted">Gradio embed example</small>
351
- <div className="card">
352
- <iframe src="https://gradio-hello-world.hf.space" width="100%" height="380" frameborder="0"></iframe>
353
- </div>
354
- <div className="card">
355
- <iframe src="https://trackio-documentation.hf.space/?project=fake-training-750735&metrics=train_loss,train_accuracy&sidebar=hidden&lang=en" width="100%" height="630" frameborder="0"></iframe>
356
- </div>
357
- <Accordion title="Code example">
358
- ```mdx
359
- <iframe frameborder="0" scrolling="no" style="width:100%; height:292px;" allow="clipboard-write" src="https://emgithub.com/iframe.html?target=https%3A%2F%2Fgithub.com%2Fhuggingface%2Fpicotron%2Fblob%2F1004ae37b87887cde597c9060fb067faa060bafe%2Fsetup.py&style=default&type=code&showBorder=on&showLineNumbers=on"></iframe>
360
- <iframe src="https://trackio-documentation.hf.space/?project=fake-training-750735&metrics=train_loss,train_accuracy&sidebar=hidden&lang=en" width="100%" height="600" frameborder="0"></iframe>
361
- <iframe src="https://gradio-hello-world.hf.space" width="100%" height="380" frameborder="0"></iframe>
362
- ```
363
- </Accordion>
364
-
365
-
366
- ### HtmlEmbed
367
-
368
- The main purpose of the ```HtmlEmbed``` component is to **embed** a **Plotly** or **D3.js** chart in your article. **Libraries** are already imported in the template.
369
-
370
- They exist in the `app/src/content/embeds` folder.
371
-
372
- For researchers who want to stay in **Python** while targeting **D3**, the [d3blocks](https://github.com/d3blocks/d3blocks) library lets you create interactive D3 charts with only a few lines of code. In **2025**, **D3** often provides more flexibility and a more web‑native rendering than **Plotly** for custom visualizations.
373
-
374
-
375
- <HtmlEmbed src="d3-line.html" title="This is a chart title" desc="Figure X: Some chart description<br/>Credit: <a href='https://example.com' target='_blank'>Example</a>" />
376
-
377
- | Prop | Required | Description
378
- |-------------|----------|----------------------------------------------------------------------------------
379
- | `src` | Yes | Path to the embed file in the `embeds` folder.
380
- | `title` | No | Short title displayed above the card.
381
- | `desc` | No | Short description displayed below the card. Supports inline HTML (e.g., links).
382
- | `frameless` | No | Removes the card background and border for seamless embeds.
383
- | `align` | No | Aligns the title/description text. One of `left` (default), `center`, `right`.
384
- | `id` | No | Adds an `id` to the outer figure for deep-linking and cross-references.
385
- | `data` | No | Path (string) or array of paths (string[]) to data file(s) consumed by the embed.
386
- | `config` | No | Optional object for embed options (e.g., `{ defaultMetric: 'average_rank' }`).
387
-
388
- <Accordion title="Code example">
389
- ```mdx
390
- import HtmlEmbed from '../../../components/HtmlEmbed.astro'
391
-
392
- <HtmlEmbed src="d3-line.html" title="This is a chart title" desc="Some chart description <br/>Credit: <a href='https://example.com' target='_blank'>Example</a>" />
393
-
394
- <HtmlEmbed
395
- src="d3-line.html"
396
- title="Comparison A vs B"
397
- data={[ 'formatting_filters.csv', 'relevance_filters.csv' ]}
398
- config={{ defaultMetric: 'average_rank' }}
399
- />
400
- ```
401
- </Accordion>
402
-
403
-
404
- #### Data
405
-
406
- If you need to link your **HTML embeds** to **data files**, there is an **`assets/data`** folder for this.
407
- As long as your files are there, they will be served from the **`public/data`** folder.
408
- You can fetch them with this address: **`[domain]/data/your-data.ext`**
409
-
410
- <Note emoji="⚠️" variant="danger"><b>Be careful</b>, unlike images, <b>data files are not optimized</b> by Astro. You need to optimize them manually.</Note>
411
 
412
  ### Glossary
413
 
@@ -536,25 +471,69 @@ import Stack from '../../../components/Stack.astro'
536
  ```
537
  </Accordion>
538
 
539
- **Layout options:**
540
- - **2-column**: Two equal-width columns
541
- - **3-column**: Three equal-width columns
542
- - **4-column**: Four equal-width columns
543
- - **auto**: Responsive grid that adapts to content and screen size
544
 
545
- **Gap options:**
546
- - **small**: 0.5rem spacing
547
- - **medium**: 1rem spacing (default)
548
- - **large**: 1.5rem spacing
549
- - **custom**: Any CSS value (e.g., "2rem", "20px", "1.5em")
550
 
551
- **Responsive behavior:**
552
- - **Mobile (≤768px)**: All layouts collapse to single column
553
- - **Tablet (769px-1100px)**: 3-column and 4-column layouts become 2-column
554
- - **Desktop (>1100px)**: Full layout as specified
555
 
556
- **Use cases:**
557
- - **Feature comparisons** and side-by-side content
558
- - **Multi-column text** and documentation
559
- - **Responsive card layouts** and grids
560
- - **Flexible content organization** for varying screen sizes
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
343
  ```
344
  </Accordion>
345
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
346
 
347
  ### Glossary
348
 
 
471
  ```
472
  </Accordion>
473
 
 
 
 
 
 
474
 
475
+ ### Iframes
 
 
 
 
476
 
477
+ You can embed external content in your article using **iframes**. For example, **TrackIO**, **Gradio** or even **Github code embeds** can be used this way.
 
 
 
478
 
479
+ <small className="muted">Gradio embed example</small>
480
+ <div className="card">
481
+ <iframe src="https://gradio-hello-world.hf.space" width="100%" height="380" frameborder="0"></iframe>
482
+ </div>
483
+ <div className="card">
484
+ <iframe src="https://trackio-documentation.hf.space/?project=fake-training-750735&metrics=train_loss,train_accuracy&sidebar=hidden&lang=en" width="100%" height="630" frameborder="0"></iframe>
485
+ </div>
486
+ <Accordion title="Code example">
487
+ ```mdx
488
+ <iframe frameborder="0" scrolling="no" style="width:100%; height:292px;" allow="clipboard-write" src="https://emgithub.com/iframe.html?target=https%3A%2F%2Fgithub.com%2Fhuggingface%2Fpicotron%2Fblob%2F1004ae37b87887cde597c9060fb067faa060bafe%2Fsetup.py&style=default&type=code&showBorder=on&showLineNumbers=on"></iframe>
489
+ <iframe src="https://trackio-documentation.hf.space/?project=fake-training-750735&metrics=train_loss,train_accuracy&sidebar=hidden&lang=en" width="100%" height="600" frameborder="0"></iframe>
490
+ <iframe src="https://gradio-hello-world.hf.space" width="100%" height="380" frameborder="0"></iframe>
491
+ ```
492
+ </Accordion>
493
+
494
+
495
+ ### HtmlEmbed
496
+
497
+ The main purpose of the ```HtmlEmbed``` component is to **embed** a **Plotly** or **D3.js** chart in your article. **Libraries** are already imported in the template.
498
+
499
+ They exist in the `app/src/content/embeds` folder.
500
+
501
+ For researchers who want to stay in **Python** while targeting **D3**, the [d3blocks](https://github.com/d3blocks/d3blocks) library lets you create interactive D3 charts with only a few lines of code. In **2025**, **D3** often provides more flexibility and a more web‑native rendering than **Plotly** for custom visualizations.
502
+
503
+
504
+ <HtmlEmbed src="d3-line.html" title="This is a chart title" desc="Figure X: Some chart description<br/>Credit: <a href='https://example.com' target='_blank'>Example</a>" />
505
+
506
+ | Prop | Required | Description
507
+ |-------------|----------|----------------------------------------------------------------------------------
508
+ | `src` | Yes | Path to the embed file in the `embeds` folder.
509
+ | `title` | No | Short title displayed above the card.
510
+ | `desc` | No | Short description displayed below the card. Supports inline HTML (e.g., links).
511
+ | `frameless` | No | Removes the card background and border for seamless embeds.
512
+ | `align` | No | Aligns the title/description text. One of `left` (default), `center`, `right`.
513
+ | `id` | No | Adds an `id` to the outer figure for deep-linking and cross-references.
514
+ | `data` | No | Path (string) or array of paths (string[]) to data file(s) consumed by the embed.
515
+ | `config` | No | Optional object for embed options (e.g., `{ defaultMetric: 'average_rank' }`).
516
+
517
+ <Accordion title="Code example">
518
+ ```mdx
519
+ import HtmlEmbed from '../../../components/HtmlEmbed.astro'
520
+
521
+ <HtmlEmbed src="d3-line.html" title="This is a chart title" desc="Some chart description <br/>Credit: <a href='https://example.com' target='_blank'>Example</a>" />
522
+
523
+ <HtmlEmbed
524
+ src="d3-line.html"
525
+ title="Comparison A vs B"
526
+ data={[ 'formatting_filters.csv', 'relevance_filters.csv' ]}
527
+ config={{ defaultMetric: 'average_rank' }}
528
+ />
529
+ ```
530
+ </Accordion>
531
+
532
+
533
+ #### Data
534
+
535
+ If you need to link your **HTML embeds** to **data files**, there is an **`assets/data`** folder for this.
536
+ As long as your files are there, they will be served from the **`public/data`** folder.
537
+ You can fetch them with this address: **`[domain]/data/your-data.ext`**
538
+
539
+ <Note emoji="⚠️" variant="danger"><b>Be careful</b>, unlike images, <b>data files are not optimized</b> by Astro. You need to optimize them manually.</Note>
app/src/content/chapters/demo/writing-your-content.mdx CHANGED
@@ -58,9 +58,10 @@ affiliations:
58
  - name: "Example University"
59
  url: "https://example.edu"
60
  doi: 10.1234/abcd.efgh
61
- licence: Diagrams and text are licensed under <a href="https://creativecommons.org/licenses/by/4.0/" target="_blank" rel="noopener noreferrer">CC‑BY 4.0</a> with the source available on <a href="https://huggingface.co/spaces/stfrere/research-article-template">Hugging Face</a>, unless noted otherwise. Figures reused from other sources are excluded and marked in their captions (Figure from …”).
62
  seoThumbImage: "https://example.com/thumb.png"
63
  tableOfContentsAutoCollapse: true
 
64
  ---
65
  ```
66
 
@@ -78,6 +79,7 @@ tableOfContentsAutoCollapse: true
78
  | licence | No | Rendered in footer; HTML supported | string (HTML allowed) |
79
  | seoThumbImage | No | Overrides default OpenGraph image | string (URL) |
80
  | tableOfContentsAutoCollapse | No | Controls TOC auto-collapse | boolean |
 
81
  </Accordion>
82
 
83
  #### **Content**
 
58
  - name: "Example University"
59
  url: "https://example.edu"
60
  doi: 10.1234/abcd.efgh
61
+ licence: Diagrams and text are licensed under <a href="https://creativecommons.org/licenses/by/4.0/" target="_blank" rel="noopener noreferrer">CC‑BY 4.0</a> with the source available on <a href="https://huggingface.co/spaces/stfrere/research-article-template">Hugging Face</a>, unless noted otherwise. Figures reused from other sources are excluded and marked in their captions ("Figure from …").
62
  seoThumbImage: "https://example.com/thumb.png"
63
  tableOfContentsAutoCollapse: true
64
+ pdfProOnly: false
65
  ---
66
  ```
67
 
 
79
  | licence | No | Rendered in footer; HTML supported | string (HTML allowed) |
80
  | seoThumbImage | No | Overrides default OpenGraph image | string (URL) |
81
  | tableOfContentsAutoCollapse | No | Controls TOC auto-collapse | boolean |
82
+ | pdfProOnly | No | Gate PDF download to Pro users only. Shows "Subscribe to Pro" button for non-Pro users | boolean |
83
  </Accordion>
84
 
85
  #### **Content**
app/src/pages/index.astro CHANGED
@@ -264,6 +264,7 @@ const licence =
264
  affiliation={articleFM?.affiliation}
265
  published={articleFM?.published}
266
  doi={doi}
 
267
  />
268
 
269
  <section class="content-grid">
 
264
  affiliation={articleFM?.affiliation}
265
  published={articleFM?.published}
266
  doi={doi}
267
+ pdfProOnly={articleFM?.pdfProOnly}
268
  />
269
 
270
  <section class="content-grid">
app/src/styles/_variables.css CHANGED
@@ -62,8 +62,8 @@
62
 
63
  /* Button tokens */
64
  --button-radius: 6px;
65
- --button-padding-x: 12px;
66
- --button-padding-y: 8px;
67
  --button-font-size: 14px;
68
  --button-icon-padding: 8px;
69
  /* Big button */
 
62
 
63
  /* Button tokens */
64
  --button-radius: 6px;
65
+ --button-padding-x: 16px;
66
+ --button-padding-y: 10px;
67
  --button-font-size: 14px;
68
  --button-icon-padding: 8px;
69
  /* Big button */