Changelog
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[Unreleased]
Fixed
- Docs: getosirion.com's
/robots.txtcomes from arobotscontent item that names the sitemap and keeps crawlers off/search; it served the generator's default rules, which named no sitemap. The packages page at/docs/packagesdeclared/componentsas its canonical address, so search engines would have dropped it; its title and description now describe the packages. Tests hold every sitemap page to its own canonical address
[4.2.0] - 2026-10-18
Added
- Osirion.Blazor.Templates: a new template package.
dotnet new install Osirion.Blazor.Templatesthendotnet new osirion-blog -n MyBlogcreates a Markdown blog on .NET 10 with static server-side rendering: the newest posts on the home page, a blog list, posts, an About page, a 404 page, RSS, Atom and a sitemap, with the menu and footer fromappsettings.json.--SiteNameand--SiteUrlfill in the name and public address. The template references the library version it is released with; tests hold the two equal.tools/verify-template.shtimes the walkthrough on a clean NuGet cache against packed packages - Osirion.Blazor.Templates:
dotnet new osirion-docscreates a Markdown documentation site: a home page, pages incontent/docswith side navigation in front matterorder, a table of contents, previous and next links, a 404 page and a sitemap;/docsleads to the first page - Osirion.Blazor.Templates:
dotnet new osirion-sitecreates a product or company site: a home page with a hero, three steps and the latest news, news incontent/newswith RSS, About and Contact pages, and a sitemap. The contact page offers an email link; the comment beside it explains how to switch on the form with the email service - Examples: the Bootstrap example site, which getosirion.com runs, renders its Google Analytics, Clarity and Matomo trackers only after the visitor accepts analytics cookies (
Osirion:Analytics:Consent:RequireConsent), and none in Development. It loaded all three on the first page view, before the cookie banner was answered, and local runs sent page views to the live accounts - Docs: the getting started guide opens with the template, three commands to a running blog, before the steps for an existing app
- Docs: the example site has its remaining pages: a home page that follows the site plan (the quickest path, a live palette picker, the packages, featured components, the latest posts),
/about,/contact(the channels that work, no form because the site has no email provider),/privacy-policy(the cookies and the three analytics services the site runs, and how to withdraw consent), and/showcase, which now lists the sites built with Osirion.Blazor in place of the 4.0 component showcase that every component page covers with a live demo. The shared privacy page stays a placeholder unlessExample:SitePrivacyPolicyis true. - Docs: the component pages that said "Available from 4.1" for
OsirionAnnouncementBar,OsirionAuthorCard, the Plausible and Umami trackers and the feeds now say 4.0, where the changelog and the v4.0.0 tag have them; pages no longer tell readers to callAddJSComponents()whenAddOsirionalready registers the script loader, and the README snippet drops the extra call - Docs: the documentation sidebar has a Guides group under Getting started, with its page at
/docs/guidesand twelve guides: installation, configuration, content providers, deployment, how content is organized, the front matter reference, localization, SEO, accessibility, security, migration from 3.x and 4.0, and troubleshooting. Each was checked against the code and its snippets compiled; the limits found in the code (for examplepermalinkignored for content items, a missinglangread asen) are stated where they matter - Docs: every component page moves to
/components/<name>, and the setup pages of each package to/docs/packages/<package>. Each of the 72 earlier addresses under/docs/componentsanswers with a 301 to its new page, fromredirect_fromfront matter. The example site gets a documentation sidebar grouped by what components are for, previous and next links in that order, and a component gallery at/components - Docs: the example site has a theme page at
/themes: the palette picker and the theme toggle, the palettes and what each is inspired by, how to make your own, and every colour token with a chip in the current palette and the pairs the contrast tests hold it to. It heads the Theming group of the sidebar - Docs: the card pages follow the component page template: a lead, the package, namespace and source link, a live demo with its compiled source, and a parameter table generated from the component (
<!-- osirion:parameters Name -->), which a test holds to the code.OsirionMetricCardhas its own page - Docs: the forms and feedback pages (
OsirionContactForm,OsirionCookieConsent,OsirionAnnouncementBar,OsirionContentNotFound) follow the component page template, with live contact form and empty state demos and generated parameter tables - Docs: the theming pages (
OsirionStyles,ThemeToggle,ThemePicker, palettes) follow the component page template, with live toggle and picker demos and generated parameter tables.ThemeToggle.OnThemeChanged, which is never raised, says so and points to theosirion-theme-updatebrowser event - Docs: the CMS page components (
ContentPage,ContentRenderer,ContentView,LocalizedContentView) follow the component page template, with live demos that load one of this site's own pages and generated parameter tables, andArticlePage,DocumentPage,HomePage,LandingPage,AboutPage,ContactPageandNotFoundPagehave their own pages - Docs:
SeoMetadataRendererand the feeds and sitemap page follow the component page template under a new SEO and feeds group page, and each analytics tracker (ClarityTracker,GA4Tracker,MatomoTracker,PlausibleTracker,UmamiTracker,YandexMetricaTracker) has its own page under a new Analytics group; the analytics package page keeps setup, configuration and the consent gate - Docs: the CMS navigation pages (
CategoriesList,TagCloud,ContentBreadcrumbs,DirectoryNavigation,LocalizedNavigation,OsirionContentNavigation,ContentSeriesNavigation,TableOfContents,SearchBox) follow the component page template, with live demos built from the site's own content and generated parameter tables - Docs: the navigation pages (
OsirionBreadcrumbs,OsirionReadMoreLink) follow the component page template, with live demos and generated parameter tables, andMenuwith its parts,ScrollToTopandEnhancedNavigationhave their own pages. The navigation package page keeps setup and configuration and links to each component - Docs: the content component pages (article metadata, callouts, tabs, accordion and FAQ, responsive image, video embed, reading progress,
OsirionHtmlRenderer) follow the component page template, with live demos and generated parameter tables, and are marked available from the release the changelog lists them in.OsirionRevealandOsirionContentDemohave their own pages - Docs: the section pages (
HeroSection,OsirionBaseSection,OsirionContactInfoSection,OsirionPricingTableandOsirionFeatureComparison,OsirionSteps,OsirionResponsiveShowcaseSection, the logo and testimonial carousels) follow the component page template, with live demos and generated parameter tables.OsirionMetricGrid,OsirionContextCardGridandOsirionPageHeadinghave their own pages. The pricing table and steps are marked available from 4.0, where the changelog lists them - Docs: the layout pages (
OsirionPageLayout,OsirionPageLoading,OsirionFooter,OsirionStickySidebar,OsirionBackgroundPattern) follow the component page template, with live demos and generated parameter tables; the parameter docs of these components say what each parameter does - Docs: the example site has
/templatesfor thedotnet newtemplates and/changelog, rendered from this file. Addresses no longer take a locale:/en/...answers with a 301 to the same path without it, and a made-up locale such as/xx/blogno longer answers 200 with an empty list. Every unknown address answers 404 with the not-found page instead of an empty response, and the Blazor template's Counter and Weather pages are gone - Osirion.Blazor.Cms.Web:
SeoMetadataOptions.SiteUrl(Osirion:Seo:SiteUrl). When it is an absolute http(s) address, the canonical URL,og:urland the addresses in structured data use it with the page's path instead of the host that served the request, so a site behind a proxy, on a preview host or on a second domain declares its public address
Fixed
- Osirion.Blazor.Core:
OsirionCookieConsentreopens its category panel for a page opened with?customize-cookiesafter the visitor answered, with their saved choices checked, so a "Cookie settings" link can change or withdraw consent. Before, the banner never rendered again once answered, and consent could only be withdrawn by deleting the cookie - Osirion.Blazor.Core: the Copy button restores its own accessible name after a copy. Two seconds later the script set a generic "Copy code to clipboard", so "Copy C# code to clipboard" lost its language
- Osirion.Blazor.Theming, Osirion.Blazor.Core: the theme, palette and announcement dismissal cookies are always
Secure. The flag followedRequest.IsHttps, so behind a proxy that ends TLS without forwarded headers an HTTPS site set them without it. Browsers treatlocalhostas secure, so local development keeps them. The theme and palette cookies stay readable by script, which applies and replaces them - Osirion.Blazor.Navigation:
MenuGroupwithCollapsiblerenders its label as thesummaryof adetailselement, so a click, Enter or Space opens and closes the items without script, andExpandedsets the starting state. The label was a plaindivand a rule hid the items of a group withExpanded="false", so they stayed hidden with no control to open them. A collapsible group without aLabelnow stays open - Osirion.Blazor.Navigation:
Menu.AutoExpandActivekeeps the submenu of the active item open in a vertical menu, as its description says. Only theosirion-menu-auto-expandclass was written. The rule written for it neededosirion-menu-item-has-submenuandosirion-menu-item-activeon one element, which no markup had (the first is on the wrapper, the second on the link), and the 4.1.0 stylesheet dropped it, so the submenu of the page the reader was on stayed closed - Osirion.Blazor.Core:
OsirionReadMoreLinkcomputes its accessible name on every render,AriaLabelor else the currentText, and draws its focus outline and button ring on:focus-visible.OnInitializedcopiedTextinto theAriaLabelparameter once, so afterTextchanged the link still announced the first text, and a mouse click drew the keyboard focus outline - Osirion.Blazor.Core:
OsirionBreadcrumbswritesStyleon itsnavelement. It never wrote it, sostyle="..."in the markup was dropped - Osirion.Blazor.Core:
OsirionBreadcrumbsdecodes percent-encoded segments for the crumb text and keeps the links encoded, and shows an invalid escape as written. Segments were shown as they sit in the URL, so/docs/caf%C3%A9read "Caf%C3%A9" - Osirion.Blazor.Navigation:
Menu,MenuItem,MenuGroup,MenuDivider,MenuCallToAction,MenuImageCard,MenuPanelandMenuPanelLinkwriteStyleon their root element. None of them did, sostyle="..."in the markup bound to the parameter and was dropped from the output.Menuwrites it before the stickyz-index - Osirion.Blazor.Navigation:
Menugives the brand logo an emptyaltbesideBrandTextandHomewithout it, and declares it 36 by 36 pixels, the size the stylesheet draws. The alt text was<BrandText> brand logo, so the name was read twice beside visible text and read as " brand logo" without it, and the 60 by 60 attributes disagreed with the 2.25rem the CSS draws - Osirion.Blazor.Navigation:
MenuImageCardrendersalt=""whenImageAltis not set, which marks the image as decorative. It rendered noaltattribute, so a screen reader read the file name of the image aloud in front of the caption - Osirion.Blazor.Theming:
theme-toggle.jsaddsSecureto theosirion-preferred-themecookie on HTTPS, asThemeServiceandtheme-picker.jsdo. Before, a theme chosen with the toggle was written withoutSecureeven on HTTPS - Osirion.Blazor.Core:
OsirionContentNotFoundrenders the suggestion description and the suggestions title only when they have text, andCreate403leads to/instead of ajavascript:history.back()address. Before, a suggestion without a description rendered an emptypandsmall, an empty title an emptyp, and the 403 button held a script URL. The staticCreate...methods still return an instance that Blazor cannot render; their documentation now says so - Osirion.Blazor.Core:
OsirionContactFormsubmits whenPrivacyPolicyTextis null or empty. Before, the checkbox was left out but the model still required it, so every submission failed validation with no message shown (AgreeToPrivacyPolicyis now set to true when no checkbox is rendered) - Osirion.Blazor.Theming:
ThemeToggle.OnThemeChangedis raised, withlightordark, when the toggle runs in an interactive render mode and a callback is set. It was declared and never raised in any mode. Under Static SSR the browser switches the theme and the server is not told, so the callback is still not raised there; listen for theosirion-theme-updatebrowser event onwindow - Osirion.Blazor.Theming:
ThemeTogglekeeps its behavior intheme-toggle.jsand passes its settings indata-default-themeanddata-frameworkon the button. It used an inlineonclickand an inline script, which a Content Security Policy without'unsafe-inline'blocks, so the toggle did nothing on such a site - Osirion.Blazor.Theming:
OsirionStylesreplaces every<inCustomVariablesand in the configuredThemingOptions.CustomVariableswith the CSS escape\3Cbefore it writes them into a<style>element. They were written as markup, so a value containing</style><script>ended the element and put a script into the page - Osirion.Blazor.Theming:
ThemeServicereads and writesinvertedin theosirion-preferred-themecookie. It parsed onlylight,darkandsystem, and wrote no cookie forThemeMode.Inverted, so an inverted choice was lost on the next request andCurrentModefell back to the default mode - Osirion.Blazor.Theming: in the dark theme a content category badge draws
--osirion-color-primary-700on--osirion-color-primary-200, the pair the active navigation item uses and the adapter contrast tests hold. It drew primary-600, 3.74:1 under the Bootstrap adapter with Bootstrap's own colors and below 4.5:1 under the MudBlazor and Fluent UI adapters too - Osirion.Blazor.Cms.Web:
ContentViewandLocalizedContentViewno longer recolor their own category and tag links when they sit inside another component's content body. Their.osirion-content-body arules never reached the rendered body, which carries no scope attribute, and only painted the badges in the link color: 4.37:1 in the light theme - Osirion.Blazor.Core: the
OsirionReadMoreLinkButtonvariant keeps its text in--osirion-action-primary-textunder a dark system preference. The dark scheme's link color rule came later at the same specificity and recolored the text on the fill: #9090d6 on #5c5cb0, 1.97:1 - Osirion.Blazor.Cms.Web: the active
CategoriesListcount uses the active link's own colors with a ring. It drew the link color on a primary tint, 3.39:1 in the light theme and 2.78:1 in the dark one - Osirion.Blazor.Cms.Web: the
TagCloudcount keeps the tag's text color. It was faded to 75% opacity, 3.66:1 on the tag background - Osirion.Blazor.Core: the
OsirionHtmlRenderertext link rules leave the links inside an embedded demo alone, so a demo looks as the component does on a page of its own. Before, the renderer's tinted link background sat under each component's own link color: theOsirionContentNotFoundsuggestions on its centered panel read 4.35:1 in the light theme - Osirion.Blazor.Core:
OsirionContentDemoloads the copy script for its Copy button, so the button works when the component is used directly on a page. Before it rendered the button with an inline handler and no script, so outsideOsirionHtmlRenderera click did nothing - Osirion.Blazor.Core:
OsirionArticleMetdatawritesClass,Style,Themeand unmatched attributes on its root element, and the author icon takes the stylesheet'sosirion-article-metadata-iconclass (the oldosirion-article-meta-iconstays). Before, the root always rendered as<div class="osirion-article-metadata">, and the author icon alone missed the no-shrink and 70% opacity rules the date and clock icons got - Osirion.Blazor.Core: the code block header and Copy button in rendered Markdown and
OsirionHtmlRenderertake the code block's own colors (--osirion-code-backgroundand--osirion-code-text, with a dark fallback that matches the Okaidia theme). Before, the button's#f8f8f2text sat on--osirion-syntax-background-color, which only the palettes define, so it read 1.06:1 on white in the light theme and light text sat on a light header under the light palettes - Osirion.Blazor.Core:
OsirionRevealhides its content only once the script has armed it (classosirion-reveal-armed), so it is visible and in place with script blocked or failing, withAnimateoff and under reduced motion. Before, content started atopacity: 0and stayed invisible without the script, andDown,LeftandRightkept a 1.25rem offset with!importantwhenever the script did not reveal them - Osirion.Blazor.Analytics:
GA4Options.DebugModewritesdebug_mode: truein thegtag('config')call and the script no longer writesgtag('require', 'linkid'), whose optionGA4Options.LinkAttributionis now[Obsolete]. The script setwindow.ga_debug, an analytics.js mechanism, so DebugView showed nothing, andrequireis not a gtag.js command (the gtag.js reference lists config, get, set, event and consent) - Osirion.Blazor.Analytics: the Yandex Metrica tracker writes no
noscriptpixel and the page view module is loaded from/_content/Osirion.Blazor.Analytics/js/osirion-analytics.js. The tracker sits in the head, where thenoscriptwith adivand animgis invalid and ends the head for a parser, and the relativesrcasked a page at/blog/postfor/blog/_content/..., a 404, unless the document had<base href="/"> - Osirion.Blazor.Analytics:
YandexMetricaOptions.HashTrackingwritestrackHash: truewhileTrackHashis not set,ClarityOptions.TrackUserAttributesandMatomoOptions.TrackDownloadsare[Obsolete], and the XML docs ofAutoTrackPageViewsandSiteIdsay which providers ignore them. The three options were bound and never read, so aHashTracking: trueconfiguration sent notrackHash. Clarity has no install-time user attribute setting (onlyclarity("identify")andclarity("set")calls) and Matomo tracks downloads with outlinks underenableLinkTracking, so those two have nothing to map to - Osirion.Blazor.Cms.Web:
SeoMetadataRendererwritesai-content-declaration,ai-content-detectionandai-content-attributiononly from front mattermeta_tagsor the newSeoMetadataOptions.AdditionalMetaTags,DefaultImageWidthandDefaultImageHeightare nullable so an explicit1200or630stands over the configured size, and a page value offalseforEmitMobileMetaTagsorEnableGeneratedFaqwins over the options. Every page saidhuman-created, the two sizes were read as "not set" and replaced by the configured ones, and the options were combined with|=so a page could not switch either off. TheSeoMetadataOptions.SiteDescriptiondoc no longer claims it feeds the organization schema; nothing reads it there - Osirion.Blazor.Cms.Web:
SeoMetadataRendereruses the front matterseo_properties.imageas the default social image (afterog_image_url, before the featured image),langforog:localeandmeta_tagsasmetaelements. Names must be a letter followed by letters, digits and- _ . :and not one the renderer writes itself, and values are HTML encoded. All three were parsed and never read, so a page that setimageshared its featured image andmeta_tagswrote nothing - Osirion.Blazor.Cms.Domain:
SeoMetadata.RobotsandOgTypeare empty by default, andSeoMetadataRendererreads the front mattertypeignoring case. Both defaults were non-empty (index, followandarticle), so an item with front matter never got the renderer's robots value withmax-snippet:-1, max-image-preview:large, max-video-preview:-1or a computedog:type(a ProfilePage saidarticle), andtype: techarticlebecame aWebPage - Osirion.Blazor.Cms.Web: the item list of a collection page and the blog posting of each listed item join the host and the item URL with exactly one slash. An item URL stored without a leading slash was appended as it was, so the item list of
/docsheldhttps://example.comdocs/install - Osirion.Blazor.Core:
OsirionTestimonialCarouselkeeps its moving track out of the page width. The stage hid the track withclip-pathalone, which still counts as scrollable overflow, so every page with the carousel scrolled sideways: 2,216 pixels on a 375 pixel screen and about 1,650 at 1280 - Osirion.Blazor.Cms.Web: the
DocumentPagetitle wraps a word longer than the line. A component name used as the title, such asOsirionResponsiveShowcaseSection, pushed a 375 pixel screen 41 pixels sideways - Osirion.Blazor.Core:
OsirionPageLoadingstops its spinner underprefers-reduced-motion: reduce. It turned continuously for visitors who ask for less motion - Osirion.Blazor.Core:
OsirionBackgroundPatternpaints theTechWaveandCircuitpatterns, uses oneosirion-bg-image-maskgradient in bothmask-imagedeclarations, andAnimatedGridandTechWavestop underprefers-reduced-motion. The two patterns wrapped a color token inrgba(), which is invalid, so the browser dropped the background; the-webkit-mask listed its stops out of order (52% then 44%); and the animations ran for visitors who ask for less motion - Osirion.Blazor.Core:
OsirionFooterrenders no top row when its only content is social links andShowSocialLinksisfalse, and the default copyright link to an externalCompanyUrlcarriesrel="noopener noreferrer"with itstarget="_blank". The switched-off links still counted as content, which left an emptyosirion-footer-mainrow, and the new tab gotwindow.opener - Osirion.Blazor.Core:
OsirionStickySidebarwritesTopOffsetto theasideas--osirion-header-heightwhen it differs from the default of 40. The component built the declaration but never rendered it, soTopOffset="64"left the sidebar sticking at 2.5rem. At the default nothing is written, so a site that sets the custom property in CSS keeps working - Osirion.Blazor.Core:
OsirionContactInfoSectionwritesClassonce on its root element. The markup appended it again afterCombineCssClasseshad, soclass="osirion-contact-info-section extra extra" - Osirion.Blazor.Core:
OsirionMetricGridappliesClassandStyleto its list, gives theulrole="list"asOsirionPricingTabledoes, and makes each card fill its cell with its own CSS instead of passing the Bootstrap-only classh-100.ClassandStylewere ignored, the list lost its list semantics where a browser drops them forlist-style: none, and the cards depended on a framework class - Osirion.Blazor.Core:
OsirionContextCardGridtakes its heading and description colors from the theme text tokens, gives each grid its own generatedHeadingId(an explicit value still works), and renders no emptyh2, nor anaria-labelledbypointing at one, whenTitleis empty. The heading used a fixed#123b2bthat reads 1.61:1 on the dark theme, two grids on a page repeated the same heading id, and an empty title left an empty heading that labelled the section - Osirion.Blazor.Cms.Web: the stylesheets of
ContentView,LocalizedContentViewandContentRendererno longer carry about 40 rules for the body's headings, paragraphs, lists, tables, code and links, which never matched;ContentViewandLocalizedContentViewkeep two,imgat most as wide as the body andprescrolling sideways, through::deep. Scoped CSS compiled.osirion-content-body ptop[b-...], and the body's elements carry no scope attribute, so nothing applied (<div class="osirion-content-body" b-32gl18udl4><h2>Setup</h2><p>Text</p>).ContentRenderergets its typography from the unscopedosirion-html-renderer.cssthatOsirionHtmlRendererlinks; the two views write the body outside it and keep the style of your CSS, so no body changes except that a wide image or code block now stays inside its column - Osirion.Blazor.Cms.Web: the page components pass
SiteDescription,TwitterSite,TwitterCreator,FacebookAppId,DefaultImageUrl,DefaultImageWidth,DefaultImageHeightand, when you set them,AllowAiDiscovery,AllowAiTraining,AllowSearchIndexing,EnableGeoOptimizationandEnableAeoOptimizationtoSeoMetadataRenderer(ArticlePageandHomePagealso passOrganizationName,SiteLogoUrlandGenerateMultipleSchemas), and the not-found state ofArticlePage,HomePage,DocumentPageandAboutPageusesNotFoundTitle,NotFoundText,ShowBackLink,BackLinkUrlandBackLinkText. None of them reached the renderer or the not-found state. A switch you leave out keeps the value ofAddOsirionSeoMetadataor theOsirion:Seosection instead of being overridden by the page's default. The not-found state now shows the declared defaults, "Article Not Found" and "The requested article could not be found.", and a "Back to Home" button where it showed the library's "Page Not Found" text and a "Go to Homepage" button; set the parameters to change them - Osirion.Blazor.Cms.Web:
LandingPagerenders the features (ShowFeatures,FeatureItems), testimonials (ShowTestimonials,TestimonialItems) and call to action (Ctaparameters) it declares, honorsHeroAlignmentandShowHeroButtons, and putsClass,Style,Themeand unmatched attributes on its root element. None of these reached the page (about 30 parameters did nothing), so the hero was always left aligned with its buttons shown andStyleandThemewere dropped. The hero is now centered by default, the declared default ofHeroAlignment.CtaBackgroundColoraccepts only a color, so it cannot add style declarations. TheTestimonialItem.Ratingsummary no longer says "image alt text"; the page does not show the rating - Osirion.Blazor.Cms.Web:
HomePageshows the newestFeaturedCountitems of itsLocalethat sayfeatured: truein the featured section, renders the recent section (ShowRecentContent,RecentSectionTitle,RecentCount,ReadMoreText) from the newest items of the locale, builds both lists' links withContentUrlFormatter, usesHeroImageUrland no longer runs an item lookup and a related items query it never rendered. The featured section listed the newest items of any locale whether featured or not, the recent section never rendered because nothing set its items, and the hero ignoredHeroImageUrl. A section with no items is left out instead of showing a not-found block, so a site with no featured item shows no featured section - Osirion.Blazor.Cms.Web:
ContentViewandLocalizedContentViewtake aHeadingLevel(1 to 6, default 1) for the title, so a view embedded in a page can write anh2and the page keeps oneh1; the featured image of both views and the related item images ofContentPagetake theiraltfrom thefeatured_image_altentry of the front matter'scustom_fields, or are empty (decorative). The title was always anh1, and every image repeated the item title asalt, which a screen reader reads twice beside the heading. The front matter model has no property for it, and a top-levelfeatured_image_altkey is ignored - Osirion.Blazor.Cms.Web:
LocalizedContentViewlooks up its previous and next items by the directory's path, asContentView,ContentPageandDocumentPagedo, keeps thePreviousItemandNextItemyou pass, and links each translation to its own address. It queried by the directory's display name, which providers do not match for a folder titled differently from its path, so no links showed there. InsideContentPageit ran a second query that replaced the neighbors the page had computed. Translations share one storedUrl, so every link pointed at the visitor's own page; a translation with the sameUrlas the shown item now links to/{locale}/{url}.OnLocaleChangedstays unraised: no code path changes the locale, and the page says so - Osirion.Blazor.Cms.Web:
ContentPage,ContentView,LocalizedContentViewandLandingPage(with aContentPaththat finds nothing) answer HTTP 404 with their not-found block, asArticlePageandDocumentPagedo. They left the status at 200, so a missing page was a soft 404 that search engines could index. A view you render without an item, or with a path that finds none, now makes the whole response a 404 - Osirion.Blazor.Cms.Web:
DocumentPageloads the new document whenContentPathchanges, keeps aPreviousItemorNextItemyou pass and looks up only the one that is missing, and no longer queries the folder on every parameter update. It loaded only whileContentwas null, so an interactive change of the path kept the first document on screen (and its neighbors and current marker), and it replaced both of your neighbors whenever one was null.OsirionContentItemPageBasehas a protectedLoadsItemthat a page turns off when its item is notItem, so calling the base fromDocumentPageruns no unfiltered query - Osirion.Blazor.Cms.Web:
ArticlePage,HomePageandDocumentPagewriteClassonce on their root element. The markup appended it after the class builder, which already added it, so a page withClass="docs"haddocstwice in itsclassattribute - Osirion.Blazor.Core:
OsirionResponsiveShowcaseSectionnames its width buttons "Desktop", "Tablet" and "Mobile", andShowBrowserChromeset tofalseleaves the browser bar out. The three buttons held onlyaria-hiddenicons, so a screen reader announced three unnamed buttons, and the parameter only toggled a class no stylesheet uses, so the bar always showed - Osirion.Blazor.Core:
InfiniteLogoCarousellinks a logo only when itsUrlis a path on the site or an absolute http, https or mailto address, the checkOsirionPricingTableuses. It wrote anyUrlintohref, so ajavascript:address from content ran script on click; all three unsafe forms tried (javascript:,data:, protocol-relative) rendered a live link - Osirion.Blazor.Core:
OsirionSubscriptionCardreports an error, and logs a warning, when nothing stores the address. Without anOnSubscribehandler, or withUseBuiltInEmailServiceon (the email service has no subscription handling and only logged the address), it thanked the visitor for a subscription that did not exist. The example sites drop the footer sign-up form, which had no handler - Osirion.Blazor.Cms.Infrastructure:
FileSystemDirectoryRepositorymatchesExcludePatternsagainst the folder's path underBasePathwith forward slashes, so**/node_modules/**skips a folder on Windows and aBasePaththat sits under abinfolder still loads its directories. It matched the full path, with backslashes on Windows, so the default patterns never matched there, and aBasePathunderbinexcluded every directory. - Osirion.Blazor.Cms.Infrastructure:
ContentBuilder.AddFileSystemregisters the file system provider once whenAddFileSystemContentProvideralready did. It added a secondIContentProviderand default setter, so the provider registry held two providers with one id. - Osirion.Blazor.Cms.Infrastructure: a page whose front matter has no
titleloads, titled with its file name, and a folder's_index.mdtakes the folder's name. The empty title reachedContentItem.SetTitle, which threw, so the page was not loaded and only a log entry showed it. - Osirion.Blazor.Cms.Admin: the SEO and social forms offer a "Default" option, with an empty value, for the robots directive and the Open Graph type, and the metadata preview leaves the robots and
og:typetags out when the value is empty. Since the renderer decides an empty value, the selects had no option for it, so they showed "Index, Follow" and "Article" for a value that was empty, and the preview printedcontent="". - Osirion.Blazor.Cms.Infrastructure: a page without
datefrom the GitHub provider takes the date of its file's last commit, andIGitHubApiClienthas a newGetLastCommitDateAsync(a default interface member that returnsnull, so other implementations keep compiling). The provider gave such a page the time of the load, so it moved on every restart. The date costs one more request per undated file, made only when the file is new or changed, since an unchanged file keeps its cached item. - Osirion.Blazor.Core:
OsirionMetricCardappliesClassandStyleto its root element, as every other component does; it took onlyCssClass, which still works - Osirion.Blazor.Core:
OsirionContentNotFoundleaves a button out and renders a suggestion without a link when its address is not a local path or an http, https or mailto URL. Ajavascript:address went tohrefunchecked. - Osirion.Blazor.Core:
OsirionSubscriptionCardsubmits whenPrivacyPolicyTextis null or empty, asOsirionContactFormdoes; the checkbox stays hidden and no consent is recorded. The model still required the hidden box, so the card could never pass validation. - Osirion.Blazor.Core:
LocalizationOptions.SupportedLocalesis empty until configured andGetEffectiveSupportedLocales()falls back toen, so a bound list is exactly the configured list. The["en"]default made["en", "sr"]bind asen,en,sr. - Osirion.Blazor.Theming:
OsirionStylestakes aNonceparameter and writes it on its inline script and inlinestyleelements, so a policy withscript-src 'nonce-...'and no'unsafe-inline'keeps the first-paint theme. The elements carried no nonce, soscript-src 'self'blocked the script and the page painted in the default theme untilosirion-styles.jsloaded. - Osirion.Blazor.Core:
OsirionFeatureCard's fixed sizes no longer grow past their column, so a card on a phone shrinks to fit; a medium card was 450 pixels wide whatever its container and made a 375 pixel page scroll sideways - Osirion.Blazor.Core:
OsirionMetricCard's value takes its accent moved a third of the way to the text colour, so every accent keeps the 3:1 large text needs in both themes and under the framework adapters (green on the Bootstrap adapter's light card read 2.78:1, indigo on its dark card 1.83:1). The count-up animation keeps a range's own separator:2-4 daysno longer turns into2en dash4 dayswhile it counts - Osirion.Blazor.Cms.Web:
RobotsTxtMiddlewareanswers/robots.txtwith the item's source text, soUser-agent: *reaches crawlers as plain text. It wroteContentItem.Content, the sanitized HTML (<p>User-agent: *</p>), under atext/plaincontent type, so a robots file kept as Markdown was unreadable as robots rules. - Osirion.Blazor.Cms.Web: the sitemap, RSS and Atom endpoints use
Osirion:Seo:SiteUrlfor absolute addresses whenOsirionFeedOptions.SiteUrlis not set, before falling back to the request. OnlyOsirionFeedOptions.SiteUrlwas read, so a site that set the SEO address still listedhttp://request addresses in its sitemap behind a proxy without forwarded headers. - Osirion.Blazor.Cms.Infrastructure:
FileSystemOptions.ExcludePatternsskips matching files in the content repository, matched against the path underBasePath, andContentPathstands in forContentRootwhen building URLs.FileSystemOptions.PollingIntervalMsandIncludePatterns,GitHubOptions.ExcludePatternsandCacheOptions(Osirion:Cms:Cache) are marked[Obsolete]with what to use instead, because no code reads them: the content repository loaded every file underBasePathincludingnode_modules,ContentPathwas ignored, and the providers keep their own caches so the caching decorator is not registered. They are removed in the next major version. - Osirion.Blazor.Cms.Infrastructure: with the legacy key
Osirion:Cms:FileSystem:Web, the file system provider is registered once.AddCmsauto-registered it for theOsirion:Cms:FileSystemsection andAddOsirionContentregistered it again, so the provider registry held two providers with one id;AddFileSystemContentProvidernow skips the provider and default setter registration when the provider is already there. - Osirion.Blazor.Cms.Web:
AddOsirionContent(Action<IContentBuilder>)builds the service collection at most once: not at all whenIConfigurationis registered as an instance, and once when it comes from a factory, as it does in every ASP.NET Core host. It built it twice, which created extra copies of the singletons registered so far - Osirion.Blazor.Cms.Infrastructure:
UseGitHubWebhookthrows at startup when no GitHub provider is configured, with a message that names the missing provider and where to add it, andAddGitHubWebhookAndPollingregisters the API client factory the webhook handler needs. With only the file system provider the scopedIGitHubWebhookHandlercould not resolveIGitHubApiClientFactory: a Development host failed scope validation with an unrelated error, and a Production host answered every request with 500. - Osirion.Blazor.Cms.Infrastructure: each GitHub provider under
Osirion:Cms:Web:GitHub:<Name>orOsirion:Cms:Admin:GitHub:<Name>polls with its ownEnablePollingandPollingIntervalSeconds, andGitHubPollingServicetakes the new optionalGitHubPollingTargetlist. The service readIOptions<GitHubOptions>, which only the single-provider path binds, so a named provider polled with the defaults (EnablePollingfalse, 86400 seconds, no owner) and never refreshed its cache. - Osirion.Blazor.Cms.Infrastructure:
IsDefault: trueon a named GitHub provider withoutProviderIdmakes that provider the default. The default setter was registered undergithub-{name}-{owner}-{repo}while the provider reportsgithub-{owner}-{repo}, so the ids never matched and the first registered provider stayed the default; settingProviderIdwas the only workaround. - Osirion.Blazor.Cms.Web: the
ContentListpagination is anavnamed "Pagination", its previous and next links are named "Previous page" and "Next page" with their icons hidden from assistive technology, and the current page carriesaria-current="page". The two icon links had no accessible name, so axe failed every paged list and search page - Osirion.Blazor.Cms.Web:
TableOfContentsfinds a heading whose text runs over several lines, reads itsidwherever it sits among the attributes, and ends its heading search after one second, with an empty outline and a logged warning. The pattern had noSingleline, so such a heading was missing from the outline, theidwas read only as the first attribute, so the link pointed at an id made from the text instead of the heading's own, and the pattern had no match timeout, so a 750 KB input of unclosed headings had not finished after 200 seconds - Osirion.Blazor.Cms.Web:
SearchBoxnames its field by its visually hiddenlabel(newLabelparameter, "Search" by default) and gives the input a stableid(newIdparameter; by default derived fromActionUrlandQueryParameterName). It setaria-labelto the placeholder, which overrode the label, and built the id fromGetHashCode(), which differs between instances and processes, so the id of a prerendered box did not match the interactive one - Osirion.Blazor.Cms.Web:
TagCloudandOsirionContentNavigationkeep what they load in private fields and leave their parameters untouched.TagCloudwrote the loaded list intoTags, so a later change ofMaxTagsorSortByCountwas ignored and the first list stayed;OsirionContentNavigationwrote the looked-up neighbours intoPreviousItemandNextItem, so afterItemchanged the old neighbours stayed and the lookup was skipped - Osirion.Blazor.Cms.Web:
DirectoryNavigationaddsClassto itsnav, and it andLocalizedNavigationtake anAriaLabelparameter for thenav("Directory navigation"and"Localized navigation"by default).DirectoryNavigationbuilt its class list withoutClass, so extra classes never reached the element, and both navs had no accessible name for a screen reader's landmark list - Osirion.Blazor.Cms.Web:
LocalizedNavigationshows the top-level directories of the locale, reaches the rest throughChildren, leaves a directory's_index.mdout of its item list, links to the item's URL and the directory's URL by default, and writes a click handler only when its callback is set. It rendered every directory of a locale at every depth as one list, so the docs site'sdocs/packagesappeared at the top level and again underdocs, listed each_index.mdas a page under its directory's title, linked to/docs/intro.mdand/en/docs, and always wroteonclick:preventDefault, which stopped every link from navigating in an interactive render mode. The locale selector still does not render: the content provider has no API that returns the available locales - Osirion.Blazor.Core, Cms.Web, Cms.Core and Cms.Admin: text and links take
--osirion-link-color(and--osirion-link-hover-coloron hover) instead of--osirion-action-primary, a button fill that the themes, palettes and adapters only hold to contrast under the button's own label. Under the Bootstrap adapter in the dark theme a read more link read 2.83:1 and the not-found page's links 3.47:1; every theme and palette keeps the link colour at 4.5:1 on the page. 74 rules in 31 stylesheets changed; the button colour stays the fallback - Osirion.Blazor.Cms.Web: on screens 768 pixels wide or less,
DocumentPagefolds its side navigation behind a "Documentation menu" button (SideNavigationToggleText), so the page starts with its content. The whole navigation came first, and a reader on a phone scrolled past every link of the documentation to reach the page. The button is a labelled checkbox and needs no script - Osirion.Blazor.Theming: in the dark theme the active side navigation item reads 4.99:1 under the Bootstrap adapter and 4.68:1 under the MudBlazor adapter, from 4.39:1 and 3.86:1. Their dark
--osirion-color-primary-700is lighter:#9ec5fe(Bootstrap's blue-200) and#9a92ed. A test now holds every adapter's active pair to 4.5:1 in both themes - Osirion.Blazor.Cms.Web:
ContentBreadcrumbs(and with itDocumentPageandArticlePage) leaves out the locale folder of localized content and links each folder at the address its_index.mddeclares, or at its path without the locale. It listedcontent/enas a crumb ("Home / en / Components") and linked folders at/en/components, which no page answers.DirectoryNavigationbuilds its links the same way: a permalink that starts with a slash gave//docs, which a browser reads as a host, and a folder without one linked to the site root - Osirion.Blazor.Cms.Web: a category, tag or author page for a name no item uses answers HTTP 404 instead of 200 with an empty list, so search engines do not index
/tag/<anything>as a page.ContentListPagedoes this on its own; a page that composesContentListturns on the newNotFoundWhenFilterMatchesNothing. A directory with nothing in it and a failed load are not reported as not found - Osirion.Blazor.Core: in rendered Markdown, only a link whose address starts with
http:,https:or//carries the external-link icon and the room for it. Every text link had it, so a link to/blog/another-postlooked as if it left the site - Osirion.Blazor.Cms.Domain:
SupportedLocalesin appsettings replaces the default list instead of adding to it.ContentProviderOptions.SupportedLocalesstarted as["en"]and configuration binding appends to an existing list, so["en", "sr"]becameen,en,srand["sr"]still kepten. The list now starts empty, andGetEffectiveSupportedLocales(), which the providers read, gives["en"]while none is configured; a list assigned as empty still accepts any language code shaped folder - Osirion.Blazor.Cms.Infrastructure: a folder's
_index.mdwithouttitlekeeps the folder name and applies its other keys.DirectoryMetadataProcessorset the name from the empty title,SetNamethrew, and the description, order, locale and permalink were skipped, with only an error in the log - Osirion.Blazor.Cms.Infrastructure:
UrlGenerator.GenerateUrltakes a file for a folder index only when its name is exactly_index.md(any case). It tested whether the whole path contained_index.md, somisc/my_index.mdgot the addressmisc, the same as the folder's own page, instead ofmisc/my-index - Osirion.Blazor.Cms.Infrastructure: a page with front matter and no
dateis dated from its source, a file's creation time, the same as a page with an unparseabledate.FrontMatter.Datedefaulted toDateTime.Now, so every undated page took the day it was loaded: it jumped to the top of newest-first lists, its feed date changed on every restart, and its sitemaplastmodchanged with it unlesslast_modifiedwas set.FrontMatter.Dateis now empty by default (FrontMatter.Create(string)still dates new content today). The GitHub provider has no file date and still uses the load time - Osirion.Blazor.Cms.Infrastructure: a file without
langtakes the locale of its folder, or the provider'sDefaultLocale, and a folder's_index.mdwithoutlangdoes the same; an explicitlangstill wins.FrontMatter.Langdefaulted toen, sosr/blog/hello.mdwithoutlangwas read as English, its translations lookup threw on the duplicateenkey and came back empty, and its folder was filtered out of its own language.GetContentTranslationsAsyncalso keeps the first item per locale and logs a warning once when two share one - Osirion.Blazor.Cms.Infrastructure: a content item with
permalinkfront matter is served at that address, and the item, sitemap, feeds and redirects all use it.UrlGenerator.GenerateUrlignored the value, so a page withpermalink: /custom/placestayed at its folder and slug address; a leading slash is optional, no language folder is added, and an_index.mdkeeps its folder address.FrontMatter.Permalinkno longer documents placeholders such as:year, which were never expanded - Osirion.Blazor.Cms.Infrastructure: a
ContentQueryfor one directory matches it by folder (guides), path (docs/guides) or display name. It compared the display name only, which a folder's_index.mdtitle replaces, soDocumentPage, which asks by path, showed an empty side navigation for such a folder, and nested folders never matched by path - Osirion.Blazor.Theming and Cms: active and hover states are readable in the dark theme. Eleven components read
--osirion-background-activeor--osirion-background-hover, which no theme defined, so their light fallbacks stayed in the dark theme under light text. The themes now define--osirion-background-hover; tinted active items (categories, languages, the document tree, the back link) use the theme's active-item pair, and solid ones (the current page number, the selected tag) use the button colours - Osirion.Blazor.Navigation: the tint behind the active menu item is 6% of the accent, not 12%. With the light theme's link colour the item read at 4.37:1; it now reads at 4.77:1. A site that sets
--osirion-menu-active-backgroundis unchanged - Osirion.Blazor.Theming: the Bootstrap adapter maps
--osirion-text-tertiaryto--bs-secondary-color. Bootstrap's tertiary colour is 50% opaque and made labels such as an article's Previous and Next read at 3.12:1 - Osirion.Blazor.Core:
OsirionContactFormreports an error, and logs a warning, when nothing can deliver the message. With neither anOnSubmithandler norUseBuiltInEmailServiceit told the visitor the message was sent, and the message was lost.ContactPagepasses itsOnSubmitthrough, so the same holds there - Osirion.Blazor.Core: a site without a CSS framework (
Framework: None) gets its page painted in the theme's colours and font. No shipped stylesheet styledbody, so in the dark theme light text sat on the browser's white page (1.1:1), and text outside Osirion's components fell back to the browser's serif font. The rule applies only whileOsirionStylesmarks the pagedata-osirion-framework="None"and has zero specificity, so a framework's or a site's ownbodyrule still wins - Osirion.Blazor.Cms.Web:
DocumentPage's active side navigation item uses the theme's active-item colours (--osirion-nav-active-texton--osirion-nav-active-background). It read--osirion-background-active, which no theme defines, so the dark theme showed light blue text on a light blue box (2.21:1) - Osirion.Blazor.Cms.Web:
DocumentPagerenders its body throughContentRendererandOsirionHtmlRenderer, asArticlePagedoes, so code is highlighted and has a copy button, and code blocks and tables scroll and can be reached from the keyboard. It wrote the HTML itself, which carries no scoped-style attribute, so none of its body styles applied: a long line of code made the page wider than a phone screen and nothing could scroll it. Its grid columns can now also shrink below their content's width - Osirion.Blazor.Cms.Web:
ContentBreadcrumbsrenders its directory links in markup, so they carry the component's styles; built in code, they showed the browser's default link colour (1.88:1 in the dark theme) - Osirion.Blazor.Core: a
HeroSectionwithout a side image keeps its text in the page's column. Its content is placed bygrid-area: content, which the grid did not define, so the browser added implicit columns and the text sat in the right half of the page (also in anArticlePagehero for a post without a cover) - Osirion.Blazor.Cms.Web: the feed link under
ContentListPagehas a 24 pixel target (WCAG 2.2, 2.5.8); it stands alone, not in a sentence, and was 15 pixels tall
[4.1.0] - 2026-10-17
Changed
- All packages: the repository moved to github.com/demonjic/Osirion.Blazor, which the package metadata and Source Link now name, and every package README was corrected against the code: their examples used APIs and parameters that do not exist
- All packages: the Microsoft.AspNetCore and Microsoft.Extensions dependencies move to the October servicing releases, 9.0.21 for net9.0 and 10.0.13 for net10.0, and AngleSharp to 1.8.5
- Osirion.Blazor.Cms.Infrastructure: a file's write time no longer stands in for a content item's last-modified date. Without
last_modifiedin its front matter, an item was last modified when it was created (itsdate). A fresh checkout gives every file the time of the checkout, which reacheddateModified,article:modified_timeand the sitemap'slastmod, often hours before the published date.FileSystemOptions.UseFileWriteTimeAsLastModifiedturns the old behaviour back on where files are edited in place
Added
- Osirion.Blazor.Cms.Web:
SeoMetadataOptions.AuthorUrland theAuthorUrlparameter ofSeoMetadataRenderername a page that identifies the author, such as/about, as the author'surlin every article and posting - Osirion.Blazor.Cms.Web:
ItemTemplateonContentList,ContentListPageandOsirionContentListSectionrenders each item through the host's markup, in anol.osirion-content-index, instead of the built-in cards. Paging, the pageh1, feeds and structured data are unchanged - Osirion.Blazor.Cms.Web:
ArticlePage.HeaderContentreplaces the hero with the host's article header - Osirion.Blazor:
Osirion:Site:CopyrightandOsirion:Site:SocialTitlereach the footerOsirionSiteLayoutrenders - Osirion.Blazor.Cms.Web:
TableOfContentsmarks the section being read as the page scrolls (osirion-toc-link-activeandaria-current="location"), through a JS component the page loads only when it lists headings;TrackActiveHeading="false"turns it off.AddOsirionContentregisters the JS component scope when the host has not - Osirion.Blazor.Theming: named palettes (
ThemePalette: Osirion, Commit, Editor, Studio, StudioBlue, Contrast), each with a light and a dark variant, set withThemingOptions.DefaultPaletteorUsePalette, kept per visitor in theosirion-preferred-palettecookie and applied byOsirionStylesbefore the first paint and after enhanced navigation. Every variant reaches WCAG 2.2 AA for text, links, buttons, status messages and code (Contrast: 7:1) and 3:1 for focus, checked by tests. Code blocks follow the palette.Framework, the default, changes nothing. Colours inspired by GitHub Primer and Visual Studio Code (MIT; notices inTHIRD-PARTY-NOTICES.md), generated bytools/Osirion.Blazor.Palettes - Osirion.Blazor.Theming:
ThemePicker, a labelled native list from which visitors pick a palette. The choice applies at once, is kept in theosirion-preferred-palettecookie and survives enhanced navigation and static exports;Palettes,LabelandFrameworkLabelshape the list. No interactive render mode, inline script or event handler attribute is needed - Osirion.Blazor.Theming:
IThemeService.CurrentPaletteandSetPalette, with default implementations so existing implementations keep compiling;ThemingOptions.AllowPaletteSelectionlinks the palette stylesheet on every page
Fixed
- Osirion.Blazor.Core: Osirion's own elements use border-box sizing. Without a CSS framework (
Framework: None) they kept content-box, so the footer, the menu and full-width buttons were wider than their container and the page scrolled sideways - Osirion.Blazor.Core: the skip link's address is relative to the document's base instead of absolute, so a page rendered on one host (a static export) works on another; with script running it moves focus to
mainwithout following the link - Osirion.Blazor.Theming:
OsirionStylesno longer linksOsirion.Blazor.Core.bundle.scp.css, which the host's{App}.styles.cssalready imports; every page downloaded it (about 250 KB) twice - Osirion.Blazor.Core:
OsirionFooter's default copyright line no longer uses an em dash - Osirion.Blazor.Cms.Web:
SeoMetadataRendererappliesAllowAiDiscovery,AllowAiTraining,AllowSearchIndexing,EnableGeoOptimization,EnableAeoOptimizationandGenerateMultipleSchemasfromSeoMetadataOptionsunless the page sets the parameter. The component's own default oftruewon over the configuration, soAllowAiTraining: falsestill renderedai-training: allow - Osirion.Blazor.Cms.Web: the
<title>element is no longer cut at 60 characters. The cut took the site name with it ("Title | Site..."); search engines shorten a title link themselves.og:titleandtwitter:titlekeep their lengths - Osirion.Blazor.Cms.Web: the structured data author no longer carries a URL guessed from the name (
/author/{name}), a page that does not exist; it carriesAuthorUrlwhen one is configured and no URL otherwise - Osirion.Blazor.Cms.Web: the publisher logo no longer declares a fixed
widthof 600 andheightof 60, whatever the logo's real size - Osirion.Blazor.Cms.Web:
SeoMetadataRendererno longer preloads the share image. The page does not show it, so browsers warned on every page that a preloaded resource went unused - Osirion.Blazor.Core:
OsirionHtmlRenderergives code blocks and tablestabindex="0". Its styles let wide code, and tables on screens up to 768px, scroll sideways, and a scroll box with nothing focusable inside cannot be scrolled from a keyboard (WCAG 2.1.1; axescrollable-region-focusable). An element with its owntabindexkeeps it;FocusableScrollBlocks="false"turns this off - Osirion.Blazor.Cms.Infrastructure: a
CategoryorTagquery matches a category or tag by its name (ignoring case) or by its slug, exactly. The category filter turned the slug back into a name by replacing hyphens with spaces and then matched part of a name, so a category with a hyphen of its own ("Multi-tenant platforms") listed nothing at its address, and "Dev" listed "Web Dev". A tag with a space is now found from its address too - Osirion.Blazor.Theming: in the dark theme, the active navigation item, danger and info messages reach 4.5:1 (they were 3.43, 3.62 and 4.07):
--osirion-nav-active-text,--osirion-status-danger-textand--osirion-status-info-textmove one step lighter on their scales - Osirion.Blazor.Cms.Infrastructure: a refresh or cold read whose cancellation token is already cancelled throws
OperationCanceledExceptionat once and no longer starts a load. It started one, so the next refresh waited for a second load, and when the load finished first the cancelled call returned as if it had succeeded
[4.0.0] - 2026-10-11
Security
- Osirion.Blazor.Cms.Admin: every routed admin page (
/osirion,/osirion/dashboard,/osirion/content,/osirion/content/edit,/osirion/settings,/osirion/login) requires the authorization policy"Osirion.Cms.Admin"(CmsAdminAuthorization.PolicyName). The default policy requires an authenticated user; a host policy with that name replaces it. These pages, and the repository writes behind them, were open to anonymous visitors - Osirion.Blazor.Cms.Admin: the configured
Osirion:Cms:Admin:Authentication:PersonalAccessTokenno longer signs in every visitor of/osirion/loginand is never sent to the browser. Admin services use it on the server only and never use a content provider'sApiToken - Osirion.Blazor.Cms.Admin: a user's own GitHub token is kept in
ProtectedLocalStorage, encrypted with Data Protection under a purpose bound to the signed-in host user, instead of plain-text localStorage (github_auth_token); plain-text copies left by earlier versions are removed - Osirion.Blazor.Cms.Admin: GitHub OAuth sends a single-use
state, compared in constant time, and a PKCE S256code_challenge, which closes login CSRF through a planted?code=. LoginreturnUrlvalues are limited to local paths, and login errors no longer show exception text - Osirion.Blazor.Cms.Infrastructure: GitHub API clients are no longer shared and send their headers per request, so a token, repository or branch set in one admin session can no longer reach another user or the public site. Cookies are turned off on the GitHub HTTP clients' primary handlers
- Osirion.Blazor.Cms.Infrastructure: GitHub paths, branch names, owner and repository are percent-encoded segment by segment, and empty,
.or..segments are rejected withArgumentExceptionbefore any request is sent.GitHubApiClient.DeleteFileAsyncthrows instead of sending a request when the owner, repository or token is missing - Osirion.Blazor.Cms.Infrastructure: the GitHub webhook (
UseGitHubWebhook,POST /api/github/webhook) verifiesX-Hub-Signature-256(HMAC-SHA256 over the raw body, constant-time comparison) against the providers'WebhookSecret, rejects every delivery while no provider has a secret, refuses bodies over 5 MiB and refreshes only providers whose owner, repository, branch and secret match exactly - Osirion.Blazor.Cms.Infrastructure:
FileSystemContentRepositoryandFileSystemDirectoryRepositoryno longer write, delete, create or move anything outsideFileSystemOptions.BasePath. Osirion.Blazor.Cms.Application:CreateContentCommandValidatorandUpdateContentCommandValidatorreject rooted paths,.,..and empty segments, and control characters - Content HTML is sanitized with an allow-list (HtmlSanitizer on the AngleSharp HTML5 parser) when markdown is rendered or ingested, and again where it is rendered as raw markup (
OsirionHtmlRenderer,ContentView,LocalizedContentView,DocumentPage,MarkdownPreviewand the admin preview). Script, event handler attributes,javascript:,vbscript:anddata:URLs, forms, iframes, inline styles,<style>,svgand thenameattribute are removed;class,id,role,aria-*and table celltext-alignare kept - Front matter JSON-LD is parsed and re-serialized with
<,>and&escaped beforeSeoMetadataRendererwrites it into a script element; invalid JSON-LD is dropped with a warning - Osirion.Blazor.Core: the cookie consent endpoint (
MapOsirionCookieConsent) requires an antiforgery token, which the banner form now carries, and redirects only to local paths - Osirion.Blazor.Cms.Admin: the admin event buses are scoped to one circuit or request. As singletons they delivered one admin's events to every other admin's editor, so opening or creating content in one session could replace another admin's unsaved draft, and authentication and error events crossed sessions
- Osirion.Blazor.Cms.Admin:
AddGitHubAdminServicesturns cookies off on the GitHub HTTP clients itself, through the same registration as Osirion.Blazor.Cms.Infrastructure, instead of relying on another registration method to do it - Osirion.Blazor.Core:
HeroSectionCSS-escapesImageUrlwhen it writes the background image, so a front matterfeatured_image(passed byArticlePage,HomePageandLandingPage) can no longer close theurl()value and add style declarations such as a full-page overlay - Analytics trackers (Clarity, Matomo, GA4, Yandex Metrica) JSON-encode every configured value they write into scripts and attributes; invalid GA4 ids, Yandex counters and tracker or CDN URLs fail at startup with a message naming the setting
- Navigation links (
MenuItem,MenuGroupfooter,MenuPanelLink,MenuImageCard,MenuCallToActionand theMenubrand link) render nohreffor schemes other than relative, http, https, mailto and tel, so ajavascript:URL from CMS-driven menu data cannot run ScrollToTop.CustomIconis sanitized; the newIconCssClassis the way to use a custom icon- The CMS query cache key includes every query field, so a draft query (
IncludeUnpublished) and a public query can no longer share a cached result and leak drafts to the public site OsirionHtmlRendererloads the pinned Prism 1.29.0 from cdnjs with Subresource Integrity (newPrismBaseUrlto self-host),OsirionBaseSectionCSS-escapesBackgroundImageUrllikeHeroSection, andOsirionContactInfoSectionvalidates website URLs, encodes phone, email and website, and sanitizes its markup fields
Added
- Osirion.Blazor.Core:
OsirionBreadcrumbs.CurrentPageText, the text of the last breadcrumb - Content HTML sanitizer in Osirion.Blazor.Core:
IHtmlContentSanitizer,HtmlContentSanitizer,HtmlContentSanitizerOptions(AllowedTags,AllowedAttributes,AllowedSchemes,AllowElement,AllowAriaAttributes,AllowTableCellAlignment),AddOsirionHtmlSanitizer,GetHtmlContentSanitizerandJsonLdNormalizer - Osirion.Blazor.Cms.Admin:
CmsAdminAuthorization.PolicyName,AuthGuard.Policy,IGitHubTokenStore,IGitHubOAuthStateStore,GitHubOAuthRequest, and theLoginViewModelmembersStartGitHubLoginAsync,LoginWithGitHubAsync(code, state, cancellationToken),IsServerCredentialConfigured,IsGitHubOAuthConfiguredandGitHubRedirectUri IGitHubTokenProvider.ExchangeCodeForTokenAsync(code, clientId, clientSecret, redirectUri, codeVerifier, cancellationToken)andIMarkdownProcessor.SanitizeHtml(string)- Osirion.Blazor.Core:
LocalUrlinOsirion.Blazor.Core.Utils(IsLocalPath,GetSafeLocalPathandDefaultPath), the local return-URL check used by the cookie consent endpoint and the admin login - Analytics consent gate, off by default:
IAnalyticsBuilder.RequireConsent()orOsirion:Analytics:Consent:RequireConsentrenders trackers only after the visitor accepted the analytics category inOsirionCookieConsent, and GA4 then starts with Google Consent Mode defaults set to denied - Matomo and Yandex Metrica report page views after Blazor enhanced navigation through a static script (
_content/Osirion.Blazor.Analytics/js/osirion-analytics.js) whenAutoTrackPageViewsis on IGitHubApiClient.GetTreeAsync(default implementation returns null),GitHubTree,GitHubRateLimitException,BaseContentRepository.GenerateMarkdownWithFrontMatterAsyncandLastRefreshSucceeded, andUseOsirionCmsAsyncexamples/Osirion.Blazor.Example.Mud: the CMS example site on MudBlazor 9, with a theme that follows Osirion's theme toggle without a round trip.osirion-visually-hiddenutility in the Core stylesheet for text that only screen readers announceexamples/Osirion.Blazor.Example.FluentUI: the CMS example site on Fluent UI Blazor 5, registered in the examples AppHost on port 5298OsirionPageLayoutrenders a skip link to the main content as the first focusable element (WCAG 2.4.1), shown only while it has keyboard focus;ShowSkipLink,SkipLinkTextandMainContentIdconfigure it, and the main element now carries that id andtabindex="-1"- Callouts: GitHub-style alerts in Markdown (
> [!NOTE],[!TIP],[!IMPORTANT],[!WARNING],[!CAUTION]) are now styled, with their icons drawn in CSS so the HTML sanitizer can keep removing inline SVG, and the newOsirionCalloutcomponent (CalloutType) renders the same look from Razor OsirionTabsandOsirionTab: tabs that switch without script (a radio group per tab set), keyboard accessible with the arrow keys, for code samples and other alternative contentOsirionAccordionandOsirionAccordionItemon the native<details>element (optionally one open at a time), andOsirionFaqwithFaqItem, which also writes FAQPage structured data- Osirion.Blazor.Cms.Web:
MapOsirionFeedsmaps/sitemap.xml, an RSS 2.0 feed at/feed.xmland an Atom feed at/atom.xmlfrom the published content (OsirionFeedOptions: site URL, title, directory, locale, item count, extra sitemap paths, URL formatter, cache duration);noindexitems stay out of the sitemap - Osirion.Blazor.Analytics: Plausible and Umami, both cookie-free.
AddPlausiblewithPlausibleTrackerrenders the site's own Plausible script (ScriptUrl, optionalEndpointand tracking switches written toplausible.initonly when set);AddUmamiwithUmamiTrackerrenders one attribute-configured script (SiteId= website ID,ScriptUrlfor self-hosting,HostUrl,Domains,Tag, exclusions, Do Not Track). Both readOsirion:Analytics:PlausibleandOsirion:Analytics:Umami, are validated at startup and honour the consent gate OsirionResponsiveImage: an<img>with intrinsicWidthandHeight(no layout shift),srcsetfromSrcSetorWidthswith aSrcFormatter,Sizes(defaultauto, 100vw), lazy loading with async decoding, aPriorityswitch for the main image (eager,fetchpriority="high") and an optionalCaptionin a<figure>OsirionVideoEmbed(VideoProvider,VideoAspectRatio): a YouTube or Vimeo thumbnail that loads the player only when pressed, privacy-enhanced by default (youtube-nocookie.com, Vimeodnt=1), with a start time, a custom thumbnail and a link to the video when script is off- Author pages:
OsirionAuthorCard(avatar or initials, name linked withrel="author", role, bio,AuthorLinklinks withrel="me"; unsafe URL schemes are not rendered),ContentList.Authorto list one author's items, andContentView.AuthorUrlFormatterto link bylines to the author's page - Series:
seriesandseries_orderfront matter (FrontMatter.Series,FrontMatter.SeriesOrder) andContentSeriesNavigation, which shows "Part 2 of 5", lists the parts and links the previous and next part OsirionPricingTable(PricingPlan) andOsirionFeatureComparison(FeatureComparisonRow,ComparisonCell): plans side by side with a highlighted plan and framework buttons, and an accessible comparison table with check marks, crosses and valuesOsirionSteps(StepItem): numbered "how it works" steps as an ordered list, side by side or stacked with a joining line (Vertical)OsirionAnnouncementBarandMapOsirionAnnouncements: a dismissible site-wide announcement that closes without script (an antiforgery-protected form post sets a per-announcement cookie and redirects back)- Live demos in Markdown content:
AddOsirionContentDemosregisters Razor components (ContentDemoRegistry,ContentDemo), and a fenceddemoblock whose text is a registered key renders the component in its place, with Preview and Code tabs when the demo has source (OsirionContentDemo,OsirionHtmlRenderer.EnableDemos) - Osirion.Blazor.Cms.Web:
UseOsirionRedirectsandredirect_fromfront matter (FrontMatter.RedirectFrom): requests for a page's earlier URLs get a permanent redirect to its current URL, with fixed redirects for pages that are not content (OsirionRedirectOptions) tools/Osirion.Blazor.ApiDocs: generates component parameter tables in Markdown content from the assemblies and their XML docs, between<!-- osirion:parameters Name -->and<!-- /osirion:parameters -->markers, with a--checkmode that fails when a table is out of dateSchemaType.TechArticle,SchemaType.SoftwareApplication(SeoMetadataRenderer.ApplicationCategory, defaultDeveloperApplication),SchemaType.ProfilePage(the author asmainEntity, link tags assameAs,og:typeprofile),SchemaType.AboutPage(the site organization asmainEntity) andSchemaType.ContactPageOsirionReadingProgress: a reading progress bar driven by a CSS scroll-driven animation, with no script; hidden whereanimation-timelineis not supported- Osirion.Blazor.Navigation:
Menu.Actions, a slot after the items for a search box, a theme toggle or a call to action. A horizontal bar aligns it to the end; a collapsed menu shows it at the bottom of the drawer - Osirion.Blazor.Navigation:
MenuCollapseBreakpoint.Large, which collapses a menu below 1200px - Osirion.Blazor.Navigation:
--osirion-menu-*theming variables for the bar height, content width, padding, logo size, colors, radii and shadow - Osirion.Blazor.Navigation: a tap on the dimmed page behind an open drawer closes the drawer
- Osirion.Blazor:
OsirionSiteLayout, the whole site shell from theOsirion:Siteconfiguration section (OsirionSiteOptions,UseSite): announcement, menu with brand, search and theme toggle, footer with link columns, social icons (github,x,linkedin,nuget,rss) and legal links, theme styles, scroll to top and optional cookie consent; use it as the router'sDefaultLayout, or wrap it to replace the header or footer. The options are validated at startup - Osirion.Blazor:
AddOsirionregisters the JS component services (AddJSComponents) thatOsirionPageLayout, the content demos and the video embed need, unless they are registered already OsirionPageHeading: a page title as theh1with an optional sentence below it, for pages without a hero; the example privacy page uses it, so it has anh1(issue 39)- Osirion.Blazor.Cms.Web:
AboutPage(fromabout.md: the title as theh1, the body,ChildContentfor author cards, AboutPage structured data, 404 when missing),ContactPage(anOsirionContactFormwith the contact details under the title, optional Markdown fromcontact.md, ContactPage structured data, works without content) andNotFoundPage(noindex, home link, suggestions, optional search; sets status 404 itself on .NET 9) - Osirion.Blazor.Cms.Web:
OsirionContentItemPageBase, the base of pages that show one content item, split out ofOsirionContentDetailPageBase, which adds navigation and related items - Osirion.Blazor.Cms.Web:
ArticlePage.ShowSeriesNavigation(on by default; renders only for articles withseriesfront matter) andArticlePage.AuthorContent, a slot after the body for an author card - Osirion.Blazor.Cms.Web:
DocumentPagewrites its SEO metadata and TechArticle structured data, lists the pages of its folder in the side navigation when noNavigationItemsare given, and links to the page's source withEditUrlFormatter("Edit this page") - Osirion.Blazor.Cms.Web:
ContentListPage, a whole list page for a blog index, category, tag or author: the title as the page'sh1with a description, paged cards, CollectionPage data, feed links (FeedUrl) and optional category and tag sidebars;ContentList.ShowTitlerenders the same heading for lists composed by hand - Osirion.Blazor.Cms.Web:
ContentRenderer.OmitLeadingTitle, which leaves out the content's opening top-level heading
Changed
OsirionHtmlRenderer.SanitizeHtmlnow defaults totrueand uses the registeredIHtmlContentSanitizerinstead of a regex sanitizer;IMarkdownProcessor.RenderToHtmlandRenderToHtmlAsyncnow defaultsanitizeHtmltotrueand sanitize the rendered HTMLIGitHubApiClientFactory.GetClientandGetDefaultClientreturn a new caller-owned client on every callIAuthenticationServiceandIGitHubAdminServiceare registered as scoped and replace any earlier registrationLoginViewModel's public constructor also takesIGitHubTokenProvider,IGitHubTokenStoreandIGitHubOAuthStateStoreAuthGuardenforces an authorization policy throughAuthorizeView;ShowLoginFormandOnAuthResultstill compile but do nothing- The Cms.Admin event buses (
IEventPublisher,IEventSubscriberandCmsEventMediator) are registered as scoped instead of singleton AddOsirionCmsAdmin, andAddOsirion(configuration)when anOsirion:Cms:Adminsection exists, register authorization and the cascading authentication state- A verified webhook delivery that matches no provider is answered with 202 instead of 400; tags and deleted branches no longer trigger a refresh
AddOsirionCookieConsentregisters the antiforgery services, and the banner posts the current path and query instead of the absolute URL- Content files excluded from markdown rendering (
.txt,.json,.yml,.log,.bak) are stored as sanitized HTML - Osirion.Blazor.Cms.Infrastructure now references Osirion.Blazor.Core, which depends on HtmlSanitizer 9.2.1039 and AngleSharp 1.8.4
- Dependencies updated to their latest releases: .NET 9 packages to 9.0.20, .NET 10 packages to 10.0.12, Markdig 1.4.0, YamlDotNet 18.1.0, Scrutor 7.0.0, FluentValidation 12.2.0 and System.IO.Abstractions 22.3.0
- Osirion.Blazor.Cms.Domain, Cms.Core and Cms.Infrastructure use the
Microsoft.AspNetCore.Appframework reference instead of the legacyMicrosoft.AspNetCore.Http2.x packages and theMicrosoft.Extensions.*packages that framework already provides - Every package ships SourceLink information and a symbol package (
.snupkg); the build is deterministic and treats warnings as errors - Menu accessibility: the mobile toggle is keyboard-focusable and labelled with
aria-controls; submenus follow the disclosure pattern (norole="menu",aria-haspopupor constantaria-expanded="false"); a disabledMenuItemrenders nohref;Menudefault ids are unique per instance (osirion-menu-{orientation}-{suffix}) EnhancedNavigationparameters are nullable, so configuredEnhancedNavigationOptionsapply unless a parameter is set;INavigationService.ScrollToTopAsyncandScrollToElementAsyncare marked[Obsolete]because they only logSeoMetadataRenderer: canonical, og:url and structured-data URLs drop the query string and fragment (keep chosen parameters with the newCanonicalQueryParameters), collection pages declare a canonical, generated FAQ data is off by default (EnableGeneratedFaq) and valid when on, viewport and theme-color tags are opt-in (EmitMobileMetaTags), and dates are written as UTCArticlePage,DocumentPageandHomePageno longer use streaming rendering and answer missing content with HTTP 404 (NavigationManager.NotFound()on .NET 10, the response status on .NET 9)ArticlePagetag badges are rendered locally with stable colours instead of loading images from img.shields.io- The GitHub provider reads the repository with one Git Trees request and downloads only files whose SHA changed (about 80 requests per refresh for 50 files in 10 folders before, 3 to 13 now); branches are paged; rate-limit responses stop a refresh and keep the current content; GitHub clients get a 30-second timeout when the host left the default
UseOsirionCookieConsentno longer sends aPermissions-Policyheader: it used the obsolete Feature-Policy syntax, overwrote the host's header and has nothing to do with cookie consent- Email providers use pooled
IHttpClientFactoryclients with a 30-second timeout and per-request authorization, with one shared mail body builder OsirionComponentBase.IsServerSideis obsolete because it is also true under Interactive Server; useIsInteractive, or the newIsRunningInBrowserfor WebAssembly- Behaviour change: content whose front matter has no
publishedkey is now published (FrontMatter.Publisheddefaults totrue, as in Jekyll and Hugo); before, such files were silently hidden as drafts. Addpublished: falseto keep a file hidden OsirionCookieConsent: the form now has the classosirion-cookie-consent-formand the consent buttons sit in adiv.osirion-cookie-consent-actions; CSS that targeted the form through the old class must target.osirion-cookie-consent-form- The FluentUI adapter reads the Fluent UI Blazor v5 tokens (
--colorNeutralBackground1,--colorBrandBackgroundand so on) and falls back to the v4 tokens, so Osirion components follow a v5 theme - With
Frameworkset toFluentUI,OsirionStyleshands Osirion's theme mode to Fluent UI v5 (Blazor.theme.setThemeMode), so Fluent components followThemeToggle - Osirion.Blazor.Navigation: menus redesigned. The bar keeps the brand, the items and the actions on one line; dropdowns are rounded panels with a soft shadow; a parent item and its chevron highlight as one control; the active item is marked with the accent color instead of a border
- Below the collapse breakpoint the drawer opens as a panel under the bar, over a dimmed page, with 48px rows and submenus that open in place as indented lists
- Nested submenus in a dropdown fly out beside their row (towards the page in a right-aligned bar) instead of stacking over it
- Dropdown and mega panels open on hover, or from the keyboard with Space on the submenu toggle; a closed panel is skipped by Tab instead of opening whenever focus passes its toggle
- A
MenuItemwith a submenu and noHref(orHref="#") renders as the submenu toggle instead of a link to# MenuItem.Iconrenders no icon slot for a name that is not built in; theblogicon is a journal and thedocumenticon a page- Vertical menus open submenus in place instead of as flyouts
- Osirion.Blazor.Theming: the Bootstrap, MudBlazor and Fluent UI adapters set
--osirion-link-colorand--osirion-link-hover-colorfrom the framework's link color (--bs-link-color,--mud-palette-primary,--colorBrandForegroundLink), in light and dark mode
Deprecated
LoginViewModel.LoginWithGitHubAsync(string code)always fails, because a code without its OAuth state cannot be verified; useStartGitHubLoginAsync()andLoginWithGitHubAsync(code, state)IAuthenticationService.AuthenticateWithGitHubAsync(string code)is marked[Obsolete]: it exchanges a code without a state check, a PKCE verifier or a redirect URI. It still works; check the state, exchange the code withIGitHubTokenProviderand callSetAccessTokenAsyncinsteadAuthGuard.ShowLoginForm,AuthGuard.OnAuthResultandAdminPage.ShowLoginFormare marked[Obsolete], because they have no effectDomainEntityBase<TId>and the unused Cms.Admin typesCommandDispatcher(Application.Core),StateManager,ErrorHandlingServiceandCmsAdminStartupService; they will be removed in the next major releaseBaseContentRepository.GenerateMarkdownWithFrontMatter; useGenerateMarkdownWithFrontMatterAsync
Removed
- Osirion.Blazor.Cms.Admin types that nothing in the library used:
ContentRepositoryAdapter,RepositoryAdapterBase,AdminContentRepository,StatePersistenceService,EditableComponent,LoadableComponentBase,ContentValidators,ComponentExtensionsandExceptionHandlingMiddleware - Osirion.Blazor.Navigation:
Menu.razor.scss,Menu.razor.min.cssandcompilerconfig.json, an out-of-date stylesheet source that a Web Compiler run would have compiled overMenu.razor.css
Fixed
- Osirion.Blazor.Theming: the theme toggle threw
NotFoundError("removeChild") when enhanced navigation replaced the page during its fade, and then scrolled the new page to the old page's position. Withprefers-reduced-motion: reducethe theme now changes without the full-screen fade - Osirion.Blazor.Core: the last breadcrumb names the current page with
OsirionBreadcrumbs.CurrentPageText, whichArticlePagesets to the article title; it showed the slug turned into words ("Pre Mvp" for "Pre-MVP") - Osirion.Blazor.Cms.Web: the last
BreadcrumbListitem in the structured data is named with the content title instead of the slug - Osirion.Blazor.Core:
OsirionFootersection titles areh2instead ofh3, so the heading outline no longer skips a level after a page'sh1 - Osirion.Blazor.Navigation: menu items show an outline on keyboard focus; focus looked the same as hover and as the current page (WCAG 2.4.7)
- Osirion.Blazor.Core: headings with an
idin rendered content stop below a sticky header when a table of contents or#fragmentlink scrolls to them (--osirion-anchor-offset, default6rem) /osirion/login,/osirion/settingsand theLogin,AdminLayout,AdminPage,CmsAdminDashboardandCmsLayoutEditorcomponents threw on render ("more than one parameter matching the name 'theme'"); their duplicatestring Themeparameter is removed- The default GitHub OAuth redirect URI is now
{BaseUri}osirion/login; it was/osirion/auth/callback, which has no route, andGitHubTokenProvidersent a hard-codedhttps://localhost:5001/auth/callback IGitHubAdminServicecould not be constructed from the registrations ofAddGitHubProvidersFromConfigurationandAddGitHubContentProvider- GitHub files and folders whose names contain
#,?,%,+or&now load - Content ingestion kept
OriginalMarkdownnull, so a save could write a lossy conversion of the rendered HTML back to the repository - Webhook deliveries for branch names that contain
/now refresh the matching provider MarkdownPreviewno longer renders and sanitizes the whole document on every render (every scroll event), and shows a fixed message instead of exception text when rendering fails- The cookie consent Customize redirect adds its flag correctly to URLs that already have a query string
- Signing out from the admin also removes the stored GitHub token
ContentView,LocalizedContentViewandDocumentPagesanitize their content once per content change instead of on every render- Osirion.Blazor.Core references AngleSharp directly. An application that references the Osirion projects from source (a
ProjectReference) resolved HtmlSanitizer's lower AngleSharp version, and every sanitizer call failed withFileNotFoundExceptionfor AngleSharp 1.7.3; package consumers were not affected IThemeServiceis registered per scope instead of as a singleton. One user's theme choice was served to other users and re-rendered every user'sOsirionStylesandThemeToggleOsirionStylesnow unsubscribes from theme changes when it is disposed; every rendered instance used to stay in memory for the life of the applicationScrollToTopManageris registered per scope instead of as a singleton, so changing it for one user no longer changes the button for every user, andScrollToTopProviderre-renders through its dispatcher- Each GitHub content provider and the FileSystem provider now keep their own directory cache; with several providers, one provider could return another provider's directories
- The admin
Dashboardpage now unsubscribes fromCmsStatewhen it is disposed - The FileSystem provider could not be resolved from DI because
System.IO.Abstractions.IFileSystemwas never registered; it is now registered withTryAdd, so a host's own registration wins FileSystemContentRepositoryreturned nothing until a refresh, because its lazy load treated the empty cache as loadedFileSystemDirectoryRepositoryfound no directories (it scanned the content root as a directory and dropped every nested directory as a circular reference), andMoveAsyncnever checked whether the destination exists- Custom directory metadata is written to
_index.mdagain; it was dropped on every save.DirectoryItem.CustomMetadataexposes the values, and only plain keys (letters, digits,_,-) are written DirectoryCacheManager.InvalidateCacheAsyncandContentCacheManagerdid nothing: invalidation never forced a reload and the content cache never returned a hitExceptionHandlingContentProviderDecoratorrejects a null inner provider and no longer wraps aContentProviderExceptionin another oneCreateDirectoryCommandHandlerthrewSystem.IO.DirectoryNotFoundExceptioninstead of the domain exception for a missing parentDirectoryItem.AddChildthrew for a child already under the directory, which also brokeCloneWithChildrenLocalizationInfo.GetBestMatchingLocalereturns the declared locale code instead of the requested casing, andLocalizationOptions.SetDefaultCultureadds a culture that is not yet supported instead of ignoring itAddOsirionGitHubContentProvider(Action<GitHubOptions>)ignored its configuration- CMS admin: a title ending in
.mdproducedtitlemd.md, a new post in a subfolder was saved with a backslash in its path on Windows, andErrorHandlingServicehandled and logged each error twice LocalizedContentViewthrewKeyNotFoundExceptionfor any content with more than one translation, soContentPagefailed to render translated content; translation links now point to the right locale- The
OsirionImageGalleryscript did not parse, so the lightbox never opened - Commands without a FluentValidation validator, such as
DeleteContentCommand, could not be dispatched: resolving the open-generic validator adapter threw. Adapters are now registered only for validated commands FrontMatter.ClonedroppedId,Order,PermalinkandLangand sharedSeoProperties, andSeoMetadata.ClonedroppedImage,LangandMetaTags, so editing metadata in the admin reset them (a translated post was saved aslang: en)ContentItem.IsPublishedis now a view ofStatus. The two disagreed, so content created throughCreateContentCommandwas saved as a draftCreateContentCommandandUpdateContentCommandkeep their markdown as the original source, so saving no longer converts the content back from HTML- Front matter is found line by line: files with LF line endings lost the first character of their body (often the
#of a heading),---inside a value ended the front matter early, and a file containing only---threw IOsirionBuilder.UseCmsAdmin(Action<ICmsAdminBuilder>)registered nothing; it now registers the admin services and applies the callback- The markdown editor and preview wrote the scroll position with the current culture, which broke scroll sync in cultures with a decimal comma
- Content item ids, default localization ids, FileSystem directory ids and the default FileSystem provider id came from
string.GetHashCode(), which is randomized per process, so they changed on every restart, differed between server instances and could collide. They are now SHA-256 based and stable; see MIGRATION for translations saved through the admin - GA4 outbound link tracking no longer blocks navigation (links did nothing when gtag was blocked) and leaves new-tab, modified and
mailto:/tel:clicks alone - GA4
SendPageView=falseis honoured - Tracker component parameters (
Enabled,SiteId,MeasurementId,DebugMode,CounterId,WebVisor,ClickMap,TrackLinks,AccurateTrackBounce) were ignored; they now override the configured options for that render - Yandex Metrica options bind from
Osirion:Analytics:YandexMetricalike the other providers (the oldYandexMetricakey is still read), and each analytics provider is registered once withIAnalyticsProviderreturning the same instance MenuItemrenderstargetandrel="noopener noreferrer"on the link instead of puttingrelon the wrapper; its active state no longer overwrites theIsActiveparameter, follows navigation when interactive, ignores query and fragment, andPrefixmatches on a segment boundaryMenuandMenuGroupkeep their classes, styles and ids when a parent renders againScrollToTopOptions.Enabledis honoured, the scroll listener is registered once, and the button id is unique per instanceSeoMetadataRendererrecomputes when its parameters change; the Event schema no longer invents an end date;keywordsandarticle:sectionare no longer duplicated; malformed hreflang entries are skipped instead of throwing; the article check ignores caseContentListpagination keeps the search query and other parameters, navigates on click, clamps invalid page numbers and fetches the list onceContentView,ContentPageand the detail pages reload when their path, url or slug changes instead of keeping the previous item, andContentPageshare links use the real page URL- Tag and category URLs come from one invariant, URL-encoded slug helper (
C#no longer turns into a fragment), andContentPagepasses its locale and URL formatters toLocalizedContentView - Cms.Web components log failures through
ILoggerinstead of the console or empty catch blocks;CategoriesListandTagCloudfetch once;DocumentPagewrites a valid schema.orgitemtype DirectoryItemandContentItemkeep the directory tree consistent from both sides: moving a child no longer lists it under two parents, andSetParentaccepts moving a directory up to a higher ancestorGenerateSlugstrips diacritics and transliterates Serbian Latin and Serbian and Russian Cyrillic; titles with no Latin form get a stable short hash instead ofuntitled. Stored slugs are unchanged- Entity equality compares the runtime type, and entities without an id are equal only to themselves
DomainEventDispatcherruns handlers registered for the event's concrete type- CMS admin:
AddOsirionCmsAdmin(Action<...>)no longer builds a second service provider; the edit page loads a file once per path change; background failures are logged; error messages shown to users no longer contain exception text; view models unsubscribe from shared state when disposed - CMS admin: the GitHub sign-in is restored on any admin page after a reload when the admin layout renders interactively
- Refreshing a CMS cache cleared the host application's whole memory cache (
Compact(1.0)); only this library's entries are evicted now, includingIContentCacheService.ClearAsyncandRemoveByPrefixAsync - Cold-start readers waited for nothing and got empty results; they now await the first load, a refresh requested during a refresh runs once more afterwards, and a failed refresh keeps the previous content instead of emptying it
- The fallback front matter writer escapes
id,slug,lang,featured_imageand control characters, so a value can no longer add YAML keys - Carousels run their animation work only while visible and animating, no longer block vertical page scrolling, pause on keyboard focus, get a pause button, show every testimonial under reduced motion and make their looping copies inert
OsirionComponentBaseapplies the theme attribute on every parameter update without changing the caller'sAttributesdictionaryOsirionContactFormno longer tells visitors they were flagged as spam when the email service is missing, and no longer shows placeholder New York contact detailsOsirionCookieConsenthonoursConsentExpiryDays, applies extra attributes once and renders nothing instead of throwing without anHttpContextThemeToggledrops a form that posted to an unmapped endpoint and its fixed ids; the image gallery uses a link trigger that works without JavaScript, traps focus in the viewer and has valid caption markup; the last breadcrumb carriesaria-current="page";OsirionHtmlRendererno longer duplicates the line-numbers class or wraps identical code blocks twice- FileSystem content provider:
AddOsirion(configuration)enabled the provider whenOsirion:Cms:Web:FileSystemexisted but bound its options only fromOsirion:Cms:FileSystem:Web; options are now read fromOsirion:Cms:Web:FileSystem(FileSystemOptions.WebSection), falling back to the old section - A host that registers the FileSystem provider (or a custom provider through
AddContentProvider<T>orContentBuilder.AddProvider<T>) failed at startup in Development with a captive dependency, because the singleton provider registry consumed scoped providers; these providers and the FileSystem repositories are now singletons - The provider marked
IsDefaultis now the default even when the host does not callUseOsirionCms; before, the first registered provider won - FileSystem content items are linked to their directory, so
ContentList Directory="blog"lists FileSystem content - Media queries written with
var(--osirion-breakpoint-*)never matched, because custom properties cannot be used in media conditions:.osirion-containernever widened at the sm to xxl breakpoints, and the cookie consent and breadcrumb mobile rules never applied; they now use the literal breakpoint values - Horizontal menus between 769px and 1439px drew the submenu chevron above the centre of its link; the toggle now sits in flow beside the link, as it already did from 1440px
- A host that registers only the FileSystem provider failed at startup:
AddCmsregisteredLocalStorageServicewithout the browser storage it needs, which only the GitHub registrations added SearchBoxandThemeToggleused Bootstrap'svisually-hiddenclass, so their screen-reader text showed on MudBlazor, FluentUI and Radzen sites; they now useosirion-visually-hidden- Every full page load painted without the framework adapter and in the light theme until
osirion-styles.jsarrived after Blazor started;OsirionStylesnow applies the framework class and the stored theme while the page is parsed OsirionCookieConsentput its actions class on the form around the whole banner, so the buttons never got the actions layout or the mobile rules, and wide framework buttons wrapped under the message- The submenu chevron in the mobile drawer sat above the centre of its link
- The MudBlazor, FluentUI and Radzen adapters lost to the Theming light theme's
:root[data-osirion-theme="light"], so in light mode Osirion components used Osirion's default palette instead of the framework's - Under Fluent UI v5, the Theming stylesheet's
[data-theme]rules matched Fluent'sbody[data-theme]and replaced the adapter's mappings for the whole page in dark mode - The FluentUI adapter's success color read the accent token
ArticlePagecontent touched the screen edges on viewports between 769px and the container width; two padding declarations in its stylesheet were invalid and ignored- On MudBlazor sites, the contact form and subscription card gave plain inputs MudBlazor's internal input classes, which only work inside MudBlazor's own input markup, so the fields rendered without a border; they now use
osirion-input,osirion-selectandosirion-textarea, which the MudBlazor adapter styles from the MudBlazor palette in both themes - The MudBlazor adapter read
--mud-palette-background-grey, which MudBlazor does not define (it writes--mud-palette-background-gray), so secondary backgrounds ignored the theme - Links in rendered content kept the light theme's color in dark mode on MudBlazor, Fluent UI and Radzen sites, because the dark override did not match
data-osirion-theme - Osirion.Blazor.Cms.Web:
DocumentPagewrapped its body in a<main>inside the layout's<main>(two main landmarks), showed the creation date as "Last updated", and linked previous and next to the source file path (/docs/page.md); the body is adiv, the date is the last-modified date, and the links use the pages' URLs - Osirion.Blazor.Cms.Web:
ArticlePageignoredRelatedContentTitle(the section was always titled "Related items") and never renderedCustomFooterContent; both are used now, so the related section is titled "Related Articles" unless set - Osirion.Blazor.Cms.Web:
SchemaType.BlogPostingwrote JSON-LD with"@type": "Article"; it now writesBlogPosting - Osirion.Blazor.Cms.Infrastructure: a content item's
LastModifiedwas the time the content was loaded, because mapping the front matter touched setters that stamp the current time. It is now the front matter'slast_modified, then the file's write time (file system provider), then the publication date; SEOdateModifiedand sorting by last-modified date follow it - Osirion.Blazor.Analytics: a tracker component whose provider is not registered (for example
<ClarityTracker />left in a layout while the Clarity configuration section is absent) failed the whole page with "Cannot provide a value for property '_provider'"; it now renders nothing - Osirion.Blazor.Cms.Web: the featured image in the header of
ContentViewandLocalizedContentViewloaded lazily, which delayed Largest Contentful Paint on article pages; it now loads eagerly withfetchpriority="high"and async decoding. Images in lists stay lazy - Osirion.Blazor.Theming: in Bootstrap dark mode
--osirion-action-primary-textfell through to the dark theme's near-black, so text on the primary color (badges, step numbers, the announcement bar) had a 2.7:1 contrast on a mid-tone primary; the Bootstrap adapter now keeps it white, as Bootstrap's own primary buttons do OsirionFooterinOsirionPageLayout'sFooterslot rendered a<footer>inside the layout's<footer>, two contentinfo landmarks (axelandmark-no-duplicate-contentinfo,landmark-contentinfo-is-top-level). Inside the slot it now renders a<div>with the same classes; on its own it is still a<footer>OsirionCookieConsent: the banner is a region named by its title, so its text is no longer content outside every landmark (axeregion)- Osirion.Blazor.Cms.Web: an
ArticlePagewith nothing related (an article alone in its folder) rendered the list's "404 Page Not Found" block, with a secondh1, as its related items; the section is now left out - Osirion.Blazor.Navigation: the drawer rules for submenus in
Menu.razor.cssnever applied, because CSS isolation scoped them toMenuwhileMenuItemrenders the submenu, and non-collapsible and vertical menus disappeared at 768px and below - Osirion.Blazor.Core:
OsirionContactFormno longer runs past the right edge of the screen on phones - Osirion.Blazor.Core: rendered Markdown tables scroll sideways on screens up to 768px wide and keep words and inline code whole, instead of breaking them a few letters per line
- Osirion.Blazor.Navigation: menu accent text (the active item, mega menu links, group footer links) uses the link color instead of the primary button color, which measured 3.5:1 on the Bootstrap example's dark background; it now measures at least 5.2:1 in both modes on all three adapters
- Osirion.Blazor.Cms.Web:
ContentListleaves out the page it is shown on, so a section's list no longer shows a card for the section's own_index.mdpage - Osirion.Blazor.Cms.Infrastructure: the file system provider serves a folder's
_index.mdas the page at the folder's URL, as the GitHub provider already did, so section overview pages such as/docs/components/coreno longer answer 404 when content comes from local files - Osirion.Blazor.Cms.Web: a page showed its title twice and had two
h1elements when the Markdown opened with# Title.ArticlePage,HomePageandLandingPage(with their hero) andContentView,DocumentPageandLocalizedContentView(under their own title) now leave that opening heading out of the body - Osirion.Blazor.Core: on phones
OsirionFooteropens with the brand, then the links, the left content and the right content, and lines the contact details up with the links. The brand used to land between the contact details and the right column, because the columns had no grid areas and fell into place by auto-placement - Osirion.Blazor.Core: with
EnableHeaderScrollBehavior,OsirionPageLayoutno longer hides the header while the pointer is over it or focus is in it. Scrolling with the pointer on an open dropdown or mega menu carried the panel away with the header and left it floating at the top of the page - Osirion.Blazor.Cms.Web:
ContentList.ContentUrlFormatterandOsirionContentListSection.PostUrlFormatterset the post links. The list section linked every post to itsUrland ignored both. Without a formatter both build the link the same way, from the site root (/blog/postinstead of the relativeblog/postthe section wrote) - Osirion.Blazor.Core:
OsirionTestimonialCarouselhung while rendering whenMaxVisibleTestimonialswas 1 or 2, threwDivideByZeroExceptionfor an emptyCustomTestimonialslist, and appended its loop padding to the host's ownCustomTestimonialslist - Osirion.Blazor.Core:
OsirionSubscriptionCardwithUseBuiltInEmailServiceand no registered email service told the visitor the submission looked like spam; it now reports a server error, asOsirionContactFormdoes - Osirion.Blazor.Core:
HeroSectionaddedClasstwice and glued it to the previous class (hero-dividerlanding) - Osirion.Blazor.Core:
OsirionResponsiveShowcaseSectionignoredClassand unmatched attributes on its root, showed a replacement character instead of×in the viewport dimensions, and renderedaria-pressedwithout atrue/falsevalue - Osirion.Blazor.Core: the cookie consent check threw
NullReferenceExceptionwhen the consent cookie held"Categories": null; it now counts as no consent - Osirion.Blazor.Cms.Infrastructure: a content query with
DirectoryandIncludeSubdirectoriesmatches whole folder names, so"blog"no longer also returns content fromblog2orblog-archive - Osirion.Blazor.Cms.Infrastructure: moving a GitHub directory without a new parent puts it at the top of
ContentPathinstead of the repository root, where the provider no longer loaded it - Osirion.Blazor.Cms.Infrastructure: a directory's
localeandurlare escaped when its_index.mdfront matter is written, so a quote and a line break can no longer add front matter keys - Osirion.Blazor.Cms.Infrastructure: the stale-while-revalidate directory cache keyed
GetByNameAsyncby locale only, so after one lookup every other name in that locale returned the first directory until the entry expired - Osirion.Blazor.Cms.Infrastructure:
ToUrlSlugthrewArgumentOutOfRangeExceptionfor a title without letters or digits ("!!!"); it now returns the untitled slug - Osirion.Blazor.Cms.Web: with
EnableGeneratedFaq, a page titled exactly "Guide to" or "Guía de" failed to render (ArgumentOutOfRangeException); such a title now produces no generated question - Osirion.Blazor.Cms.Web:
HomePagethrewInvalidCastExceptionwhenCategoryUrlFormatterorTagUrlFormatterwas set with the browse section shown - Osirion.Blazor.Cms.Web:
ContentPagegiven aContentItemwithout aPathshowed "Loading content..." forever - Osirion.Blazor.Cms.Web:
LocalizedContentViewtranslation links now carry theosirion-translation-linkclass its stylesheet targets - Osirion.Blazor.Cms.Web:
ContentPageappliesPathReplacePatternwith a 250 ms match timeout and keeps the path unchanged on timeout, so a pattern with catastrophic backtracking can no longer hold a request on a crafted URL - Osirion.Blazor.Cms.Admin: the dashboard is disposed by the renderer and stops listening to the circuit's admin state;
CmsAdminDashboardhad aDisposemethod but did not implementIDisposable - Osirion.Blazor.Cms.Admin: "Auto-generate" in the SEO form keeps the selected schema type instead of resetting it to Article
- Osirion.Blazor.Cms.Admin: the file explorer URL-encodes the path it opens in the editor, so files whose names contain
&,#or+open correctly - Osirion.Blazor.Cms.Admin: the admin layout no longer throws for a signed-in user with an empty user name
- Osirion.Blazor.Cms.Domain: a front matter list containing a lone quote (
tags: a, ") no longer makes the file fail to load - Osirion.Blazor.Theming:
OsirionStylesno longer writes<link rel="stylesheet" href="">when the framework has no adapter, which made browsers request the page again as a stylesheet - Osirion.Blazor.Theming:
ThemeService.ToggleThemereturns the mode actually in effect; with dark mode disabled it reported "dark" while staying light - Osirion.Blazor.Core:
OsirionMetricCardkeeps a trailing dash out of the animated number ("~5-"counted up to a range ending in 0) - Osirion.Blazor.Cms.Domain:
FrontMatter.Create(title, description, DateTime)writes the date asyyyy-MM-ddinstead of the culture's date and time format - Osirion.Blazor.Cms.Infrastructure: GitHub polling refreshes only the provider whose id equals the configured one; providers of other repositories whose id merely contained it were refreshed with the wrong commit, and a configured id without "github" in it was never polled
- Osirion.Blazor.Cms.Infrastructure: content search matches case-insensitively in every culture (under Turkish, "GUIDE" did not find "Guide")
- Osirion.Blazor.Cms.Infrastructure: the markdown pre-sanitizer replaces the whole
javascript:link destination, including nested parentheses, leading whitespace and reference-style definitions; each of its patterns runs with a 250 ms match timeout, so a pathological document cannot hold a request - Osirion.Blazor.Cms.Web: LocalBusiness structured data no longer writes the schema type as the
telephone - Osirion.Blazor.Cms.Web: generated FAQ questions match question words only as whole words, so titles such as "However we ship" are no longer treated as questions
- Osirion.Blazor.Cms.Web: an empty
SchemaTypesarray is treated as not set, soog:type,content-typeand the JSON-LD type agree - Osirion.Blazor.Cms.Admin: admin error messages show generic text instead of exception messages (which could carry GitHub responses or internal URLs); the details stay in the log. Markdown preview failures are reported through the admin error path instead of being swallowed
- Osirion.Blazor.Cms.Admin:
DefaultNavigationlinks to the routed admin pages under/osirion(they pointed to/admin/..., which has no pages), marks only the current page active, and drops the links to pages that do not exist (media, pull requests, branches, users, logs)
Build
- The coverage workflow runs every test project except the Playwright suite and fails when a test fails or is not discovered, instead of uploading a partial report. Coverage settings live in
coverlet.runsettings(generated code and auto-property accessors are not counted) and Codecov targets incodecov.yml(90% for the project and for each change, informational for now) - CRAP scores (threshold 30) are measured by
tools/run-crap.ps1and checked bytools/verify-crap.ps1, usingcrap4dotnetpinned in.config/dotnet-tools.jsonfor complexity and the merged coverlet reports for coverage. The coverage workflow fails when a method exceeds CRAP 30 or a package gains offenders against the last snapshot indocs/crap/, and Codecov blocks a pull request below 90% project or patch coverage
[3.0.1] - 2026-09-24
Added
- Mega menu building blocks in Osirion.Blazor.Navigation:
MenuPanel+MenuPanelLink: high-contrast promotional aside with pill links ("Platform highlights", "Build your business case"); brandable via--osirion-menu-panel-background/--osirion-menu-panel-colorMenuImageCard: linked image card with overlay caption for mega menu asidesMenuItem.SubmenuAsiderender fragment: dedicated right-hand aside column for mega submenusMenuGroup.Description: column subtitle with divider (mega menu header style)MenuGroup.FooterText/FooterHref/FooterTarget: "Explore X →" footer links per group
- bUnit coverage for all new mega menu components and parameters
Changed
- Mega submenus now center under the whole navigation bar on desktop, use a responsive auto-fit column grid with column headers, and keep an invisible hover bridge so the pointer can travel from the trigger into the panel
- Mega submenu asides and group descriptions are hidden in the collapsed drawer and mobile views to keep the drawer scannable
- Collapsed drawer submenu toggle no longer paints an opaque hover background over the row label
Fixed
MenuGroupwithout anIdgenerated a new identifier on every read, so the items container'saria-labelledbypointed at an element that did not exist and the label and items ids changed on every render; the generated identifier is now created once per instance- Navigation tests updated to the current
Menu,MenuItemandMenuGroupmarkup, andNavigationServicelog assertions no longer try to match theLogInformationextension method with NSubstitute
[1.5.0] - 2025-04-21
Added
- Improved service registration with fluent API through
AddOsirionBlazormethod - New
IOsirionBlazorBuilderinterface for better extensibility OsirionStyleOptionsclass separated fromGitHubCmsOptionsfor better separation of concerns- CSS framework integration support for Bootstrap, FluentUI, MudBlazor, and Radzen
- CssFramework enum for specifying framework integration options
- Integration classes for mapping Osirion variables to CSS framework variables
- Updated OsirionStyles component with FrameworkIntegration parameter
- Framework integration documentation with examples for each supported framework
- Comprehensive unit tests for framework integration
Changed
- Refactored service collection extensions for better organization
- Renamed
osirion-cms.csstoosirion.cssfor better clarity on its broader purpose - Enhanced the styling documentation to include framework integration examples
- Updated the README.md and Quick Reference with CSS framework integration examples and fluent API usage
- Styling options moved from
GitHubCmsOptionsto dedicatedOsirionStyleOptionsclass
[1.4.0] - 2025-04-20
Added
- ScrollToTop component for easy navigation back to the top of the page
- ButtonPosition enum for positioning UI elements
- ScrollToTopOptions for DI-based configuration
- Extension methods for registering ScrollToTop configuration
- Comprehensive unit tests for ScrollToTop component
- Updated documentation for navigation components
- CSS variables for styling the ScrollToTop button
Changed
- Enhanced the navigation documentation to include ScrollToTop
- Updated the README.md and Quick Reference with ScrollToTop examples
- Improved the usability of the navigation components
[1.3.0] - 2025-04-20
Added
- GitHub CMS service and components for markdown-based content management
- Support for frontmatter parsing in markdown files
- Content caching with configurable duration
- Category and tag management components
- Full-text search functionality
- Directory-based navigation
- Default styling with customizable CSS variables
- OsirionStyles component for theme customization
- Comprehensive unit tests for GitHub CMS
- Detailed documentation and examples
Changed
- Updated package metadata to include CMS capabilities
- Enhanced documentation with GitHub CMS examples
- Added Markdig dependency for markdown processing
[1.2.0] - 2025-04-20
Added
- ClarityTracker component for Microsoft Clarity analytics integration
- MatomoTracker component for Matomo analytics integration
- TrackerBaseOptions abstract class for common analytics properties
- AnalyticsServiceCollectionExtensions for easy dependency injection configuration
- Comprehensive unit tests for all analytics components
- Detailed documentation for analytics components
- Quick reference guide for component usage
- Migration guide for upgrading
Changed
- Updated package description to include analytics components
- Added analytics-related tags to package metadata
- Improved README with proper usage examples for all components
[1.1.0] - 2025-01-18
Added
- EnhancedNavigationInterceptor component for automatic scroll behavior
- SSR compatibility for all components
- Support for .NET 8, .NET 9, and future versions
- Navigation documentation
- Enhanced examples and usage patterns
Changed
- Improved package metadata and documentation
- Added navigation-related tags to package metadata
[1.0.0] - 2025-01-15
Added
- Core package structure
- Basic documentation and licensing
- Service collection extensions
- Initial project setup