# Una bibliografía desde una exportación BibTeX de Zotero

> Lecturas de un seminario con la exportación BibTeX de Zotero tal cual, citada en Chicago autor-año, y nocite para las obras que el texto no cita.

- Versión HTML: https://postext.dev/es/cookbook/zotero-bibtex-nocite
- Receta N.º 097 · Estructura del libro · Nivel 2 (Intermedio) · Salidas: Canvas
- Géneros: Libros de texto, Informes y memorias
- Requiere postext ≥ 1.12.1 · probada con 1.12.1 el 2026-10-01
- Páginas: [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)
- Abrir en el Sandbox: https://postext.dev/es/sandbox#recipe=zotero-bibtex-nocite&lang=es (.postext: https://postext.dev/cookbook/zotero-bibtex-nocite/es/zotero-bibtex-nocite.postext)
- Última actualización: 2026-10-01
- Otros idiomas: [en](https://postext.dev/en/cookbook/zotero-bibtex-nocite.md), [zh](https://postext.dev/zh/cookbook/zotero-bibtex-nocite.md)

## En pocas palabras

La lista de lecturas de un seminario universitario. El profesor pega el archivo que exporta Zotero; Postext compone las citas y una lista que incluye también libros que el texto no menciona.

## Lo que vas a componer

La lista de lecturas de un seminario de seis semanas sobre historia y sociología de la ciencia, en una página de 210 × 280 mm a dos columnas. Un campo verde oscuro ocupa la mitad superior de la primera página, con el título del curso en Spectral Light de 50 pt; debajo, la guía del profesor para cada semana. Las referencias son la colección de Zotero del seminario, exportada en BibTeX y pegada en el documento sin tocar una línea: las claves que genera Zotero, sus llaves alrededor de las mayúsculas, los acentos `{\"o}`, `month = dec` y los campos `file`, `keywords` y `urldate`. El texto cita ocho de las obras. Las otras cinco, lecturas de fondo que el seminario no comenta, entran en la lista con `nocite`.

**Esta receta responde a:**

- ¿Cómo uso una exportación BibTeX de Zotero y listo obras que el texto no cita?
- ¿Cómo cito obras y compongo la bibliografía en APA, IEEE u otro estilo de cita?

## La respuesta corta

```js
// script.js, líneas 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
  },
};
```

## Ingredientes

**Enseña**

- [Bibliografía a partir de las referencias](https://postext.dev/es/docs/document-format.md#citas-y-bibliografía): La lista de obras citadas, construida a partir de las referencias del documento (front matter o un bloque BibTeX) donde está :::bibliography o tras el último capítulo.
- [Citas en un estilo de cita](https://postext.dev/es/docs/document-format.md#citas-y-bibliografía): Obras citadas como [@clave, pág. 33] y formateadas en un estilo CSL (APA, Chicago, MLA, IEEE, Vancouver, ISO 690, GB/T 7714…) elegido en los ajustes, enlazadas con sus entradas.

**También usa**

- [Aperturas diseñadas](https://postext.dev/es/docs/configuration.md#span-y-diseño-avanzado)
- [Atributos de título](https://postext.dev/es/docs/document-format.md#atributos-de-encabezado)
- [Una o dos columnas](https://postext.dev/es/docs/configuration.md#tipos-de-disposición)
- [Cabeceras y folios](https://postext.dev/es/docs/configuration.md#encabezados-y-pies)
- [Estilos de párrafo](https://postext.dev/es/docs/configuration.md#estilos-de-párrafo)
- [Paleta de color semántica](https://postext.dev/es/docs/configuration.md#paleta-de-colores)
- [Banda de capítulo a todo el ancho](https://postext.dev/es/docs/configuration.md#span-y-diseño-avanzado)
- [Cabeceras según el tipo de página](https://postext.dev/es/docs/configuration.md#elementos-de-texto)

**La configuración de un vistazo**

- [`bodyText`](https://postext.dev/es/docs/configuration.md#texto-de-cuerpo), [`citations`](https://postext.dev/es/docs/configuration.md#citas), [`colorPalette`](https://postext.dev/es/docs/configuration.md#paleta-de-colores), [`footer`](https://postext.dev/es/docs/configuration.md#encabezados-y-pies), [`header`](https://postext.dev/es/docs/configuration.md#encabezados-y-pies), [`headings`](https://postext.dev/es/docs/configuration.md#encabezados), [`layout`](https://postext.dev/es/docs/configuration.md#disposición), [`locale`](https://postext.dev/es/docs/configuration.md#separación-silábica), [`page`](https://postext.dev/es/docs/configuration.md#página), [`paragraphStyles`](https://postext.dev/es/docs/configuration.md#estilos-de-párrafo)

**API**

- [`LOCALES`](https://postext.dev/es/docs/document-format.md#citas-y-bibliografía), [`STYLES`](https://postext.dev/es/docs/document-format.md#citas-y-bibliografía), [`buildDocument`](https://postext.dev/es/docs/configuration.md#construir-un-documento), [`clearMeasurementCache`](https://postext.dev/es/docs/configuration.md#caché-de-medidas), [`createCiteprocEngine`](https://postext.dev/es/docs/document-format.md#citas-y-bibliografía), [`registerCitationEngine`](https://postext.dev/es/docs/document-format.md#citas-y-bibliografía), [`renderPageToCanvas`](https://postext.dev/es/docs/configuration.md#renderizar-una-página-a-un-bitmap)

**Tipografías**

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

## Elaboración

### 1 · El estilo se nombra una vez

El código es [la respuesta corta](#la-respuesta-corta) de arriba. El motor de citas se registra antes de la primera composición y el estilo es un solo ajuste: Chicago autor-año escribe «(Shapin 1984)» para `[@shapin_pump_1984]` y «Shapin y Schaffer (1985)» para `@shapin_leviathan_1985` dentro de la frase. Las claves son las que Zotero arma con el primer autor, la primera palabra con peso del título y el año, así que `gieryn_boundary-work_1983` conserva su guion; Postext lee un guion o un guion bajo dentro de una clave como parte de ella.

### 2 · La exportación, tal como la escribe Zotero

El último bloque del documento guarda la exportación, una entrada por obra y sangrada con tabuladores, como la escribe 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 quita las llaves que protegen las mayúsculas, compone `{\"o}` y `{\'e}` en ö y é, convierte `--` en semirraya y lee `month = dec` como un nombre de mes sin llaves. `urldate` pasa a ser la fecha de consulta y `language` la lengua de la obra; `file`, `keywords`, `abstract` y `shorttitle` se saltan, así que nada de tu disco llega a la página.

### 3 · Las obras que el texto no cita

El preámbulo las enumera por su clave:

```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 lista recoge todas las obras citadas más estas cinco, ordenadas juntas por autor como pide Chicago; ninguna marca distingue una entrada no citada. `nocite: "@*"` lista la exportación entera, y `bibliography.includeUncited: true` hace lo mismo desde la configuración, para un diseño que siempre imprime toda la biblioteca.

### 4 · Un campo verde para la tarjeta

```js
// script.js, líneas 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 es un recuadro anclado al corte, de 132 mm, y las dos columnas empiezan 9 mm por debajo. El título, el filete y la entradilla se encadenan con anclas `below`, de modo que un título más largo empuja la entradilla hacia abajo en lugar de chocar con ella.

```js
// script.js, líneas 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 receta completa

Un solo archivo, compuesto a partir de la carpeta de la receta con el texto de ejemplo y el kit común del Recetario ya incluidos; construye su propia página. Para ejecutarlo, ponlo en un `<script type="module">` de una página vacía o pégalo en el panel JS de un pen nuevo de CodePen (como módulo). Importa postext desde esm.sh, así que no hay nada que instalar ni compilar.

- Carpeta de la receta: 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 ───────────────────────────────────────────────────────────────────────
```

## Variantes

### Lista la colección entera de Zotero

Cuando la exportación es la lista, sobran las claves: se imprimen todas las entradas del bloque, citadas o no.

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

### Cambia el estilo

Las mismas claves dan una lista Harvard (Cite Them Right) sin ningún otro cambio.

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

## Errores frecuentes

- **Una clave de nocite que no coincide con nada se descarta sin aviso.** Una clave citada sin referencia da el aviso unknownCitationKey, pero una clave de nocite que no corresponde a ninguna entrada se omite sin avisar. Copia las claves de la exportación y cuenta las entradas de la lista.
- **Cualquier objeto headings desactiva el salto de página del H1.** Por defecto un H1 salta a una página impar (always-odd), pero cualquier objeto headings anula ese valor, así que los capítulos van seguidos y span: 'page' no hace nada. Vuelve a declarar headings.levels[0].breakBefore: { enabled: true, parity } en cada configuración.
- **Carga todas las fuentes antes de componer.** La composición mide el texto con las fuentes que el navegador ha cargado y guarda los anchos, así que una fuente que llega después de la primera composición deja cortes de línea erróneos y un PDF que ya no coincide con la pantalla. Carga antes todos los pesos y estilos, y llama a clearMeasurementCache() antes de recomponer si alguna llega tarde.
- **Entrecomilla cada valor del frontmatter.** YAML lee title: 1984 como un número y una fecha como un objeto Date, y los valores que no son cadenas se imprimen vacíos en los marcadores y dejan el PDF sin título. Entrecomilla cada valor: title: "1984".
- **Una configuración se cachea por identidad: crea un objeto nuevo.** El motor guarda en caché las configuraciones resueltas según la identidad del objeto, así que modificar el mismo objeto y volver a componer reutiliza el resultado anterior. Crea un objeto nuevo en cada composición: por eso la configuración de una receta es una función, config().

- Una clave de `nocite` que no corresponde a ninguna entrada desaparece sin aviso. Copia las claves de la exportación en vez de teclearlas y cuenta las entradas en la página.
- Zotero arma las claves a partir de los datos: si corriges el nombre de un autor o un título en Zotero, la siguiente exportación puede cambiar la clave y romper todas sus citas. Fija las claves antes de escribir el texto: Zotero lee una línea `Citation Key: …` en el campo Extra de cada registro.
- Zotero exporta en UTF-8 por omisión, con los acentos tal cual. El bloque de esta receta se exportó con la codificación Western, que escribe `{\"o}`; Postext lee las dos.
- La 18.ª edición de Chicago omite el lugar de edición, así que el campo `address` no se imprime, y un artículo con DOI muestra el DOI y no el `url` de JSTOR.

## Créditos

- Receta: Ignacio Ferro ([@drnachio](https://github.com/drnachio))
- Tipografías: Spectral (OFL-1.1), IBM Plex Sans Condensed (OFL-1.1)
- Código: MIT · Contenido de ejemplo: CC-BY-4.0

## Relacionadas

- [N.º 087 · Un capítulo de tesis citado en APA 7](https://postext.dev/es/cookbook/apa-thesis-with-bibtex.md): Un capítulo de tesis doctoral cuyas citas [@clave, p. 33] salen en APA 7 gracias a citeproc-js, con la lista de referencias tomada de un bloque BibTeX. · Nivel 2 (Intermedio) · Artículos y trabajos académicos, Informes y memorias
- [N.º 093 · Un mismo texto en cuatro estilos de cita](https://postext.dev/es/cookbook/one-text-four-citation-styles.md): Un ensayo breve con seis citas, compuesto en APA 7, Chicago autor-año, IEEE e ISO 690: el ajuste del estilo es lo único que cambia. · Nivel 2 (Intermedio) · Artículos y trabajos académicos, Informes y memorias
- [N.º 073 · Un índice onomástico y un índice de materias](https://postext.dev/es/cookbook/names-and-subjects-indexes.md): Las marcas con index="names" van al índice de personas y las demás al de materias; :::index imprime cada uno a dos columnas y la maquetación fija las páginas. · Nivel 2 (Intermedio) · Libros de texto
