SJS.Permalink 1.0.6

dotnet add package SJS.Permalink --version 1.0.6
                    
NuGet\Install-Package SJS.Permalink -Version 1.0.6
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="SJS.Permalink" Version="1.0.6" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="SJS.Permalink" Version="1.0.6" />
                    
Directory.Packages.props
<PackageReference Include="SJS.Permalink" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add SJS.Permalink --version 1.0.6
                    
#r "nuget: SJS.Permalink, 1.0.6"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package SJS.Permalink@1.0.6
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=SJS.Permalink&version=1.0.6
                    
Install as a Cake Addin
#tool nuget:?package=SJS.Permalink&version=1.0.6
                    
Install as a Cake Tool

WordPress-style media permalinks for the Dynamicweb Files archive: a folder is enabled for permalinks, every file in it can get a permanent URL with a custom slug, and the file behind that URL can be replaced with a different file at any time without the link changing.

https://www.example.com/permalink/product-brochure   →  /Files/Documents/Brochure_2026_v3.pdf

What it contains

Part Location Purpose
Request pipeline Web/PermalinkPipeline.cs, Web/PermalinkMiddleware.cs IPipeline add-in (rank 5000, before the classic frontend) that answers GET/HEAD /{prefix}/{slug}: streams the current file (content type, Content-Disposition: inline; filename=…, ETag/Last-Modified, 304 on conditional requests, Cache-Control: public, max-age=0, must-revalidate) or, in Redirect mode, 302s to the file. Unknown slugs fall through to Dynamicweb's normal 404. …/{slug}.pdf also resolves when the extension matches the current file.
Data Data/ – tables SJS_PermalinkFolder, SJS_Permalink, SJS_PermalinkHistory Enabled folders, permalinks (slug, current file, download name, note, active, hits, last access) and the replacement history. Created automatically on first use.
Rules Services/PermalinkService.cs, Services/SlugHelper.cs, Services/FilePaths.cs Folder enablement is inherited by sub folders. Slugs are lower case a-z 0-9 - _ . with / for sub paths, unique, suggested from the file name (Product Brochure 2026.pdf → product-brochure-2026, -2, -3… when taken). All lookups use an in-memory cache (5 min, invalidated on every change).
Files area integration UI/Injectors/AssetListScreenInjector.cs Injects into Dynamicweb's asset list without modifying it: the folder's Actions menu gets Enable permalinks for this folder / Enable and create permalinks for all files, or – when enabled – Permalinks of this folder, Create permalinks for all files, Disable permalinks for this folder. Each file row in an enabled folder gets Create permalink… / Permalink / replace file… and Open /permalink/…. Sub folder rows get Enable permalinks / Disable permalinks.
Screens UI/Screens/ PermalinkEditScreen (slide-over: slug, URL, active, File selector (general file selector, all file types) to replace the file, download name, note; History tab with hits and replacements), PermalinkListScreen (all permalinks or one folder; edit, open, go to folder, delete; bulk delete), PermalinkSettingsEditScreen (enabled, URL prefix, delivery mode, status).
Categories & tags (v1.1) Data/PermalinkRepository.cs (table SJS_PermalinkCategory, columns PermalinkCategoryId, PermalinkTags), UI/Screens/PermalinkCategoryScreens.cs, UI/Queries/PermalinkFilterProvider.cs Each permalink has one category (managed under Permalinks → Settings → Categories / Manage categories button) and any number of tags (free text). Tags are edited with Dynamicweb's selector component: selected tags appear as chips with a remove button and Select opens a searchable slide-over listing every tag in use; tags that do not exist yet are typed into Add new tags (comma separated, existing tags suggested while typing). Tags are de-duplicated case-insensitively and take the spelling already in use. The permalink list shows both as columns (tags as badges) and offers filters for Category, Tags, Active and File exists; a category's context menu has Show permalinks. Deleting a category leaves its permalinks uncategorized.
Tree UI/Tree/PermalinkNodes.cs Files → Permalinks section (All permalinks, Settings → Categories) and Settings → Areas → Assets → Permalinks.
Settings PermalinkSettings.cs – /Globalsettings/SJS/Permalink/* Enabled (default true), Prefix (default permalink; files and admin are refused), Mode (Serve default / Redirect).

How to use it

  1. Build the solution. The post-build step copies SJS.Permalink.dll to Solmetex/wwwroot/Files/System/AddIns/Local/.
  2. In the backend open Settings → Apps, source Local, and install SJS.Permalink. Bump <Version> in the csproj for every new build.
  3. Restart the site (app pool recycle). The request pipeline is only picked up at startup; the screens appear immediately.
  4. Open Files, go to a folder and choose Actions → Enable permalinks for this folder (or Enable and create permalinks for all files to link every existing file right away). Sub folders inherit the setting.
  5. Right-click a file → Create permalink…. Adjust the suggested slug if wanted and save. Share the URL shown in the editor.
  6. To replace the file: right-click the file (or open Files → Permalinks → All permalinks) → Permalink / replace file…, pick the new file in the File selector (upload it to the archive first with the normal Upload button), optionally add a note, save. The URL stays the same; the replacement is recorded on the History tab. The old file is left in the archive.
  7. Settings (Files → Permalinks → Settings): switch the feature off, change the URL prefix, or switch to redirect mode.

Disabling a folder stops new permalinks from being created there; existing ones keep working until they are deleted. Deleting a permalink never deletes a file.

Behaviour worth knowing

  • Moving, renaming or deleting a file in the Files area does not update its permalinks (Dynamicweb raises no notification for file operations). The permalink then answers 404 with a short text, the list screen shows a warning icon in File exists, and the editor shows a warning; point it at the new location to repair it.
  • The permalink URL is public, like the /Files/... URL of the file itself. Files that must stay private should not be in an enabled folder.
  • Hits are counted asynchronously (Hits, Last opened in the list and editor).
  • Everything can be driven through the Management API with a bearer token, for example:
POST /Admin/Api/SJS.Permalink.UI.Commands.PermalinkFolderEnableCommand   {"FolderPath":"/Files/Documents","Enabled":true,"CreateForExistingFiles":false}
GET  /Admin/Api/SJS.Permalink.UI.Queries.PermalinkByFileQuery?FilePath=/Files/Documents/brochure.pdf
POST /Admin/Api/SJS.Permalink.UI.Commands.PermalinkSaveCommand           {"Model":{"Id":0,"Slug":"product-brochure","FilePath":"/Files/Documents/brochure.pdf","Active":true}}
POST /Admin/Api/SJS.Permalink.UI.Commands.PermalinkSaveCommand           {"Model":{"Id":1,"Slug":"product-brochure","FilePath":"/Files/Documents/brochure-2026.pdf","Note":"2026 edition","Active":true}}
GET  /Admin/Api/SJS.Permalink.UI.Queries.PermalinksQuery?FolderPath=/Files/Documents
GET  /Admin/Api/SJS.Permalink.UI.Queries.PermalinksQuery?Filter=CategoryFilter|in|1,2;TagsFilter|in|uk     (operators: in, ==, <, <=, >, >=)
POST /Admin/Api/SJS.Permalink.UI.Commands.PermalinkSaveCommand           {"Model":{"Id":1,"Slug":"product-brochure","FilePath":"/Files/Documents/brochure.pdf","CategoryId":2,"Tags":["uk"],"NewTags":"2026, print","Active":true}}
POST /Admin/Api/SJS.Permalink.UI.Commands.PermalinkCategorySaveCommand   {"Model":{"Id":0,"Name":"Brochures","Description":"","Sort":10}}
POST /Admin/Api/SJS.Permalink.UI.Commands.PermalinkCategoryDeleteCommand {"Id":2}
POST /Admin/Api/SJS.Permalink.UI.Commands.PermalinkDeleteCommand         {"Id":1}

Implementation notes (Dynamicweb 10.28)

  • Custom frontend URLs from an add-in: implement Dynamicweb.Host.Core.IPipeline and register middleware in RegisterApplicationComponents. Pipelines run ordered by Rank; the classic frontend uses int.MaxValue. Exceptions thrown from RegisterApplicationComponents are swallowed silently by the host, so keep it trivial.
  • Adding actions to a Dynamicweb list screen: ListScreenInjector<AssetListScreen, AssetListDataModel, AssetDataModel> with GetScreenActions() (folder Actions menu) and GetListItemActions(model) (row context menu). ActionGroup.Name must be set, otherwise the entries render disabled. GetListItemActions runs once per row – use the cache, not the database.
  • A NavigationSection with an explicit id builds its tree path from Name but is resolved by Id, so its nodes never load. Leave the id out; the type's full name is then used for both.
  • Icons are static members of Dynamicweb.CoreUI.Icons.Icon (Link, LinkAdd, LinkBroken, ExternalLinkAlt…); invented names fail to compile.
  • Do not bind a file to a SelectedMedia property for documents: Dynamicweb's default editor for it is the media selector, which filters folders to video/audio extensions and shows a PDF folder as empty. Use a string property with SelectorBuilder.CreateFileSelector(value, folder: …, lockedToFolder: false) instead (fixed in 1.0.2).
  • List filters: implement FilterProvider<TRowModel> (auto-discovered for the row model), mark the model properties isFilterable: true, and use FilterOperator.In with MultiValueFilter. Values are applied in memory on the mapped rows: an int property matches when it is in the selection; a List<string> property matches when any element is in the selection (case-sensitive). The URL/query syntax is Filter=Name|in|a,b;Other|==|True.
  • Multi-value pickers: SelectorBuilder.CreateOptionSelector(options, multiselect: true) binds to an IEnumerable<string> model property and renders chips + a searchable slide-over (the same component Dynamicweb uses for user groups etc.). Do not set Hint on its options: the hint is appended to the chip label. Text.PredefinedValues renders a <datalist> for autocomplete. TagCloudList exists but is unused by Dynamicweb itself and its add-action wiring is undocumented.
Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.6 45 11/10/2026
1.0.5 44 11/10/2026

### 1.0.4
- Current release