Releases: adamsalves/terminal-mono
Release list
v0.10.0
Fixed
-
v0.9.0 only built if you installed the theme as a Hugo module. It made
the AEO module its own[[module.imports]], which reads as tidy and is
wrong: Hugo resolves a module import only when the consuming project is
module-based. With the theme sitting inthemes/as a directory — which is
what a git submodule, a manual copy,--themesDirand both demo deploys all
produce — Hugo looked for the import on disk under
themes/github.com/adamsalves/aeo-hugoand failed the build outright. The
GitHub Pages and Netlify demos stopped deploying, and so did
hugo server --themesDir ../...The site declares the module now, not the theme. One more
[[module.imports]], or one more submodule, and every installation path
works the same way:# Hugo Modules [module] [[module.imports]] path = "github.com/adamsalves/terminal-mono" [[module.imports]] path = "github.com/adamsalves/aeo-hugo"
# submodule or manual copy — aeo-hugo first, the array is precedence order theme = ["aeo-hugo", "terminal-mono"]
This is a breaking change for a site that installed v0.9.0 as a Hugo
module, which is why it is a minor and not a patch. That site had the AEO
module transitively and never named it; upgrading without adding the import
fails at config time, with a message that does not say why:ERROR failed to create config from modules config: unknown output format "llmsfull" for kind "home"The
LLMS,LLMSFULLandMARKDOWNoutput formats come from the module, so
the[outputs]block is left naming formats nothing defines. Add the second
import and it builds. Sites on any other installation path were not building
at all on v0.9.0, so there is nothing there to break.All five paths are now built in CI or by hand before release: modules,
submodule, manual copy,--themesDir, and the two demo deploys.
Full changelog: v0.9.0...v0.10.0
v0.9.0
Changed
-
AEO is a module now, not 816 lines of this theme.
llms.txt,
llms-full.txt, the markdown twins,robots.txt, the sitemap templates and
the JSON-LD graph move to [aeo-hugo][aeo], which this theme imports — so a
site using the theme gets all of it transitively and installs nothing. The
templates were built and battle-tested here; they left because a second theme
wanting the same thing had no way to get it without copying them, and because
a bug fixed in one copy stays broken in the other. Ten files and the
[outputFormats]block are gone from the theme;head.htmlcalls
aeo-schema.htmlandaeo-indexable.htmlinstead of its own.The published output is unchanged: of the 118 files the
exampleSitebuilds,
116 are byte-identical to the pre-migration build, minified and not. The two
that differ arellms.txtandpt/llms.txt, by one line each — this theme
printed its[params.hero]subtitle under the description, and a component
cannot know where a theme keeps its hero.Migrating a site: two config moves, both because the module reads its own
namespace rather than this theme's params.-
[params] allowIndexing→[params.aeo] allowIndexing. The old spelling
still works and warns once. -
Publisher identity, which the theme used to read from
[params.hero] subtitleand[params.footer.socialNetworks], is stated directly:[params.aeo.publisher] type = "Person" sameAs = ["https://github.com/you", "https://linkedin.com/in/you"] [languages.en.params.aeo.publisher] jobTitle = "Front-End Developer"
A site that skips the second one keeps its whole JSON-LD graph and loses
Person.jobTitleandPerson.sameAs.The module warns at build time when it finds itself unwired — another
component'srobots.txtwinning thethemearray, or[outputs]missing —
but only for a site that wrote a[params.aeo]table, so a site not using
AEO hears nothing.ignoreLogs = ['aeo-no-llms']silences one,
[params.aeo] quiet = trueall of them.The theme's README AEO section shrinks from 208 lines to 60: what stays is
the[outputs]block a site must write itself, the publisher config, and
where to read the rest.scripts/check_aeo.pystays — it asserts this
theme's published output, which is worth checking whoever wrote the
templates. -
Full changelog: v0.8.0...v0.9.0
v0.8.0
Added
-
[params.hero] fuseSubtitle, which joins the name and the subtitle into a
single<h1>— "Robin Vale — Studio" — and drops the.hero__roleline under
it. That pair is not new:head.htmlhas always built it for the<title>
tag, with the same em dash. What was new was that the page never showed it, so
a theme whose browser tab read "Robin Vale — Studio" had a heading that read
only "Robin Vale", and the h1 an answer engine or a search result quotes was
the half without the role in it. The role line is dropped rather than kept
beside the fused heading, because it renders that same subtitle and two copies
of one string is not a layout.Only an explicit
trueturns it on. It reads throughparams-bool.html, so
that is the same ruleenablefollows insections.htmlrather than a second
spelling of it: an absent key is off and a non-boolean warns instead of being
guessed at. The switch also tests the subtitle before it stands, because the
failure it can otherwise produce is a heading ending in a dash with nothing
after it — and an absent subtitle isnil, whichprintf "%v"renders as the
string<nil>, so the obvious guard is the one that lets it through. CI
asserts both directions of the switch, the missing-subtitle case, the explicit
false, and that a fused headline holds exactly one copy of the subtitle.The subtitle itself is guarded now that it reaches the
<h1>. It is read in
three places — the fused heading, the role line and the terminal'sdata-role
— from oneparams-scalar.htmlcall, so a table written there is warned about
and dropped rather than printingmap[a:1]into the element a search result
quotes and typing it into the terminal besides. -
[params.hero] tagline, one short line under the headline for what a
visitor should know before scrolling — where you are, what you are open to. It
works with either headline shape and emits no element at all when unset, so a
site that does not set it gets no empty<div>where the line would be. It
goes throughparams-scalar.htmllike every other value that reaches the page
as text: a table written here warns and is dropped instead of aborting a build
over one short line.
Changed
[params.hero] introcan now be removed, by writing it as"". It was
read throughdefault, which fires on an empty string, so an empty intro
rendered the theme's "hi, my name is" and there was no way to drop the line at
all. That was survivable while the h1 held a name; it stopped being survivable
next to a fused headline, where "hi, my name is" introduces a name and a job
title, which is not what anyone is called.issetseparates the two states
defaultcannot — absent still gets the theme's string, written-and-empty now
emits no element rather than an empty one. Same shapelatestPostsuses to
keep a configured0from reading as unset. An intro written as a table warns
and drops the line, the same wayparams-scalar.htmltreats every other value
that reaches the page as text — it does not fall back to the theme's string,
which is what an absent key means.
Fixed
-
A site with no
[params.hero] subtitleno longer renders an empty role
line..hero__rolewas emitted unconditionally, so a site that never set a
subtitle shipped<div class="hero__role">> </div>— an orphan chevron
under the name, with 18px of margin below it holding space for a line that had
nothing in it. It was easy to miss while it was one configuration nobody was
steered toward;fuseSubtitlegave that state a second way in, since a site
that fuses without a subtitle falls back to exactly this shape, and "the name
alone" has to mean the name alone. CI pins both routes into it. -
The hero's bio no longer sits flush against a fused headline.
.hero h1
carries 8px of bottom margin, which was only ever half of a gap the.hero__role
line finished with 18 more. Fused with notaglineunder it — the shortest
configuration the README documents — nothing was left to finish it, and the bio
landed 8px under a 46px display heading..hero h1 + .hero__biorestores the
26px of whitespace both other shapes leave there; the adjacent sibling matches
only when nothing rendered in between, which is precisely that case.
Full changelog: v0.7.1...v0.8.0
v0.7.1
Fixed
- The terminal traffic lights rendered as slivers on a phone.
.dotdeclared
a width, a height andborder-radius:50%, but noflex-shrink:0— and every
bar that holds the dots is a flex row with a text sibling that runs long:
.card__baron the post and project cards,.term__barin the hero, the 404.
A path like~/blog/testes-em-vue-na-prática-vitest-testing-library.md
overflows the bar on a narrow screen, and flex hands the deficit out in
proportion to each item's base size, so the dots surrendered their share of the
width while keeping their height. On the exampleSite at 500px they measured
6.03×11 instead of 11×11, and the longer the post title the flatter they got.
.card__pathalready carries theoverflow:hiddenand the ellipsis that make
it the item meant to absorb the shrink; now it is the only one that can.
.lang-doton the project cards was the same rule with the same omission and
got the same fix before a long enough language label found it. CI asserts both
now — a desktop build never shows this, and the stylesheet reads correct.
Full changelog: v0.7.0...v0.7.1
v0.7.0
Added
-
[params.aeo] flatSitemap, which publishes/sitemap.xmlas one flat
<urlset>covering every language instead of Hugo's<sitemapindex>. On a
multilingual site the index is a shape a crawler has to know to follow, and a
good number do not: they read/sitemap.xml, take the<loc>s and audit those
as pages — two XML files with no title, no structured data and no prose — while
the site itself is never opened.npx aeo.js checkv0.0.16 does exactly this,
and measured against a bilingual portfolio it cost 12 of 100 points (Meta "80%+
pages have titles", Schema "Article/WebPage", Citability "Structured lists") for
content already in the build: the one post it never reached carries a
BlogPostingnode and a<ul>. The flat urlset keeps everyhreflang
alternate, and the per-language sitemaps are still built and served at their own
URLs, so nothing that already indexed one starts 404ing.Off by default, for the reason
allowTrainingis on:/sitemap.xmlis a
published contract with every crawler that already knows the site. Inert on a
single-language site, where Hugo builds no index in the first place. Like the
other switches it must be a real boolean — a string warns and changes nothing.It is a whole-site param, not a per-language one:
/sitemap.xmlis one file, so
it is read from the first language by weight and any other language that sets a
different value is named in a warning. The README teaches
[languages.<lang>.params…]for other params, and silence here would be exactly
the failureparams-bool.htmlexists to prevent.
Fixed
- The README's price for
keepQuoteswas wrong. It said "about 150 bytes",
uncompressed and unattributed; on the exampleSite home page it is 436 (11,762 →
12,198) and 29 gzipped. Both numbers are documented now, and the snippet gained
the[minify]parent table it needs to be pasted into ahugo.tomlas written.
Full changelog: v0.6.0...v0.7.0
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...
v0.5.0
Fixed
- Hero terminal: the section switch reaches the terminal. v0.4.0 taught the hero
that its "view projects" button must not point at a section that does not
render, but the terminal directly above it went on typingls projects/and
listing the repositories of a section the reader could not scroll to. Both
remaining commands now follow the plan:ls projects/follows theprojects
section, andcat stack.txtfollowsabout, which is where the skills live. - Hero terminal: a command with no output is no longer typed at all. Only the
blog listing had ever followed that rule;cat stack.txtandls projects/
were unconditional, so a site that filled nothing in — the one CI has been
building all along — greeted its reader with two commands and two blank lines
under them. This half is fixed in the script rather than the template, so an
empty value produces no command whatever put it there.whoamiis the stated
exception and always runs: its output is the page's own identity, not a
section's data, so no switch can empty it. - Hero terminal: the reserved height counts the rows the text takes, not the
lines it is written in.--hero-lineswas a count of logical lines, and
.term__bodywraps — so on a 360px phone five of the exampleSite's fifteen
lines take two rows each, the box reserved 369px for 480px of text, and it grew
under the reader line by line as the animation typed: the exact shift the
reservation exists to prevent, on the viewport where it is scored hardest. Only
the browser knows the box's width and the font's advance, soterminal.jsnow
measures both and writes the corrected count back before the first character is
typed — and again whenever the box changes size or JetBrains Mono arrives under
font-display:swap. Width is counted in terminal cells rather than characters,
so a full-width glyph costs the two it really takes: a Japanese subtitle would
otherwise model at half its width and the box would come out short again, which
is this bug wearing the fix as a disguise. The blinking cursor is counted too —
it belongs to no line of the script, and the closing prompt is where it comes to
rest. What the template emits is unchanged and becomes the floor: the number a
reader gets before the script runs, or without it. Not a regression from the
entry below: v0.4.0 reserved the identical height by a different route and
carried the samepre-wrap, so this is older than both. - Hero terminal: the reserved height follows the commands that actually render.
It was a constant12lines plus one per post, which was right only while all
three commands always rendered; drop one and the box reserved three lines it
never filled, leaving a block of dead space under the hero — the same class of
bug assection--lastsitting on a section that was no longer last. The
template now counts the lines it is about to emit and passes them as
--hero-lines, replacing the--hero-postsvariable and the
.term__body--postsclass, which are gone. CI recomputes the count from the
rendereddata-*attributes and holds the variable to it. - Config: a param written as the wrong type no longer takes the build down.
sections.htmlhas always stated the rule in its header — no configuration
may fail the build — and normalised the four section tables to keep it. But
every partial that readsite.Params.<x>straight bypassed that, so the
promise held in the one file that made it and nowhere else. Thirteen
configurations aborted a build:[params.hero],[params.contact],
[params.footer]and[params.navbar]written as scalars;
[params.footer.socialNetworks],[params.hero.socialLinks]and
[params.about.skills]the same one level down;itemswritten as a scalar
underabout.skills,projectsandexperience; ajobslist inside an
experience entry; and a list of bare strings whereprojects,experienceor
socialLinksexpect a list of tables. Each now warns, names the param, and
falls back.
Four of the thirteen broke every page, not the home alone —head.html,
footer.htmlandnav.htmlrun site-wide, so a scalarhero,contact,
footerornavbartook the 404 and every blog post with it.navbarwas
the sharpest of them:nav.htmlis the file the invariant names, and
sections.htmlalready guarded that same param for itsshowBloggate, so
the theme disagreed with itself about[params.navbar]in two files. - Config: three new partials carry that —
params-table.html,
params-list.htmlandparams-scalar.html— and every consumer reads through
them. A guard on a parent says nothing about its children, so nested tables go
through the same helper; that is the whole reason six of the thirteen existed.
Lists are tested for being lists rather than for truth:firstdoes not
reject a string, it slices its bytes, soitems = "Go"would have rendered
71 · 111instead of failing anywhere a consumer could see it. Entries
dropped from a list are counted in the warning, so a typo that silently
removes one project from six is reported rather than simply absent — and
entries are checked in both directions now, so a table written where a bare
skill belongs is dropped and counted rather than renderingmap[a:1]into a
skill chip. - Config: the mirror direction is covered too — a scalar-shaped param written as
a table or a list. It fails differently and had to be guarded separately:
these never reach arangeor a field lookup, they reachplainify,
relURL,absURL,markdownifyorurlize, all of which cast to string and
abort when the cast fails. Eight more configurations, on top of the thirteen
above:[params.description],[params.favicon]and[params.ogImage], read
inhead.html, so all three took whole sites down; a post's ownimage, from
its front matter, inhead.htmlforog:imageand again insingle.htmlfor
the featured banner; and the prose fields —[params.about] content, a
project entry'scontentandtitle, and an experience job'scontent.
The per-entry ones name the entry in the warning, byrepoor bytitleonce
the title itself has been through the guard, so a list of six says which one.
A param that is only ever printed is left alone on purpose:map[a:1]on the
page is wrong but does not stop the build, and warning about it would report
the same mistake twice for the reads that do go through a cast. - Config: gate 3 type-checks
[params.about] contentfor the same reason it
type-checks the lists — a table there is truthy, so it admitted the section
and then rendered it as an empty shell. - Config: a warning about an experience entry with no
companyno longer
degrades into printf's own error syntax.%qon a nil printed the warning as
items %!q(<nil>) jobs must be a list, turning the half that names the broken
entry into noise. CI now watches the warnings for printf garbage as well as
the pages, since the log is the only place this one could ever appear. - Config: gate 3 in
sections.htmlnow type-checks the list-shaped params as
well as testing them for emptiness. A scalaritemsis truthy, so it used to
pass the gate and reach the partial; with the partials guarded the build
survives, but the section would render with nothing in it — the defect v0.4.0
closed. The warning has to come from the gate for the same reason: once the
gate drops the section its partial never runs, so a guard that only warned
inside the partial would go quiet exactly when the config is wrong. - SEO: a site that never set
[params.hero] subtitleno longer publishes
<title>Site — %!s(<nil>)</title>. Go'sprintfhas no nil case for
%s, so the missing param was formatted straight into the page — on the home
page of every site that skipped it, including the bare site CI has been
building all along. Nothing warned, so--panicOnWarningcould not see it and
the build stayed green. The title now falls back to the site name alone. CI
asserts no built page contains printf's error syntax, across every fixture. - SEO:
jobTitleanddescriptionare omitted from the JSON-LD when unset
instead of emitted asnull.jsonifyrenders a nil as valid JSON, so this
was never the defect above — but"jobTitle":nullasserts that the person has
no job title, where saying nothing asserts only that this site left the field
empty.
Changed
- Hero terminal:
cat stack.txtnow follows[params.about.skills] enable,
which has to be an explicittrue— an absent key counts as off, the same
reading the skills block inside the about section and the section index have
always used. The switch used to hide that block while the terminal above kept
announcing the same list — one switch that meant two different things
depending on where you looked. Sites that writeenable = true, the
exampleSite among them, are unaffected. A site that listsitemswithout
anenablekey does losecat stack.txt: it was already not rendering the
skills block, and the terminal now agrees with the page instead of announcing
a list the reader cannot find. - Docs:
RELEASING.mdnow says what0.xdoes with a breaking change. The
SemVer table had no row for it, and themajorrow's example read0.3.0→
1.0.0— which taken literally made v0.4.0, breaking by the table's own
definition, a1.0.0. It was cut as0.4.0deliberately, per
SemVer §4: while the theme is0.xa
breaking change goes in the minor, and1.0.0is reserved for the deliberate
statement that the contracts are stable. Themajorrow now counts from
1.0.0so it stops implying otherwise. Wording only — the script chooses
nothing, the number is still yours to pass.
Full changelog: https://github.com/adamsalves/terminal-mono...
v0.4.0
Added
- Sections:
[[menu.main]]now drives the order the home page renders its
sections in, not just the nav. The menu is the page's index — a reader who
sees "projects · about · experience" at the top expects the page in that
order — and keeping two lists that are allowed to disagree is an invitation
for them to. Delete an entry and the section goes with the link, which is the
answer to "I don't want the experience section" that used to require
overriding a layout in the consuming site. Non-section identifiers (blog,
external links) stay nav-only, and the hero stays the page header rather than
a section, so it cannot be moved or removed. - Sections: every section takes an
enableswitch —[params.about] enable,
[params.projects],[params.experience],[params.contact]. It is a veto
and never a summons:falseremoves the section and its nav link whatever the
menu says,truegrants nothing the menu and the section's own content do not
already grant. Forcing inclusion would rebuild the two defects this release
closes — a section outside the index, and a section rendered empty. Being an
ordinary param, it is language-scoped: a section can be on in one language and
off in the other without a second menu. - Sections:
[menu.main.params] showInNav = falsekeeps a section on the page
and takes only its link out of the nav, and[params.sections] order = [...]
is the escape hatch for a site that wants the nav and the page in different
orders on purpose. Order resolves asparams.sections.order, then the menu,
then the previous default. - Sections: no configuration can fail a build. Every misconfiguration warns
and falls back — an unknown section name, a duplicate, anorderthat is not a
list or is empty,params.sectionsor[params.<section>]written as
something other than a table, andenableorshowInNavset to something that
is not a boolean (enable = "false"is a string, and used to be read as "on"
in silence). A menu entry whoseurlis the wrong anchor for the section it
names —url = "#sobre"onidentifier = "about"— warns and has its link
pointed at the right anchor, because the section's id is fixed by the theme and
the typo has exactly one possible fix. One naming a section but linking
somewhere else entirely (a page, an external URL) warns and is left alone: that
may well be a real destination. - Sections: the theme warns when a section has content configured, is not turned
off, and nothing in the index renders it — naming the section and the two ways
to resolve it. This is the shape of a[[menu.main]]written for v0.3.0, where
the menu drove only the nav: such a menu can now leave a home page with no
sections at all, and this warning is what keeps that from happening quietly.
[params.<section>] enable = falsestates that the omission is deliberate and
silences it; so does deleting the section's params.
Fixed
- Sections: a site that fills nothing in no longer ships links to sections that
are not there. Each section decided to exist a different way —projects
behind awith,experiencebehindenable,aboutandcontactbehind
nothing at all — so an unconfigured site renderedaboutandcontactas
empty shells (heading, rule, nothing) while the nav offered#projectsand
#experience, two anchors that scrolled nowhere and announced normally to a
screen reader. A fourth dead link, the hero's own "view projects" button, went
the same way. All four now answer to one resolution, and CI walks every page of
every build asserting that no link points at an anchor that is not on the page
it targets — run against the previous release's bare output, that check reports
17 dead anchors. - Sections:
section--lastfollows the last section that actually renders. It
was hardcoded ontocontact, which was only correct while contact was
guaranteed to be last; with contact removed or reordered, the page lost the
96px of breathing room at its end. - Nav: a site with no links and one language no longer ships a hamburger button
and an empty mobile menu for it to open. - Release: the branch cleanup no longer reports failure for a branch that is
already gone. GitHub answers a delete of an absent ref with 422 "Reference
does not exist", not 404, and only 404 was mapped to the already-gone case —
so every release on a repo that deletes the head branch on merge printed
"remove it by hand" for a branch the merge had already removed.v0.3.0did.
Nothing ever accumulated on the remote — the merge had done the work; only
the report was wrong. The allowance is scoped to the caller that asks for it,
so an unexplained 422 stays fatal everywhere else.
Changed
- Sections: the menu now moves and removes the sections. A site that adopted
[[menu.main]]in v0.3.0 — where the menu drove only the nav, and the README
said so — will see its sections move on upgrade if its nav order differs from
the layout order, and lose any section the menu does not name. A menu
written for the nav alone, listing sayaboutand an external link, now leaves
the home page with one section instead of four; one naming no section at all
leaves it with none. Both cases warn, naming each section that went missing.
Set[params.sections] order = [...]to pin the previous layout and section
set; the nav keeps following the menu. - Sections: the four section partials (
about,projects,experience,
contact) now expect a context ofdict "last" <bool>and no longer decide
for themselves whether to render —sections.htmldoes. A site that overrode
layouts/index.htmland calls them with the page ({{ partial "about.html" . }})
has to pass the dict instead, or read the plan the way the theme's own
index.htmldoes. - Experience: the section is now opt-out like the other three, where it used to
be opt-in.[params.experience] enablewas the only switch of its kind in the
theme, and making all four consistent meant picking one default for all of
them; a section that hasitemsfilled in and no explicitenablenow
renders rather than staying hidden. If that is your config and you want it
hidden, setenable = false. Sites that already setenable = true— the
exampleSite among them — are unaffected: their minified output is byte-identical.
Full changelog: v0.3.0...v0.4.0
v0.3.0
Added
- Hero: the terminal lists the newest posts as a fourth command,
ls ~/blog --latest,
with each filename linking to its post. The home page gave no sign a blog existed
unless you read the nav; this surfaces it without adding a section or touching the
order of the existing ones. It appears on its own once a language has posts, and
the names are built from post titles so they follow the reader's language rather
than the file on disk —post.mdandpost.pt.mdwould otherwise both read as
English. Ordered by date newest-first regardless ofweight. Each post's title is
the link's accessible name.params.hero.latestPostssets how many (0disables),
per site or per language, andprefers-reduced-motiongets the links immediately. - Nav: the menu order is configurable through Hugo's native
[[menu.main]], sorted by
weight, instead of being hardcoded in the partial. Labels are translated from each
entry'sidentifier, so one block serves every language — an explicitnamewins
when you want a literal label. Anchors resolve against the current language's home, so
they keep working from inside a blog post, andpageRefkeeps internal links on the
right language. Sites without[[menu.main]]render exactly as before: the default
order is the fallback, and the minified output is byte-identical. The blog entry stays
conditional on the language having posts,params.navbar.showBlogstill overrides, and
desktop and mobile now render from a single partial so they cannot drift apart.
Changed
- Blog cards: the
~/blog/….mdlabel follows the reader's language. It came from
the file on disk, andpost.mdandpost.pt.mdcollapse to one name, so every
language showed the English one. It is built from the post title now, sharing
partials/post-filename.htmlwith the hero listing so the two cannot disagree.
A long name is ellipsized rather than wrapping into the reading time. - Release:
scripts/release.pynow deletes therelease/vX.Y.Zbranch from the
remote once the tag is pushed. It already removed the local copy, so the remote
one accumulated — one orphan per release, asv0.2.3left behind. The cleanup
runs after the release is complete and only reports if it fails, since a
leftover branch is litter rather than a broken release.
Full changelog: v0.2.3...v0.3.0
v0.2.3
Added
- Release automation:
scripts/release.pycuts a release end to end (changelog
promotion, release PR, CI gate, merge, annotated tag), and arelease.yml
workflow publishes the GitHub Release from the changelog section on tag push.
A tag and a GitHub Release are separate objects and the sidebar reads the
Release —v0.1.0andv0.2.2were both tagged without one.RELEASING.md
documents the process, including recovery when a step fails. - Tests:
scripts/test_release.pycovers the logic that decides what gets
published — changelog promotion, note extraction, tag ordering, token scoping
and the CI gate — against the realCHANGELOG.md.ci.ymlruns it on every
pull request, so a release PR validates its own promoted changelog before it
can merge. Tooling that merges tomainand pushes tags should not be the
one part of the repository nothing checks.
Changed
- Docs:
images/screenshot.pngandimages/tn.pngare now captures of the bundled
exampleSite/— what the demo and the theme gallery actually serve — instead of a
personal site with unrelated branding and content. Both were also recaptured without
the browser scrollbar that had been baked into them, and the README image now uses a
relative path so it resolves outside github.com too.
Full changelog: v0.2.2...v0.2.3