Appearance
Antikythera
The Antikythera client queries articles, annotations, journal settings, and custom fonts, and coordinates annotation highlights with the web components. Instantiate it in the browser and call the methods below.
INFO
Please contact Antikythera to get the correct 'entry' slug for your article.
javascript
import { Antikythera } from '@antikythera/antikythera'
import '@antikythera/antikythera/fonts'
const antikythera = new Antikythera({ entry: 'example-entry' })init()
By default, the Antikythera class automatically initializes the antikythera-menu Web Component and crawls the DOM for .annotation tagged elements.
You can override this behavior by passing the manual: true parameter to the Antikythera class and then calling the init() method manually:
javascript
const antikythera = new Antikythera({ entry: 'example-entry', manual: true })
// wait 2 seconds before initializing:
setTimeout(() => {
antikythera.init()
}, 2000)When manually initializing the antikythera-menu, your optional naming overrides are passed directly to the init() method:
javascript
antikythera.init({
menuName: 'antikythera-menu', // should match the name you've specified in the Antikythera class
annotationClass: '.annotation',
detectAnnotationsOnInit: true // set false here if for some reason you want to call detectAnnotations() method at a later point in time
})init() Options
| Name | Type | Required | Usage | Default |
|---|---|---|---|---|
menuName | String | false | Name of Antikythera Menu Web Component | antikythera-menu |
annotationClass | String | false | Name of Selector Class used to find inline annotations | .annotation |
detectAnnotationsOnInit | Boolean | false | Scan the current body and begin observing the configured annotation class. | true |
reinit()
When a site switches between Antikythera articles, reinit() updates the current entry and reruns initialization. After reinitialization it dispatches antikythera:initComplete on document; event.detail.entry contains the new slug.
javascript
const antikythera = new Antikythera({ entry: 'example-entry' })
document.addEventListener('antikythera:initComplete', (e) => {
console.log('reinitialisation complete')
console.log(e.detail.entry) // <-- latest entry
// do further processing / cleanup here...
})
setTimeout(() => {
antikythera.reinit('other-entry')
}, 3000)reinit() Options
| Name | Type | Required | Usage | Default |
|---|---|---|---|---|
entry | String | true | Name of the new antikythera entry to use | undefined |
menuName | String | false | Name of Antikythera Menu Web Component | antikythera-menu |
annotationClass | String | false | Name of Selector Class used to find inline annotations | .annotation |
detectAnnotationsOnInit | Boolean | false | Scan the current body and begin observing the configured annotation class. | true |
detectAnnotations()
This method scans the document body for elements with the configured annotation class, applies the package's highlight metadata/styles, and connects them to the menu. It also starts a document observer: client-side pagination, route transitions, and DOM moves trigger a queued rescan, while already-marked highlights are adopted without wrapping their text again. Multiple connected elements may share one annotation ID and are tracked independently.
javascript
<span id="my-annotation" class="annotation">my highlighted phrase</span>init() calls this method by default. That initial call also starts the document observer, so later pagination or route-driven DOM changes are handled automatically. Call it manually when automatic detection was disabled, or when application code needs to await an immediate rescan after replacing article content (for example, a language switch).
javascript
const toggleLanguageToEnglish = async () => {
langIsEng.value = true
await antikythera.detectAnnotations()
}
const toggleLanguageToFrench = async () => {
langIsEng.value = false
await antikythera.detectAnnotations()
}getEntry()
javascript
const article = await antikythera.getEntry()The response includes a top-level about object sourced from the journal document (about text, aside text, and contributors), plus the entry's pdf, markdown, and a pdfPreview with a computed 520px-wide preview url. Related entries include introduction and shortIntroduction descriptions plus their own pdf, markdown, and pdfPreview; the official menu intentionally offers Markdown export only for the current entry.
In v2.1, full responses also include top-level custom-font metadata (fonts, fontFaceCss). Entry-level globalFontSettings, tertiary-content controls, and media-item invertImagesOnHover flags pass through for article-template consumers. Boolean opt-ins are emitted only when true; revealTertiaryContentOnAnchorHover also coalesces the hidden legacy click-reveal field for backward compatibility.
Response
Details
json
{
"settings": {
"externalLinks": [
{
"linkTitle": "instagram",
"linkUrl": "https://www.instagram.com"
}
],
"shortDescription": "Journal of Philosophy of Planetary Computation.",
"blurb": "Journal of Philosophy of Planetary Computation."
},
"about": {
"text": "About the journal...",
"asideText": "Aside text shown next to the about section...",
"credits": [
{
"title": "Editors",
"externalTitle": "Editorial",
"creditLine": [
{
"name": "Firstname Lastname",
"title": "Editor"
}
]
}
]
},
"fonts": [
{
"title": "Example Sans",
"slug": "example-sans",
"cssFamily": "example-sans",
"files": [
{
"url": "https://cdn.sanity.io/files/y9ih15q4/production/example-sans.woff2",
"weight": 400,
"style": "normal",
"filename": "example-sans.woff2",
"format": "woff2"
}
]
}
],
"fontFaceCss": "@font-face{font-family:'example-sans';font-style:normal;font-weight:400;font-display:swap;src:url('https://cdn.sanity.io/files/y9ih15q4/production/example-sans.woff2') format('woff2');}",
"entry": {
"doi": "123456789",
"globalFontSettings": "example-sans",
"revealTertiaryContentOnAnchorHover": true,
"alignAllTertiaryContentToParagraphStart": true,
"designers": [
{
"title": "Firstname Lastname",
"externalLink": null,
"featuredImage": {
"url": null,
"alt": null,
"caption": null
},
"biography": null
}
],
"featuredImage": {
"url": "https://cdn.sanity.io/images/y9ih15q4/production/fe543c33bd7f472edd9d40b7c899e637033e609b-3000x2024.jpg",
"alt": "a very religious scene",
"caption": null
},
"credits": "Yuri Bultheel",
"releaseDate": "2024-09-04",
"bgColor": "#266fff",
"textColor": "#ffffff",
"theme": 1,
"authors": [
{
"title": "Philip Maughan",
"externalLink": null,
"featuredImage": {
"url": null,
"alt": null,
"caption": null
},
"biography": null
}
],
"annotations": [
{
"featuredImage": {
"url": "https://cdn.sanity.io/images/y9ih15q4/production/fe543c33bd7f472edd9d40b7c899e637033e609b-3000x2024.jpg",
"alt": "religious scene",
"caption": "a painful scene"
},
"id": "example-annotation",
"title": "Example Annotation",
"doi": "example DOI",
"content": "Just some text about a certain annotation...",
"annotationType": "commentary",
"externalLink": "https://www.google.com"
},
{
"externalLink": "https://www.dataexcess.xyz",
"featuredImage": {
"url": "https://cdn.sanity.io/images/y9ih15q4/production/fe543c33bd7f472edd9d40b7c899e637033e609b-3000x2024.jpg",
"alt": null,
"caption": "good picture"
},
"id": "second-annotation",
"title": "A second Annotation",
"doi": null,
"content": "This is another annotation...",
"annotationType": "related"
}
],
"title": "Example Entry",
"introduction": "Just an introduction to the article.\n\n\n\nPart 1.\n\nSome more text.\n\nPart 2.\n\nSome more text.\n\n\n\nFinal words.",
"bibliography": "start of bibliography.\n\n\n\nthis is actual portable text by the way....\n\n\n\nend of bibliography.",
"pdf": {
"url": "https://cdn.sanity.io/files/y9ih15q4/production/example-entry.pdf",
"pdfMimeType": "application/pdf",
"pdfFilename": "example-entry.pdf"
},
"markdown": {
"url": "https://cdn.sanity.io/files/y9ih15q4/production/example-entry.txt",
"markdownMimeType": "text/plain",
"markdownFilename": "example-entry.txt"
},
"pdfPreview": {
"altText": null,
"crop": null,
"hotspot": null,
"asset": {
"_id": "image-abc123-1200x1553-png",
"url": "https://cdn.sanity.io/images/y9ih15q4/production/example-preview.png",
"metadata": {
"dimensions": {
"aspectRatio": 0.7727
}
}
},
"override": false,
"downloadLabel": null,
"downloadUrl": null,
"url": "https://cdn.sanity.io/images/y9ih15q4/production/example-preview.png?w=520&auto=format"
},
"relatedEntries": [
{
"title": "A Related Entry",
"url": "related-entry",
"doi": "123456789",
"doiUrl": "https://doi.org/123456789",
"releaseDate": "2024-10-04",
"introduction": "A related article introduction.",
"shortIntroduction": "A shorter description for the related article card.",
"externalLink": null,
"featuredImage": {
"url": "https://cdn.sanity.io/images/y9ih15q4/production/related.jpg",
"alt": "related image",
"caption": null
},
"featuredImageSquare": {
"url": "https://cdn.sanity.io/images/y9ih15q4/production/related-square.jpg",
"alt": "related square image"
},
"authors": [
{
"title": "Firstname Lastname",
"externalLink": null
}
],
"designers": [],
"pdf": {
"url": "https://cdn.sanity.io/files/y9ih15q4/production/related-entry.pdf",
"pdfMimeType": "application/pdf",
"pdfFilename": "related-entry.pdf"
},
"markdown": {
"url": "https://cdn.sanity.io/files/y9ih15q4/production/related-entry.md",
"markdownMimeType": "text/markdown",
"markdownFilename": "related-entry.md"
},
"pdfPreview": {
"asset": {
"_id": "image-def456-1200x1553-png",
"url": "https://cdn.sanity.io/images/y9ih15q4/production/related-preview.png",
"metadata": {
"dimensions": {
"aspectRatio": 0.7727
}
}
},
"override": false,
"downloadLabel": null,
"downloadUrl": null,
"url": "https://cdn.sanity.io/images/y9ih15q4/production/related-preview.png?w=520&auto=format"
}
}
]
}
}v2.1 field paths
The feature flags used by article templates live at these paths:
| Path | Emission rule | Meaning |
|---|---|---|
fonts[] / fontFaceCss | Top-level on full entry responses | Available custom font families and generated document-level CSS. |
entry.globalFontSettings | Passed through when defined | Entry-wide font-family default; block-level settings may override it. |
entry.revealTertiaryContentOnAnchorHover | Present only as true | Reveal anchored tertiary content on pointer hover or keyboard focus; also reflects the legacy click field. |
entry.alignAllTertiaryContentToParagraphStart | Present only as true | Override all individual tertiary alignment toggles. |
entry.content[].carousel[].invertImagesOnHover | Present only as true | Invert that carousel image on hover. |
Advanced
Parameters
By default, getEntry() returns Portable Text for any richtext content. The getEntry() method exposes an optional parameter to return a String or raw HTML instead:
javascript
const articleWithStrings = await antikythera.getEntry({ textStyle: 'string' })
const articleWithHtml = await antikythera.getEntry({ textStyle: 'html' })| Name | Type | Required | Usage | Default |
|---|---|---|---|---|
textStyle | String | false | Format of returned richtext | portabletext |
getEntryMeta()
javascript
const articleMeta = await antikythera.getEntryMeta()Use getEntryMeta() when you only need fast entry chrome, card, or listing data. It calls GET /api/v1/entries/:id/meta and returns the same top-level { settings, entry } envelope as getEntry(), but omits heavy article fields such as content, annotations, bibliography, related entries, the journal about object, and full Sanity asset documents. Since v2, the metadata response includes the entry's markdown file and formatted pdfPreview alongside the pdf.
Response
Details
json
{
"settings": {
"externalLinks": [
{
"linkTitle": "instagram",
"linkUrl": "https://www.instagram.com"
}
],
"shortDescription": "Journal of Philosophy of Planetary Computation.",
"blurb": "Journal of Philosophy of Planetary Computation."
},
"entry": {
"title": "Example Entry",
"url": "example-entry",
"id": "example-entry",
"blurb": "A short article description.",
"doi": "123456789",
"doiUrl": "https://doi.org/123456789",
"releaseDate": "2024-09-04",
"apiExpandable": true,
"annotationVisibility": true,
"bgColor": "#266fff",
"textColor": "#ffffff",
"theme": 1,
"featuredImage": {
"url": "https://cdn.sanity.io/images/y9ih15q4/production/example.jpg",
"alt": "Example image",
"caption": null
},
"featuredImageSquare": {
"url": "https://cdn.sanity.io/images/y9ih15q4/production/example-square.jpg",
"alt": "Example square image"
},
"authors": [
{
"title": "Firstname Lastname",
"externalLink": null
}
],
"designers": [],
"pdf": {
"url": "https://cdn.sanity.io/files/y9ih15q4/production/example-entry.pdf",
"pdfMimeType": "application/pdf",
"pdfFilename": "example-entry.pdf"
},
"markdown": {
"url": "https://cdn.sanity.io/files/y9ih15q4/production/example-entry.txt",
"markdownMimeType": "text/plain",
"markdownFilename": "example-entry.txt"
},
"pdfPreview": {
"altText": null,
"crop": null,
"hotspot": null,
"asset": {
"_id": "image-abc123-1200x1553-png",
"url": "https://cdn.sanity.io/images/y9ih15q4/production/example-preview.png",
"metadata": {
"dimensions": {
"aspectRatio": 0.7727
}
}
},
"override": false,
"downloadLabel": null,
"downloadUrl": null,
"url": "https://cdn.sanity.io/images/y9ih15q4/production/example-preview.png?w=520&auto=format"
},
"annotationsCount": 2
}
}Advanced
Parameters
getEntryMeta() supports the same textStyle parameter as getEntry() for rich text fields included in the metadata response:
javascript
const articleMetaWithHtml = await antikythera.getEntryMeta({ textStyle: 'html' })| Name | Type | Required | Usage | Default |
|---|---|---|---|---|
textStyle | String | false | Format of returned richtext | portabletext |
getAnnotations()
javascript
const annotations = await antikythera.getAnnotations()Side Note
The Antikythera API will only return annotations that have been added to a specific entry from within the Antikythera CMS.
Response
Details
json
[
{
"content": "is a systematic study of general and fundamental questions concerning topics like",
"scanText": {
"enableScanText": true,
"scanSegments": [
{
"scanKeyword": "philosophy",
"scanPhrase": "Sciences are born when philosophy learns to ask the right questions"
},
{
"scanKeyword": "Philosophie",
"scanPhrase": "Wissenschaften entstehen, wenn die Philosophie lernt, die richtigen Fragen zu stellen"
}
]
},
"url": "philosophy",
"annotationType": "commentary",
"externalLink": null,
"doi": null,
"title": "philosophy",
"id": "philosophy",
"featuredImage": {
"url": "https://cdn.sanity.io/images/y9ih15q4/production/cc9a720edc9fec8bde756dbb6fc655195528a9bf-520x693.jpg",
"alt": "penseur",
"caption": ""
}
},
{
"url": "cosmology",
"scanText": {
"enableScanText": true,
"scanSegments": [
{
"scanKeyword": "cosmology",
"scanPhrase": "This is the planetary in planetary computation: a cosmology in every sense of the word"
},
{
"scanKeyword": "Kosmologie",
"scanPhrase": "Dies ist das Planetarische in der planetarischen Berechnung: eine Kosmologie im wahrsten Sinne des Wortes"
}
]
},
"annotationType": "commentary",
"doi": null,
"content": "Ancient Greek",
"featuredImage": {
"url": "https://cdn.sanity.io/images/y9ih15q4/production/3d5387885f63443a8bb6e31a702eb76fba04dbbd-620x541.jpg",
"alt": null,
"caption": ""
},
"externalLink": null,
"title": "cosmology",
"id": "cosmology"
}
]Advanced
Parameters
By default, getAnnotations() returns Portable Text for any richtext content. The getAnnotations() method exposes an optional parameter to return a String or raw HTML instead:
javascript
const annotationsWithStrings = await antikythera.getAnnotations({ textStyle: 'string' })
const annotationsWithHtml = await antikythera.getAnnotations({ textStyle: 'html' })| Name | Type | Required | Usage | Default |
|---|---|---|---|---|
textStyle | String | false | Format of returned richtext | portabletext |
getAnnotation()
Fetch one annotation by its Sanity slug or document reference. The menu uses this on demand when a rich-text Annotation Link points to an annotation that is not attached to the current entry.
javascript
const annotation = await antikythera.getAnnotation('annotation-slug')Portable Text annotation marks retain Sanity's standard _ref. The singular endpoint accepts that document reference and returns the annotation's public slug as id. This lets the menu replace the visible source card without changing the legacy collection or entry projections. Each fetched target retains its own references, so cyclic and recursively linked annotations do not require a fixed projection depth.
Annotation content can contain classifier blocks, inline secondary text, dividers, regular links, and annotation references. With textStyle: 'html', classifier and legacy secondary blocks render as muted paragraphs, the secondary decorator keeps its inline opacity, dividers render as <hr>, and annotation references render as local hash links.
Advanced
javascript
const annotationAsString = await antikythera.getAnnotation('annotation-slug', {
textStyle: 'string'
})| Name | Type | Required | Usage | Default |
|---|---|---|---|---|
textStyle | String | false | Format of returned richtext | portabletext |
getSettings()
javascript
const settings = await antikythera.getSettings()The settings route includes the journal fields plus the same top-level fonts and fontFaceCss returned by getEntry(). Both client methods install non-empty fontFaceCss into one #antikythera-custom-fonts style element in the host document; a later response replaces that element instead of appending duplicate rules.
Response
Details
json
{
"externalLinks": [
{
"linkTitle": "instagram",
"linkUrl": "https://www.instagram.com"
}
],
"shortDescription": [
{
"markDefs": [],
"children": [
{
"_type": "span",
"marks": [],
"text": "A Journal of Philosophy of Planetary Computation.\n\n",
"_key": "45fb4d058d160"
}
],
"_type": "block",
"style": "normal",
"_key": "2d544012f8bd"
},
{
"markDefs": [],
"children": [
{
"marks": [],
"text": "Coming Spring 2025",
"_key": "d079d3f606f00",
"_type": "span"
}
],
"_type": "block",
"style": "normal",
"_key": "5ee97e6097ec"
}
],
"blurb": "A Journal of Philosophy of Planetary Computation.",
"fonts": [
{
"title": "Example Sans",
"slug": "example-sans",
"cssFamily": "example-sans",
"files": [
{
"url": "https://cdn.sanity.io/files/y9ih15q4/production/example-sans.woff2",
"weight": 400,
"style": "normal",
"filename": "example-sans.woff2",
"format": "woff2"
}
]
}
],
"fontFaceCss": "@font-face{font-family:'example-sans';font-style:normal;font-weight:400;font-display:swap;src:url('https://cdn.sanity.io/files/y9ih15q4/production/example-sans.woff2') format('woff2');}"
}Advanced
Parameters
By default, getSettings() returns Portable Text for any richtext content. The getSettings() method exposes an optional parameter to return a String or raw HTML instead:
javascript
const settingsWithStrings = await antikythera.getSettings({ textStyle: 'string' })
const settingsWithHtml = await antikythera.getSettings({ textStyle: 'html' })| Name | Type | Required | Usage | Default |
|---|---|---|---|---|
textStyle | String | false | Format of returned richtext | portabletext |
HTTP caching and errors
Production API reads use public cache headers. Metadata and settings responses are cached more aggressively than full entry and annotation responses; non-production environments and error responses use Cache-Control: no-store.
Missing entries on strict metadata routes return HTTP 404 with a stable JSON error envelope:
json
{
"error": {
"message": "Entry \"example-entry\" was not found",
"statusCode": 404
}
}Options
When creating the Antikythera class, entry is required for entry-specific queries and components. getSettings() can still be used without it.
| Name | Type | Required | Usage | Default |
|---|---|---|---|---|
entry | String | true* | Entry slug for entry-specific queries and components. | '' |
menuName | String | false | Custom element name registered for the menu. | antikythera-menu |
footerName | String | false | Custom element name registered for the footer. | antikythera-footer |
annotationClass | String | false | Selector class used to find inline annotations. | .annotation |
customCss | Boolean | false | When true, do not inject the package's default annotation-highlight CSS. | false |
manual | Boolean | false | Skip automatic initialization; call antikythera.init() yourself. | false |
detectAnnotationsOnInit | Boolean | false | Scan and observe annotations during initialization. | true |
environment | String | false | Sanity dataset passed as the env query parameter. | production |
apiUrl | String | false | Runtime API host override; must be an http or https origin without credentials. | baked-in host/fallback |
analytics | Boolean / Object | false | Disable analytics or configure its mode, replay, exceptions, autocapture, and PostHog overrides. | true |
entry is optional only for settings-only use.
Advanced queries
The Antikythera npm package is built on top of a Sanity CMS backend. If its capabilities are insufficient, you can use the Sanity API directly to perform more complex queries. Please consult the Sanity Client Documentation to learn how to use the Sanity API. Once you have the projectId, you can query the database with the Sanity client like this:
INFO
Please contact Antikythera to get the sanity projectId.
javascript
import { createClient } from '@sanity/client'
const client = createClient({
projectId: '${antikythera-project-id}',
dataset: 'production',
apiVersion: '2023-06-01',
useCdn: false
})
const query = `{
'entry': *[_type == "entry" && url.current == "example-entry"][0]{
title,
introduction,
content[]{
...,
"markDefs": markDefs[] {
...,
"slug": @->url.current
}
}
}
}`
const fetchEntry = async () => {
try {
const result = await client.fetch(query)
console.log('Entry data:', result.entry)
return result.entry
} catch (error) {
console.error('Error fetching entry:', error)
}
}
const entry = await fetchEntry()