thibaud frere commited on
Commit
b2ac38d
·
1 Parent(s): e329457
README.md CHANGED
@@ -1,5 +1,5 @@
1
  ---
2
- title: 'The science article template'
3
  emoji: 📝
4
  colorFrom: blue
5
  colorTo: indigo
 
1
  ---
2
+ title: 'Research article template'
3
  emoji: 📝
4
  colorFrom: blue
5
  colorTo: indigo
app/.astro/astro/content.d.ts CHANGED
@@ -0,0 +1,169 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ declare module 'astro:content' {
2
+ interface Render {
3
+ '.mdx': Promise<{
4
+ Content: import('astro').MarkdownInstance<{}>['Content'];
5
+ headings: import('astro').MarkdownHeading[];
6
+ remarkPluginFrontmatter: Record<string, any>;
7
+ components: import('astro').MDXInstance<{}>['components'];
8
+ }>;
9
+ }
10
+ }
11
+
12
+ declare module 'astro:content' {
13
+ interface RenderResult {
14
+ Content: import('astro/runtime/server/index.js').AstroComponentFactory;
15
+ headings: import('astro').MarkdownHeading[];
16
+ remarkPluginFrontmatter: Record<string, any>;
17
+ }
18
+ interface Render {
19
+ '.md': Promise<RenderResult>;
20
+ }
21
+
22
+ export interface RenderedContent {
23
+ html: string;
24
+ metadata?: {
25
+ imagePaths: Array<string>;
26
+ [key: string]: unknown;
27
+ };
28
+ }
29
+ }
30
+
31
+ declare module 'astro:content' {
32
+ type Flatten<T> = T extends { [K: string]: infer U } ? U : never;
33
+
34
+ export type CollectionKey = keyof AnyEntryMap;
35
+ export type CollectionEntry<C extends CollectionKey> = Flatten<AnyEntryMap[C]>;
36
+
37
+ export type ContentCollectionKey = keyof ContentEntryMap;
38
+ export type DataCollectionKey = keyof DataEntryMap;
39
+
40
+ type AllValuesOf<T> = T extends any ? T[keyof T] : never;
41
+ type ValidContentEntrySlug<C extends keyof ContentEntryMap> = AllValuesOf<
42
+ ContentEntryMap[C]
43
+ >['slug'];
44
+
45
+ /** @deprecated Use `getEntry` instead. */
46
+ export function getEntryBySlug<
47
+ C extends keyof ContentEntryMap,
48
+ E extends ValidContentEntrySlug<C> | (string & {}),
49
+ >(
50
+ collection: C,
51
+ // Note that this has to accept a regular string too, for SSR
52
+ entrySlug: E,
53
+ ): E extends ValidContentEntrySlug<C>
54
+ ? Promise<CollectionEntry<C>>
55
+ : Promise<CollectionEntry<C> | undefined>;
56
+
57
+ /** @deprecated Use `getEntry` instead. */
58
+ export function getDataEntryById<C extends keyof DataEntryMap, E extends keyof DataEntryMap[C]>(
59
+ collection: C,
60
+ entryId: E,
61
+ ): Promise<CollectionEntry<C>>;
62
+
63
+ export function getCollection<C extends keyof AnyEntryMap, E extends CollectionEntry<C>>(
64
+ collection: C,
65
+ filter?: (entry: CollectionEntry<C>) => entry is E,
66
+ ): Promise<E[]>;
67
+ export function getCollection<C extends keyof AnyEntryMap>(
68
+ collection: C,
69
+ filter?: (entry: CollectionEntry<C>) => unknown,
70
+ ): Promise<CollectionEntry<C>[]>;
71
+
72
+ export function getEntry<
73
+ C extends keyof ContentEntryMap,
74
+ E extends ValidContentEntrySlug<C> | (string & {}),
75
+ >(entry: {
76
+ collection: C;
77
+ slug: E;
78
+ }): E extends ValidContentEntrySlug<C>
79
+ ? Promise<CollectionEntry<C>>
80
+ : Promise<CollectionEntry<C> | undefined>;
81
+ export function getEntry<
82
+ C extends keyof DataEntryMap,
83
+ E extends keyof DataEntryMap[C] | (string & {}),
84
+ >(entry: {
85
+ collection: C;
86
+ id: E;
87
+ }): E extends keyof DataEntryMap[C]
88
+ ? Promise<DataEntryMap[C][E]>
89
+ : Promise<CollectionEntry<C> | undefined>;
90
+ export function getEntry<
91
+ C extends keyof ContentEntryMap,
92
+ E extends ValidContentEntrySlug<C> | (string & {}),
93
+ >(
94
+ collection: C,
95
+ slug: E,
96
+ ): E extends ValidContentEntrySlug<C>
97
+ ? Promise<CollectionEntry<C>>
98
+ : Promise<CollectionEntry<C> | undefined>;
99
+ export function getEntry<
100
+ C extends keyof DataEntryMap,
101
+ E extends keyof DataEntryMap[C] | (string & {}),
102
+ >(
103
+ collection: C,
104
+ id: E,
105
+ ): E extends keyof DataEntryMap[C]
106
+ ? Promise<DataEntryMap[C][E]>
107
+ : Promise<CollectionEntry<C> | undefined>;
108
+
109
+ /** Resolve an array of entry references from the same collection */
110
+ export function getEntries<C extends keyof ContentEntryMap>(
111
+ entries: {
112
+ collection: C;
113
+ slug: ValidContentEntrySlug<C>;
114
+ }[],
115
+ ): Promise<CollectionEntry<C>[]>;
116
+ export function getEntries<C extends keyof DataEntryMap>(
117
+ entries: {
118
+ collection: C;
119
+ id: keyof DataEntryMap[C];
120
+ }[],
121
+ ): Promise<CollectionEntry<C>[]>;
122
+
123
+ export function render<C extends keyof AnyEntryMap>(
124
+ entry: AnyEntryMap[C][string],
125
+ ): Promise<RenderResult>;
126
+
127
+ export function reference<C extends keyof AnyEntryMap>(
128
+ collection: C,
129
+ ): import('astro/zod').ZodEffects<
130
+ import('astro/zod').ZodString,
131
+ C extends keyof ContentEntryMap
132
+ ? {
133
+ collection: C;
134
+ slug: ValidContentEntrySlug<C>;
135
+ }
136
+ : {
137
+ collection: C;
138
+ id: keyof DataEntryMap[C];
139
+ }
140
+ >;
141
+ // Allow generic `string` to avoid excessive type errors in the config
142
+ // if `dev` is not running to update as you edit.
143
+ // Invalid collection names will be caught at build time.
144
+ export function reference<C extends string>(
145
+ collection: C,
146
+ ): import('astro/zod').ZodEffects<import('astro/zod').ZodString, never>;
147
+
148
+ type ReturnTypeOrOriginal<T> = T extends (...args: any[]) => infer R ? R : T;
149
+ type InferEntrySchema<C extends keyof AnyEntryMap> = import('astro/zod').infer<
150
+ ReturnTypeOrOriginal<Required<ContentConfig['collections'][C]>['schema']>
151
+ >;
152
+
153
+ type ContentEntryMap = {
154
+
155
+ };
156
+
157
+ type DataEntryMap = {
158
+ "fragments": Record<string, {
159
+ id: string;
160
+ collection: "fragments";
161
+ data: any;
162
+ }>;
163
+
164
+ };
165
+
166
+ type AnyEntryMap = ContentEntryMap & DataEntryMap;
167
+
168
+ export type ContentConfig = never;
169
+ }
app/scripts/export-pdf.mjs CHANGED
@@ -2,7 +2,7 @@
2
  import { spawn } from 'node:child_process';
3
  import { setTimeout as delay } from 'node:timers/promises';
4
  import { chromium } from 'playwright';
5
- import { resolve, basename } from 'node:path';
6
  import { promises as fs } from 'node:fs';
7
  import process from 'node:process';
8
 
@@ -180,26 +180,12 @@ async function main() {
180
  });
181
  console.log(`✅ PDF generated: ${outPath}`);
182
 
183
- // Compatibility copy into dist/article.pdf (for servers that only serve dist)
184
- const distCompatPath = resolve(cwd, 'dist', 'article.pdf');
185
- try {
186
- if (basename(outPath) !== 'article.pdf') {
187
- await fs.copyFile(outPath, distCompatPath);
188
- console.log(`✅ PDF copied (dist compat): ${distCompatPath}`);
189
- }
190
- } catch (e) {
191
- console.warn('Unable to copy PDF to dist/article.pdf:', e?.message || e);
192
- }
193
-
194
- // Also copy into public under 2 names: slug.pdf and article.pdf (compat)
195
  const publicSlugPath = resolve(cwd, 'public', `${outFileBase}.pdf`);
196
- const publicCompatPath = resolve(cwd, 'public', 'article.pdf');
197
  try {
198
  await fs.mkdir(resolve(cwd, 'public'), { recursive: true });
199
  await fs.copyFile(outPath, publicSlugPath);
200
- await fs.copyFile(outPath, publicCompatPath);
201
  console.log(`✅ PDF copied to: ${publicSlugPath}`);
202
- console.log(`✅ PDF copied (compat): ${publicCompatPath}`);
203
  } catch (e) {
204
  console.warn('Unable to copy PDF to public/:', e?.message || e);
205
  }
 
2
  import { spawn } from 'node:child_process';
3
  import { setTimeout as delay } from 'node:timers/promises';
4
  import { chromium } from 'playwright';
5
+ import { resolve } from 'node:path';
6
  import { promises as fs } from 'node:fs';
7
  import process from 'node:process';
8
 
 
180
  });
181
  console.log(`✅ PDF generated: ${outPath}`);
182
 
183
+ // Copy into public only under the slugified name
 
 
 
 
 
 
 
 
 
 
 
184
  const publicSlugPath = resolve(cwd, 'public', `${outFileBase}.pdf`);
 
185
  try {
186
  await fs.mkdir(resolve(cwd, 'public'), { recursive: true });
187
  await fs.copyFile(outPath, publicSlugPath);
 
188
  console.log(`✅ PDF copied to: ${publicSlugPath}`);
 
189
  } catch (e) {
190
  console.warn('Unable to copy PDF to public/:', e?.message || e);
191
  }
app/src/components/Header.astro CHANGED
@@ -1,4 +1,6 @@
1
  ---
 
 
2
  interface Props {
3
  title: string;
4
  description?: string;
@@ -6,11 +8,10 @@ interface Props {
6
  const { title, description } = Astro.props as Props;
7
  ---
8
  <section class="hero">
9
- <h1 class="hero-title">{title}</h1>
10
  <div class="hero-banner">
11
- <slot name="banner" />
12
  {description && <p class="hero-desc">{description}</p>}
13
  </div>
14
  </section>
15
 
16
-
 
1
  ---
2
+ import HtmlFragment from "./HtmlFragment.astro";
3
+
4
  interface Props {
5
  title: string;
6
  description?: string;
 
8
  const { title, description } = Astro.props as Props;
9
  ---
10
  <section class="hero">
11
+ <h1 class="hero-title" set:html={title}></h1>
12
  <div class="hero-banner">
13
+ <HtmlFragment src="banner.html" />
14
  {description && <p class="hero-desc">{description}</p>}
15
  </div>
16
  </section>
17
 
 
app/src/content/article.mdx CHANGED
@@ -1,6 +1,7 @@
1
  ---
2
  title: "From Idea to Interactive:\n A Modern Template for Scientific Writing
3
  "
 
4
  description: "A modern, MDX-first research article template with math, citations, and interactive figures."
5
  authors:
6
  - "John Doe"
@@ -40,7 +41,7 @@ import visualPoster from "../assets/images/visual-vocabulary-poster.png";
40
 
41
  This template is inspired by [**Distill**](https://distill.pub); we aim to preserve the best of it while modernizing the stack. Their work is highly inspiring.
42
 
43
- ## Features
44
 
45
  <div className="tag-list">
46
  <span className="tag">Markdown based</span>
@@ -94,7 +95,7 @@ The easiest way to get online is to clone [this Hugging Face Space](https://hugg
94
 
95
  ### Large files (Git LFS)
96
 
97
- Track binaries (e.g., `.png`, `.wav`) with Git LFS to keep the repository lean. This project is preconfigured to store such files via LFS.
98
 
99
 
100
  ## Writing Your Content
@@ -103,15 +104,16 @@ Track binaries (e.g., `.png`, `.wav`) with Git LFS to keep the repository lean.
103
 
104
  Your article lives in two places:
105
 
106
- - `app/src/content/` — where you can find the article.mdx and bibliography.bib.
107
  - `app/src/assets/` — images, audio, and other static assets. (handled by git lfs)
108
 
109
- The initial skeleton of an article looks like this:
110
 
111
  ```mdx
112
  {/* HEADER */}
113
  ---
114
- title: "The science template:\nCraft Beautiful Blogs"
 
115
  description: "A modern, MDX-first research article template with math, citations, and interactive figures."
116
  authors:
117
  - "John Doe"
@@ -139,7 +141,7 @@ This is a short paragraph written in Markdown. Below is an example image:
139
 
140
 
141
 
142
- **Available blocs**:
143
 
144
  <div className="tag-list">
145
  <a className="tag" href="#math">Math</a>
@@ -177,7 +179,7 @@ $$
177
 
178
  ### Images
179
 
180
- Responsive images automatically generate an optimized `srcset` and `sizes` so the browser downloads the most appropriate file for the current viewport and DPR. You can also request multiple output formats (e.g., **AVIF**, **WebP**, fallback **PNG/JPEG**) and control lazy loading/decoding for better performance.
181
 
182
  **Optional:** Zoomable (Medium-like lightbox): add `data-zoomable` to opt-in. Only images with this attribute will open full-screen on click.
183
 
@@ -209,7 +211,7 @@ import myImage from '../assets/images/placeholder.jpg'
209
  <Image src={myImage} data-zoomable alt="Example with caption and credit" loading="lazy" />
210
  <figcaption>
211
  Optimized image with a descriptive caption.
212
- <span className="image-credit">Crédit: Photo by <a href="https://example.com">Author</a></span>
213
  </figcaption>
214
  </figure>
215
  ```
@@ -243,11 +245,11 @@ greet("Astro")
243
 
244
  Here are a few variations using the same bibliography:
245
 
246
- 1) In-text citation with brackets: [@example2023].
247
 
248
- 2) Narrative citation: As shown by @vaswani2017attention, transformers enable efficient sequence modeling.
249
 
250
- 3) Multiple citations and a footnote together: see [@vaswani2017attention; @example2023] for related work. Also note this footnote[^f1].
251
 
252
  [^f1]: Footnote attached to the sentence above.
253
 
@@ -265,9 +267,9 @@ Here are a few variations using the same bibliography:
265
  ### Asides
266
 
267
  <Aside>
268
- This paragraph presents a key idea concisely.
269
  <Fragment slot="aside">
270
- Side note for brief context or a definition.
271
  </Fragment>
272
  </Aside>
273
 
@@ -348,7 +350,7 @@ import audioDemo from '../assets/audio/audio-example.wav'
348
 
349
  #### Html Fragments
350
 
351
- The main purpose of the ```HtmlFragment``` component is to embed a plotly or d3 chart in your article. Libraries are already imported in the template.
352
 
353
  <div className="plot-card">
354
  <HtmlFragment src="line.html" />
@@ -362,7 +364,7 @@ import HtmlFragment from '../components/HtmlFragment.astro'
362
 
363
  #### Iframes
364
 
365
- You can embed external content in your article using **iframes**. For example, TrackIOa lightweight dashboard to monitor machine learning experiments—can be used this way. The example below opens a demo project and displays a couple of metrics. You can customize the query parameters (`project`, `metrics`, `sidebar`, etc.) to fit your needs.
366
 
367
  <iframe className="plot-card" 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>
368
 
@@ -407,14 +409,14 @@ Choosing colors well is critical: a **palette** encodes **meaning** (categories,
407
 
408
  ### Use the right chart
409
 
410
- Picking the right visualization depends on your goal (compare values, show distribution, part-to-whole, trends, relationships, etc.). The Visual Vocabulary poster below provides a concise mapping from analytical task to chart types.
411
 
412
  <figure>
413
- <a href="https://ft-interactive.github.io/visual-vocabulary/" target="_blank" rel="noopener noreferrer">
414
  <Image src={visualPoster} alt="Visual Vocabulary: choosing the right chart by task" />
415
  </a>
416
  <figcaption>
417
- Visual Vocabulary: a handy reference to select chart types by purpose (comparison, distribution, part-to-whole, correlation, and more).
418
  — <a href="https://ft-interactive.github.io/visual-vocabulary/" target="_blank" rel="noopener noreferrer">Website</a>
419
  </figcaption>
420
  </figure>
@@ -422,5 +424,5 @@ Picking the right visualization depends on your goal (compare values, show distr
422
 
423
  ## Conclusions
424
 
425
- This template provides a practical baseline for writing and sharing technical articles with math, citations, and interactive figures. Start simple, iterate on structure and style, and keep content maintainable for future readers and collaborators.
426
 
 
1
  ---
2
  title: "From Idea to Interactive:\n A Modern Template for Scientific Writing
3
  "
4
+ subtitle: "It's nice to have a cute interactive banner"
5
  description: "A modern, MDX-first research article template with math, citations, and interactive figures."
6
  authors:
7
  - "John Doe"
 
41
 
42
  This template is inspired by [**Distill**](https://distill.pub); we aim to preserve the best of it while modernizing the stack. Their work is highly inspiring.
43
 
44
+ #### Features
45
 
46
  <div className="tag-list">
47
  <span className="tag">Markdown based</span>
 
95
 
96
  ### Large files (Git LFS)
97
 
98
+ **Track binaries** (e.g., `.png`, `.wav`) with **Git LFS** to keep the repository lean. This project is preconfigured to store such files via **LFS**.
99
 
100
 
101
  ## Writing Your Content
 
104
 
105
  Your article lives in two places:
106
 
107
+ - `app/src/content/` — where you can find the article.mdx, bibliography.bib and html fragments.
108
  - `app/src/assets/` — images, audio, and other static assets. (handled by git lfs)
109
 
110
+ The **initial skeleton** of an article looks like this:
111
 
112
  ```mdx
113
  {/* HEADER */}
114
  ---
115
+ title: "This is the main title"
116
+ subtitle: "This will be displayed just below the banner"
117
  description: "A modern, MDX-first research article template with math, citations, and interactive figures."
118
  authors:
119
  - "John Doe"
 
141
 
142
 
143
 
144
+ **Available blocks**:
145
 
146
  <div className="tag-list">
147
  <a className="tag" href="#math">Math</a>
 
179
 
180
  ### Images
181
 
182
+ **Responsive images** automatically generate an optimized `srcset` and `sizes` so the browser downloads the most appropriate file for the current viewport and DPR. You can also request multiple output formats (e.g., **AVIF**, **WebP**, fallback **PNG/JPEG**) and control **lazy loading/decoding** for better **performance**.
183
 
184
  **Optional:** Zoomable (Medium-like lightbox): add `data-zoomable` to opt-in. Only images with this attribute will open full-screen on click.
185
 
 
211
  <Image src={myImage} data-zoomable alt="Example with caption and credit" loading="lazy" />
212
  <figcaption>
213
  Optimized image with a descriptive caption.
214
+ <span className="image-credit">Credit: Photo by <a href="https://example.com">Author</a></span>
215
  </figcaption>
216
  </figure>
217
  ```
 
245
 
246
  Here are a few variations using the same bibliography:
247
 
248
+ 1) **In-text citation** with brackets: [@example2023].
249
 
250
+ 2) **Narrative citation**: As shown by @vaswani2017attention, transformers enable efficient sequence modeling.
251
 
252
+ 3) **Multiple citations** and a **footnote** together: see [@vaswani2017attention; @example2023] for related work. Also note this footnote[^f1].
253
 
254
  [^f1]: Footnote attached to the sentence above.
255
 
 
267
  ### Asides
268
 
269
  <Aside>
270
+ This paragraph presents a **key idea** concisely.
271
  <Fragment slot="aside">
272
+ **Side note** for brief context or a definition.
273
  </Fragment>
274
  </Aside>
275
 
 
350
 
351
  #### Html Fragments
352
 
353
+ The main purpose of the ```HtmlFragment``` component is to embed a **Plotly** or **D3.js** chart in your article. Libraries are already imported in the template.
354
 
355
  <div className="plot-card">
356
  <HtmlFragment src="line.html" />
 
364
 
365
  #### Iframes
366
 
367
+ You can embed external content in your article using **iframes**. For example, **TrackIO**—a lightweight dashboard to monitor machine learning experiments—can be used this way. The example below opens a demo project and displays a couple of metrics. You can customize the **query parameters** (`project`, `metrics`, `sidebar`, etc.) to fit your needs.
368
 
369
  <iframe className="plot-card" 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>
370
 
 
409
 
410
  ### Use the right chart
411
 
412
+ Picking the right visualization depends on your goal (compare values, show distribution, part-to-whole, trends, relationships, etc.). The Visual Vocabulary poster below provides a concise mapping from **analytical task** to **chart types**.
413
 
414
  <figure>
415
+ <a href={visualPoster.src} target="_blank" rel="noopener noreferrer">
416
  <Image src={visualPoster} alt="Visual Vocabulary: choosing the right chart by task" />
417
  </a>
418
  <figcaption>
419
+ A handy reference to select chart types by purpose. Click to enlarge.
420
  — <a href="https://ft-interactive.github.io/visual-vocabulary/" target="_blank" rel="noopener noreferrer">Website</a>
421
  </figcaption>
422
  </figure>
 
424
 
425
  ## Conclusions
426
 
427
+ This template provides a **practical baseline** for writing and sharing technical articles with **math**, **citations**, and **interactive figures**. **Start simple**, **iterate** on structure and style, and keep content **maintainable** for future readers and collaborators.
428
 
app/src/pages/index.astro CHANGED
@@ -3,6 +3,7 @@ import Article, { frontmatter as articleFM } from '../content/article.mdx';
3
  import Meta from '../components/Meta.astro';
4
  import HtmlFragment from '../components/HtmlFragment.astro';
5
  import Footer from '../components/Footer.astro';
 
6
  import ThemeToggle from '../components/ThemeToggle.astro';
7
  import SeoHead from '../components/SeoHead.astro';
8
  import ogDefault from '../assets/images/visual-vocabulary-poster.png';
@@ -13,6 +14,7 @@ const docTitle = articleFM?.title ?? 'Untitled article';
13
  const docTitleHtml = (articleFM?.title ?? 'Untitled article')
14
  .replace(/\\n/g, '<br/>')
15
  .replace(/\n/g, '<br/>');
 
16
  const description = articleFM?.description ?? '';
17
  const authors = articleFM?.authors ?? [];
18
  const published = articleFM?.published ?? undefined;
@@ -71,13 +73,8 @@ const bibtex = `@misc{${bibKey},\n title={${titleFlat}},\n author={${authorsBi
71
  </head>
72
  <body>
73
  <ThemeToggle />
74
- <section class="hero">
75
- <h1 class="hero-title" set:html={docTitleHtml}></h1>
76
- <div class="hero-banner">
77
- <HtmlFragment src="banner.html" />
78
- <p class="hero-desc">It's nice to have a cute interactive banner!</p>
79
- </div>
80
- </section>
81
 
82
  <section class="article-header">
83
  <Meta title={docTitle} authors={articleFM?.authors} affiliation={articleFM?.affiliation} published={articleFM?.published} />
 
3
  import Meta from '../components/Meta.astro';
4
  import HtmlFragment from '../components/HtmlFragment.astro';
5
  import Footer from '../components/Footer.astro';
6
+ import Header from '../components/Header.astro';
7
  import ThemeToggle from '../components/ThemeToggle.astro';
8
  import SeoHead from '../components/SeoHead.astro';
9
  import ogDefault from '../assets/images/visual-vocabulary-poster.png';
 
14
  const docTitleHtml = (articleFM?.title ?? 'Untitled article')
15
  .replace(/\\n/g, '<br/>')
16
  .replace(/\n/g, '<br/>');
17
+ const subtitle = articleFM?.subtitle ?? '';
18
  const description = articleFM?.description ?? '';
19
  const authors = articleFM?.authors ?? [];
20
  const published = articleFM?.published ?? undefined;
 
73
  </head>
74
  <body>
75
  <ThemeToggle />
76
+ <Header title={docTitleHtml} description={subtitle}>
77
+ </Header>
 
 
 
 
 
78
 
79
  <section class="article-header">
80
  <Meta title={docTitle} authors={articleFM?.authors} affiliation={articleFM?.affiliation} published={articleFM?.published} />
app/src/styles/_base.scss CHANGED
@@ -186,7 +186,7 @@ figcaption { text-align: center; font-size: 0.9rem; color: var(--muted-color); m
186
  .image-credit a { color: inherit; text-decoration: underline; text-underline-offset: 2px; }
187
 
188
  // ============================================================================
189
- // Buttons (minimal, épuré)
190
  // ============================================================================
191
  .meta .meta-container-cell button {
192
  appearance: none;
 
186
  .image-credit a { color: inherit; text-decoration: underline; text-underline-offset: 2px; }
187
 
188
  // ============================================================================
189
+ // Buttons (minimal, clean)
190
  // ============================================================================
191
  .meta .meta-container-cell button {
192
  appearance: none;
app/src/styles/_layout.scss CHANGED
@@ -22,9 +22,9 @@ main > nav:first-of-type { display: none; }
22
 
23
  // Mobile TOC accordion
24
  .toc-mobile { display: none; margin: 8px 0 16px; }
25
- .toc-mobile > summary { cursor: pointer; list-style: none; padding: 8px 12px; border: 1px solid var(--border-color); border-radius: 8px; background: var(--surface-bg); color: var(--text-color); font-weight: 600; }
26
  .toc-mobile[open] > summary { border-bottom-left-radius: 0; border-bottom-right-radius: 0; }
27
- .toc-mobile nav { border-left: none; padding: 10px 12px; font-size: 14px; border: 1px solid var(--border-color); border-top: none; border-bottom-left-radius: 8px; border-bottom-right-radius: 8px; background: var(--surface-bg); }
28
  .toc-mobile nav ul { margin: 0 0 6px; padding-left: 1em; }
29
  .toc-mobile nav li { list-style: none; margin: .25em 0; }
30
  .toc-mobile nav a { color: var(--text-color); text-decoration: none; border-bottom: none; }
 
22
 
23
  // Mobile TOC accordion
24
  .toc-mobile { display: none; margin: 8px 0 16px; }
25
+ .toc-mobile > summary { cursor: pointer; list-style: none; padding: 8px 12px; border: 1px solid var(--border-color); border-radius: 8px; color: var(--text-color); font-weight: 600; }
26
  .toc-mobile[open] > summary { border-bottom-left-radius: 0; border-bottom-right-radius: 0; }
27
+ .toc-mobile nav { border-left: none; padding: 10px 12px; font-size: 14px; border: 1px solid var(--border-color); border-top: none; border-bottom-left-radius: 8px; border-bottom-right-radius: 8px; }
28
  .toc-mobile nav ul { margin: 0 0 6px; padding-left: 1em; }
29
  .toc-mobile nav li { list-style: none; margin: .25em 0; }
30
  .toc-mobile nav a { color: var(--text-color); text-decoration: none; border-bottom: none; }
app/src/styles/components/_code.scss CHANGED
@@ -9,7 +9,7 @@ section.content-grid pre { overflow-x: auto; width: 100%; max-width: 100%; box-s
9
  section.content-grid pre code { display: inline-block; min-width: 100%; }
10
 
11
  /* Wrap long lines on mobile to avoid overflow (URLs, etc.) */
12
- @media (max-width: 700px) {
13
  .astro-code,
14
  section.content-grid pre { white-space: pre-wrap; overflow-wrap: anywhere; word-break: break-word; }
15
  section.content-grid pre code { white-space: pre-wrap; display: block; min-width: 0; }
 
9
  section.content-grid pre code { display: inline-block; min-width: 100%; }
10
 
11
  /* Wrap long lines on mobile to avoid overflow (URLs, etc.) */
12
+ @media (max-width: 1100px) {
13
  .astro-code,
14
  section.content-grid pre { white-space: pre-wrap; overflow-wrap: anywhere; word-break: break-word; }
15
  section.content-grid pre code { white-space: pre-wrap; display: block; min-width: 0; }
app/src/styles/components/_footer.scss CHANGED
@@ -1,7 +1,7 @@
1
  .distill-footer { contain: layout style; font-size: 0.8em; line-height: 1.7em; margin-top: 60px; margin-bottom: 0; border-top: 1px solid rgba(0, 0, 0, 0.1); color: rgba(0, 0, 0, 0.5); }
2
  .footer-inner { max-width: 1280px; margin: 0 auto; padding: 60px 16px 48px; display: grid; grid-template-columns: 220px minmax(0, 680px) 260px; gap: 32px; align-items: start; }
3
 
4
- // Utiliser la grille parent (3 colonnes comme .content-grid)
5
  .citation-block,
6
  .references-block { display: contents; }
7
  .citation-block > h3,
 
1
  .distill-footer { contain: layout style; font-size: 0.8em; line-height: 1.7em; margin-top: 60px; margin-bottom: 0; border-top: 1px solid rgba(0, 0, 0, 0.1); color: rgba(0, 0, 0, 0.5); }
2
  .footer-inner { max-width: 1280px; margin: 0 auto; padding: 60px 16px 48px; display: grid; grid-template-columns: 220px minmax(0, 680px) 260px; gap: 32px; align-items: start; }
3
 
4
+ // Use the parent grid (3 columns like .content-grid)
5
  .citation-block,
6
  .references-block { display: contents; }
7
  .citation-block > h3,
fragments/plotly/banner.py CHANGED
@@ -2,62 +2,62 @@ import plotly.graph_objects as go
2
  import numpy as np
3
  import pandas as pd
4
 
5
- # Paramètres de la scène (mêmes ranges que l'intégration Astro)
6
- cx, cy = 1.5, 0.5 # centre
7
- a, b = 1.3, 0.45 # étendue max en x/y (ellipse pour l'anisotropie)
8
-
9
- # Paramètres de la galaxie en spirale
10
- num_points = 3000 # plus de dots
11
- num_arms = 3 # nombre de bras spiraux
12
- num_turns = 2.1 # nombre de tours par bras
13
- angle_jitter = 0.12 # écart angulaire pour évaser les bras
14
- pos_noise = 0.015 # bruit de position global
15
-
16
- # Génération des points sur des bras spiraux (spirale d'Archimède)
17
- t = np.random.rand(num_points) * (2 * np.pi * num_turns) # progression le long du bras
18
  arm_indices = np.random.randint(0, num_arms, size=num_points)
19
  arm_offsets = arm_indices * (2 * np.pi / num_arms)
20
 
21
  theta = t + arm_offsets + np.random.randn(num_points) * angle_jitter
22
 
23
- # Rayon normalisé (0->centre, 1->bord). Puissance <1 pour densifier le centre
24
  r_norm = (t / (2 * np.pi * num_turns)) ** 0.9
25
 
26
- # Bruit radial/lateral qui augmente légèrement avec le rayon
27
  noise_x = pos_noise * (0.8 + 0.6 * r_norm) * np.random.randn(num_points)
28
  noise_y = pos_noise * (0.8 + 0.6 * r_norm) * np.random.randn(num_points)
29
 
30
- # Projection elliptique
31
  x_spiral = cx + a * r_norm * np.cos(theta) + noise_x
32
  y_spiral = cy + b * r_norm * np.sin(theta) + noise_y
33
 
34
- # Bulbe central (points supplémentaires très proches du centre)
35
  bulge_points = int(0.18 * num_points)
36
  phi_b = 2 * np.pi * np.random.rand(bulge_points)
37
- r_b = (np.random.rand(bulge_points) ** 2.2) * 0.22 # bulbe compact
38
  noise_x_b = (pos_noise * 0.6) * np.random.randn(bulge_points)
39
  noise_y_b = (pos_noise * 0.6) * np.random.randn(bulge_points)
40
  x_bulge = cx + a * r_b * np.cos(phi_b) + noise_x_b
41
  y_bulge = cy + b * r_b * np.sin(phi_b) + noise_y_b
42
 
43
- # Concaténation
44
  x = np.concatenate([x_spiral, x_bulge])
45
  y = np.concatenate([y_spiral, y_bulge])
46
 
47
- # Intensité centrale (pour tailles/couleurs). 1 au centre, ~0 au bord
48
  z_spiral = 1 - r_norm
49
- z_bulge = 1 - (r_b / max(r_b.max(), 1e-6)) # bulbe très lumineux
50
  z_raw = np.concatenate([z_spiral, z_bulge])
51
 
52
- # Tailles: conserver l'échelle 5..10 pour cohérence
53
  sizes = (z_raw + 1) * 5
54
 
55
- # Suppression du filtre intermédiaire: on garde tous les points posés, on filtrera à la toute fin
56
 
57
  df = pd.DataFrame({
58
  "x": x,
59
  "y": y,
60
- "z": sizes, # réutilisé pour size+color comme avant
61
  })
62
 
63
  def get_label(z):
@@ -70,10 +70,10 @@ def get_label(z):
70
  else:
71
  return "biiig dot"
72
 
73
- # Labels basés sur l'intensité centrale
74
  df["label"] = pd.Series(z_raw).apply(get_label)
75
 
76
- # Ordonnancement pour le rendu: petits d'abord, gros ensuite (au-dessus)
77
  df = df.sort_values(by="z", ascending=True).reset_index(drop=True)
78
 
79
  fig = go.Figure()
 
2
  import numpy as np
3
  import pandas as pd
4
 
5
+ # Scene parameters (same ranges as the Astro integration)
6
+ cx, cy = 1.5, 0.5 # center
7
+ a, b = 1.3, 0.45 # max extent in x/y (ellipse for anisotropy)
8
+
9
+ # Spiral galaxy parameters
10
+ num_points = 3000 # more dots
11
+ num_arms = 3 # number of spiral arms
12
+ num_turns = 2.1 # number of turns per arm
13
+ angle_jitter = 0.12 # angular jitter to fan out the arms
14
+ pos_noise = 0.015 # global position noise
15
+
16
+ # Generate points along spiral arms (Archimedean spiral)
17
+ t = np.random.rand(num_points) * (2 * np.pi * num_turns) # progression along the arm
18
  arm_indices = np.random.randint(0, num_arms, size=num_points)
19
  arm_offsets = arm_indices * (2 * np.pi / num_arms)
20
 
21
  theta = t + arm_offsets + np.random.randn(num_points) * angle_jitter
22
 
23
+ # Normalized radius (0->center, 1->edge). Power <1 to densify the core
24
  r_norm = (t / (2 * np.pi * num_turns)) ** 0.9
25
 
26
+ # Radial/lateral noise that slightly increases with radius
27
  noise_x = pos_noise * (0.8 + 0.6 * r_norm) * np.random.randn(num_points)
28
  noise_y = pos_noise * (0.8 + 0.6 * r_norm) * np.random.randn(num_points)
29
 
30
+ # Elliptic projection
31
  x_spiral = cx + a * r_norm * np.cos(theta) + noise_x
32
  y_spiral = cy + b * r_norm * np.sin(theta) + noise_y
33
 
34
+ # Central bulge (additional points very close to the core)
35
  bulge_points = int(0.18 * num_points)
36
  phi_b = 2 * np.pi * np.random.rand(bulge_points)
37
+ r_b = (np.random.rand(bulge_points) ** 2.2) * 0.22 # compact bulge
38
  noise_x_b = (pos_noise * 0.6) * np.random.randn(bulge_points)
39
  noise_y_b = (pos_noise * 0.6) * np.random.randn(bulge_points)
40
  x_bulge = cx + a * r_b * np.cos(phi_b) + noise_x_b
41
  y_bulge = cy + b * r_b * np.sin(phi_b) + noise_y_b
42
 
43
+ # Concatenation
44
  x = np.concatenate([x_spiral, x_bulge])
45
  y = np.concatenate([y_spiral, y_bulge])
46
 
47
+ # Central intensity (for sizes/colors). 1 at center, ~0 at edge
48
  z_spiral = 1 - r_norm
49
+ z_bulge = 1 - (r_b / max(r_b.max(), 1e-6)) # very bright bulge
50
  z_raw = np.concatenate([z_spiral, z_bulge])
51
 
52
+ # Sizes: keep the 5..10 scale for consistency
53
  sizes = (z_raw + 1) * 5
54
 
55
+ # Remove intermediate filtering: keep all placed points, filter at the very end
56
 
57
  df = pd.DataFrame({
58
  "x": x,
59
  "y": y,
60
+ "z": sizes, # reused for size+color as before
61
  })
62
 
63
  def get_label(z):
 
70
  else:
71
  return "biiig dot"
72
 
73
+ # Labels based on central intensity
74
  df["label"] = pd.Series(z_raw).apply(get_label)
75
 
76
+ # Rendering order: small points first, big ones after (on top)
77
  df = df.sort_values(by="z", ascending=True).reset_index(drop=True)
78
 
79
  fig = go.Figure()
fragments/plotly/line.py CHANGED
@@ -120,11 +120,11 @@ fig.update_layout(
120
  ),
121
  )
122
 
123
- # Écrit le fragment de manière robuste à côté de ce fichier, dans src/fragments/line.html
124
  output_path = os.path.join(os.path.dirname(__file__), "fragments", "line.html")
125
  os.makedirs(os.path.dirname(output_path), exist_ok=True)
126
 
127
- # Injecte un petit script post-rendu pour arrondir les coins de la boîte de hover
128
  post_script = """
129
  (function(){
130
  function attach(gd){
 
120
  ),
121
  )
122
 
123
+ # Write the fragment next to this file into src/fragments/line.html (robust path)
124
  output_path = os.path.join(os.path.dirname(__file__), "fragments", "line.html")
125
  os.makedirs(os.path.dirname(output_path), exist_ok=True)
126
 
127
+ # Inject a small post-render script to round the hover box corners
128
  post_script = """
129
  (function(){
130
  function attach(gd){
fragments/plotly/pyproject.toml CHANGED
@@ -1,7 +1,7 @@
1
  [tool.poetry]
2
  name = "blogpost-fine-tasks-python"
3
  version = "0.1.0"
4
- description = "Scripts de génération de fragments Plotly et conversions HTML/Markdown pour le blogpost."
5
  package-mode = false
6
 
7
  [tool.poetry.dependencies]
 
1
  [tool.poetry]
2
  name = "blogpost-fine-tasks-python"
3
  version = "0.1.0"
4
+ description = "Plotly fragment generation scripts and HTML/Markdown conversions for the blogpost."
5
  package-mode = false
6
 
7
  [tool.poetry.dependencies]