v0.6.0
Added
-
llms.txt,llms-full.txtand a markdown twin per post, as Hugo output
formats./llms.txtis the index an answer engine can read in one request instead
of crawling — title, summary, and every post as a linked list with a line of
context;/llms-full.txtis the content behind those links in one file, each post
preceded by its canonical URL; and each post publishes anindex.mdnext to its
HTML. All three are per language: a bilingual site gets/llms.txtand
/pt/llms.txt, each listing its own posts.llms.txtlinks to the twins rather
than the HTML, which is what the spec asks for, and the canonical URL is the first
line inside each twin so a citation that follows the link still knows where to
point. Both use.RenderShortcodes— the markdown as written, headings and code
fences intact, with the shortcodes resolved — rather than.Plain, which is what is
left after throwing that structure away, or.RawContent, which hands the reader
unrendered Hugo template syntax where the figure was supposed to be. Part of #34.All three honour
[params.aeo] disallowand the same build condition robots.txt
uses. A path excluded from crawlers whose full body sits inllms-full.txtis not
excluded, and answer engines are the audience that key names — the exclusion has to
reach the files written for them or it is not one. An excluded post is named nowhere,
its body is nowhere, and its twin says why instead of carrying it. A build that is
not for indexing publishes the files with the same answer robots.txt gives, which
keeps the four from contradicting each other the way robots.txt's own comment warns
about.Posts in
llms-full.txtare separated by a--- post: <url> ---line rather than a
bare---. A thematic break is ordinary markdown that someone writes inside a post
without thinking about this file at all, and it was indistinguishable from the line
that separates two posts — so was the setext underline under a heading. The boundary
now carries the answer to the question the split is being made to answer.Every label and heading is English in every language's copy. They are keys, not prose:
a reader parsing/pt/llms.txtshould not have to know the site is Portuguese to find
the post list, and## Blogtranslated is a section a parser written against the
spec's example cannot find. The values carry the language and- Language:states
which one. Half of it used to be translated, which was the worst of the two — neither
parseable by key nor readable as prose — and CI now asserts the two copies use the
same keys.Every value that reaches a line is normalised for it. These are lines in a plain-text
file with no forgiving renderer behind them: a newline inside a title ends the list
it is in, and a]—TIL: array[0],Reading [a spec]— closes the markdown link
early and turns the rest of the item into something else.TrimSpacewas covering
the ends of the first of those.The theme defines the three formats; the site declares them, because Hugo's default
config merge does not bring a theme's[outputs]into the site's. One exception, now
documented and asserted: a site with_merge = "deep"does inherit them, and one
that also declares its own[outputs]inherits the kinds it did not restate — which
means a markdown twin of every page it has. The README says so and CI asserts every
half: that the files appear when a site declares the block, that a site that never
does still builds and keeps its RSS feed, and what a deep merge actually gets. -
scripts/check_aeo.pycounts what it was supposed to check rather than only what it
managed to match. A list item whose link the parser cannot read is a problem now, not
silence — a title carrying a]broke the link it sat in, the regex stopped matching
it, and the item was simply not verified: two of four posts were corrupt in the
fixture that found this and the file still came back clean, because links in another
section matched. A line that continues the item above is caught alongside the blank
line that was already, and a twin that names a different page is compared on the whole
path rather than its last segment. -
scripts/check_aeo.pygrew the other half of its job: every link in every
llms.txtresolves to a file the build published, each language's index is rooted
at the site rather than at its own language directory, and every markdown twin
names its own page back. The link check found two of its own bugs while it was
being written — a percent-encoded tag (tags/migração/is linked as
tags/migra%C3%A7%C3%A3o/) and the second language's index, whose- Home:is
/pt/while its links are rooted at/. -
The AEO score is printed to the GitHub Pages job summary on every deploy, as
information and never as a gate — with its two limits printed next to it, because
the number is a floor rather than a measurement.npx aeo.js checkscans
new URL(target).origin, so for a project site published under a path it reads the
host root, which belongs to no deploy of this theme; and its HTML checks require
quoted attributes whilehugo --minifyemits valid unquoted HTML5, so canonical
and JSON-LD read as absent whatever is on the page. The authoritative check is
check_aeo.py, which now also runs on the bytes about to be deployed. -
scripts/check_aeo.pyreads every page that carries a graph, not the home and the
posts only. The gate had beenif "BlogPosting" in html, which left the lists, the
taxonomies, the term pages and the wholeWebPagebranch unverified — a
/blogs/index.htmlwith every one of its blocks corrupted came back clean. Redirect
stubs are skipped rather than read as pages that lost their JSON-LD, which is what a
site withdefaultContentLanguageInSubdirpublishes at its root.--not-indexable
works against a preview build now: it was checking for aSitemap:line that the
template correctly does not emit there, so the script's only preview mode rejected the
theme's own output. An unknown flag exits 2 instead of being accepted in silence, and
the usage line names the flags the code actually reads. -
layouts/robots.txt. Hugo's built-in one isUser-agent: *and nothing else — no
Sitemap:line, and nothing said either way about the crawlers that feed answer
engines. This one names them, in two groups that are not the same request: answer
engines fetch a page to answer a question now and cite the source back to the reader,
and dataset crawlers collect it into a corpus with no citation and no referral.
[params.aeo] allowAIandallowTrainingswitch them independently, both defaulting
totrue— which is what the bareUser-agent: *already meant, so an upgrade does
not quietly change what a site publishes.[params.aeo] disallowkeeps paths out of
every group, the AI ones included: robots.txt groups do not inherit, so a path excluded
only from*would have stayed open to exactly the crawlers a site had just named.
A build that is not for indexing publishesDisallow: /instead, matching thenoindex
meta head.html already emits — the two files disagreeing is how a deploy preview gets
crawled. RequiresenableRobotsTXT = truein the site's config: it is a root key and
a theme's config is not merged for it. Part of #34. -
JSON-LD for what the theme actually renders.
Person(orOrganization) and
WebSiteon every page,BlogPostingon a post,WebPageon any other single page,
andBreadcrumbListon everything but the home page. Before this the site emitted a
Personon the home page and nothing anywhere else — a blog whose posts never said
they were posts, which is where the 0/20 on Schema Presence came from. The nodes are
linked rather than repeated: the publisher carries an@idand the post'sauthorand
publisherpoint at it.BlogPostingcarriesheadline(capped at the 110 characters
Google's documentation caps it at, since a longer one drops the field entirely),
datePublished,dateModified,author,image,keywordsfrom the page's tags,
wordCountandinLanguage. Breadcrumbs are built from.Ancestors— the real content
tree, not the URL string — so a crumb cannot point somewhere that is not a page.
The 404 is the one page that emits none: it is not in the content tree, so a
breadcrumb there describes a hierarchy that does not contain it, and aWebSitenode
invites a crawler to treat an error as a document. A section index, a tag list
and a term page carry aCollectionPage— the narrower true statement about a page
whose content is the set of pages it links to, and the node theirBreadcrumbList
needed: without it those pages published a trail leading to something the graph said
nothing about.Every string that reaches the graph is plain text, and getting there took two passes.
truncateescapes a plain string and leaves atemplate.HTMLalone, soheadline—
the one field the theme transforms rather than copies — came out as
Vue &amp; Vitestfor a title as ordinary asVue & Vitest: HTML entities inside a
JSON string, where the consumer reads them literally, contradicting thenamebuilt
from the same title in the same node.namehad the opposite problem, carrying
whatever markup the front matter wrote. Both are plainified now, in the breadcrumb
trail too, andcheck_aeo.pyasserts the invariant that catches either drifting again:
headlineandnamecome from one title, so one has to be the start of the other.
The 110-character cap was also 111 in practice —truncateappends its ellipsis
after the limit — which the theme's own checker rejected. CI now builds a post whose
title carries an ampersand, an apostrophe, markup, a quote, an emoji and 118
characters, because every title in the exampleSite is short, plain ASCII. -
[params.schema] type = "Organization"with alogo, for the sites that genuinely are
one.Personstays the default because this is a portfolio theme, and the type is a
statement about the site rather than a lever: an Organization on a personal site is
schema that lies. The two carry different fields —jobTitlebelongs to a person,
logoto an organization, and each is dropped for the other type. An unknown value
warns and falls back toPerson, the way an unknown palette falls back to lime. The
logoresolves throughasset-path.html, the same partial as[params.favicon],
[params.ogImage]and a post'simage: it had spelled the trim out inline, which is
how it came to differ from the other three and rewrite//cdn.example/logo.png— a
URL naming another host — onto this site. -
allowIndexingis read throughparams-bool.html, in one partial bothhead.html
androbots.txtcall. Read raw it reached anif, andifaccepts anything:
allowIndexing = "false"is a non-empty string, so the spelling that most plainly
means do not index was switching indexing on — silently, on exactly the preview
deploy the flag exists for. It now warns and holds, and the two files cannot answer
the question differently, which is the failurerobots.txt's own comment names. -
scripts/check_aeo.py --training-blocked, for a site that means it. Three of the four
crawlers it checks are training crawlers, so a site using[params.aeo] allowTraining = falsefailed the check three times for doing exactly what the switch is for. The
flag is an assertion rather than a mute: with it, a build that still allows them
fails. -
partials/params-bool.html, the fourth of theparams-*.htmlfamily and the one whose
failure mode is quietest. The other three reach arange, a field lookup or a cast, so
a wrong type either stops the build or renders something a consumer can see. A boolean
reaches anif, andifaccepts anything:allowAI = "false"is a non-empty string,
which is true, so the switch a site wrote to turn something off turns it on instead,
and nothing warns. Only a real boolean counts and only an explicitfalsevetoes. -
JetBrains Mono is self-hosted. The font ships with the theme in
static/fonts/,
declared by an@font-faceat the top ofterminal.css, and the twopreconnect
hints and thefonts.googleapis.comstylesheet are gone — the theme now makes no
third-party request at all. Behind Google Fonts the font was a three-hop critical
path the browser could not start until it had parsed the HTML: document, then
Google's CSS, then the.woff2that CSS named. That last hop landed at 1,602 ms in
a throttled trace, later thanterminal.cssat 1,261 ms, which made the font the
critical path and the theme's own stylesheet not. Lighthouse put the saving at
1.33–1.59 s per page and scoredrender-blocking-resources,
render-blocking-insightandnetwork-dependency-tree-insightat zero on every
page of the site. Measured after: mobile performance 92 → 100, FCP 2.5 s →
1.1 s, Speed Index 4.4 s → 1.1 s, and the longest critical chain 1,602 ms →
44 ms. Part of #34.Two subsets are published —
latin(31.4 KB) andlatin-ext(11.6 KB) — both
variable fonts covering400 800, so one file per subset serves every weight the
theme uses. Asking Google forwght@400;500;600;700;800had been returning 30
@font-faceblocks and 12.4 KB of CSS to arrive at the same.woff2that
wght@400..800returns in six.latin-extcarries aunicode-range, so a site
that never writes those codepoints never requests it and shipping it costs nothing;
onlylatinis preloaded, since a preload the page does not use is a wasted round
trip and a console warning. The font is SIL OFL 1.1, redistributed with its licence
atstatic/fonts/OFL.txtand named inLICENSE, which is where someone vendoring
the theme looks.The binaries are the ones
fonts.gstatic.comserves for v24, committed unmodified,
andstatic/fonts/SHA256SUMSis what makes that checkable: CI verifies it, so the
claim is not a sentence in a comment. It also enforces the premise theimmutable
cache header rests on — the font's version is in the filename because serving it for
a year is only safe if replacing the bytes means writing a different name, and
swapping them in place now fails the build instead. -
[params.fonts] latinExt = trueadds the second<link rel=preload>, for sites
that write mostly in Polish, Czech, Turkish, Romanian or Hungarian. It changes
nothing about what is published — both subsets always are — only which of them
starts downloading in the first round trip. -
scripts/check_contrast.py, run by CI, asserts three properties of all five palettes
at once: every token the stylesheet reads as a text color clears 4.5:1 against every
ground the palette declares, and--dim < --dim-2 < --muted-2 < --muted < --prose < --soft < --textby relative luminance. The first is the defect below. The second is
the half a contrast fix quietly breaks, and it caught exactly that while this change
was being written. It is the same argument as the existing mount check: a sixth
palette is meant to be a list of hex values, and this is what stops one from entering
the theme with a rung that fails. It reads the stylesheet only — no build, no
browser, no network.The third property is the checker checking itself. It reads
#rgband#rrggbb, and
anything else —rgb(),color-mix(),#rrggbbaa— is now a named failure rather
than a token it walks past: a--dimwrittenrgb(106,106,106)used to never enter
the parser at all, so a stylesheet failing AA came back clean, which is the exact rot
the checker exists to prevent. A ground written that way was worse, because it also
fell back tolime's value and made the failure line name a hex the palette never
declared. It also fails if the stylesheet declares a background token that is not
among the four it compares against, since that list is written by hand while the text
tokens are discovered. -
The palette is configuration.
[params.theme] palette = "…"picks one of five —
lime(the default, and what every earlier version rendered),amber,cyberpunk,
iceandmono— and[params.theme.colors]overrides individual tokens on top of
whichever won. Until now the README's answer to "change the accent" was to edit
--accentinassets/css/terminal.css, which is advice a site can only follow if it
vendored the theme; consumed as a Hugo Module or a submodule the file is not the
site's to edit, so the theme was effectively one color. Closes #30.Palettes live in the stylesheet as
:root[data-palette="…"]blocks, stamped onto
<html>bybaseof.html—terminal.cssstays static, so it keeps being minified,
fingerprinted and cached the way it was, and a site that configures nothing gets the
bare:root, which is still lime. Overrides are the one thing that has to come from
the config, and they arrive as a<style>element after the stylesheet. It is written
:root:rooton purpose: a palette selector is 0,2,0 and a plain:rootis 0,1,0, so
the obvious spelling would have worked on lime and silently done nothing on the other
four — the harder half of that bug to notice.An unknown palette name, a
[params.theme]written as a scalar, an unknown color key
and a value that is not a color each warn and are ignored. None of them fails the
build, which is the rulesections.htmlset and theparams-*.htmlguards enforce.
Fixed
-
The typewriter no longer runs the HTML parser once per character.
wrap()for an
unfinished segment always produces the same shape — one span with a color — so all
442tailEl.innerHTMLassignments were rebuilding a node identical to the one they
had just destroyed. The tail is now one span holding one text node, both built once,
with.datareassigned. Measured over three Lighthouse runs on a production build,
mobile: HTML parsing 62 ms → 12 ms, consistently and well outside the run-to-run
spread. Part of #34.The loop moved from
setTimeout(…, 15)torequestAnimationFrame, writing once per
frame instead of once per character — and only the frames in which a character came
due, since assigning the same string still marks the node dirty and the opening pause
is some twenty-five frames of exactly that. On a machine fast enough to render every
16 ms that is about the same number of writes minus the timer churn; on the throttled
profile the original measurement came from, two to four characters come due within
one frame and now cost one write between them. It also stops entirely in a background tab, where
the old timer kept typing to nobody. A frame boundary the reader was away for is
capped at 100 ms so a backgrounded tab resumes typing rather than dumping the rest of
the terminal in one paint. The opening pause, the per-character and per-newline
timings, andprefers-reduced-motionare all unchanged..term__bodygetscontain: layout style. Themin-heightabove it already
guarantees the box does not change size while it fills, and this is the browser being
told so: without it every write invalidated the layout of the whole document, measured
at 172 invalidations across one run. Notpaintorcontent— paint containment
clips, and the links inside carry anoutline-offsetthat would be clipped with them.Two things the original report projected did not survive being measured, and are
recorded here rather than quietly dropped. The 1.4 s it attributed to the
typewriter is the cost of the animation existing — it came from an A/B against
--force-prefers-reduced-motion, which does not run it at all — not the cost of how
it is written; the implementation change is worth the 50 ms of parsing above plus the
work it stops doing in a hidden tab. Andforced-reflow-insightcannot be cleared:
it fires on about half of Lighthouse's runs both before and after, because what is
forced is the document's first layout, which the hero's height reservation genuinely
needs. Reusing one persistent probe instead of building one per call was tried and
reverted — it moved the audit not at all and left the ruler's0123456789inside the
terminal'stextContentfor the life of the page, read by anything that extracts text
from the rendered DOM, which on this theme now includes the answer enginesllms.txt
is for. Deferring the first measurement intorequestAnimationFramewould clear the
audit by giving up the reservation, which is the layout shift the reservation exists
to prevent. -
[params.favicon],[params.ogImage]and a post'simagenow resolve inside the
baseURL, not at the host root.relURLandabsURLread a path with a leading
slash as rooted at the host, so on a site published under a sub-path — the demo is
one —"/img/og.png"dropped the sub-path and resolved to a URL that 404s, while
"img/og.png"resolved correctly. The theme's ownexampleSitedocumented the first
spelling. All three call sites now go through one partial,asset-path.html, that
trims the leading slash before the conversion, so both spellings work — and a path
that names another host still passes through untouched, whether it carries a scheme
or is written//cdn.example/og.png. Fixes #37.Nothing about it was visible: a favicon that 404s is a blank tab, an
og:imagethat
404s only fails in someone else's link card, and a post banner that 404s renders as a
reserved empty block that looks deliberate. The build was green because the templates
did exactly what they were told. A site that really did mean the host root — an image
served from outside Hugo — writes the absolute URL, which is the spelling that always
meant that unambiguously.The other half was the gap that let it through: the
exampleSiteleaves all three
params empty, so the documented path was never built by CI. A new step builds under a
sub-pathbaseURLwith the three filled in, in both spellings, and asserts that every
URL the build emits names a file the build actually published, that the two spellings
agree, and that both spellings of an off-site URL arrive untouched. -
--dimfailed WCAG AA in all five palettes, against all four grounds. Lighthouse
reported it as two selectors —.term__titleon the home page and.toc__labelon
a post — but the token is read by thirteen rules (.footer,.post-meta,.pager,
.tl-date,.lang-switch,figcaption, Chroma's line numbers…), all of it at
12–13.5 px, where the threshold is 4.5:1 rather than the 3:1 large text gets. It
measured between 3.44:1 and 3.81:1 depending on the palette, worst against
--surface-2— the lightest ground, not--bg. Each palette's value is lifted the
minimum needed to clear 4.5:1 against that worst case, preserving its hue:
#6b7263 → #798071(lime),#776a56 → #887b67(amber),#6d6390 → #8076a3
(cyberpunk),#62737f → #6e7f8b(ice),#6a6a6a → #7d7d7d(mono). Accessibility
goes to 100 on every page. Part of #34.Two of these were not in the original report and came out of checking the whole ramp
rather than the one token:mono's--dim-2was failing the same way at 4.45:1, and
liftingamber's--dimpushed it past the--dim-2it is named to sit under —
a palette that passes AA while contradicting its own token names. Both--dim-2
values move with it (#7c7c7c → #868686,#8c7a5c → #95866a). -
The entrance animation no longer holds the largest element on the page invisible.
.fadeupanimatedopacityfrom 0 withanimation-fill-mode: both, and Chrome does
not count an element atopacity: 0towards FCP or LCP — so for the first 500 ms of
every page in the site the metric was reporting whatever it could see instead of
the main content..fadeupwraps six layouts: the hero's.hero__id, the whole
<article>insingle.html, and the containers inlist.html,term.html,
terms.htmland404.html. Mobile was worse, because.hero__id{order:-1}at 760px
puts exactly that<h1>at the top of the viewport. Confirmed by A/B on a production
build: with the rule disabled the LCP element changes (nodeId 18 → 86) to the<h1>
that renders immediately. The keyframes now animatetransformalone — the motion is
unchanged, and a partial fade would not have worked, since Chrome's heuristic is not
a threshold on the computed value.prefers-reduced-motionstill switches it off.
CI asserts the keyframes animatetransformalone — it is the one change here that
renders identically when it regresses, so nothing else would have caught it coming
back. -
Colors that did not follow the accent because they never went through it. Eight
rgba(182,255,60,…)washes were spelled out interminal.css— the solid button's
glow, the reading-progress bar, the contact box, the tag hover, the blockquote, the
post banner's dot grid, the 404's text-shadow and the gradient behind the whole body —
along with the typewriter's five colors interminal.js, five inlinestyle="color:#…"
attributes in404.html, and the favicon. They were correct while there was one
palette and would have stayed lime green in the other four. The washes are now
color-mix()againstvar(--accent); the script and the 404 emitvar(--accent-dim)
and friends rather than hex, which also removes the question of when it is safe to
read a computed color, since nothing is resolved in JavaScript at all; and each palette
ships its own favicon. The neutral overlays stay literal — the white scanlines and the
black drop shadows are not palette colors and read the same on every ground.Under
limeevery one of these resolves to the value it had before, exactly, with two
exceptions — both derivations of a value that had been picked by hand, and both under
2/255 of where they were:- the border on code blocks,
#232a1c, now derived from--borderand--border-soft:
+1 on red and +2 on blue, on a 1px rule; - the post banner's gradient,
#11160d, now--surface-2(#11140d): −2 on green.
- the border on code blocks,
-
Browser chrome follows the palette too:
<meta name="theme-color">carries the
palette's ground, or the[params.theme.colors]override of it, so an amber site does
not get a lime address bar drawn around it — the same argument as the per-palette
favicon, one layer out. -
color-mix()has a literal fallback where it reaches something structural. A
declaration carryingcolor-mix()parses fine and only becomes invalid at
computed-value time, which resolves to the property's initial value rather than to
an earlier declaration — so the usual two-declaration fallback does not work through a
custom property, and on a browser without the function (pre-2023) the sticky nav and
the open mobile menu both went transparent over the scrolling page, and the code-block
borders disappeared. The derived tokens now carry the values the theme shipped before
palettes existed and are upgraded inside@supports. Decorative mixes — glows, hover
tints, shadows — are deliberately left to degrade to nothing. -
[params.theme.colors]keys accept the spelling you copied out of the stylesheet:
accentDim,accentdim,accent-dimandaccent_dimare one key. TOML bare keys
do permit-, soaccent-dimwas reaching the theme intact and being reported as an
unknown key. -
[params.theme.colors]rejects two values it used to emit. A non-string —accent = true,
accent = 255— is never a color, and emitted CSS that was valid to parse and invalid
to compute, so everyvar(--accent)on the page fell back to nothing with no warning.
A value with an unclosed parenthesis —accent = "rgb("— swallowed every declaration
after it in the block, so one typo silently deleted the other overrides. Both now
warn and are ignored. -
The warning for an unknown palette no longer reports printf's error syntax.
palette = 3
arrives as an integer and%qon an integer is a rune literal, so it announced that the
requested palette was'\x03';palette = truereported%!q(bool=true). -
An untranslated language rendered a blank interface. Hugo's fallback for a missing
i18n key is the site'sdefaultContentLanguage, not the theme'si18n/en.toml, so a
site whose default language the theme ships no strings for —defaultContentLanguage = "es"with noi18n/es.toml— rendered every label as the empty string://for
the hero intro,[ ]for the contact button, nav links with no text, the 404 with no
message. The build was green and said nothing. A language declared alongsideen
was never affected, which is why the bilingual exampleSite never showed it.Every UI string now goes through
partials/t.html, which asks Hugo first and the
theme's own English table second, so the floor is a readable page in the wrong
language rather than an unreadable one in the right language. That table is the
theme's reali18n/en.toml, not a copy of it, whether the theme is vendored, a
submodule or a Hugo Module — reached through a second mount the theme's own
hugo.tomldeclares atdata/terminal_mono_i18n, which is what keeps a site's own
i18n/en.tomlfrom taking its place:os.ReadFilereturns the first match on Hugo's
union filesystem instead of merging, so overriding three English strings used to leave
a three-key fallback behind and blank the other 46 labels. Your English is merged over
the theme's now, key by key, the way the translation lookup itself merges. Hugo parses
that mount, so no template has to guess a file format either — one comma in a comment
used to maketransform.Unmarshalread the file as CSV and abort the build.partials/i18n-check.htmlwarns once per language, naming the file to create when a
language has no strings at all and listing the missing keys when it has some, with
other = ""counting as missing. The count comes from the translation files — the
theme's through that same mount, the site's through the union filesystem, unioned —
because a resolved value cannot say where it came from: in any language but the
default one Hugo has already substituted the default language's string, so asking
i18nfor it saw a fully translated site every time.[languages.es]alongside a
translateden, the multilingual setup the README walks you through, warned about
nothing at all.Every lookup is wrapped in
try(). With--printI18nWarningsand--panicOnWarning
together — what CI runs the exampleSite under — a missing key makesi18nreturn an
error rather than an empty string, which would otherwise abort the build inside the
partial that exists to prevent exactly that. Nav labels go throught.htmltoo, so a
menu identifier the theme has no string for falls back to the theme's English rather
than to the identifier.Strings that interpolate a value keep working through the fallback:
{{ .count }}and
friends are substituted literally, which is what the one such string in the theme
(posts_tagged) needs — a$in the value survives, and aoneform added to
i18n/en.tomlis used when the count is 1 instead of being ignored forother.
Nothing in the rendered output changed for a site that has its translations — the
exampleSite builds byte-identical to v0.5.0. -
README: the language section says which half of adding a language is configuration
(all of it — switcher,hreflang,og:locale:alternate, menu and dates follow from
[languages.<lang>]) and which half is a file you write. It also records thatlabel
andlocaleare the current spellings —languageNameandlanguageCodeare
deprecated as of Hugo 0.158, the theme's own minimum — and that dates localize from
the language key with or withoutlocale. Finally it states what the theme does not
do:languageDirection = "rtl"reaches the page as<html dir="rtl">, but the
stylesheet is written in physicalleft/rightproperties and does not mirror, so
RTL is untested and unclaimed rather than quietly broken.
Changed
netlify.tomlserves/css/*,/js/*and/fonts/*.woff2with
max-age=31536000, immutable. All three are immutable by construction — the CSS and
the JS carry a SHA-256 of their contents in the filename and the fonts carry the
font's version — and were going out with Netlify's defaultmax-age=600, which
Lighthouse scored at an 8% cache hit probability. The README now documents the
equivalent for Cloudflare Pages, Vercel and nginx, and says plainly that GitHub Pages
cannot set headers at all, because nothing in the built output tells a consumer that
the theme fingerprints its assets. The glob is*.woff2rather than the whole
directory becauseOFL.txtandSHA256SUMSlive there too, and nothing renames
those when they change.
Full changelog: v0.5.0...v0.6.0