Changelog
Atom feedChanged since your last visit
All notable changes to this project are documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
0.9.3
Added
- A server's variables are filled in with their defaults in the samples, listed with the values they can take on the overview, and offered value by value in the playground and the forwarding route. A variable with no
enumis only its default - A property marked
readOnlyis left out of a request body, its samples and the playground, and one markedwriteOnlyis left out of responses - A
discriminatoris shown: which property picks a variant, the value for each, and thedefaultMapping. Examples carry that value - In the playground, a list of objects is filled in an item at a time, with Add item and Remove, where it was a box of JSON
Fixed
- In the playground, an operation signed with HTTP Basic showed "[object HTMLDivElement]" where its username and password fields belong. Both fields are there
0.9.2
Added
- Webhooks: each of the spec's
webhooksgets a page in a Webhooks folder, with the headers and payload it arrives with, an example payload beside them, and what to answer with. It has no samples or playground, and the spec's top-level security does not apply to it - Callbacks: an operation's page lists the requests the API sends back, with the address, the payload and the statuses to answer with
- Samples a spec writes itself under
x-codeSamples(orx-code-samples) come first among the request samples, and take the place of a generated one in the same language - A Python sample, with requests, is generated alongside curl, PHP and JavaScript.
openapi.sampleslistspythonby default; a published config keeps its own list - Every field in a parameter list or schema has an address made of the names that lead to it, such as
#body-captain-name, and its name links to it
Changed
- The build's link check lets a
#fragmentland on anything on the page with that id, not only a heading, so links to a field of the reference or to a footnote pass
0.9.1
Added
- OpenAPI 3.2 specs are read. A tag's
summarynames its folder,parentputs it inside another tag's on the overview and in the sidebar, and a tag whosekindis notnavno longer becomes a folder. A Redoc spec gets the same fromx-displayNameandx-tagGroups - An operation with the
QUERYmethod, or one of the API's own underadditionalOperations, gets a page, a colour of its own for QUERY, samples and the playground. The PHP sample sends such a method withsend() - A form-encoded
querystringparameter is listed, sampled and filled in as the query parameters it is made of - A server's
nameis shown on the overview, and a security scheme markeddeprecatedsays so where it is listed - An example given as
dataValueis used, and a parameter's namedexamplesnow give its value in samples and the playground
Fixed
- On an operation's page, the examples beside the documentation showed a scrollbar laid over the edge of their panels on a screen too short for both. The samples are now sized to the screen so the request and the response fit together, and the column has no bar of its own
- On a page with no date and no reading time, Copy Markdown and Open had a row to themselves under the description. They sit beside the title, and under the description on a narrow screen
- Try it sat further from the right end of the request line than the method from its left end. Both ends match
0.9.0
Added
- An API reference generated from an OpenAPI 3.0 or 3.1 spec, turned on with
openapi.enabled. Each operation is a page of its own in a sidebar folder per tag, with the method beside its title, and the reference has an overview page. The pages are ordinary pages to the rest of Vellum: they are searched, exported, listed inllms.txtand the sitemap, and gated withopenapi.access. Their raw Markdown is written for them, with fields as tables and samples as fenced blocks. Request samples are generated for curl, the Laravel HTTP client and fetch. Withdedoc/scrambleinstalled and noopenapi.specset, the spec comes from Scramble, and in local development the reference follows the app's code without a build - An operation's page opens with its request line, tinted with its method's colour, and sets each section in a panel: authorization, parameters, the request body and each response. Request samples and response examples are code tabs beside the documentation on a wide screen, where they stay in view and take the place of the table of contents
- A playground on every operation's page: Try it opens a drawer beside the page with a form made from the spec, the curl command it makes, and in a tab beside that the response with its status, time, size and headers. Requests go from the reader's browser, as the reader on the docs' own origin and with nothing of theirs on any other. A response is shown as text and never rendered.
openapi.playgroundset tofalseleaves it out. Its script is fetched when a reader first opens it. An object inside a body has a field for each of its own, such asauthor.name openapi.proxysends a playground request to another origin through the app, for an API that does not let a browser on the docs' origin read its answers. It is off by default, reaches only the servers the spec names and only at a public address unlessopenapi.proxy_privateallows a private one, follows no redirect, forwards no cookie of the reader's and no header the spec does not declare, and is limited to 60 requests a minute for one reader- The reference has a sidebar of its own, with a switch between it and the docs at the top of both.
openapi.sidebarset todocslists it as a folder of the docs' sidebar instead.ContentRepository::sidebar()returns the tree for one page andsections()the sidebars a reader can switch between
Fixed
- A terminal code block showed a prompt before every line of a quoted argument that ran over several lines, such as a JSON body passed to curl. It shows one, before the command
- A code block inside a directive, such as a tab or a callout, lost an indent of one to three spaces from each of its lines, so YAML or JSON indented by two came out flat. The indent is kept
- A line of
:::inside a fenced code block closed the directive the block was in. It is a line of code
Security
league/commonmark2.10.2 or later is required. Earlier versions let a table take a very long time to parse and let a disallowed HTML tag through in one case
0.8.2
Changed
- On a wide screen the page sits in the middle of the space beside the sidebar, with the table of contents to its right, where it used to sit against the sidebar. The gap to the table of contents widens from 1536px up. On a narrower screen nothing moves
0.8.1
Fixed
- The two table of contents images on the Theming page were left out of the installed package, so a strict build of the bundled docs failed with two broken images
0.8.0
Added
jimmyverburgt/vellum-core, the content engine as a package of its own: pages, navigation, versions, gating, Markdown, the compiled cache, search and the changelog, with no routes, layout or assets.jimmyverburgt/vellumincludes it, so installing Vellum is unchanged. See Core on its own in the docsContentRepository::render(), which returns a page's HTML with its components rendered- An app can replace the markup of a built-in component, for both the
:::and the<x-vellum::…>form, by publishing its view toresources/views/vendor/vellum/components/. Before,:::blocks ignored a published copy - The sidebar remembers its scroll position between pages and scrolls the current page into view when it is hidden
- A dot in the sidebar on pages that changed since the reader last opened them. What a reader has opened is kept in their browser
- The highlight on the current page slides over from the page the reader came from, and fills as they read down the page. Moving the pointer over the sidebar moves a lighter highlight with it. Both sit under the text and respect reduced-motion settings
- Folders in the sidebar have a guide line, shown in the primary colour for the folder holding the current page
- A new table of contents style,
window, now the default: one highlight behind every entry whose section is on screen, whose edges hold on to whole entries and spring to the next one as sections come and go. Nested entries hang from a guide line, and a lighter highlight follows the pointer. The newlayout.tockey (orVELLUM_LAYOUT_TOC) set tolinekeeps the line style - Steps from a
:::stepsblock show their number in the table of contents, filled while the step is on screen. In the line style the numbers sit on the line - A line under each page's title says when the page last changed, as "3 days ago" for the last month, and how long it takes to read for pages of three minutes or more. When the page changed since the reader last opened it, the line says so, with the sidebar's dot. The page's actions sit on the same line
- Steps follow the reader: the line between the numbers fills as they read, each number fills once its step is reached, and the table of contents keeps those numbers filled too. The number of the step being read stays in view, and clicking it goes back to the start of the step
- A shell block without a title is shown as a terminal, with a
$before each command. Blank lines,#comments and continuations of a line ending in\get none, and the$is never selected or copied.consoleis accepted as a shell language - Copying a code block sweeps a light wash down the code that was copied
- Every callout names its type, "Note", "Warning" and so on, on its first line, or says it to screen readers before an author's title. Before, the type was shown only by colour and a glyph screen readers skip
- Tab groups that share a
persistkey follow each other at once on the page, not only on the next page load, and the group the reader clicked stays where it is on screen while the others change height - Cards can have a description: any text after the link on a
::cardline.:::cards describe="pages"gives cards without one the linked page's own frontmatter description, filled in from the navigation when the page is shown, so it never goes stale and a gated page's description is only shown to readers who may open it. A card linking outside the docs shows the site's domain - The header row of a table that fits the page stays at the top of the window while the reader scrolls it, and on a phone each row becomes a block with every value under its column's heading instead of a table that scrolls sideways
- An image shown smaller than it is can be opened at full size with a click or Enter, and closed with Escape
- The search dialog groups results by page, with each page named once and its sections under it, and lists the last five results the reader opened before they type. The recent list stays in the reader's browser. A footer shows the keys, and the arrow keys scroll the active result into view
- The previous and next links are labelled, and name the neighbouring page's folder when it differs from the current page's, as in "Next · Components".
adjacent()returns that folder assection - A page of any version but the latest starts with a notice saying it is an older or an unreleased version, with a link to the same page in the latest. A version listed before
latestinversions.listcounts as unreleased, andVersionLabel::kind()says which a version is - The version switcher tags the latest and unreleased versions, and says when the page being read is not in a version, where choosing it opens that version's start page
Changed
- Classes that only the site uses moved namespace:
Vellum\Support\Theme,Color,Assets,CnandNavTreeare now inVellum\View, andVellum\Support\LlmsTxt,Vellum\Support\SitemapandVellum\Changelog\ChangelogFeedare inVellum\Http. This affects code that used these classes, and views copied from 0.7 intoresources/views/vendor/vellum/: the sidebar, the layout and theuiviews name them. The upgrade guide lists what to change - Errors in the docs, such as an unknown directive, implement
Vellum\Exceptions\ContentError. The site renders these as its error page; with core alone, Laravel handles them like any other exception - The copy buttons on code blocks and headings are marked with
data-vellum-copy-codeanddata-vellum-heading-copy, and one script handles every button, instead of an Alpine component on each. Rendered Markdown no longer contains Alpine attributes, apart from tabs - Every rounded corner now follows
theme.radius, as the theming docs promised: code blocks, tab groups, tables, images and the floated sidebar were fixed at their own values. Surfaces are rounded 4px pastradius, and anything inset in one, such as a tab panel, is rounded to nest evenly inside it - The table of contents marks the active entry with colour instead of a heavier weight, which made entries shift sideways, and the line style no longer has a dot
- The last-updated date moved from the breadcrumb row to the line under the title, now labelled "Updated" and with a valid
datetime, which was empty. The rule under the page actions is gone - Step titles in a
:::stepsblock are rendered one heading level below the section the block sits in, and headings inside a step move with them. Steps under a## Setupheading are nowh3, so they nest under that section in the table of contents and the page outline. A steps block before any section heading is unchanged. Authors still start each step with##; only CSS that targets step headings by tag, rather than by Vellum's classes, is affected - A code block without a title no longer has a header, unless it is a shell block: its language and copy button sit in its corner. A titled block's header is drawn as an editor tab. Highlighted lines have a solid bar in the primary colour, and their line numbers are no longer faded. Code blocks carry
data-vellum-code-kind(file,terminalorsnippet) - Callouts are no longer boxes: a bar in the type's colour runs down the left edge and the glyph sits on the first line, inside the text column. Warnings and dangers keep a light tint so they stand out from notes and tips
- Every tab group now looks like code tabs: one card with an underlined row of labels, the underline sliding to the selected label, and the panel inset in the card
- Cards have an arrow that moves on hover, and the hover highlight glides from card to card like the sidebar's. Cards in a row are as tall as the tallest
- Code in a table's first column, such as a config key, is no longer broken across lines; a table too wide for the page scrolls sideways. Table cells align to the top, column heads are smaller, and the row under the pointer is highlighted
- An image on a line of its own sits on a bordered mat and is held to a readable height, with its caption inside the frame
- The reader's words in a search result are marked with a bold underline in the primary colour instead of a yellow fill, and the browser's own clear button no longer shows in the search field. The results scroll with the sidebar's thin scrollbar
- At the foot of a page, Next is a wide card with the page's description on up to two lines, and Previous is a plain link beside it without one. Before, both were equal boxes with one cut-off line
- The version switcher sits above the search field in the sidebar, at the same size, instead of beside the site name. The latest version is shown by its slug or label with a "Latest" tag, not as "Latest" alone, and
VersionLabel::for()returns the slug for it - With
layout.searchset toheader, the button that collapses the sidebar is in the header beside the site name, and reopens it too. The sidebar no longer keeps an empty row for it
Removed
- The
vellum:buildwarning about a colour preset removed in 0.5. The site still falls back toneutral
Fixed
- Inline Markdown in a tab panel written straight under its
::tab[Label]line, such ascode, bold or a link, was rendered as plain text, and bold text disappeared
0.7.0
Added
- Search results are sections: a heading and the text under it, shown with the page title and heading, and linking to that heading. At most two sections of one page are listed before other pages' sections
- Each section is indexed with questions derived from its content at build time: from its heading, the Artisan and Composer commands in it, the config keys it documents and its warning callouts. A page can add its own with a
questions:list in frontmatter - Page aliases. A page's
aliases:frontmatter adds terms it can be found by, and a query that names a section's heading, page title or alias ranks that section first - A short built-in list of synonyms for common docs terms, such as "night mode" for "dark mode"
- English stemming in search, so "gate", "gated" and "gating" match each other and "configured" matches "configure"
- A search check for CI. List the questions readers ask in a
questions.ymlnext to your Markdown, with the page and heading that answer each one, andvellum:buildreports how many are found in the top five results.checks.search_minorVELLUM_SEARCH_MINfails the build below a given share, and a question whose target no longer exists is reported as broken.checks.searchturns the check off, andvellum:build --check-searchruns it for one build regardless {prefix}/_vellum/search?q=, a JSON search endpoint for agents and scripts. It uses the same ranking as the browser and only returns pages the caller can open.agents.searchturns it off{prefix}/llms.txtand{prefix}/llms-full.txt.llms.txtlists every public page in sidebar order, grouped under a heading per top-level folder, with a link to each page's raw Markdown and its description.llms-full.txtcontains the Markdown of every public page, each with its title, URL and version. Both cover the latest version and leave gated pages out.agents.llms_txtturns them off- The same two files at
/llms.txtand/llms-full.txt, unless the app already routes those paths. They followroute.domain, andagents.llms_txt_rootturns them off vellum:exportwritesllms.txtandllms-full.txtat the export root- Content negotiation. A docs page requested with an
Acceptheader that preferstext/markdownreturns the page's raw Markdown. Browsers, whose defaultAcceptdoes not nametext/markdown, still get HTML, and page responses sendVary: Accept.agents.content_negotiationturns it off - Every page links to its raw Markdown with
<link rel="alternate" type="text/markdown">and a matchingLinkresponse header - Raw Markdown responses send
Link: <page>; rel="canonical"for the HTML page,Last-Modifiedwhen the page has a date, andX-Vellum-Docs-Versionwhen versions are enabled - A last-updated date for each page, from an
updatedfrontmatter key or the file's last git commit. It appears under the page title, as<lastmod>in the sitemap, asLast-Modifiedon raw Markdown and inllms-full.txt. Pages without either have no date. Shallow clones are treated as having no git history - The changelog has a raw Markdown URL at
{prefix}/_vellum/raw/changelog.md, supports content negotiation, and is included inllms.txt,llms-full.txtand the sitemap. Withchangelog.unreleasedoff, the[Unreleased]section is left out of the raw Markdown too checks.search,checks.search_min,agents.llms_txt,agents.llms_txt_root,agents.content_negotiationandagents.searchinconfig/vellum.php
Changed
- Search no longer uses MiniSearch. The built-in driver loads a section index from
{prefix}/_vellum/answers.jsonand ranks it in the browser, and the search script is 2.2 KB gzipped instead of 6.4 KB.search.driveris nowbuiltin;minisearchstill works as the old name vellum:indexrebuilds the section index, and the Scout index when that driver is on- The sidebar's page tree is rendered once per request and reused in the mobile drawer. On a 390-page site this took the median page time from 15.3 ms to 13.1 ms
- The last-updated date under a page title comes from frontmatter or git. It used to show the file's modification time, which a checkout or
composer installresets docs/seo.mdnow suggestsAllow: /docs/_vellum/raw/alongsideDisallow: /docs/_vellum/, since raw Markdown pages declare their HTML page as canonical andllms.txtlinks to them
Removed
- The MiniSearch index, its
{prefix}/_vellum/search.jsonroute for the built-in driver, and its builder. The route remains for the Scout driver
Fixed
- Search on a static export hosted under a path, such as
export.base_urlset to/handbook/, loaded its index from the site root and found nothing, and its results linked to the root too. The exported pages now take both from the links the export rewrites
0.6.4
Fixed
- A folder's
titlein_meta.mdwas ignored, although the navigation docs said it could be set there._meta.mdfrontmatter is now read likemeta.json, and where both set the same key,_meta.mdwins, as it already did foraccess - Several pages described behaviour the code does not have. Card links to external URLs open in the same tab, inline code is only highlighted with a
{:lang}suffix, a block of raw HTML is dropped along with its text,vellum:indexrecompiles pages as well as the index, andvttfiles are served as assets. The docs now say so
Changed
- The documentation, README, contributing guide, issue templates, starter pages and
config/vellum.phpcomments have been rewritten to be easier to read. The upgrade guide covers the 0.6 patch releases, and the commands page documentsvellum:build --strict
0.6.3
Run vellum:build after upgrading, so the sidebar is rebuilt with the new access rules and compiled pages move into their own folder. If you export into the same directory each time, delete it once before the next export, so pages dropped since the last one are gone. If a page on your docs uses one of your own Blade components that shows something about the signed-in user, readers may have been shown someone else's: check what that component displays.
Security
- A page using one of your own Blade components could show one reader's output to the next. The rendered page was cached by its content alone, so a component that read the signed-in user, such as one using
@auth,@canorauth()->user(), was rendered for the first reader and then served to everyone after them, guests included. A page with a host component is now rendered on every request, and only pages built from Vellum's own components are cached - A link listed in a gated folder's
meta.jsonwas shown to guests, and with it the folder's title, since a link defaulted toguestwhatever folder it sat in. A link now takes its folder's access the way a page does, frommeta.jsonor_meta.md, unless it setsaccessitself - A
meta.jsonlink to a gated page, such as{ "title": "Plan", "slug": "internal/plan" }at the top level, was shown to every reader in the sidebar and prev/next, and its URL was listed insitemap.xml. The page itself stayed a 404. A link to a gated page is now shown only to readers who can open that page, and the sitemap leaves it out - A
---Section---heading over pages a reader could not see stayed in their sidebar when another heading followed it, and it then sat above the next section's pages instead. Withindex, ---Acquisition Plans---, admin, ---Reference---, webhooksandadmingated, a guest saw Acquisition Plans above Webhooks. Of two headings in a row, the second is now the one kept, as the docs already said - With the Scout driver,
vellum:buildonly ever added and updated records, so a page that was deleted, renamed or moved into a gated folder kept its old record, content andguestaccess included. Each sync now empties the index and writes every version's pages, including under--docs-version - A page named
nav.mdormanifest.mdwas compiled to the same file as the sidebar or the build manifest, so visiting/docs/navoverwrote the sidebar and every page then failed with a 500 until the next build. Compiled pages now live in apagesfolder of their own insidecache.path - A page whose file was deleted, or whose
slugchanged, kept being served from the compiled cache aftervellum:build, and in local too. A page moved behind a gate by giving it a new slug stayed public at its old URL.vellum:buildnow removes compiled pages it did not produce, and local stops serving a page once its file is gone vellum:exportinto a directory that held an earlier export left every page it no longer wrote in place, so a page gated or deleted since stayed published even though the command logged it as dropped. An export now keeps a list of what it wrote in.vellum-export.jsonand removes what the last one wrote and this one did not. Nothing it did not write is touched. An export made before 0.6.3 has no list, so delete that directory once before exporting into it again- A
javascript:URL was written as it stood into a card's link, from::card[...](...)or<x-vellum::card href="...">, and into a sidebar link frommeta.json. Markdown links have always refused them. These now follow the same rule, and an unsafe URL becomes#
Fixed
- The Scout driver did not work.
vellum:buildandvellum:indexsaidlaravel/scoutwas not installed even when it was, and a search would have looked for its records in a database table that does not exist. Search now reads the engine's own hits, from Meilisearch, Typesense or Algolia.docs/search.mdcovers theversionfilter a versioned site needs the engine to allow - Compiled pages, the sidebar and the search index were written straight over the old file, so a request reading a page while production compiled it on a cache miss could load half a file and fail. Each is now written to a temporary file and renamed into place, and a compiled PHP file is dropped from OPcache when it changes
vellum:exportturned each page's canonical link andog:urlinto relative paths wheneverexport.base_urlwas/, the default, or the same origin asapp.url, since it strippedapp.urlfrom the whole page. Search engines were told a different URL thansitemap.xmllisted. Both now stay absolute- The redirect stubs
vellum:exportwrites at a latest page's version-prefixed URL named the relative redirect target as their canonical. They now give the page's absolute URL, as the sitemap lists it, and leave the canonical out when there is no origin - With
route.domainset, canonical links,og:urlandsitemap.xmlnamed the host inapp.url, where the docs routes do not answer. They now useroute.domainas the host, keeping the scheme fromapp.url. A domain with a{parameter}in it names no single host, so it still falls back toapp.url - On the changelog page every release's Added, Changed and Fixed headings shared one id, so a link to a later release's Fixed landed on the first one on the page. Heading ids are now prefixed with their release, such as
0.6.3-fixed vellum:exportremovedapp.urlwherever it appeared in a page, sohttps://example.com.au/pricingbecame.au/pricingwhenapp.urlwashttps://example.com. It is now only removed where it is the whole origin at the start of a link- A file named entirely in a non-Latin script, such as
guides/入门.md, got the folder's own URL, since its name had nothing left once reduced to a-z, and silently replacedguides/index.mdat/docs/guides. Such a name now keeps its letters in the URL,/docs/guides/入门. Names that already had a Latin letter or digit keep the URLs they had - A
meta.jsonlink without aslug, such as one to GitHub, was given the docs home's empty slug. On the home page the sidebar could mark the link as the current page, and prev/next matched the link instead of the page. Such a link now names no page, and prev/next skip it - The changelog parser split a release at any line starting with
##, including one inside a fenced code block, which cut the release short and added a bogus entry to the feed. Code blocks are now skipped - A release marked
[YANKED], as Keep a Changelog writes it, lost its date and got an id with the marker in it. The date is read, the marker stays in the heading, and the id is the version alone. Ids are also cleaned of spaces and other characters a URL fragment cannot hold, so1.0.0 betabecomes1.0.0-beta - The Atom feed had no
<author>, which the format requires. It now namesvellum.name - A relative path in
vellum.path,changelog.path,cache.pathorexport.out, for exampleVELLUM_CHANGELOG=CHANGELOG.md, was read from whatever directory PHP was running in: the project root for Artisan, butpublic/for a web request, so the changelog was a 404 on the site while the build found it. Relative paths are now read from the app root, as Laravel reads its own - A page opening with an empty frontmatter block,
---followed straight away by---, failed with "closing --- is missing". It is read as no frontmatter - Before a build had run, the first request compiled every page to build the sidebar, and a single page with a typo in it, such as an unknown directive, turned every page of the site into an error. That page is now logged and left out of the sidebar, the rest are served, and the broken page shows its own error at its own URL
docs/value-tags.mdnow says that<x-vellum::env>reads the process environment, which no longer includes.envonceconfig:cachehas run, and points at theconfigtag for values your config already holds- A page with both a frontmatter
titleand a# Headingin its body rendered twoh1s without a word.vellum:buildnow reports it next to heading level skips, as a notice that never fails a build - Every docs page walked the whole docs directory three times per request, in production too, to decide whether the sidebar had changed, and then used the built sidebar whatever the answer. Production now serves the sidebar
vellum:buildwrote without looking, and a request loads it once. - In local, the sidebar was only rebuilt when a file changed size, so reordering
meta.jsonfrom"order": 2to"order": 3left the old order in place. A changed modification time now counts too - HTML, JSON and assets are gzipped at level 6 rather than 9, which is about three times faster for output 1.5% larger
- The changelog was read and rendered from Markdown on every request to its page and its feed, which took longer than serving a docs page. The result is now cached until the file, the settings that shape it, or
vellum:clearchange - Outside
local, a request for a page the build had not compiled read the frontmatter of every Markdown file to look for it, so every 404, from a mistyped link or a bot, cost a scan of the whole docs directory. Oncevellum:buildhas run, production serves what it compiled and answers anything else with a 404 straight away. A page added since the last build is served once it is built, as the sidebar already worked
0.6.2
If you publish a static export, check what is in it before deploying the next one. Anything that sat in your docs directory and was not Markdown or a meta.json was copied into the export, including dotfiles. Look for .env, editor drafts, notes and anything under a dot-directory, delete them from the published site, and rotate any secret that was exposed.
Security
vellum:exportcopied every file in the docs directory that was not Markdown or ameta.json, dotfiles and dot-directories included, into_vellum/files/. The asset route has always refused those, so a file unreachable on the served site could still be published by an export of the same docs. The export now copies only what that route serves: the listed image, font, media and document extensions, never a dotfile and never anything under a dot-directory. Both share one rule
Fixed
- An external link in
meta.jsonno longer lands in the sitemap as a docs URL (https://docs.example.com/https://github.com/...). The sitemap lists pages of these docs only
0.6.1
Changed
docs/why.mdis now What is Vellum: what the package is, how it works, and what it does not do, without the case against other tools. The URL stays/docs/why- New
docs/comparisons.mdsets Vellum against LaRecipe, the other package that serves Markdown docs from inside a Laravel app, as a table of facts with where each fits better - The docs no longer describe 0.5 as the current release. Config keys, frontmatter and syntax are still frozen, as they have been since 0.5
0.6.0
Added
{prefix}/sitemap.xml, built from the same navigation that renders the sidebar. Gated pages are excluded, including for a signed-in reader, since a sitemap is a single public file, and every version is listed when versions are enabled. It returns a 404 rather than publishing relative URLs whenapp.urlis not an origin, the same rule the canonical link already followedvellum:exportwritessitemap.xmlat the export root, usingexport.base_urlwhen it names an origin and falling back toapp.url. A static host cannot generate one for itself. Skipped with a warning when neither is an origin- Build-time checking of internal links, images and heading levels.
vellum:buildwarns about a link to a page that is not there, a#fragmentwith no matching heading, an image the renderer could not resolve, and a heading that skips a level. Missing images are reported by the renderer itself rather than inferred from the URL it emitted, so the check cannot go quiet if that fallback ever changes. Heading skips are notices and do not fail a strict build. Both failures are silent at runtime: a stale link still renders as a link, and a missing image compiles into a page whose only symptom is a broken image nobody reloaded.--strictorchecks.strictturns the warnings into a failed build, for CI docs/seo.md, covering the metadata every page emits, the sitemap, and therobots.txtworth adding, including disallowing_vellum/, since the page actions link to a raw Markdown copy of every page and it would otherwise be crawled and indexed as a near-duplicate
Changed
- The table of contents now marks every heading whose section is on screen, not only the last one scrolled past. The rail covers the whole run rather than a single entry. It used to watch heading elements, so a long section went dark the moment its title scrolled off the top; it now measures each section from its heading to the next one. The dot on the rail marks reading position, sliding between two headings' anchors by how far you have read between them, so the rail says what is on screen and the dot says where in it you are
docs/why.mdgrew from a 235-word note into a comparison page. It leads with the two ways Vellum ships the same Markdown, served from the app or exported static, including the limits of the static snapshot, then names the alternatives directly (Fumadocs, Mintlify, VitePress, Docusaurus, a wiki) and says what each is better at. The gating and export claims in it are the onesGatingTestandCommandsTestalready cover
0.5.2
Fixed
- A
versions.latestleft in config whileversions.enabledisfalseno longer reaches URL building. The changelog page declared a canonical of/docs/v2/changelog, a URL the router does not match, so search engines were pointed at a 404 and the page would not have been indexed. The 404 page written byvellum:exportbuilt its version switcher the same way.latestVersion()now returnsnullwhen versioning is off, which is what every other caller already checked for itself
0.5.1
Fixed
- Prose links now take
--primary, which is what the documentation has always said.theming.mdand the README both describedtheme.accentas recolouring links, andPresetContrastTestheld--primaryagainst--backgroundat 4.5:1 under the label "link", but links were--foregroundand--primaryonly reached a highlighted-line background, a border and the code tab underline. Setting an accent had no effect on link colour. Measured on rendered pixels after the change: neutral 18.04:1 light and 18.32:1 dark, ocean 12.31 and 13.90, laravel 4.54 and 5.54. Neutral's--primaryis all but black, so that preset looks unchanged
Changed
- Code blocks and table wrappers use the same thin, rounded scrollbar as the sidebar instead of the browser default, 6px rather than the sidebar's 8px since they sit inside content rather than beside it
0.5.0
0.5 stability freeze. Config keys and frontmatter names do not change until 1.0.
Security
- Path traversal in the docs slug. A request like
/docs/..%2FREADMEresolved outside the content root, so any.mdon disk could be read through the page route and the raw Markdown route, and compiling it wrote a PHP file outside the cache directory. Slugs with empty,.or..segments are now refused, a resolved path is checked against the content root withrealpath, andCompiledStorerefuses to build a path for an unsafe slug - Blade execution through component attributes. Attribute values were interpolated into the template string, and
e()escapes quotes but not braces, so<x-vellum::callout title="{{ php_uname() }}">ran. Values are now bound and passed as data, so docs attributes stay literal as documented - Gated content readable through the asset route.
/{prefix}/_vellum/files/served anything undervellum.path, including gated pages,meta.json,_meta.mdand dotfiles. It now serves an allowlist of asset extensions only. Assets themselves are still not gated, which is documented on the gating page
Added
- Package docs for why Vellum, versions, gating, search drivers, changelog, export, theming, and the 0.2 to 0.5 upgrade path
laravelcolour preset, matching laravel.com/docs warm sand neutrals and the Laravel redtheme.accent: a brand colour applied to links, buttons and the focus ring on any preset, as one value or['light' => ..., 'dark' => ...]. Hex,hsl()andoklch()are accepted, the button label colour is derived from it, and an unparseable value is ignoredVellum\Support\Color: colour parsing, relative luminance and WCAG contrast- Contrast test over the real stylesheets: every shipped preset clears WCAG AA (4.5:1) for body text, secondary text, links and button labels, in both modes
CONTRIBUTING.md,SECURITY.md, GitHub issue and pull request templatesversions.labels: switcher display names (1.x (LTS),Next) while folders and URLs stay the list slug- Docs for the features that shipped without any: Markdown, code blocks, images, navigation, an artisan command reference, page actions, troubleshooting, and credits
- Canonical link, Open Graph and Twitter card tags on every page. Canonical is omitted rather than guessed when
app.urlis not an origin, and a static export prefersexport.base_urlwhen that names one vellum:exportwrites a404.htmlat the export root, with root-relative asset paths so it works for a miss at any depth- A Vellum error page for content that cannot render, instead of the framework's generic 500. The reason and file appear only when
APP_DEBUGis on UnknownDirectiveException: a:::typo now names the directive and the file instead of raisingNoMatchingRendererExceptionfor an internal classDuplicateSlugException:vellum:buildrefuses to build when two files resolve to one URL, and names both.gitattributeswithexport-ignore, taking the released archive from 1860 KB to 830 KB
Changed
- Colour presets retuned to clear WCAG AA in light mode. Neutral's secondary text was 4.21:1, Laravel's link and button colours 3.96:1
- Laravel preset uses
#e32c03for text and fills and keeps#f53003for the active wash and the focus ring, the way laravel.com does - Code blocks take the shared
--mutedsurface instead of a hard-coded white and#191919, so they match the page in every preset - The sidebar takes the shared
--cardsurface, instead of a tint on some presets and the page background on others - Surfaces follow one rule in every preset and both modes:
--backgroundis the canvas at the light or dark extreme,--cardis one step toward mid-grey for the sidebar, callouts and popovers, and--mutedis a second step for code blocks, table headers, tab strips and step markers. Surfaces only ever move away from the canvas, darker in light and lighter in dark, so prose sits on the cleanest area and nesting reads correctly - Light mode canvases are near-white and dark canvases near-black, replacing a mid-grey page with lighter elements sitting on top of it
- Light-mode surfaces are lighter: the code block sat at 1.19:1 from the canvas, roughly twice the step GitHub, VitePress and Fumadocs use, and is now 1.08:1
- Code tabs are tighter: the active underline sits closer to the label, the header is 39px instead of 49px, and the code is inset by 0.25rem instead of 0.5rem
- Code tabs take the same two surfaces as everything else,
--cardfor the frame and--mutedfor the code, instead of a hard-coded grey frame around a pure white block - Syntax highlighting is more colourful. Strings are green, numbers teal, types blue, variables orange, keywords red, and every token clears 4.5:1 on every preset's code surface in both modes
- Neutral's
--mutedwas 1.003:1 against the page in light mode, so table headers, tab strips and step markers had no visible surface - Markdown tabs and UI tabs share
vellumTabs()for arrow keys, Home/End, andaria-controls - Search dialog labelled for assistive tech (
combobox+listbox); version switcher menu has Home/End andaria-controls - The active code tab label uses
--foregroundand the accent carries the underline. With the accent the docs use as an example it measured 1.42:1 against a documented 4.5:1, and now measures 18.80:1 in light and 15.04:1 in dark - The asset route sends
max-age=86400, must-revalidateinstead of a year ofimmutable, since those URLs carry no content hash and a replaced image was stale for a year guestandauthare matched without regard to case, soaccess: Authworks. Gate names are still passed toGate::allows()exactly as written
Fixed
access: Authhid a page from everyone. A capitalised value fell through toGate::allows('Auth'), false for guests and signed-in readers alike, with no error anywhere- A UTF-8 BOM before the opening
---made the whole frontmatter block parse as body text, and the title fall back to the filename - Images inside a version folder resolved against the global docs root and 404d. Each version now renders through its own pipeline and the emitted URL carries the version segment
- A page whose title fell back to its first heading shipped two
h1s, since the layout renders one of its own vellum:install --forcenever republished the config; the flag was ignored- An impossible changelog date such as
2026-13-45passed into the Atom feed, which readers reject. Dates are validated and fall back to the file mtime - An unquoted numeric frontmatter
title, which YAML parses as an int, was discarded in favour of the filename - The checked-checkbox tick had its colour baked into a data URL, which had drifted from
--muted-foregroundin light mode and never matched theoceanorlaravelpresets at all - The last sidebar item sat against the footer bar with no clearance
- The index page title rendered as "Vellum · Vellum" when the page title equalled the site name
- A long URL or identifier in inline backticks had no break opportunity and pushed the page wider than a phone viewport. It wraps now
vellum:buildwarns when a page is shadowed by the changelog route
Removed
- Colour presets
black,vitepress,dusk,catppuccin,purple,solar,emerald,rubyandaspen. Most changed one accent colour, whichtheme.accentnow does on any preset; several could not reach WCAG AA in light mode without losing the character that named them. A removed name falls back toneutralandvellum:buildwarns once - Cloudflare Workers demo (
bin/build-demo,wrangler.toml, committeddemo-dist). The public site is now a Laravel app that servesdocs/through the package
0.4.0
Versioned docs URLs and search drivers.
Added
php artisan vellum:indexrebuilds the MiniSearch compile cache and, whensearch.driverisscout, syncs Laravel Scout- Page
accessfrontmatter (guest,auth, or a gate name). Foldermeta.jsonor_meta.mdinherits downward; a page overrides. Export logs each dropped gated page
Changed
- When
vellum.versions.enabledis true, the latest version is served at/docs/.... Older versions stay at/docs/{version}/..../docs/{latest}/...redirects to the unprefixed URL with HTTP 301. The version switcher labels the latest folder "Latest". - Search is a driver (
vellum.search.driver): MiniSearch by default, Laravel Scout optional. The live MiniSearch index is a package route that filters pages the current user cannot access, cached per visibility set, with an ETag somust-revalidatecan 304. Static export always writes MiniSearch JSON and drops gated pages.
0.3.0
Markdown plus components. Pre-1.0: the API may change.
Added
<x-…>component islands in Markdown. Slots are Markdown and may nest. Attributes are quoted strings only: no{{ }}, Blade directives, or@phpin docs. Tags inside fenced or inline code stay as text- Host Blade components via
vellum.components.namespaces(default['vellum']allows<x-vellum::…>only; add''or'app'for<x-alert>). Unknown or disallowed tags fail locally and at build - Built-in
:::callout,:::tabs,:::steps, and:::cardsrender the same views as<x-vellum::callout>, tabs, steps, and cards. Prefer:::for those <x-vellum::env />,config, androutevalue tags, gated byvellum.components.allowlist(empty lists refuse every key). They resolve when the page is rendered, including inside:::tabs, not when Markdown is compiled- Request-time fragment cache for those islands, cleared by
vellum:clearandoptimize:clear - Changelog page at
/docs/changelogand Atom feed at/docs/changelog.atom(vellum.changelog.path, defaultCHANGELOG.md). Setpathtonullto disable.[Unreleased]stays in the source file; the page hides it unlessvellum.changelog.unreleasedis true; the feed never includes it - Package docs in
docs/(the Cloudflare demo exports that tree).vellum:installstubs stay a short getting-started plus one page per built-in
0.2.0
Docs chrome and Markdown polish. Pre-1.0: the API may change.
Added
- Footnotes via CommonMark, with styled list and backlinks
vellum.layout.search:sidebar(default) orheader- Collapsible desktop sidebar, persisted in
localStorage, with a floating expand/search pill, width animation, and a rounded left-edge hover peek - Mobile "On this page" popover: sticky bar, reading-progress circle, overlay rail dropdown (
max-h-[50vh]) - Page actions: copy Markdown, Open menu (ChatGPT, Claude, raw Markdown, Edit on GitHub)
- Raw Markdown route
/docs/_vellum/raw/{slug}.md, included invellum:export successandideacallout aliases (map to tip and note)vellum.theme.preset: Fumadocs palettes (neutral,black,vitepress,dusk,catppuccin,ocean,purple,solar,emerald,ruby,aspen)
Fixed
- Collapsed sidebar peek only opens from the left-edge hotzone after a short cooldown, and hides when the pointer leaves
- Theme menu in the sidebar footer opens upward so it stays on screen
bin/build-demoregisters the package throughtestbench.yamlsovellum:exportis available
Changed
- Plain inline code uses the same chip as tagged inline code
- Line numbers are CSS counters (
user-select: none) and are omitted from copied text - Sidebar item text aligns with the brand; nested items indent from that edge
- Default chrome has no top header; search and theme live in the sidebar
- Copyright footer removed; footer is an empty optional slot
- Previous/next cards use arrows, a bold title, and the page description; they sit after footnotes, half width when both exist, full width for a single direction, stacked on small screens
- Content stays left when there is no table of contents; the TOC column is still reserved
- Last-updated date sits above the H1 next to breadcrumbs
- Tighter content-column padding
- Hotkey chip defaults to
Ctrland switches to⌘on Apple platforms after hydration - TOC uses a continuous SVG rail with heading-level jogs, a 420ms active clip, and a path-following thumb
- Heading permalinks are a hover/focus copy-link control (no visible
#) - Code copy control is icon-only; title bar uses Phosphor language file marks
- Chrome icons use Phosphor (search, theme, sidebar, carets, GitHub, heading permalinks)
- TOC active and hovered section titles are semibold
- Fenced code uses GitHub Light/Dark token colours on a Fumadocs-like card surface
- Code tabs sit in a lighter grey chrome; the inner fence uses the same surface as standalone code blocks
- Author
---rules are lighter; theme no longer inserts rules between headings :::cardsrender two per row (one on mobile)- Tempest token classes are styled in the built CSS
- Callouts: rounded card, inset rounded-pill accent rail, Phosphor fill icons aligned to the title/first line, bold title, muted body
- Mobile navigation is a Fumadocs-style right sheet: 85% width, sun/moon header, a heavily blurred page overlay, and the same 280ms sidebar slide
- Mobile "On this page" popover shares one frosted surface with the title bar so the blur matches
- Neutral palette matches Fumadocs (soft gray light, charcoal dark instead of OLED black)
- Copy on embedded/tab code sits on the first line
- Docs images align to the content edge
- Task-list checkboxes use muted-foreground grey so the check stays visible
Removed
- Default copyright / "Built with Vellum" footer content
0.1.0
Initial public release. Pre-1.0: the API may change.
Added
- Laravel package that serves documentation from Markdown (
resources/docsby default); supports Laravel 11, 12, and 13 vellum:installto publish config, starter stubs, and public dist assetsvellum:build/vellum:clearfor an OPcache-friendly compile cache- Markdown extensions: callouts, tabs, steps, cards, fenced code blocks, inline code highlighting, images, external links, heading anchors
- Docs UI: sidebar navigation, table of contents, breadcrumbs, search (MiniSearch), light/dark/system theme
- Optional version folders with latest redirect and version switcher
vellum:exportfor static HTML output suitable for GitHub Pages or any static host- Config surface in
config/vellum.php(path, routes, versions, theme, search, cache, export) - GitHub Actions CI (Pest on Laravel 11/12/13, Pint, PHPStan, frontend build / size gate)
- Demo site workflow that exports package stubs for Cloudflare Workers (
bin/build-demo,wrangler.toml)
Publishing note
Tag v0.1.0 and push the tag, then submit https://github.com/VerburgtJimmy/vellum on Packagist if the package is not listed yet. Do not hardcode "version" in composer.json.