Releases: marutisoftwaresolutions/inkwell
Release list
v1.0.5
Inkwell v1.0.5
Release date: 2026-09-05
Type: Feature release (backward-compatible — existing tenants keep their layout, theme, content and settings)
v1.0.4 gave operators visibility into content that needs work. v1.0.5 turns that into
prevention: the platform now stops broken structured data from shipping, tells you what a post
is missing before an answer engine passes it over, proposes the internal links it should carry, and
shows which AI crawlers are actually reading the site. It also adds the Redirects manager that
v1.0.4's 301/302/410 support was missing, publisher identity settings for the knowledge graph,
an import dry-run, and a set of indexation fixes for category and tag archives.
Highlights
✅ Pre-publish structured-data linter
Every post is validated before it goes live. A post whose structured data would not validate is
saved as a draft instead of published, with the reason shown in the editor.
- Catches the failure that used to be invisible: a malformed FAQ, Key Facts, How-To or roundup
block was silently swallowed by the renderer, so a broken block looked identical to a missing one. - Enforces the rules that matter for search: every roundup entry must link the vendor's real site
with an absolute URL, the internal review path stays inctaUrl, scores sit on the 0–10 scale,
and a first-party product may be featured but never carries a self-authored score. - Advisory issues (a thin FAQ answer, a duplicate label, a single-step How-To, duplicate ranks)
surface as warnings and never block publishing.
📊 Answer-engine readiness score
Every post now carries a 0–100 score for how ready it is to be quoted by an answer engine,
shown in the editor sidebar with a per-signal breakdown and in Admin → Content Health as a
column, a tile and a filter.
- The linter says what is broken; this says what is absent: answer capsule, Key Facts, FAQ,
meta description, section headings, internal links, freshness dates, depth and feature image,
each weighted by how much it contributes to being cited and each carrying concrete advice. - The answer capsule carries the most weight, because it is the passage an engine actually lifts.
- Advisory only — it never blocks a publish. A short note that scores low may be exactly
right, and forcing every post into one shape would produce filler. - Nothing unobservable is scored: no accuracy or authority judgement, no invented ranking prediction.
💬 Answer capsules
A post can now carry a short direct answer, written to be quoted, shown above the article body
and emitted as schema.org abstract on the BlogPosting. Answer engines and featured snippets lift
passages rather than pages; the capsule is the sentence they take. It is visible copy, never hidden
text, so the same words serve the reader. Authored in the editor sidebar with a 40–60 word target;
the linter warns when a capsule is too short to be an answer or too long to be quoted whole. Posts
without one are unaffected.
🔗 Internal-link suggester and Link Audit
- In the editor: the sidebar proposes links in both directions while you write — published
posts this one should link to, and posts that should link back to it — with the reason for each
suggestion (a product this post names but has not linked, or a shared topic). It shows how the
post stands against the two-in / two-out minimum and flags it as an orphan when nothing links to
it. Suggestions are ranked, never applied, and loaded on demand. - Admin → Link Audit scans every published post and page for internal links that dead-end,
point at a URL retired with 410, or take an avoidable redirect hop. Dead ends and retired targets
are separated from redirect hops because they cost different things; multi-hop chains are called
out so the final destination can be linked directly. Generated routes and external links are out
of scope, so the report contains faults rather than false positives. Read-only; Editor and Admin.
↪️ Redirects manager with a triaged 404 worklist
v1.0.4 shipped 301/302/410 redirect rules with no way to manage them except SQL. Admin →
Redirects now lists, creates, edits and removes rules — including the ones written automatically
by post renames and imports, which were previously invisible.
Alongside it, the 404 log becomes a worklist. Most 404s on a public site are vulnerability
scanners, so instead of listing all of them the screen asks for evidence a 404 is reader-facing:
another page links to it, or it is close enough to a real published slug to be a rename, a typo or a
stale external link. Those rank first with a one-click 301 to the suggested post or 410 to
retire it; everything else is collapsed away, since repeated probing is the IP Firewall's business.
Rules that would loop, chain into another redirect, or target an already-retired URL are refused with
the real destination named. Admin-only, anti-forgery protected, every change audited.
🤖 AI-crawler analytics
Admin → AI Crawlers reports which AI answer engines and search crawlers actually fetch the site,
how often, and which pages they read. This traffic passed through the site every day and was
discarded, because bot hits must not pollute human analytics; it is now recorded to its own
CrawlerVisits table, so existing visitor numbers are untouched.
- Shows a daily trend, the pages drawing the most AI attention, and GPTBot, ClaudeBot,
PerplexityBot, Google-Extended, Applebot-Extended, CCBot, Bytespider and the rest, with each
operator's published purpose, separating AI engines from ordinary search crawlers. - Every engine Inkwell can name is listed including those with zero visits — "no AI engine has
read this site" is the most useful finding the page can report. - A user-agent is self-asserted; the report says it shows claimed identity and is never used to
allow or deny a request.
🏛️ Publisher identity for the knowledge graph
Admin → Settings gains the entity fields search and answer engines use to work out who a
publication is: publisher type (Organization or Person), legal name, founder, founding date,
authority profiles (Wikipedia, Wikidata, a company register, an ORCID) and an identity statement
written for answer engines. Profiles join the existing social links in sameAs; the facts become
legalName / founder / foundingDate on the publisher node; the statement is published verbatim in
the Identity section of llms.txt. This matters most on a domain that previously published
something else. Every field is optional and nothing is inferred — an unparseable date or a
non-absolute profile URL is dropped rather than guessed at. Blogs that never open Settings are
unchanged.
🔍 Import dry-run
The import wizard now offers Preview first alongside Start import. It shows exactly what a
WordPress or Ghost file would do before anything is written: posts and pages to be created, slugs
that would be suffixed because of a clash, content that would be overwritten, items skipped and
why, images to be fetched, and internal links that would not resolve. Overwrites are called out
prominently. The preview is read-only by construction — it uses a separate analyzer with no write
path — and it predicts rather than simulates: images are counted, not downloaded.
🗂️ Topic index in llms-full.txt
The AI content index now carries a ## Topics section alongside the category grouping, listing each
tag with its archive URL, article count and the titles it covers. Only topics deep enough to have an
indexable archive are listed, so the file never points a crawler at a noindex page. Generated per
tenant; blogs with no tags are unaffected.
Changed
- Page titles no longer overflow the search-result budget. The
" | Site Name"suffix is now
appended only when the finished title still fits inside roughly 60 characters. Previously it was
added unconditionally, so search engines cut off the part that distinguished the page while keeping
boilerplate they already show as the site name. Titles that already contain the site name, and
category/tag/series archives, are unchanged. - Category and tag links go straight to the canonical archive. Selecting a single category or
tag from the home-feed filters links directly to/category/{slug}or/tag/{slug}instead of a
query-string URL that 301s there. - Multi-tag filter links from an archive no longer mint duplicate URLs. Selecting a second tag
while on/tag/{slug}built a relative link the archive route ignored, silently dropping the
second selection and generating a crawlable near-duplicate per chip. Those links are now rooted at
the home feed, where multi-value filtering actually runs.
Fixed
- Unknown category and tag URLs return 404.
/category/{slug}and/tag/{slug}answered 200
for any slug, rendering an empty archive titled from the raw slug — an unbounded soft-404 surface.
Both routes now check the taxonomy exists first, as/author/{slug}already did. - Empty categories and tags are no longer offered as filters. The public filter chips listed
every category and tag in the database, including those with no published posts. Reader-facing
lists now count published posts only; admin screens still show drafts and empty taxonomies. - Tag archives held up only by drafts no longer reach the sitemap. The indexability test
counted unpublished posts, so a tag with three drafts could be advertised while its archive
answerednoindex. - Archive pagination no longer repeats the facet in the query string. Page 2 of
/tag/{slug}
linked to?page=2&tags={slug}; archive pagers now emit?page=2alone. - **A fresh install from
DBScripts/init.sqlnow produces a complete...
v1.0.4
Inkwell v1.0.4
Release date: 2026-08-15
Type: Feature release (backward-compatible — existing tenants keep their layout, theme, content and settings)
This release makes an Inkwell blog safer to run and easier to maintain. It adds an IP Firewall
that blocks hostile traffic on its own, a Content Health dashboard that turns post maintenance
into a worklist, and a set of indexation controls — 410 retirement, filter-URL canonicalization,
and depth-gated tag archives. It also ships the WordPress & Ghost importer and Series &
collections, plus fixes for HEAD requests and archive pages.
Highlights
🛡️ IP Firewall — automatic blocking
Public sites are probed continuously. Every failing request is now weighed by a threat scorer, and
addresses that cross the threshold block themselves.
- Exploit probes (
.php,wp-admin,.env,.git, path traversal, SQL-injection payloads,
app-server consoles) score 5; a rejected sign-in scores 3; an ordinary 404 scores 1. - At 10 points within 10 minutes (both configurable) the address is blocked — 24 hours on the
first offense, a week on the second, permanently on the third — and further requests get a bare
403 before routing, views or logging run. - Admin → Security shows active blocks with reason and denied-request counts, a live watchlist of
addresses building a score, and the thresholds, allowlist and alert settings. Addresses can be
blocked or allowlisted by hand, and Admin → Error Monitor now records each signature's last
client IP with a one-click Block. - Rules live in a new
IpFirewallRulestable so blocks survive a restart; enforcement reads an
in-memory snapshot, so an ongoing attack costs no per-request database work.
Safe by default: loopback and private ranges are never blocked, signed-in staff are never scored,
search and AI crawlers (Googlebot, Bingbot, GPTBot, ClaudeBot, …) are never blocked for 404s, proxy
headers are ignored unless you declare a trusted proxy, and the firewall fails open if the
database is unreachable. Every rule change is written to the audit trail.
🩺 Content Health dashboard
Admin → Content Health lists published posts that need maintenance: reviews overdue or due within
30 days, posts never verified, posts missing a Key Facts or FAQ block, meta descriptions that are
missing or long enough to truncate in search results, and titles still promising a year that has
passed. Tiles double as filters and every row links to the editor. Read-only — it surfaces work, it
never edits. Available to Editors and Admins.
🔎 Indexation controls
- Retire a URL with 410 Gone. Redirect rules now carry a status code — 301 (default), 302, or
410. A 410 serves a dedicated "no longer available" page and tells search engines the removal
was deliberate, which de-indexes far faster than a 404 that keeps getting re-crawled. - Filter-URL canonicalization. A single-value home-feed filter (
/?tags=x,/?category=y) now
301s to the matching/tag/{slug}or/category/{slug}archive, so filter permutations stop
competing as separate indexed URLs. Multi-value combinations are unchanged, pagination survives the
redirect, and only well-formed slugs redirect so a crafted query string can't become a redirect target. - Tag archives are indexable once they have depth. An archive with at least three posts becomes a
real topic page; thinner ones staynoindex, andsitemap.xmlnow lists only archives that are
actually indexable — it no longer advertisesnoindexURLs.
📥 WordPress & Ghost importer
Migrate an existing blog from Admin → Import: upload a WordPress WXR (.xml) or Ghost
JSON (.json/.zip) export, preview it, choose exactly what to bring in (posts, pages,
categories/tags, images, comments, with a published-only filter and slug-conflict policy), then run a
batched, resumable import with live progress, a per-item exception log and a downloadable .xlsx
report. Images are downloaded (SSRF-guarded), converted to WebP and re-linked; Ghost images
bundled in a .zip are read straight from the archive; old permalinks become redirects; imported HTML
is sanitized. An import can be undone. Admin-only.
📚 Series & collections
Group posts into an ordered reading sequence. Admin → Series creates and orders them; each series
gets a public /series/{slug} page plus a /series directory, member posts show a "Part N of M ·
Previous / Next" banner, and series emit CollectionPage + ordered ItemList + BreadcrumbList
structured data and appear in the sitemap.
🖼️ Shared image pipeline
The automatic WebP conversion introduced for uploads is now a shared service reused by the importer,
so every image entering the media library — uploaded or imported — is optimized through one path.
🐛 Fixes
- HEAD requests no longer return 405. Routing matches methods exactly and every page route is
GET-only, so every HEAD request was refused — includingHEAD /andHEAD /robots.txt, and even
HEAD for a URL that does not exist (405 instead of 404). Uptime monitors, link checkers and some
crawlers rely on HEAD. It now returns exactly what GET would: same status, same headers, no body.
HEAD requests are excluded from page-view analytics and logged as HEAD, not GET. - Category and tag archives render the full listing chrome again. They share the home-feed view
but never populated its filter chips, popular-posts sidebar or the configured layout, so those pages
silently fell back to defaults. All three listing routes now populate the same view data, and an
archive seeds its own filter selection so adding a second filter keeps the first. - Post pages no longer 500 when a post has no categories or tags. The related-posts query emitted
an invalidORDER BY (0); it now falls back to recency ordering.
Upgrade notes
- Schema: the new
IpFirewallRulestable, theRedirects.StatusCodecolumn,ErrorLogs.LastIpAddress,
and theSeries/SeriesPostsandImportJobs/ImportItemstables are created automatically on
startup byMigrationService. Dated scripts inDBScripts/mirror them for manual or production
application — run them withsqlcmd -f 65001(UTF-8; omitting it corrupts em dashes and curly quotes). - The IP Firewall is on by default with conservative settings (10 points / 10 minutes, 24-hour first
block, crawler protection on, proxy headers not trusted). Review it in Admin → Security. If your
site sits behind Cloudflare or another reverse proxy, enable Trust proxy headers there — otherwise
every visitor appears as the proxy's address. - Backward-compatible: existing redirect rules keep serving 301, tag archives that lack depth keep
their previousnoindexbehaviour, and a tenant that never opens the new screens is unaffected. No
settings changes are required.
See CHANGELOG.md for the complete list.
v1.0.2
Inkwell v1.0.2
Release date: 2026-07-29
Type: Feature release (backward-compatible — existing tenants keep their layout, theme, and content)
Inkwell is a free, open-source, self-hosted, multi-tenant blogging platform built on ASP.NET Core (.NET 10) with SQL Server. v1.0.2 is a major step up in SEO / AEO (AI-search) readiness, adds a scored-roundup content system, new layouts, instant indexing, and an error-monitoring subsystem.
Highlights
🔎 SEO & AI-search overhaul
- Rich structured data across the site:
Organization,WebSite+SearchAction,CollectionPage,BlogPosting,BreadcrumbList,FAQPage,HowTo, authorPerson, and roundupItemList/Review/SoftwareApplication. - Per-tenant
llms.txt+ newllms-full.txtfor AI answer engines (ChatGPT, Claude, Perplexity, Gemini), a curated AI-crawler allow-list inrobots.txt, and entity/disambiguation signals. - Author E-E-A-T pages (
/author/{slug}), configurable content language (lang/hreflang/og:locale), image sitemap, sitelinks search box, and Search Console / Bing verification fields. - Performance: precompiled Tailwind, preloaded brand fonts, deferred non-critical scripts, LCP-prioritized images.
⚡ IndexNow instant indexing
Enable in Admin → Settings; publishing or updating a post pings Bing/Yandex/Seznam/Naver to re-crawl within minutes. Ownership key auto-generated and served at the site root. Per-tenant, off by default, best-effort.
🏆 Verdict roundup content system
A scored "Top N Best … Software" format with a dedicated layout, per-entry scores/pros/cons/CTAs, internal-cluster linking, verified outbound vendor links, first-party disclosure, and rich-result-safe ItemList/Review schema.
🎨 New layouts
The Catalog layout (single-column boxed cards, category filter bar, numbered pagination) joins the library, selectable from Admin → Theme with safe fallback for legacy values.
🚨 Error Monitor + alerts
A global exception handler and status-code logger record 4xx/5xx errors grouped by signature with occurrence counts and first/last-seen. Admin → Error Monitor lists them with filters and summary tiles; optional email alerts notify configured recipients the first time a new server error appears.
✍️ Authoring
Key Facts ("At a glance") and How-To step blocks, a live meta-description length helper, smarter related-posts/internal linking, and consistent | Brand page titles.
Upgrade notes
- Schema:
MigrationServiceapplies all required changes on startup (newErrorLogstable;Posts.RoundupJson/KeyFactsJson/HowToJsoncolumns). The dated scripts inDBScripts/mirror these for manual/prod application (sqlcmd -f 65001). - New settings (IndexNow, error-notification recipients, content language, verification codes) live in the settings JSON blob and default to safe/off — no action required.
- Fully backward-compatible: no breaking changes; a tenant that upgrades without touching settings keeps its current appearance and behavior.
See CHANGELOG.md for the complete list.
v1.0.1
What's new in v1.0.1
This release is a major feature expansion over the initial v1.0.0 launch,
adding seven new subsystems, a complete design system, and full audit coverage
across every admin action.
Analytics Dashboard
Built-in page view tracking — no Google Analytics needed.
- Captures every public page hit: path, referrer, UTM parameters (
source,
medium,campaign,term,content), IP hash, user agent, country,
and region. - Traffic source classifier: Organic, Direct, Social, Paid, Email, Referral.
- Admin-only dashboard at
/admin/analytics— daily/weekly trends, top pages,
top referrers, source breakdown. - Authenticated admin visits are never counted.
Audit Trail
Immutable record of every admin write action — accessible only at /admin/audit.
- Covers all 12 admin controllers: Posts, Pages, Comments, Media, Users,
Settings, Theme, Categories, Tags, Newsletter, Subscribers, Redirects. - Before/after JSON snapshots for settings and role changes.
- Filter by entity type, action, user, and date range. CSV export up to 10,000 rows.
- Read-only from the UI — no delete or truncate endpoint is exposed.
Inkwell Theme System
- 10 curated color presets: Warm Cream, Ocean Breeze, Midnight Blue,
Forest Canopy, Obsidian Rose, Terracotta Sun, Slate Professional, Ivory Gold,
Lavender Mist, and Charcoal Modern. - 6+ layout variants: Magazine, Grid, Minimal, Neutral, Classic, Modern —
independently configurable for index, navbar, and footer. - Live CSS variable overrides stored per-owner in
CustomThemeSettings. - All ViewComponents have explicit supported-values guards — unknown DB values
fall back safely, no crashes on upgrade.
Newsletter & Subscribers
- Compose and send newsletters from
/admin/newsletter. - Double opt-in subscriber management with confirm/unsubscribe tokens.
- Label-based member segmentation.
- CSV import/export at
/admin/subscribers.
Redirects Manager
- Rule-based HTTP redirects at
/admin/redirects. - Hit count tracking per rule.
SEO Enhancements
- OG image generation at
/og-image/{slug}for every post. - FAQ schema (JSON-LD) stored per post via
FaqJsoncolumn. LastVerifiedAt/NextReviewAtfields for clinical/medical content review
workflows._SchemaJsonLd.cshtmlstructured data partial on all post pages.
User Profile Enhancements
Credentials,Specialty, andLicenseNumberfields on author profiles —
designed for medical and professional blogging contexts.
Bug Fixes
- PageView tracking was silently failing — fire-and-forget middleware was
creating scopes from the request-scoped DI container (disposed before the async
task ran). Fixed by capturing all values synchronously and creating scopes from
the rootIServiceProvider. - Admin visits were being counted — middleware now checks
context.User.Identity.IsAuthenticatedand skips all authenticated requests.
Database Changes
All changes are applied automatically on startup via MigrationService.
Manual scripts are in DBScripts/ if needed.
New tables: PageViews, Members, Labels, MemberLabels, Newsletters,
Emails, MemberNewsletters, Redirects, Snippets, AuditLogs
New columns: Posts.FaqJson, Posts.LastVerifiedAt, Posts.NextReviewAt,
Users.Credentials, Users.Specialty, Users.LicenseNumber,
Categories.Color, Tags.Color, Media.OriginalFileName,
PageViews.Country, PageViews.Region
Upgrading from v1.0.0
git pull origin main
dotnet run --project Blog.Web