かんたんな説明
大学のゼミの文献リストです。教員はZoteroが書き出したファイルをそのまま貼り付け、Postextが引用と、本文で触れない本も含めた一覧を刷ります。
できあがり
科学史・科学社会学を扱う6週のゼミの文献リストを、210×280mmのページに2段組みで組みます。1ページの上半分を深緑の面が占め、50ptのSpectral Lightで授業の題を置きます。その下の本文は、担当教員による各週の手引きです。文献はゼミのZoteroコレクションで、BibTeXとして書き出し、1行も手を加えずに文書に貼り付けています。Zoteroの引用キー、大文字を守る波括弧、{\"o}のアクセント、month = dec、file、keywords、urldateのフィールドもそのままです。本文はそのうち8つの文献を引用します。残りの5つは、ゼミでは扱わない参考書で、nociteを通じて一覧に入ります。
このレシピが答える質問
- ZoteroからBibTeXで書き出した文献を使い、本文で引用しない文献も一覧に載せるには?
- APA、IEEEなどの引用スタイルで、文献を引用し参考文献を作るには?
手短な答え
// 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
},
};
材料
- 併用する機能
- 段抜きの章見出し帯ページの役割ごとの柱
- 種類
- Spectral, IBM Plex Sans Condensed (SIL OFL 1.1)
- 素材
- なし:図はすべてコードで描画
作り方
#1 · スタイルは一度だけ指定する
コードは上の手短な答えにあります。引用エンジンは最初のビルドの前に登録し、スタイルは1つの設定で決まります。シカゴ方式の著者年形式は、[@shapin_pump_1984]を「(Shapin 1984)」と、文中の@shapin_leviathan_1985を「Shapin and Schaffer (1985)」と書きます。キーはZoteroが第1著者、題の最初の主要な語、年から作ったものなので、gieryn_boundary-work_1983はハイフンを含んだままです。Postextはキーの中のハイフンやアンダースコアをキーの一部として読みます。
#2 · 書き出しをZoteroが書いたまま貼る
文書の最後のブロックが書き出しで、文献1つにつき1項目、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},
}読み込み処理は、大文字を守る波括弧を外し、{\"o}と{\'e}をöとéに合成し、--をenダッシュに変え、month = decを素の月名として読みます。urldateは閲覧日に、languageはその項目の言語になります。file、keywords、abstract、shorttitleは読み飛ばすので、ハードディスク上の情報がページに出ることはありません。
#3 · 本文で引かない文献を加える
フロントマターにキーで列挙します。
---
title: "Science as a Social Practice"
nocite: "@fleck_genesis_1979, @merton_sociology_1973, @latour_science_1987, @rheinberger_toward_1997, @lopez_pinero_ciencia_1979"
---一覧には、引用したすべての文献にこの5つを加え、シカゴ方式の求めるとおり著者順にまとめて並べます。引用していない項目に印は付きません。nocite: "@*"は書き出し全体を並べます。bibliography.includeUncited: trueは同じことを設定の側から行うので、常にライブラリー全体を刷るデザインに向きます。
#4 · 案内カードの緑の面
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' } } },
] },
};
帯は仕上がりに固定した深さ132mmの箱で、2つの段はその9mm下から始まります。題、罫、リードはbelowのアンカーでつないであるので、授業の題が長くなるとリードは重ならずに下へ押し下げられます。
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' } }));
レシピの全体
// ═══ 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 = '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 = () => ({ // 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`---Markdownの見本 · 204行 · 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 ─────────────────────────────────────────────────────── 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' }) });キット · core, fonts, viewer:全レシピ共通 · 235行
// ─── 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 ───────────────────────────────────────────────────────────────────────
組み立てたscript.jsはそのまま動きます。任意のページのモジュールスクリプトに貼り付けるか、レシピをCodePenで開いてください。 GitHub上のレシピのフォルダー ↗ (新しいタブで開きます)
アレンジ
#Zoteroのコレクション全体を並べる
書き出しそのものが一覧になる場合は、キーを省きます。ブロックの項目は、引用の有無にかかわらずすべて刷られます。
-nocite: "@fleck_genesis_1979, @merton_sociology_1973, @latour_science_1987, …"
+nocite: "@*"#スタイルを変える
同じキーのまま、ほかに何も変えずにハーバード方式(Cite Them Right)の一覧になります。
- style: 'chicago-author-date', // '(Gieryn 1983)', 'Shapin and Schaffer (1985)'
+ style: 'harvard-cite-them-right',よくあるつまずき
つまずき
何にも一致しないnociteのキーは何の知らせもなく捨てられる
文献のない引用キーはunknownCitationKeyを出しますが、どの項目にも一致しないnociteのキーは警告なしに飛ばされます。キーは書き出したデータからコピーし、文献一覧の項目数を数えてください。 参考文献から作る文献一覧 →
つまずき
headingsオブジェクトを渡すとH1の改ページが消える
既定ではH1は奇数ページへ改ページします(always-odd)。ところがheadingsオブジェクトを渡すと中身にかかわらずこの既定がリセットされ、章は改ページせずに続けて組まれ、span: 'page'も効かなくなります。どの設定でもheadings.levels[0].breakBefore: { enabled: true, parity }を書き直してください。 奇数ページから始まる章 →
つまずき
レイアウトの前にすべてのフォントを読み込む
レイアウトはブラウザーが読み込んだフォントで文字を計測し、その幅をキャッシュします。最初のビルドのあとに届いたフォントがあると改行位置が狂い、PDFも画面と一致しなくなります。すべてのウェイトとスタイルを先に読み込み、遅れて届いたときは再ビルドの前にclearMeasurementCache()を呼んでください。 レイアウト前のフォント読み込み →
つまずき
フロントマターの値はすべて引用符で囲む
YAMLはtitle: 1984を数値として、日付をDateオブジェクトとして読みます。文字列でない値はプレースホルダーに空で出力され、PDFにもタイトルが付きません。値はすべて引用符で囲んでください(title: "1984")。 文書のメタデータ →
つまずき
設定はオブジェクトの同一性でキャッシュされる。毎回新しいオブジェクトを作る
エンジンは解決済みの設定をオブジェクトの同一性でキャッシュします。そのため、設定をその場で書き換えて再ビルドすると前の結果が再利用されます。ビルドのたびに新しいオブジェクトを作ってください。レシピの設定がファクトリー関数config()になっているのはこのためです。 キャンバス上のページ →
nociteのキーがどの項目にも一致しないと、警告なしに捨てられます。キーは手で入力せず書き出しからコピーし、ページ上の項目数を数えて確かめます。- Zoteroはデータからキーを作ります。Zoteroで著者名や題を直すと、次の書き出しでキーが変わることがあり、そのキーの引用がすべて壊れます。本文を書く前にキーを固定しておきます。Zoteroは項目の「その他」(Extra)フィールドの
Citation Key: …という行を読みます。 - Zoteroは既定ではUTF-8で書き出し、アクセントはそのままの文字になります。ここのブロックは西欧のエンコーディングのオプションで書き出したので
{\"o}の形になっています。Postextはどちらも読めます。 - シカゴ方式の第18版は出版地を省くので、
addressフィールドは刷られません。DOIのある論文はDOIを刷り、JSTORのurlは刷りません。
クレジット
- 本文
- 書き下ろしの文章, CC BY 4.0
- フォント
- Spectral (SIL OFL 1.1) · IBM Plex Sans Condensed (SIL OFL 1.1)


