# Una bibliografia des d'una exportació BibTeX de Zotero

> Lectures d'un seminari amb l'exportació BibTeX de Zotero tal qual, citada en Chicago autor-any, i nocite per a les obres que el text no cita.

- Versió HTML: https://postext.dev/ca/cookbook/zotero-bibtex-nocite
- Recepta Núm. 097 · Estructura del llibre · Nivell 2 (Intermedi) · Sortides: Canvas
- Gèneres: Llibres de text, Informes i memòries
- Requereix postext ≥ 1.12.1 · provada amb 1.12.1 el 2026-10-01
- Pàgines: [1](https://postext.dev/cookbook/zotero-bibtex-nocite/es/p01.webp?v=e11674e3), [2](https://postext.dev/cookbook/zotero-bibtex-nocite/es/p02.webp?v=e11674e3)
- Obre al Sandbox: https://postext.dev/ca/sandbox#recipe=zotero-bibtex-nocite&lang=es (.postext: https://postext.dev/cookbook/zotero-bibtex-nocite/es/zotero-bibtex-nocite.postext)
- Última actualització: 2026-10-01
- Altres idiomes: [en](https://postext.dev/en/cookbook/zotero-bibtex-nocite.md), [es](https://postext.dev/es/cookbook/zotero-bibtex-nocite.md), [zh](https://postext.dev/zh/cookbook/zotero-bibtex-nocite.md), [ar](https://postext.dev/ar/cookbook/zotero-bibtex-nocite.md)

## En poques paraules

La llista de lectures d'un seminari universitari. El professor enganxa el fitxer que exporta Zotero; Postext compon les cites i una llista que inclou també llibres que el text no esmenta.

## Què compondràs

La llista de lectures d'un seminari de sis setmanes sobre història i sociologia de la ciència, en una pàgina de 210 × 280 mm a dues columnes. Un camp verd fosc ocupa la meitat superior de la primera pàgina, amb el títol del curs en Spectral Light de 50 pt; a sota, la guia del professor per a cada setmana. Les referències són la col·lecció de Zotero del seminari, exportada en BibTeX i enganxada al document sense tocar-ne ni una línia: les claus de cita que genera Zotero, les claus que posa al voltant de les majúscules, els accents `{\"o}`, `month = dec` i els camps `file`, `keywords` i `urldate`. El text cita vuit de les obres. Les altres cinc, lectures de fons que el seminari no comenta, entren a la llista amb `nocite`.

**Aquesta recepta respon a:**

- Com faig servir una exportació BibTeX de Zotero i llisto obres que el text no cita?
- Com cito obres i componc la bibliografia en APA, IEEE o un altre estil de cita?

## La resposta curta

```js
// script.js, línies 28–40
// 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
  },
};
```

## Ingredients

**Ensenya**

- [Bibliografia a partir de les referències](https://postext.dev/ca/docs/document-format.md#citacions-i-bibliografia): La llista d'obres citades, construïda a partir de les referències del document (front matter o un bloc BibTeX) on hi ha :::bibliography o després de l'últim capítol.
- [Cites en un estil de cita](https://postext.dev/ca/docs/document-format.md#citacions-i-bibliografia): Obres citades com a [@clau, pàg. 33] i formatades en un estil CSL (APA, Chicago, MLA, IEEE, Vancouver, ISO 690, GB/T 7714…) triat a la configuració, enllaçades amb les seves entrades.

**També fa servir**

- [Obertures dissenyades](https://postext.dev/ca/docs/configuration.md#span-i-disseny-avançat)
- [Atributs de títol](https://postext.dev/ca/docs/document-format.md#atributs-dencapçalament)
- [Una o dues columnes](https://postext.dev/ca/docs/configuration.md#tipus-de-disposició)
- [Capçaleres i folis](https://postext.dev/ca/docs/configuration.md#capçaleres-i-peus)
- [Estils de paràgraf](https://postext.dev/ca/docs/configuration.md#estils-de-paràgraf)
- [Paleta de color semàntica](https://postext.dev/ca/docs/configuration.md#paleta-de-colors)
- [Banda de capítol d'amplada completa](https://postext.dev/ca/docs/configuration.md#span-i-disseny-avançat)
- [Capçaleres segons el tipus de pàgina](https://postext.dev/ca/docs/configuration.md#elements-de-text)

**La configuració d'un cop d'ull**

- [`bodyText`](https://postext.dev/ca/docs/configuration.md#text-de-cos), [`citations`](https://postext.dev/ca/docs/configuration.md#cites), [`colorPalette`](https://postext.dev/ca/docs/configuration.md#paleta-de-colors), [`footer`](https://postext.dev/ca/docs/configuration.md#capçaleres-i-peus), [`header`](https://postext.dev/ca/docs/configuration.md#capçaleres-i-peus), [`headings`](https://postext.dev/ca/docs/configuration.md#encapçalaments), [`layout`](https://postext.dev/ca/docs/configuration.md#disposició), [`locale`](https://postext.dev/ca/docs/configuration.md#partició-de-mots), [`page`](https://postext.dev/ca/docs/configuration.md#pàgina), [`paragraphStyles`](https://postext.dev/ca/docs/configuration.md#estils-de-paràgraf)

**API**

- [`LOCALES`](https://postext.dev/ca/docs/document-format.md#citacions-i-bibliografia), [`STYLES`](https://postext.dev/ca/docs/document-format.md#citacions-i-bibliografia), [`buildDocument`](https://postext.dev/ca/docs/configuration.md#construir-un-document), [`clearMeasurementCache`](https://postext.dev/ca/docs/configuration.md#memòria-cau-de-mesures), [`createCiteprocEngine`](https://postext.dev/ca/docs/document-format.md#citacions-i-bibliografia), [`registerCitationEngine`](https://postext.dev/ca/docs/document-format.md#citacions-i-bibliografia), [`renderPageToCanvas`](https://postext.dev/ca/docs/configuration.md#renderitzar-una-pàgina-a-un-bitmap)

**Tipus de lletra**

- Spectral (OFL-1.1), IBM Plex Sans Condensed (OFL-1.1)

## Elaboració

### 1 · L'estil s'anomena una vegada

El codi és [la resposta curta](#la-resposta-curta) de més amunt. El motor de cites es registra abans de la primera composició i l'estil és un sol ajust: Chicago autor-any escriu «(Shapin 1984)» per a `[@shapin_pump_1984]` i «Shapin y Schaffer (1985)» per a `@shapin_leviathan_1985` dins de la frase. Les claus són les que Zotero construeix amb el primer autor, la primera paraula amb pes del títol i l'any, així que `gieryn_boundary-work_1983` conserva el seu guionet; Postext llegeix un guionet o un guió baix dins d'una clau com a part de la clau.

### 2 · L'exportació, tal com l'escriu Zotero

L'últim bloc del document guarda l'exportació, una entrada per obra i sagnada amb tabuladors, com l'escriu Zotero:

```bibtex
@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},
}
```

El lector treu les claus que protegeixen les majúscules, compon `{\"o}` i `{\'e}` en ö i é, converteix `--` en guió mitjà i llegeix `month = dec` com un nom de mes sense claus. `urldate` passa a ser la data de consulta i `language` la llengua de l'obra; `file`, `keywords`, `abstract` i `shorttitle` se salten, així que res del teu disc no arriba a la pàgina.

### 3 · Les obres que el text no cita

El preàmbul les enumera per la seva clau:

```yaml
---
title: "La ciencia como práctica social"
nocite: "@fleck_genesis_1979, @merton_sociology_1973, @latour_science_1987, @rheinberger_toward_1997, @lopez_pinero_ciencia_1979"
---
```

La llista recull totes les obres citades més aquestes cinc, ordenades juntes per autor com demana Chicago; cap marca no distingeix una entrada no citada. `nocite: "@*"` llista l'exportació sencera, i `bibliography.includeUncited: true` fa el mateix des de la configuració, per a un disseny que sempre imprimeix tota la biblioteca.

### 4 · Un camp verd per a la targeta

```js
// script.js, línies 44–68
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 banda és un requadre ancorat al tall, de 132 mm, i les dues columnes comencen 9 mm per sota. El títol, el filet i l'entradeta s'encadenen amb àncores `below`, de manera que un títol més llarg empeny l'entradeta cap avall en lloc de xocar-hi.

```js
// script.js, línies 15–21
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 recepta completa

Un sol fitxer, compost a partir de la carpeta de la recepta amb el text d'exemple i el kit comú del Receptari ja inclosos; construeix la seva pròpia pàgina. Per executar-lo, posa'l en un `<script type="module">` d'una pàgina buida o enganxa'l al tauler JS d'un pen nou de CodePen (com a mòdul). Importa postext des d'esm.sh, així que no cal instal·lar ni compilar res.

- Carpeta de la recepta: https://github.com/drnachio/postext/tree/main/cookbook/zotero-bibtex-nocite

### script.js

```js
// ═══ 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.12.1
import {
  buildDocument, renderPageToCanvas, clearMeasurementCache, registerCitationEngine,
} from 'https://esm.sh/postext';
import { createCiteprocEngine, STYLES, LOCALES } from 'https://esm.sh/postext-citeproc';

const LANG = 'es'; // @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 = () => ({ // a factory: the engine caches resolved configs per object
  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: [
      // Restated: any headings object drops the H1 break (gotcha: headings-drop-h1-break).
      { 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`---
title: "La ciencia como práctica social"
nocite: "@fleck_genesis_1979, @merton_sociology_1973, @latour_science_1987, @rheinberger_toward_1997, @lopez_pinero_ciencia_1979"
---

# La ciencia como práctica social {kicker="HPS 214 · Lecturas del seminario · Otoño de 2026" lead="Seis semanas sobre cómo se produce, se pone a prueba y se acredita el conocimiento científico, del paradigma al cuaderno de laboratorio."}

Este seminario lee la historia y la sociología de la ciencia como una sola materia. Su pregunta es cómo una afirmación sobre la naturaleza llega a ser un hecho en el que confían personas que nunca vieron el experimento. Las respuestas que leeremos vienen de historiadores del siglo XVII, de sociólogos que pasaron dos años dentro de un laboratorio y de filósofos que miraron lo que hacen los científicos y no lo que dicen sus manuales.

Cada semana tiene una lectura obligatoria, que todos leen antes de la sesión, y uno o dos textos complementarios para quien presenta el debate. Las lecturas obligatorias están en el dosier del curso; las demás, en la reserva de la biblioteca. Casi todas están en inglés; las que tienen traducción española la tienen también en la reserva.

## Semana 1 · Paradigmas y ciencia normal

Empezamos por el libro que puso de moda la disciplina. Leed las cinco primeras secciones y el epílogo de @kuhn_structure_1970, que Kuhn añadió a la segunda edición para responder a quienes le reprochaban que «paradigma» significaba demasiadas cosas a la vez. En el prefacio reconoce su deuda con una monografía de Ludwik Fleck, escrita en 1935 y casi olvidada hasta que Kuhn la mencionó; está en la lista final, y quien presente la semana debe leer su segundo capítulo.

## Semana 2 · Ser testigo de un experimento

La lectura obligatoria son los dos primeros capítulos de @shapin_leviathan_1985, sobre la bomba de vacío de Robert Boyle y la disputa con Thomas Hobbes sobre si un experimento podía zanjar algo. El texto complementario es el artículo, más breve, en el que Shapin describió por primera vez la «tecnología literaria» de Boyle: un informe en prosa llana y detallada que convertía a sus lectores en testigos [@shapin_pump_1984].

## Semana 3 · Dentro del laboratorio

Bruno Latour pasó dos años en el Instituto Salk como observador de un laboratorio de neuroendocrinología. Leed los capítulos segundo y tercero de @latour_laboratory_1979 y seguid cómo un enunciado pierde sus cautelas («parece que», «X ha afirmado») al pasar de un artículo a otro, hasta que se imprime sin ninguna.

## Semana 4 · Dónde acaba la ciencia

El problema de la demarcación pregunta qué separa la ciencia de lo que no lo es. Los filósofos buscaron un criterio; @gieryn_boundary-work_1983 miró a quienes trazan la línea en la práctica y a lo que ganan trazándola donde la trazan. Traed un ejemplo de la prensa del último mes.

## Semana 5 · La objetividad tiene historia

Leed primero el artículo [@daston_image_1992] y después el primer capítulo y el dedicado al «juicio experto» del libro en que se convirtió [@daston_objectivity_2007]. La tesis es que la objetividad, como virtud del científico, apareció en el siglo XIX, y que los atlas de esqueletos, copos de nieve y nubes muestran su llegada.

## Semana 6 · A quién creer

La última semana vuelve al siglo XVII con una pregunta más amplia: ¿cómo decidían los caballeros filósofos qué informes aceptar? Leed el primer capítulo de @shapin_social_1994 y venid dispuestos a comparar su respuesta con la manera en que decidís a quién creer cuando leéis un artículo de otra especialidad.

## Para seguir leyendo

La lista que sigue es la colección de Zotero del seminario, exportada en BibTeX y compuesta tal como salió de Zotero. Incluye todas las obras citadas arriba y cinco que el seminario no comenta pero que encontraréis en los ensayos que leáis: la monografía de Fleck, los ensayos de Merton sobre las normas de la ciencia, el manual posterior de Latour, el estudio de Rheinberger sobre los sistemas experimentales y una historia de la ciencia española de los siglos XVI y XVII.

:::bibliography{title=""}

:::paragraphs{style="colophon"}
Compuesto en Spectral e IBM Plex Sans Condensed (SIL OFL) · Lista y notas: originales, CC BY 4.0 · Las obras son reales; las rutas de archivo y las fechas de consulta de Zotero son ilustrativas.
:::

:::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 ───────────────────────────────────────────────────────
await loadFonts(FONTS, markdown);
const doc = await buildWithFonts(() => buildDocument({ markdown }, config()), markdown);
showPages(doc, { title: t({ en: 'A reading list from Zotero', es: 'Lecturas desde Zotero' }) });

// ─── 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 v1 ── the same in every recipe · postext.dev/cookbook ────────
// Postext measures text with the faces the browser has loaded, and caches the
// widths, so every face must be ready before the first build. Faces come from
// Fontsource: the same static files the PDF embeds, so screen and PDF agree.

/** faces = { 'Family Name': ['400', '400i', '700'] }. `text` is the sample:
 *  letters beyond Latin-1 (č, ł, ő…) also load the latin-ext files. With
 *  `optional`, a face Fontsource does not ship is skipped instead of failing.
 *  Resolves to the number of faces added. */
async function loadFonts(faces, text = '', { optional = false } = {}) {
  kitStatus('Loading fonts…');
  const ranges = {
    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',
  };
  const subsets = /[Ā-˿Ḁ-ỿ]/.test(text) ? ['latin', 'latin-ext'] : ['latin'];
  const jobs = [];
  let added = 0;
  for (const [family, specs] of Object.entries(faces)) {
    const id = fontsourceId(family);
    const meta = optional ? await fontsourceMeta(family) : null;
    for (const spec of new Set(specs)) {
      const weight = parseInt(spec, 10);
      const style = spec.endsWith('i') ? 'italic' : 'normal';
      if (hasFace(family, weight, style)) continue;
      if (optional && !(meta?.weights.includes(weight) && meta.styles.includes(style))) continue;
      for (const subset of subsets) {
        const url = `https://cdn.jsdelivr.net/npm/@fontsource/${id}@5/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().then((ready) => { document.fonts.add(ready); added++; }, () => {
          if (subset === 'latin' && !optional) throw new Error(`Fontsource has no ${family} ${weight} ${style}`);
        }));
      }
    }
  }
  await Promise.all(jobs).catch((error) => { kitFail(error); throw error; });
  return added;
}

/** Runs `build` (a buildDocument or buildBundle call) and checks the faces
 *  the pages use. A regular face missing from FONTS is loaded with a warning;
 *  bold and italic variants are loaded when the family ships them. Then the
 *  measurement caches are cleared and the build runs again. */
async function buildWithFonts(build, text = '') {
  const tried = new Set();
  for (let round = 0; round < 3; round++) {
    kitStatus('Laying out…');
    await new Promise(requestAnimationFrame);          // let the status paint first
    const result = await Promise.resolve().then(build).catch((error) => { kitFail(error); throw error; });
    const wanted = { base: {}, variants: {} };
    for (const { font, base } of [result].flat().flatMap(fontStringsOf)) {
      const { family, weight, style } = parseFont(font);
      const key = `${family}|${weight}|${style}`;
      if (tried.has(key) || hasFace(family, weight, style)) continue;
      tried.add(key);
      (wanted[base ? 'base' : 'variants'][family] ??= []).push(`${weight}${style === 'italic' ? 'i' : ''}`);
    }
    if (Object.keys(wanted.base).length) {
      console.warn(`[cookbook] FONTS does not list ${JSON.stringify(wanted.base)}: loading them.`);
    }
    const added = await loadFonts(wanted.base, text) + await loadFonts(wanted.variants, text, { optional: true });
    if (added === 0) return result;
    clearMeasurementCache();
  }
  throw new Error('The fonts did not settle after three builds.');
}

/** Every font string of the layout. `base` marks a block's own face; its
 *  bold, italic and bold-italic variants are listed whether or not used. */
function fontStringsOf(doc) {
  const found = new Map();
  const walk = (node) => {
    if (!node || typeof node !== 'object') return;
    if (Array.isArray(node)) { node.forEach(walk); return; }
    for (const [key, value] of Object.entries(node)) {
      if (typeof value === 'string' && /fontString$/i.test(key)) {
        found.set(value, found.get(value) || key === 'fontString');
      } else if (value && typeof value === 'object') walk(value);
    }
  };
  walk(doc.pages);
  walk(doc.blocks);
  return [...found].map(([font, base]) => ({ font, base }));
}

/** '700 37.5px Open Sans' / 'italic 400 13px "Source Serif 4"' → { family, weight, style }.
 *  A string with no weight ('95.8px Young Serif', from a design text) is 400. */
function parseFont(font) {
  const m = /^(?:(italic|oblique)\s+)?(?:small-caps\s+)?(?:(\d+|bold|normal)\s+)?[\d.]+px\s+(.+)$/.exec(font.trim());
  if (!m) throw new Error(`Unexpected font string: ${font}`);
  const weight = m[2] === 'bold' ? 700 : !m[2] || m[2] === 'normal' ? 400 : Number(m[2]);
  return { family: m[3].replace(/^["']|["']$/g, ''), weight, style: m[1] ? 'italic' : 'normal' };
}

/** True when a loaded FontFace covers exactly this family, weight and style
 *  (document.fonts.check() is also true 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;
}

/** Fontsource's id for a family: 'Source Serif 4' → 'source-serif-4'. */
function fontsourceId(family) { return family.toLowerCase().replace(/\s+/g, '-'); }

/** The weights and styles a family ships ({ weights: [400, 700], styles: ['normal', 'italic'] }), 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 v1 ── the same in every recipe · postext.dev/cookbook ───────
/** Shows the pages as facing spreads on a dark desk: the first page is a
 *  recto on its own, then verso | recto pairs, as in a bound book. Pages
 *  are painted when they scroll near the screen. */
function showPages(docs, { title, width = 460 } = {}) {
  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, { 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 ───────────────────────────────────────────────────────────────────────
```

## Variants

### Llista la col·lecció sencera de Zotero

Quan l'exportació és la llista, les claus sobren: s'imprimeixen totes les entrades del bloc, citades o no.

```diff
-nocite: "@fleck_genesis_1979, @merton_sociology_1973, @latour_science_1987, …"
+nocite: "@*"
```

### Canvia l'estil

Les mateixes claus donen una llista Harvard (Cite Them Right) sense cap altre canvi.

```diff
-  style: 'chicago-author-date', // '(Gieryn 1983)', 'Shapin and Schaffer (1985)'
+  style: 'harvard-cite-them-right',
```

## Errors freqüents

- **Una clau de nocite que no coincideix amb res es descarta sense avís.** Una clau citada sense referència dona l'avís unknownCitationKey, però una clau de nocite que no correspon a cap entrada s'omet sense avisar. Copia les claus de l'exportació i compta les entrades de la llista.
- **Qualsevol objecte headings desactiva el salt de pàgina de l'H1.** Per defecte un H1 salta a una pàgina senar (always-odd), però qualsevol objecte headings anul·la aquest valor, de manera que els capítols van seguits i span: 'page' no fa res. Torna a declarar headings.levels[0].breakBefore: { enabled: true, parity } a cada configuració.
- **Carrega totes les fonts abans de compondre.** La composició mesura el text amb les fonts que el navegador ha carregat i en desa les amplades, així que una font que arriba després de la primera composició deixa talls de línia erronis i un PDF que ja no coincideix amb la pantalla. Carrega abans tots els pesos i estils, i crida clearMeasurementCache() abans de recompondre si alguna arriba tard.
- **Posa entre cometes cada valor del frontmatter.** YAML llegeix title: 1984 com un nombre i una data com un objecte Date, i els valors que no són cadenes s'imprimeixen buits als marcadors i deixen el PDF sense títol. Posa entre cometes cada valor: title: "1984".
- **Una configuració es desa a la memòria cau per identitat: crea un objecte nou.** El motor desa a la memòria cau les configuracions resoltes segons la identitat de l'objecte, de manera que modificar el mateix objecte i tornar a compondre reutilitza el resultat anterior. Crea un objecte nou a cada composició: per això la configuració d'una recepta és una funció, config().

- Una clau de `nocite` que no correspon a cap entrada desapareix sense avís. Copia les claus de l'exportació en comptes de teclejar-les i compta les entrades a la pàgina.
- Zotero construeix les claus a partir de les dades: si corregeixes el nom d'un autor o un títol a Zotero, la propera exportació pot canviar la clau i trencar totes les seves cites. Fixa les claus abans d'escriure el text: Zotero llegeix una línia `Citation Key: …` al camp Extra de cada registre.
- Zotero exporta en UTF-8 per defecte, amb els accents tal qual. El bloc d'aquesta recepta es va exportar amb la codificació Western, que escriu `{\"o}`; Postext llegeix totes dues.
- La 18a edició de Chicago omet el lloc d'edició, així que el camp `address` no s'imprimeix, i un article amb DOI mostra el DOI i no l'`url` de JSTOR.

## Crèdits

- Recepta: Ignacio Ferro ([@drnachio](https://github.com/drnachio))
- Tipus de lletra: Spectral (OFL-1.1), IBM Plex Sans Condensed (OFL-1.1)
- Codi: MIT · Contingut d'exemple: CC-BY-4.0

## Relacionades

- [Núm. 087 · Un capítol de tesi citat en APA 7](https://postext.dev/ca/cookbook/apa-thesis-with-bibtex.md): Un capítol de tesi doctoral amb citacions [@clau, p. 33] que surten en APA 7 gràcies a citeproc-js, amb la llista de referències presa d'un bloc BibTeX. · Nivell 2 (Intermedi) · Articles i treballs acadèmics, Informes i memòries
- [Núm. 093 · Un mateix text en quatre estils de citació](https://postext.dev/ca/cookbook/one-text-four-citation-styles.md): Un assaig breu amb sis citacions, compost en APA 7, Chicago autor-any, IEEE i ISO 690: l'opció d'estil és l'únic que canvia. · Nivell 2 (Intermedi) · Articles i treballs acadèmics, Informes i memòries
- [Núm. 073 · Un índex onomàstic i un índex de matèries](https://postext.dev/ca/cookbook/names-and-subjects-indexes.md): Les marques amb index="names" van a l'índex de persones i la resta al de matèries; :::index els imprimeix a dues columnes i la maquetació en fixa les pàgines. · Nivell 2 (Intermedi) · Llibres de text
