En bref
La liste de lectures d’un séminaire universitaire. L’enseignant colle le fichier qu’exporte Zotero ; Postext imprime les citations et une liste qui contient aussi des livres que le texte ne mentionne pas.
Ce que vous allez composer
La liste de lectures d’un séminaire de six semaines sur l’histoire et la sociologie des sciences, composée sur une page de 210 × 280 mm en deux colonnes. Un aplat vert profond occupe la moitié supérieure de la première page, avec le titre du cours en Spectral Light à 50 pt ; le texte en dessous est le guide de chaque semaine rédigé par le responsable du séminaire. Les références sont la collection Zotero du séminaire, exportée en BibTeX et collée dans le document sans toucher une ligne : les clés de citation de Zotero, ses accolades autour des capitales, ses accents {\"o}, month = dec, les champs file, keywords et urldate. Le texte cite huit des ouvrages. Les cinq autres, des lectures de fond dont le séminaire ne discute pas, entrent dans la liste par nocite.
Cette recette répond à
- Comment utiliser un export BibTeX de Zotero et lister des ouvrages que le texte ne cite jamais ?
- Comment citer des ouvrages et construire la bibliographie en APA, en IEEE ou dans un autre style de citation ?
La réponse courte
// The works come from a :::references{format=bibtex} block: the Zotero export pasted as it
// is. The text cites some of them; `nocite` in the front matter adds the rest by key, and
// `nocite: "@*"` would add the whole export. citeproc-js writes both in the style named.
registerCitationEngine(createCiteprocEngine({ styles: STYLES, locales: LOCALES }));
const citations = {
style: 'chicago-author-date', // '(Gieryn 1983)', 'Shapin and Schaffer (1985)'
link: true, // a citation jumps to its entry
bibliography: {
// includeUncited: true is the same as nocite: "@*", set from the design side.
fontSize: em(0.94), lineHeight: pt(12.4), hangingIndent: mm(5), entrySpacing: pt(3.5),
doi: 'link', // Zotero's doi field prints as https://doi.org/…; its url field is dropped
},
};
Ingrédients
- Fonctionnalités
- Bibliographie tirée des référencesCitations dans un style bibliographiqueOuvertures composéesAttributs de titreUne ou deux colonnesTitres courants et foliosStyles de paragraphePalette de couleurs sémantique
- Typographie
- Spectral, IBM Plex Sans Condensed (SIL OFL 1.1)
- Ressources
- Aucune : chaque image est dessinée en code
Préparation
#1 · Nommer le style une seule fois
Le code est dans la réponse courte plus haut. Le moteur de citations est enregistré avant la première composition et le style tient en un réglage : Chicago auteur-date écrit « (Shapin 1984) » pour [@shapin_pump_1984] et « Shapin and Schaffer (1985) » pour @shapin_leviathan_1985 dans une phrase. Les clés sont celles que Zotero a construites à partir du premier auteur, du premier mot significatif du titre et de l’année : gieryn_boundary-work_1983 garde donc son trait d’union ; Postext lit un trait d’union ou un tiret bas à l’intérieur d’une clé comme faisant partie de la clé.
#2 · Coller l’export tel que Zotero l’écrit
Le dernier bloc du document contient l’export, une entrée par ouvrage, en retrait par tabulations comme l’écrit Zotero :
@article{gieryn_boundary-work_1983,
title = {Boundary-{Work} and the {Demarcation} of {Science} from {Non}-{Science}: …},
url = {https://www.jstor.org/stable/2095325},
doi = {10.2307/2095325},
urldate = {2026-08-21},
journal = {American Sociological Review},
author = {Gieryn, Thomas F.},
month = dec,
year = {1983},
keywords = {demarcation, week 4},
pages = {781--795},
file = {JSTOR Full Text PDF:/Users/hps214/Zotero/storage/2HJ8M6PA/Gieryn - 1983 - ….pdf:application/pdf},
}Le lecteur retire les accolades qui protègent les capitales, compose {\"o} et {\'e} en ö et é, transforme -- en tiret demi-cadratin et lit month = dec comme un simple nom de mois. urldate devient la date de consultation et language la langue de l’ouvrage ; file, keywords, abstract et shorttitle sont ignorés, si bien que rien de votre disque dur n’arrive sur la page.
#3 · Faire entrer les ouvrages que le texte ne cite pas
Le frontmatter les liste par clé :
---
title: "Science as a Social Practice"
nocite: "@fleck_genesis_1979, @merton_sociology_1973, @latour_science_1987, @rheinberger_toward_1997, @lopez_pinero_ciencia_1979"
---La liste contient tous les ouvrages cités plus ces cinq-là, triés ensemble par auteur comme le demande Chicago ; rien ne signale une entrée comme non citée. nocite: "@*" liste tout l’export, et bibliography.includeUncited: true fait de même depuis la configuration, pour une maquette qui imprime toujours toute la bibliothèque.
#4 · Un aplat vert pour la fiche
const BAND = 132; // mm from the trim's top to the band's foot
const onBand = { color: col('paper'), align: 'left', overflow: 'wrap' };
const opener = {
enabled: true,
minHeight: mm(BAND - TOP + 9), // the columns start 9 mm under the band
slot: { elements: [
{ kind: 'box', id: 'band', style: { backgroundColor: col('accent') },
placement: { anchor: { to: 'page', edge: 'top-left' },
size: { width: 'fill', height: mm(BAND) } } },
{ kind: 'text', id: 'kicker', content: '{attr.kicker}', ...onBand, fontFamily: LABEL,
fontSize: pt(8.5), fontWeight: 600, letterSpacing: pt(1.7), textTransform: 'uppercase',
placement: { anchor: { to: 'container', edge: 'top-left' }, offset: { y: mm(14) } } },
{ kind: 'text', id: 'title', content: '{titleText}', ...onBand, fontFamily: TEXT,
fontSize: pt(50), fontWeight: 300, lineHeight: 1.02,
placement: { anchor: { to: '#kicker', edge: 'below' }, offset: { y: mm(7) },
size: { width: mm(150), height: 'auto' } } },
{ kind: 'rule', id: 'rule', direction: 'horizontal', thickness: pt(1), color: col('tint'),
placement: { anchor: { to: '#title', edge: 'below' }, offset: { y: mm(7) },
size: { width: mm(18) } } },
{ kind: 'text', id: 'lead', content: '{attr.lead}', ...onBand, fontFamily: TEXT,
italic: true, fontSize: pt(13), lineHeight: 1.3, color: col('tint'),
placement: { anchor: { to: '#rule', edge: 'below' }, offset: { y: mm(6) },
size: { width: mm(128), height: 'auto' } } },
] },
};
La bande est une boîte ancrée à la coupe, de 132 mm de haut, et les deux colonnes commencent 9 mm sous elle. Le titre, le filet et le chapeau sont enchaînés par des ancrages below, si bien qu’un titre de cours plus long repousse le chapeau vers le bas au lieu de le heurter.
const palette = {
ink: '#1c211f', accent: '#2d5b48', tint: '#dfe9e2', rule: '#c3cdc6', muted: '#5c6862',
paper: '#ffffff',
};
const col = (id) => ({ hex: palette[id], model: 'hex', paletteId: id });
const colorPalette = Object.entries({ ...palette, 'main-color': palette.accent })
.map(([id, hex]) => ({ id, name: id, value: { hex, model: 'hex' } }));
La recette complète
// ═══ Postext Cookbook · Nº 097 · A reading list from a Zotero BibTeX export ═══════ // https://postext.dev/en/cookbook/zotero-bibtex-nocite // Code: MIT · Text: original (CC BY 4.0) · Pictures: none // Fonts: Spectral, IBM Plex Sans Condensed (SIL OFL 1.1) · Needs postext ≥ 1.25.0 import { buildDocumentWithFonts, renderPageToCanvas, registerCitationEngine, } from 'https://esm.sh/postext'; import { createCiteprocEngine, STYLES, LOCALES } from 'https://esm.sh/postext-citeproc'; const LANG = 'en'; // @lang: the language of the sample document ('en' | 'es') const RECIPE = 'zotero-bibtex-nocite'; // ─── 1 · Design ───────────────────────────────────────────────────────────── // #region palette: a department green, one tint, everything else in ink const palette = { ink: '#1c211f', accent: '#2d5b48', tint: '#dfe9e2', rule: '#c3cdc6', muted: '#5c6862', paper: '#ffffff', }; const col = (id) => ({ hex: palette[id], model: 'hex', paletteId: id }); const colorPalette = Object.entries({ ...palette, 'main-color': palette.accent }) .map(([id, hex]) => ({ id, name: id, value: { hex, model: 'hex' } })); // #endregion const [TEXT, LABEL] = ['Spectral', 'IBM Plex Sans Condensed']; const LEAD = 13.4; // pt const [TOP, OUTER] = [24, 18]; // mm // #region answer: Chicago author-date, and a list that holds more than the text cites // The works come from a :::references{format=bibtex} block: the Zotero export pasted as it // is. The text cites some of them; `nocite` in the front matter adds the rest by key, and // `nocite: "@*"` would add the whole export. citeproc-js writes both in the style named. registerCitationEngine(createCiteprocEngine({ styles: STYLES, locales: LOCALES })); const citations = { style: 'chicago-author-date', // '(Gieryn 1983)', 'Shapin and Schaffer (1985)' link: true, // a citation jumps to its entry bibliography: { // includeUncited: true is the same as nocite: "@*", set from the design side. fontSize: em(0.94), lineHeight: pt(12.4), hangingIndent: mm(5), entrySpacing: pt(3.5), doi: 'link', // Zotero's doi field prints as https://doi.org/…; its url field is dropped }, }; // #endregion // #region opener: a green field over the top half, the course in white on it const BAND = 132; // mm from the trim's top to the band's foot const onBand = { color: col('paper'), align: 'left', overflow: 'wrap' }; const opener = { enabled: true, minHeight: mm(BAND - TOP + 9), // the columns start 9 mm under the band slot: { elements: [ { kind: 'box', id: 'band', style: { backgroundColor: col('accent') }, placement: { anchor: { to: 'page', edge: 'top-left' }, size: { width: 'fill', height: mm(BAND) } } }, { kind: 'text', id: 'kicker', content: '{attr.kicker}', ...onBand, fontFamily: LABEL, fontSize: pt(8.5), fontWeight: 600, letterSpacing: pt(1.7), textTransform: 'uppercase', placement: { anchor: { to: 'container', edge: 'top-left' }, offset: { y: mm(14) } } }, { kind: 'text', id: 'title', content: '{titleText}', ...onBand, fontFamily: TEXT, fontSize: pt(50), fontWeight: 300, lineHeight: 1.02, placement: { anchor: { to: '#kicker', edge: 'below' }, offset: { y: mm(7) }, size: { width: mm(150), height: 'auto' } } }, { kind: 'rule', id: 'rule', direction: 'horizontal', thickness: pt(1), color: col('tint'), placement: { anchor: { to: '#title', edge: 'below' }, offset: { y: mm(7) }, size: { width: mm(18) } } }, { kind: 'text', id: 'lead', content: '{attr.lead}', ...onBand, fontFamily: TEXT, italic: true, fontSize: pt(13), lineHeight: 1.3, color: col('tint'), placement: { anchor: { to: '#rule', edge: 'below' }, offset: { y: mm(6) }, size: { width: mm(128), height: 'auto' } } }, ] }, }; // #endregion const head = (id, content, parity, edge, x, extra = {}) => ({ kind: 'text', id, content, parity, pages: 'body', fontFamily: LABEL, fontSize: pt(7.5), fontWeight: 600, letterSpacing: pt(1.2), textTransform: 'uppercase', color: col('muted'), placement: { anchor: { to: 'page', edge }, offset: { x: mm(x), y: mm(13) } }, ...extra, }); const folio = { color: col('accent') }; const listName = t({ en: 'HPS 214 · Reading list', es: 'HPS 214 · Lecturas' }); const config = () => ({ locale: t({ en: 'en-us', es: 'es' }), colorPalette, citations, page: { sizePreset: 'custom', width: mm(210), height: mm(280), dpi: 150, margins: { top: mm(TOP), bottom: mm(22), left: mm(20), right: mm(OUTER), mirror: true }, }, layout: { layoutType: 'double', gutterWidth: mm(7) }, bodyText: { fontFamily: TEXT, fontSize: pt(9.6), lineHeight: pt(LEAD), color: col('ink'), boldColor: col('ink'), italicColor: col('ink'), referenceColor: col('accent'), textAlign: 'justify', firstLineIndent: mm(4), indentAfterHeading: false, hyphenation: { enabled: true }, optimalLineBreaking: true, avoidWidows: true, avoidOrphans: true, avoidRunts: true, }, headings: { fontFamily: TEXT, color: col('ink'), fontWeight: 600, levels: [ // parity 'any': the H1 opens on the next page, either side (the default waits for a recto). { level: 1, span: 'page', breakBefore: { enabled: true, parity: 'any' }, marginBottom: pt(0), advancedDesign: opener }, { level: 2, fontFamily: LABEL, fontSize: pt(10), lineHeight: pt(LEAD), fontWeight: 600, color: col('accent'), marginTop: pt(LEAD), marginBottom: pt(0), }, ], }, paragraphStyles: [ { id: 'colophon', fontFamily: LABEL, fontSize: pt(7), lineHeight: pt(9.5), color: col('muted'), textAlign: 'left', firstLineIndent: pt(0), marginTop: pt(LEAD) }, ], header: { elements: [ head('verso-folio', '{pageNumber}', 'even', 'top-left', OUTER, folio), head('verso-title', '{title}', 'even', 'top-left', OUTER + 8), head('recto-title', listName, 'odd', 'top-right', -(OUTER + 8)), head('recto-folio', '{pageNumber}', 'odd', 'top-right', -OUTER, folio), ] }, footer: { elements: [] }, }); // ─── 2 · Content ──────────────────────────────────────────────────────────── const markdown = String.raw`---Exemple Markdown · 204 lignes · content.en.md
title: "Science as a Social Practice" nocite: "@fleck_genesis_1979, @merton_sociology_1973, @latour_science_1987, @rheinberger_toward_1997, @lopez_pinero_ciencia_1979" --- # Science as a Social Practice {kicker="HPS 214 · Seminar reading list · Autumn term 2026" lead="Six weeks on how scientific knowledge is made, tested and trusted, from the paradigm to the laboratory notebook."} This seminar reads the history and sociology of science as one subject. Its question is how a claim about nature becomes a fact that people who never saw the experiment are willing to rely on. The answers we read come from historians of the seventeenth century, from sociologists who sat in laboratories for two years, and from philosophers who looked at what scientists actually do rather than at what their textbooks say they do. Each week has one set reading, which everyone reads before the seminar, and one or two companion texts for the person introducing the discussion. The set readings are available in the course reader; the others are in the library’s reserve collection. ## Week 1 · Paradigms and normal science We start with the book that made the subject fashionable. Read the first five sections and the Postscript of @kuhn_structure_1970, which Kuhn added for the second edition to answer the charge that “paradigm” meant too many things at once. His preface credits a monograph by Ludwik Fleck, written in 1935 and nearly forgotten until Kuhn mentioned it; it is in the list at the end, and the student introducing the week should read its second chapter. ## Week 2 · Witnessing an experiment The set reading is the first two chapters of @shapin_leviathan_1985, on Robert Boyle’s air-pump and the dispute with Thomas Hobbes over whether an experiment could settle anything. The companion text is the shorter article in which Shapin first described Boyle’s “literary technology”, the way a report written in plain, circumstantial prose turned its readers into witnesses [@shapin_pump_1984]. ## Week 3 · Inside the laboratory Bruno Latour spent two years at the Salk Institute as an observer of a neuroendocrinology laboratory. Read the second and third chapters of @latour_laboratory_1979 and follow how a statement loses its qualifications (“it seems that”, “X has claimed”) as it travels from one paper to the next until it is printed without any. ## Week 4 · Where science ends The demarcation problem asks what separates science from what is not science. Philosophers looked for a criterion; @gieryn_boundary-work_1983 looked at the people who draw the line in practice, and at what they gain by drawing it where they do. Bring an example from the news of the last month. ## Week 5 · Objectivity has a history Read the article first [@daston_image_1992], then the first chapter and the chapter on “trained judgment” in the book it grew into [@daston_objectivity_2007]. The argument is that objectivity, as a virtue of the scientist, appeared in the nineteenth century, and that atlases of skeletons, snowflakes and clouds show it arriving. ## Week 6 · Whom to believe The last week returns to the seventeenth century with a broader question: how did gentlemen-philosophers decide whose reports to accept? Read the first chapter of @shapin_social_1994 and come ready to compare its answer with how you decide whom to believe when you read a paper outside your field. ## Further reading The list below is the seminar’s Zotero collection, exported as BibTeX and set as it came out of Zotero. It includes every work cited above and five that the seminar does not discuss but that you will meet in the essays you read: Fleck’s monograph, Merton’s collected essays on the norms of science, Latour’s later handbook, Rheinberger’s study of experimental systems, and a history of Spanish science in the sixteenth and seventeenth centuries for those who read Spanish. :::bibliography{title=""} :::paragraphs{style="colophon"} Set in Spectral and IBM Plex Sans Condensed (SIL OFL) · Reading list and notes: original, CC BY 4.0 · The works are real; the Zotero file paths and access dates are illustrative. ::: :::references{format=bibtex} @book{kuhn_structure_1970, address = {Chicago}, edition = {2}, title = {The {Structure} of {Scientific} {Revolutions}}, abstract = {Normal science, anomaly and crisis, and the paradigm changes that end them, with the Postscript of 1969.}, language = {en}, publisher = {University of Chicago Press}, author = {Kuhn, Thomas S.}, year = {1970}, keywords = {paradigms, week 1}, file = {Kuhn - 1970 - The Structure of Scientific Revolutions.pdf:/Users/hps214/Zotero/storage/7QW2KD9N/Kuhn - 1970 - The Structure of Scientific Revolutions.pdf:application/pdf}, } @book{fleck_genesis_1979, address = {Chicago}, title = {Genesis and {Development} of a {Scientific} {Fact}}, language = {en}, publisher = {University of Chicago Press}, author = {Fleck, Ludwik}, editor = {Trenn, Thaddeus J. and Merton, Robert K.}, translator = {Bradley, Fred and Trenn, Thaddeus J.}, year = {1979}, keywords = {thought collectives, week 1}, } @book{shapin_leviathan_1985, address = {Princeton, NJ}, title = {Leviathan and the {Air}-{Pump}: {Hobbes}, {Boyle}, and the {Experimental} {Life}}, shorttitle = {Leviathan and the {Air}-{Pump}}, language = {en}, publisher = {Princeton University Press}, author = {Shapin, Steven and Schaffer, Simon}, year = {1985}, keywords = {experiment, week 2}, } @article{shapin_pump_1984, title = {Pump and {Circumstance}: {Robert} {Boyle}'s {Literary} {Technology}}, volume = {14}, shorttitle = {Pump and {Circumstance}}, url = {https://doi.org/10.1177/030631284014004001}, doi = {10.1177/030631284014004001}, language = {en}, number = {4}, urldate = {2026-08-21}, journal = {Social Studies of Science}, author = {Shapin, Steven}, month = nov, year = {1984}, keywords = {literary technology, week 2}, pages = {481--520}, file = {Shapin - 1984 - Pump and Circumstance.pdf:/Users/hps214/Zotero/storage/V3NC8RTE/Shapin - 1984 - Pump and Circumstance.pdf:application/pdf}, } @book{latour_laboratory_1979, address = {Beverly Hills}, title = {Laboratory {Life}: {The} {Social} {Construction} of {Scientific} {Facts}}, shorttitle = {Laboratory {Life}}, language = {en}, publisher = {Sage}, author = {Latour, Bruno and Woolgar, Steve}, year = {1979}, keywords = {ethnography, week 3}, } @book{latour_science_1987, address = {Cambridge, MA}, title = {Science in {Action}: {How} to {Follow} {Scientists} and {Engineers} through {Society}}, shorttitle = {Science in {Action}}, language = {en}, publisher = {Harvard University Press}, author = {Latour, Bruno}, year = {1987}, } @book{rheinberger_toward_1997, address = {Stanford, CA}, title = {Toward a {History} of {Epistemic} {Things}: {Synthesizing} {Proteins} in the {Test} {Tube}}, language = {en}, publisher = {Stanford University Press}, author = {Rheinberger, Hans-J{\"o}rg}, year = {1997}, keywords = {experimental systems}, } @article{gieryn_boundary-work_1983, title = {Boundary-{Work} and the {Demarcation} of {Science} from {Non}-{Science}: {Strains} and {Interests} in {Professional} {Ideologies} of {Scientists}}, volume = {48}, shorttitle = {Boundary-{Work} and the {Demarcation} of {Science} from {Non}-{Science}}, url = {https://www.jstor.org/stable/2095325}, doi = {10.2307/2095325}, language = {en}, number = {6}, urldate = {2026-08-21}, journal = {American Sociological Review}, author = {Gieryn, Thomas F.}, month = dec, year = {1983}, keywords = {demarcation, week 4}, pages = {781--795}, file = {JSTOR Full Text PDF:/Users/hps214/Zotero/storage/2HJ8M6PA/Gieryn - 1983 - Boundary-Work and the Demarcation of Science from Non-Science.pdf:application/pdf}, } @article{daston_image_1992, title = {The {Image} of {Objectivity}}, url = {https://www.jstor.org/stable/2928741}, doi = {10.2307/2928741}, language = {en}, number = {40}, urldate = {2026-08-22}, journal = {Representations}, author = {Daston, Lorraine and Galison, Peter}, year = {1992}, keywords = {objectivity, week 5}, pages = {81--128}, file = {JSTOR Full Text PDF:/Users/hps214/Zotero/storage/F9XK4C2B/Daston and Galison - 1992 - The Image of Objectivity.pdf:application/pdf}, } @book{daston_objectivity_2007, address = {New York}, title = {Objectivity}, language = {en}, publisher = {Zone Books}, author = {Daston, Lorraine and Galison, Peter}, year = {2007}, keywords = {objectivity, week 5}, } @book{shapin_social_1994, address = {Chicago}, series = {Science and {Its} {Conceptual} {Foundations}}, title = {A {Social} {History} of {Truth}: {Civility} and {Science} in {Seventeenth}-{Century} {England}}, language = {en}, publisher = {University of Chicago Press}, author = {Shapin, Steven}, year = {1994}, keywords = {trust, week 6}, } @book{merton_sociology_1973, address = {Chicago}, title = {The {Sociology} of {Science}: {Theoretical} and {Empirical} {Investigations}}, language = {en}, publisher = {University of Chicago Press}, author = {Merton, Robert K.}, editor = {Storer, Norman W.}, year = {1973}, } @book{lopez_pinero_ciencia_1979, address = {Barcelona}, title = {Ciencia y t{\'e}cnica en la sociedad espa{\~n}ola de los siglos {XVI} y {XVII}}, language = {es}, publisher = {Labor}, author = {L{\'o}pez Pi{\~n}ero, Jos{\'e} Mar{\'i}a}, year = {1979}, } :::`; // ─── 3 · Fonts ────────────────────────────────────────────────────────────── const FONTS = { Spectral: ['300', '300i', '400', '400i', '600', '600i'], 'IBM Plex Sans Condensed': ['400', '600'], }; // ─── 4 · Build & show ─────────────────────────────────────────────────────── const doc = await buildDocumentWithFonts({ markdown }, config(), kitFonts(FONTS)); showPages(doc, { title: t({ en: 'A reading list from Zotero', es: 'Lecturas desde Zotero' }) });Kit · core, fonts, viewer : identique dans toutes les recettes · 224 lignes
// ─── Kit ── helpers shared by every Cookbook recipe · postext.dev/cookbook ───── // ─── Kit · core v1 ── the same in every recipe · postext.dev/cookbook function mm(value) { return { value, unit: 'mm' }; } function pt(value) { return { value, unit: 'pt' }; } function em(value) { return { value, unit: 'em' }; } /** The sample language's string: t({ en: 'Figure', es: 'Figura' }). */ function t(strings) { return strings[LANG] ?? Object.values(strings)[0]; } /** A file in this recipe's assets folder, served from the Postext repo by jsDelivr. */ function asset(file) { return `https://cdn.jsdelivr.net/gh/drnachio/postext@main/cookbook/${RECIPE}/assets/${file}`; } // ─── Kit · fonts v3 ── the same in every recipe · postext.dev/cookbook // Postext loads the faces a config asks for before layout (prepareFonts, // buildDocumentWithFonts) and asks the kit for the files: Fontsource's, the // files the PDF embeds too. /** The options of buildDocumentWithFonts / prepareFonts / withLoadedFonts: * the Fontsource resolver, and the faces of FONTS loaded with the ones the * config names. FONTS = { 'Family Name': ['400', '400i', '700'] }. */ function kitFonts(FONTS = {}) { const faces = Object.entries(FONTS).flatMap(([family, specs]) => specs.map((spec) => ({ family, weight: parseInt(spec, 10), style: spec.endsWith('i') ? 'italic' : 'normal' }))); return { resolve: fontsourceResolver, faces }; } /** A face's Fontsource files, in a fixed order: latin, then latin-ext and greek * when `text` needs them and the family ships them (kitSubsetsFor), each with * its unicode-range. Null for a weight or slant Fontsource does not ship. */ async function fontsourceResolver(family, weight, style, { text = '' } = {}) { const id = fontsourceId(family); const meta = await fontsourceMeta(family); if (meta && !(meta.weights.includes(weight) && meta.styles.includes(style))) return null; kitStatus('Loading fonts…'); const at = await fontsourceAt(id); return ['latin', ...kitSubsetsFor(text, meta)].map((subset) => ({ source: `${at}files/${id}-${subset}-${weight}-${style}.woff2`, unicodeRange: kitRanges()[subset], })); } /** The unicode-range of each Fontsource file the kit loads (a function: the * kit is inlined after the pen's code, which runs before a const would be set). */ function kitRanges() { return { latin: 'U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,' + 'U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD', 'latin-ext': 'U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,' + 'U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF', greek: 'U+0370-03FF', }; } /** For the cjk, arabic and comics blocks, which add their own files: the * faces of `faces` = { 'Family Name': ['400', '400i', '700'] }. `text` is the sample: * č ł † α χ also load latin-ext and greek files (kitSubsetsFor). With * `optional`, a face Fontsource does not ship is skipped instead of failing. * The files are added once all have loaded, in the order asked (latin, then * latin-ext, then greek), whatever order they arrive in: the last one added * wins a character two files hold. Resolves to the number of faces added. */ async function loadFonts(faces, text = '', { optional = false } = {}) { kitStatus('Loading fonts…'); const ranges = kitRanges(); const jobs = []; for (const [family, specs] of Object.entries(faces)) { const id = fontsourceId(family); const todo = [...new Set(specs)].map((spec) => [parseInt(spec, 10), spec.endsWith('i') ? 'italic' : 'normal']) .filter(([weight, style]) => !hasFace(family, weight, style)); // before any await const at = fontsourceAt(id); const meta = optional || /[^\0-ÿ]/u.test(text) ? await fontsourceMeta(family) : null; const subsets = ['latin', ...kitSubsetsFor(text, meta)]; for (const [weight, style] of todo) { if (optional && !(meta?.weights.includes(weight) && meta.styles.includes(style))) continue; for (const subset of subsets) { const url = `${await at}files/${id}-${subset}-${weight}-${style}.woff2`; const face = new FontFace(family, `url(${url}) format('woff2')`, { weight: String(weight), style, unicodeRange: ranges[subset] }); jobs.push(face.load().catch(() => { if (subset === 'latin' && !optional) throw new Error(`Fontsource has no ${family} ${weight} ${style}`); })); } } } const ready = await Promise.all(jobs).catch((error) => { kitFail(error); throw error; }); return ready.filter((face) => face && document.fonts.add(face)).length; } /** A loaded FontFace covers this family, weight and style (fonts.check() would * also say yes for families nobody declared). */ function hasFace(family, weight, style) { for (const face of document.fonts) { if (face.status !== 'loaded' || face.style !== style) continue; if (face.family.replace(/^["']|["']$/g, '') !== family) continue; const [low, high = low] = face.weight.split(' ').map(Number); if (weight >= low && weight <= high) return true; } return false; } /** The files beyond latin `text` needs that `meta`'s family ships. */ function kitSubsetsFor(text, meta) { return [[/[Ā-˿ᴀ-ᶿḀ-ỿ†ℓⱠ-Ɀ꜠-ꟿ]/u, 'latin-ext'], [/[Ͱ-Ͽ]/u, 'greek']] .filter(([re, x]) => re.test(text) && meta?.subsets?.includes(x)).map(([, x]) => x); } /** Fontsource's id for a family: 'Source Serif 4' → 'source-serif-4'. */ function fontsourceId(family) { return family.toLowerCase().replace(/\s+/g, '-'); } /** A package's folder at the one version @5 names now (#766). */ function fontsourceAt(id, u = `https://cdn.jsdelivr.net/npm/@fontsource/${id}@`) { return (fontsourceAt.c ??= {})[id] ??= fetch(u + '5/package.json').then((r) => r.json()) .then((p) => p.version || 5, () => 5).then((v) => `${u}${v}/`); } /** The family's Fontsource metadata (weights, styles, subsets), or null. */ function fontsourceMeta(family) { fontsourceMeta.cache ??= new Map(); const id = fontsourceId(family); if (!fontsourceMeta.cache.has(id)) { fontsourceMeta.cache.set(id, fetch(`https://api.fontsource.org/v1/fonts/${id}`) .then((res) => (res.ok ? res.json() : null), () => null)); } return fontsourceMeta.cache.get(id); } // ─── Kit · viewer v2 ── the same in every recipe · postext.dev/cookbook /** The pages as spreads on a dark desk, page 1 alone, then verso | recto, * each painted when it scrolls near. Other options go to renderPageToCanvas. */ function showPages(docs, { title, width = 460, ...render } = {}) { const root = viewer(title); const pages = [docs].flat().flatMap((doc) => doc.pages.map((page) => ({ doc, page, n: (doc.pageIndexOffset ?? 0) + page.index }))); const spreads = []; let verso = null; for (const p of pages) { if (p.n % 2 === 1) { if (verso) spreads.push([verso, null]); verso = p; } else { spreads.push([verso, p]); verso = null; } } if (verso) spreads.push([verso, null]); const density = Math.min(window.devicePixelRatio || 1, 2); showPages.painter?.disconnect(); const painter = new IntersectionObserver((entries) => { for (const { isIntersecting, target } of entries) { if (!isIntersecting) continue; painter.unobserve(target); const { doc, page } = target.postext; renderPageToCanvas(page, doc, target, { ...render, scale: (width * density) / page.width }); } }, { rootMargin: '800px' }); showPages.painter = painter; root.replaceChildren(...spreads.map((pair) => { const spread = document.createElement('div'); spread.className = 'pt-spread'; for (const p of pair) { const figure = document.createElement('figure'); if (p) { const label = p.page.pageLabel || String(p.n + 1); const canvas = document.createElement('canvas'); canvas.postext = p; canvas.style.aspectRatio = `${p.page.width} / ${p.page.height}`; canvas.setAttribute('role', 'img'); canvas.setAttribute('aria-label', `Page ${label}`); const folio = document.createElement('figcaption'); folio.textContent = label; figure.append(canvas, folio); painter.observe(canvas); } else figure.className = 'pt-blank'; spread.append(figure); } return spread; })); kitStatus(`${pages.length} ${pages.length === 1 ? 'page' : 'pages'}`); document.documentElement.dataset.postext = 'ready'; return pages.length; } /** The desk, the bar and the error reporting, created once. */ function viewer(title) { if (!document.getElementById('pt-kit')) { document.head.insertAdjacentHTML('beforeend', `<style id="pt-kit"> :root { color-scheme: dark; } body { margin: 0; background: #0e1014; color: #b9bcc4; font: 13px/1.45 system-ui, sans-serif; } #pt-bar { position: sticky; top: 0; z-index: 1; display: flex; flex-wrap: wrap; align-items: center; gap: 6px 16px; padding: 10px 16px; background: rgb(14 16 20 / .92); backdrop-filter: blur(6px); border-bottom: 1px solid #23262d; } #pt-bar strong { color: #f4f1ea; font-weight: 600; } #pt-actions { display: flex; gap: 12px; margin-left: auto; } #pt-actions a, #pt-actions button { color: #d8a21a; font: inherit; background: none; border: 0; padding: 0; cursor: pointer; } #pages { display: grid; justify-items: center; gap: 48px; padding: 32px 16px 72px; } .pt-spread { display: flex; } .pt-spread figure { margin: 0; width: min(460px, 44vw); } .pt-spread canvas { display: block; width: 100%; background: #fff; box-shadow: 0 1px 2px rgb(0 0 0 / .5), 0 22px 44px -16px rgb(0 0 0 / .8); } .pt-spread figure:first-child canvas { box-shadow: inset -14px 0 14px -14px rgb(0 0 0 / .18), 0 1px 2px rgb(0 0 0 / .5), 0 22px 44px -16px rgb(0 0 0 / .8); } .pt-spread figcaption { margin-top: 10px; text-align: center; font: 600 10px/1 system-ui, sans-serif; letter-spacing: .18em; text-transform: uppercase; color: #6c7079; } .pt-blank { visibility: hidden; } @media (max-width: 760px) { .pt-spread { flex-direction: column; gap: 32px; } .pt-spread figure { width: min(460px, 92vw); } .pt-blank { display: none; } } </style>`); document.body.insertAdjacentHTML('afterbegin', '<header id="pt-bar"><strong id="pt-title"></strong><span id="pt-status" role="status"></span><span id="pt-actions"></span></header>'); document.getElementById('pt-title').textContent = document.title || 'Postext'; addEventListener('error', (event) => kitFail(event.error ?? event.message)); addEventListener('unhandledrejection', (event) => kitFail(event.reason)); } if (title) document.getElementById('pt-title').textContent = title; return document.getElementById('pages') ?? document.body.appendChild(Object.assign(document.createElement('main'), { id: 'pages' })); } function kitStatus(text) { viewer(); document.getElementById('pt-status').textContent = text; } function kitFail(error) { document.documentElement.dataset.postext = 'error'; kitStatus(`Error: ${error?.message ?? error}`); } // ─── /Kit ───────────────────────────────────────────────────────────────────────
Le script.js composé s’exécute tel quel : collez-le dans le script de module de n’importe quelle page, ou ouvrez la recette sur CodePen. Dossier de la recette sur GitHub ↗ (s’ouvre dans un nouvel onglet)
Variantes
#Lister toute la collection Zotero
Quand l’export est la liste, passez-vous des clés : chaque entrée du bloc s’imprime, citée ou non.
-nocite: "@fleck_genesis_1979, @merton_sociology_1973, @latour_science_1987, …"
+nocite: "@*"#Changer de style
Les mêmes clés donnent une liste Harvard (Cite Them Right) sans aucun autre changement.
- style: 'chicago-author-date', // '(Gieryn 1983)', 'Shapin and Schaffer (1985)'
+ style: 'harvard-cite-them-right',Erreurs fréquentes
Erreur fréquente
Une clé nocite qui ne correspond à rien est ignorée sans bruit
Une clé citée sans référence déclenche unknownCitationKey, mais une clé de nocite qui ne correspond à aucune entrée est ignorée sans avertissement. Copiez les clés depuis l’export et comptez les entrées de la liste. Bibliographie tirée des références →
- Une clé de
nocitequi ne correspond à aucune entrée est abandonnée sans avertissement. Copiez les clés depuis l’export au lieu de les taper, et comptez les entrées sur la page. - Zotero construit les clés à partir des données : corrigez le nom d’un auteur ou un titre dans Zotero et l’export suivant peut changer la clé, ce qui casse toutes ses citations. Figez les clés avant d’écrire le texte : Zotero lit une ligne
Citation Key: …dans le champ Extra d’un document. - Zotero exporte par défaut en UTF-8, avec les accents tels quels. Le bloc d’ici a été exporté avec l’option d’encodage occidental, qui écrit
{\"o}; Postext lit les deux. - La 18e édition de Chicago omet le lieu de publication : le champ
addressne s’imprime donc pas, et un article avec un DOI imprime le DOI et non l’urlJSTOR.
Crédits
- Recette
- Ignacio Ferro
- Texte
- Texte original, CC BY 4.0
- Polices
- Spectral (SIL OFL 1.1) · IBM Plex Sans Condensed (SIL OFL 1.1)
Modifier ce texte ↗ (s’ouvre dans un nouvel onglet)Dossier de la recette sur GitHub ↗ (s’ouvre dans un nouvel onglet)


