Load themes at startup (#20)
- Adjust release packaging (#20) - Fix default theme for beta-5 changes (#24) - Remove RethinkDB case fix (cleanup from #21) - Bump versions for next release
This commit is contained in:
parent
99ccdebcc7
commit
4514c4864d
15
build.fsx
15
build.fsx
@ -36,7 +36,7 @@ let zipTheme (name : string) (_ : TargetParameter) =
|
||||
!! $"{path}/**/*"
|
||||
|> Zip.filesAsSpecs path //$"src/{name}-theme"
|
||||
|> Seq.filter (fun (_, name) -> not (name.EndsWith ".zip"))
|
||||
|> Zip.zipSpec $"{releasePath}/{name}.zip"
|
||||
|> Zip.zipSpec $"{releasePath}/{name}-theme.zip"
|
||||
|
||||
/// Publish the project for the given runtime ID
|
||||
let publishFor rid (_ : TargetParameter) =
|
||||
@ -45,11 +45,12 @@ let publishFor rid (_ : TargetParameter) =
|
||||
/// Package published output for the given runtime ID
|
||||
let packageFor (rid : string) (_ : TargetParameter) =
|
||||
let path = $"{projectPath}/bin/Release/net6.0/{rid}/publish"
|
||||
let prodSettings = $"{path}/appsettings.Production.json"
|
||||
if File.exists prodSettings then File.delete prodSettings
|
||||
[ !! $"{path}/**/*"
|
||||
|> Zip.filesAsSpecs path
|
||||
|> Zip.moveToFolder "app"
|
||||
Seq.singleton ($"{releasePath}/admin.zip", "admin.zip")
|
||||
Seq.singleton ($"{releasePath}/default.zip", "default.zip")
|
||||
Seq.singleton ($"{releasePath}/admin-theme.zip", "admin-theme.zip")
|
||||
Seq.singleton ($"{releasePath}/default-theme.zip", "default-theme.zip")
|
||||
]
|
||||
|> Seq.concat
|
||||
|> Zip.zipSpec $"{releasePath}/myWebLog-{version}.{rid}.zip"
|
||||
@ -86,7 +87,7 @@ Target.create "RepackageLinux" (fun _ ->
|
||||
Shell.mkdir workDir
|
||||
Zip.unzip workDir zipArchive
|
||||
Shell.cd workDir
|
||||
sh "chmod" [ "+x"; "app/MyWebLog" ]
|
||||
sh "chmod" [ "+x"; "./MyWebLog" ]
|
||||
sh "tar" [ "cfj"; $"../myWebLog-{version}.linux-x64.tar.bz2"; "." ]
|
||||
Shell.cd "../.."
|
||||
Shell.rm zipArchive
|
||||
@ -96,8 +97,8 @@ Target.create "RepackageLinux" (fun _ ->
|
||||
Target.create "All" ignore
|
||||
|
||||
Target.create "RemoveThemeArchives" (fun _ ->
|
||||
Shell.rm $"{releasePath}/admin.zip"
|
||||
Shell.rm $"{releasePath}/default.zip"
|
||||
Shell.rm $"{releasePath}/admin-theme.zip"
|
||||
Shell.rm $"{releasePath}/default-theme.zip"
|
||||
)
|
||||
|
||||
Target.create "CI" ignore
|
||||
|
@ -1,171 +0,0 @@
|
||||
|
||||
// Category
|
||||
r.db('myWebLog').table('Category').map({
|
||||
Description: r.row('description'),
|
||||
Id: r.row('id'),
|
||||
Name: r.row('name'),
|
||||
ParentId: r.row('parentId'),
|
||||
Slug: r.row('slug'),
|
||||
WebLogId: r.row('webLogId')
|
||||
})
|
||||
|
||||
// Page
|
||||
r.db('myWebLog').table('Page').map({
|
||||
AuthorId: r.row('authorId'),
|
||||
Id: r.row('id'),
|
||||
Metadata: r.row('metadata').map(function (meta) {
|
||||
return { Name: meta('name'), Value: meta('value') }
|
||||
}),
|
||||
Permalink: r.row('permalink'),
|
||||
PriorPermalinks: r.row('priorPermalinks'),
|
||||
PublishedOn: r.row('publishedOn'),
|
||||
Revisions: r.row('revisions').map(function (rev) {
|
||||
return {
|
||||
AsOf: rev('asOf'),
|
||||
Text: rev('text')
|
||||
}
|
||||
}),
|
||||
IsInPageList: r.row('showInPageList'),
|
||||
Template: r.row('template'),
|
||||
Text: r.row('text'),
|
||||
Title: r.row('title'),
|
||||
UpdatedOn: r.row('updatedOn'),
|
||||
WebLogId: r.row('webLogId')
|
||||
})
|
||||
|
||||
// Post
|
||||
r.db('myWebLog').table('Post').map({
|
||||
AuthorId: r.row('authorId'),
|
||||
CategoryIds: r.row('categoryIds'),
|
||||
Episode: r.branch(r.row.hasFields('episode'), {
|
||||
Duration: r.row('episode')('duration'),
|
||||
Length: r.row('episode')('length'),
|
||||
Media: r.row('episode')('media'),
|
||||
MediaType: r.row('episode')('mediaType').default(null),
|
||||
ImageUrl: r.row('episode')('imageUrl').default(null),
|
||||
Subtitle: r.row('episode')('subtitle').default(null),
|
||||
Explicit: r.row('episode')('explicit').default(null),
|
||||
ChapterFile: r.row('episode')('chapterFile').default(null),
|
||||
ChapterType: r.row('episode')('chapterType').default(null),
|
||||
TranscriptUrl: r.row('episode')('transcriptUrl').default(null),
|
||||
TranscriptType: r.row('episode')('transcriptType').default(null),
|
||||
TranscriptLang: r.row('episode')('transcriptLang').default(null),
|
||||
TranscriptCaptions: r.row('episode')('transcriptCaptions').default(null),
|
||||
SeasonNumber: r.row('episode')('seasonNumber').default(null),
|
||||
SeasonDescription: r.row('episode')('seasonDescription').default(null),
|
||||
EpisodeNumber: r.row('episode')('episodeNumber').default(null),
|
||||
EpisodeDescription: r.row('episode')('episodeDescription').default(null)
|
||||
}, null),
|
||||
Id: r.row('id'),
|
||||
Metadata: r.row('metadata').map(function (meta) {
|
||||
return { Name: meta('name'), Value: meta('value') }
|
||||
}),
|
||||
Permalink: r.row('permalink'),
|
||||
PriorPermalinks: r.row('priorPermalinks'),
|
||||
PublishedOn: r.row('publishedOn'),
|
||||
Revisions: r.row('revisions').map(function (rev) {
|
||||
return {
|
||||
AsOf: rev('asOf'),
|
||||
Text: rev('text')
|
||||
}
|
||||
}),
|
||||
Status: r.row('status'),
|
||||
Tags: r.row('tags'),
|
||||
Template: r.row('template').default(null),
|
||||
Text: r.row('text'),
|
||||
Title: r.row('title'),
|
||||
UpdatedOn: r.row('updatedOn'),
|
||||
WebLogId: r.row('webLogId')
|
||||
})
|
||||
|
||||
// TagMap
|
||||
r.db('myWebLog').table('TagMap').map({
|
||||
Id: r.row('id'),
|
||||
Tag: r.row('tag'),
|
||||
UrlValue: r.row('urlValue'),
|
||||
WebLogId: r.row('webLogId')
|
||||
})
|
||||
|
||||
// Theme
|
||||
r.db('myWebLog').table('Theme').map({
|
||||
Id: r.row('id'),
|
||||
Name: r.row('name'),
|
||||
Templates: r.row('templates').map(function (tmpl) {
|
||||
return {
|
||||
Name: tmpl('name'),
|
||||
Text: tmpl('text')
|
||||
}
|
||||
}),
|
||||
Version: r.row('version')
|
||||
})
|
||||
|
||||
// ThemeAsset
|
||||
r.db('myWebLog').table('ThemeAsset').map({
|
||||
Data: r.row('data'),
|
||||
Id: r.row('id'),
|
||||
UpdatedOn: r.row('updatedOn')
|
||||
})
|
||||
|
||||
// WebLog
|
||||
r.db('myWebLog').table('WebLog').map(
|
||||
{ AutoHtmx: r.row('autoHtmx'),
|
||||
DefaultPage: r.row('defaultPage'),
|
||||
Id: r.row('id'),
|
||||
Name: r.row('name'),
|
||||
PostsPerPage: r.row('postsPerPage'),
|
||||
Rss: {
|
||||
IsCategoryEnabled: r.row('rss')('categoryEnabled'),
|
||||
Copyright: r.row('rss')('copyright'),
|
||||
CustomFeeds: r.row('rss')('customFeeds').map(function (feed) {
|
||||
return {
|
||||
Id: feed('id'),
|
||||
Path: feed('path'),
|
||||
Podcast: {
|
||||
DefaultMediaType: feed('podcast')('defaultMediaType'),
|
||||
DisplayedAuthor: feed('podcast')('displayedAuthor'),
|
||||
Email: feed('podcast')('email'),
|
||||
Explicit: feed('podcast')('explicit'),
|
||||
FundingText: feed('podcast')('fundingText'),
|
||||
FundingUrl: feed('podcast')('fundingUrl'),
|
||||
PodcastGuid: feed('podcast')('guid'),
|
||||
AppleCategory: feed('podcast')('iTunesCategory'),
|
||||
AppleSubcategory: feed('podcast')('iTunesSubcategory'),
|
||||
ImageUrl: feed('podcast')('imageUrl'),
|
||||
ItemsInFeed: feed('podcast')('itemsInFeed'),
|
||||
MediaBaseUrl: feed('podcast')('mediaBaseUrl'),
|
||||
Medium: feed('podcast')('medium'),
|
||||
Subtitle: feed('podcast')('subtitle'),
|
||||
Summary: feed('podcast')('summary'),
|
||||
Title: feed('podcast')('title')
|
||||
},
|
||||
Source: feed('source')
|
||||
}
|
||||
}),
|
||||
IsFeedEnabled: r.row('rss')('feedEnabled'),
|
||||
FeedName: r.row('rss')('feedName'),
|
||||
ItemsInFeed: r.row('rss')('itemsInFeed'),
|
||||
IsTagEnabled: r.row('rss')('tagEnabled')
|
||||
},
|
||||
Slug: r.row('slug'),
|
||||
Subtitle: r.row('subtitle'),
|
||||
ThemeId: r.row('themePath'),
|
||||
TimeZone: r.row('timeZone'),
|
||||
Uploads: r.row('uploads'),
|
||||
UrlBase: r.row('urlBase')
|
||||
})
|
||||
|
||||
// WebLogUser
|
||||
r.db('myWebLog').table('WebLogUser').map({
|
||||
AccessLevel: r.row('authorizationLevel'),
|
||||
FirstName: r.row('firstName'),
|
||||
Id: r.row('id'),
|
||||
LastName: r.row('lastName'),
|
||||
PasswordHash: r.row('passwordHash'),
|
||||
PreferredName: r.row('preferredName'),
|
||||
Salt: r.row('salt'),
|
||||
Url: r.row('url'),
|
||||
Email: r.row('userName'),
|
||||
WebLogId: r.row('webLogId'),
|
||||
CreatedOn: r.branch(r.row.hasFields('createdOn'), r.row('createdOn'), r.expr(new Date(0))),
|
||||
LastSeenOn: r.row('lastSeenOn').default(null)
|
||||
})
|
10
src/Directory.Build.props
Normal file
10
src/Directory.Build.props
Normal file
@ -0,0 +1,10 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<DebugType>embedded</DebugType>
|
||||
<AssemblyVersion>2.0.0.0</AssemblyVersion>
|
||||
<FileVersion>2.0.0.0</FileVersion>
|
||||
<Version>2.0.0</Version>
|
||||
<VersionSuffix>rc1</VersionSuffix>
|
||||
</PropertyGroup>
|
||||
</Project>
|
@ -1,11 +1,5 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<DebugType>embedded</DebugType>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\MyWebLog.Domain\MyWebLog.Domain.fsproj" />
|
||||
</ItemGroup>
|
||||
|
@ -1,11 +1,5 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<DebugType>embedded</DebugType>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="SupportTypes.fs" />
|
||||
<Compile Include="DataTypes.fs" />
|
||||
|
@ -244,7 +244,10 @@ let private updateAssets themeId (zip : ZipArchive) (data : IData) = backgroundT
|
||||
/// Get the theme name from the file name given
|
||||
let getThemeName (fileName : string) =
|
||||
let themeName = fileName.Split(".").[0].ToLowerInvariant().Replace (" ", "-")
|
||||
if Regex.IsMatch (themeName, """^[a-z0-9\-]+$""") then Ok themeName else Error $"Theme name {fileName} is invalid"
|
||||
if themeName.EndsWith "-theme" then
|
||||
if Regex.IsMatch (themeName, """^[a-z0-9\-]+$""") then Ok (themeName.Substring (0, themeName.Length - 6))
|
||||
else Error $"Theme name {fileName} is invalid"
|
||||
else Error "Theme .zip file name must end in \"-theme.zip\""
|
||||
|
||||
/// Load a theme from the given stream, which should contain a ZIP archive
|
||||
let loadThemeFromZip themeName file clean (data : IData) = backgroundTask {
|
||||
@ -260,6 +263,8 @@ let loadThemeFromZip themeName file clean (data : IData) = backgroundTask {
|
||||
let! theme = updateTemplates theme zip
|
||||
do! data.Theme.Save theme
|
||||
do! updateAssets themeId zip data
|
||||
|
||||
return theme
|
||||
}
|
||||
|
||||
// POST /admin/theme/update
|
||||
@ -271,7 +276,7 @@ let updateTheme : HttpHandler = requireAccess Administrator >=> fun next ctx ->
|
||||
let data = ctx.Data
|
||||
use stream = new MemoryStream ()
|
||||
do! themeFile.CopyToAsync stream
|
||||
do! loadThemeFromZip themeName stream true data
|
||||
let! _ = loadThemeFromZip themeName stream true data
|
||||
do! ThemeAssetCache.refreshTheme (ThemeId themeName) data
|
||||
TemplateCache.invalidateTheme themeName
|
||||
do! addMessage ctx { UserMessage.success with Message = "Theme updated successfully" }
|
||||
|
@ -128,43 +128,6 @@ let edit usrId : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> tas
|
||||
| None -> return! Error.notFound next ctx
|
||||
}
|
||||
|
||||
// POST /admin/user/save
|
||||
let save : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
|
||||
let! model = ctx.BindFormAsync<EditUserModel> ()
|
||||
let data = ctx.Data
|
||||
let tryUser =
|
||||
if model.IsNew then
|
||||
{ WebLogUser.empty with
|
||||
Id = WebLogUserId.create ()
|
||||
WebLogId = ctx.WebLog.Id
|
||||
CreatedOn = DateTime.UtcNow
|
||||
} |> someTask
|
||||
else data.WebLogUser.FindById (WebLogUserId model.Id) ctx.WebLog.Id
|
||||
match! tryUser with
|
||||
| Some user when model.Password = model.PasswordConfirm ->
|
||||
let updatedUser = model.UpdateUser user
|
||||
if updatedUser.AccessLevel = Administrator && not (ctx.HasAccessLevel Administrator) then
|
||||
return! goAway next ctx
|
||||
else
|
||||
let updatedUser =
|
||||
if model.Password = "" then updatedUser
|
||||
else
|
||||
let salt = Guid.NewGuid ()
|
||||
{ updatedUser with PasswordHash = hashedPassword model.Password model.Email salt; Salt = salt }
|
||||
do! (if model.IsNew then data.WebLogUser.Add else data.WebLogUser.Update) updatedUser
|
||||
do! addMessage ctx
|
||||
{ UserMessage.success with
|
||||
Message = $"""{if model.IsNew then "Add" else "Updat"}ed user successfully"""
|
||||
}
|
||||
return! bare next ctx
|
||||
| Some _ ->
|
||||
do! addMessage ctx { UserMessage.error with Message = "The passwords did not match; nothing saved" }
|
||||
return!
|
||||
(withHxRetarget $"#user_{model.Id}" >=> showEdit { model with Password = ""; PasswordConfirm = "" })
|
||||
next ctx
|
||||
| None -> return! Error.notFound next ctx
|
||||
}
|
||||
|
||||
// POST /admin/user/{id}/delete
|
||||
let delete userId : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
|
||||
let data = ctx.Data
|
||||
@ -237,3 +200,44 @@ let saveMyInfo : HttpHandler = requireAccess Author >=> fun next ctx -> task {
|
||||
return! showMyInfo { model with NewPassword = ""; NewPasswordConfirm = "" } user next ctx
|
||||
| None -> return! Error.notFound next ctx
|
||||
}
|
||||
|
||||
// User save is not statically compilable; not sure why, but we'll revisit it at some point
|
||||
#nowarn "3511"
|
||||
|
||||
// POST /admin/user/save
|
||||
let save : HttpHandler = requireAccess WebLogAdmin >=> fun next ctx -> task {
|
||||
let! model = ctx.BindFormAsync<EditUserModel> ()
|
||||
let data = ctx.Data
|
||||
let tryUser =
|
||||
if model.IsNew then
|
||||
{ WebLogUser.empty with
|
||||
Id = WebLogUserId.create ()
|
||||
WebLogId = ctx.WebLog.Id
|
||||
CreatedOn = DateTime.UtcNow
|
||||
} |> someTask
|
||||
else data.WebLogUser.FindById (WebLogUserId model.Id) ctx.WebLog.Id
|
||||
match! tryUser with
|
||||
| Some user when model.Password = model.PasswordConfirm ->
|
||||
let updatedUser = model.UpdateUser user
|
||||
if updatedUser.AccessLevel = Administrator && not (ctx.HasAccessLevel Administrator) then
|
||||
return! goAway next ctx
|
||||
else
|
||||
let toUpdate =
|
||||
if model.Password = "" then updatedUser
|
||||
else
|
||||
let salt = Guid.NewGuid ()
|
||||
{ updatedUser with PasswordHash = hashedPassword model.Password model.Email salt; Salt = salt }
|
||||
do! (if model.IsNew then data.WebLogUser.Add else data.WebLogUser.Update) toUpdate
|
||||
do! addMessage ctx
|
||||
{ UserMessage.success with
|
||||
Message = $"""{if model.IsNew then "Add" else "Updat"}ed user successfully"""
|
||||
}
|
||||
return! bare next ctx
|
||||
| Some _ ->
|
||||
do! addMessage ctx { UserMessage.error with Message = "The passwords did not match; nothing saved" }
|
||||
return!
|
||||
(withHxRetarget $"#user_{model.Id}" >=> showEdit { model with Password = ""; PasswordConfirm = "" })
|
||||
next ctx
|
||||
| None -> return! Error.notFound next ctx
|
||||
}
|
||||
|
||||
|
@ -128,6 +128,8 @@ let importLinks args sp = task {
|
||||
// Loading a theme and restoring a backup are not statically compilable; this is OK
|
||||
#nowarn "3511"
|
||||
|
||||
open Microsoft.Extensions.Logging
|
||||
|
||||
/// Load a theme from the given ZIP file
|
||||
let loadTheme (args : string[]) (sp : IServiceProvider) = task {
|
||||
if args.Length > 1 then
|
||||
@ -142,8 +144,10 @@ let loadTheme (args : string[]) (sp : IServiceProvider) = task {
|
||||
use stream = File.Open (args[1], FileMode.Open)
|
||||
use copy = new MemoryStream ()
|
||||
do! stream.CopyToAsync copy
|
||||
do! Handlers.Admin.loadThemeFromZip themeName copy clean data
|
||||
printfn $"Theme {themeName} loaded successfully"
|
||||
let! theme = Handlers.Admin.loadThemeFromZip themeName copy clean data
|
||||
let fac = sp.GetRequiredService<ILoggerFactory> ()
|
||||
let log = fac.CreateLogger "MyWebLog.Themes"
|
||||
log.LogInformation $"{theme.Name} v{theme.Version} ({ThemeId.toString theme.Id}) loaded"
|
||||
| Error message -> eprintfn $"{message}"
|
||||
else
|
||||
eprintfn "Usage: MyWebLog load-theme [theme-zip-file-name] [*clean-load]"
|
||||
|
@ -2,11 +2,8 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<PublishSingleFile>true</PublishSingleFile>
|
||||
<SelfContained>false</SelfContained>
|
||||
<DebugType>embedded</DebugType>
|
||||
<NoWarn>3391</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -82,6 +82,7 @@ let showHelp () =
|
||||
Task.FromResult ()
|
||||
|
||||
|
||||
open System.IO
|
||||
open Giraffe
|
||||
open Giraffe.EndpointRouting
|
||||
open Microsoft.AspNetCore.Authentication.Cookies
|
||||
@ -135,7 +136,7 @@ let rec main args =
|
||||
|> ignore
|
||||
builder.Services.AddScoped<IData, SQLiteData> () |> ignore
|
||||
// Use SQLite for caching as well
|
||||
let cachePath = Option.ofObj (cfg.GetConnectionString "SQLiteCachePath") |> Option.defaultValue "./session.db"
|
||||
let cachePath = defaultArg (Option.ofObj (cfg.GetConnectionString "SQLiteCachePath")) "./session.db"
|
||||
builder.Services.AddSqliteCache (fun o -> o.CachePath <- cachePath) |> ignore
|
||||
| _ -> ()
|
||||
|
||||
@ -162,7 +163,11 @@ let rec main args =
|
||||
| Some it ->
|
||||
printfn $"""Unrecognized command "{it}" - valid commands are:"""
|
||||
showHelp ()
|
||||
| None ->
|
||||
| None -> task {
|
||||
// Load all themes in the application directory
|
||||
for themeFile in Directory.EnumerateFiles (".", "*-theme.zip") do
|
||||
do! Maintenance.loadTheme [| ""; themeFile |] app.Services
|
||||
|
||||
let _ = app.UseForwardedHeaders ()
|
||||
let _ = app.UseCookiePolicy (CookiePolicyOptions (MinimumSameSitePolicy = SameSiteMode.Strict))
|
||||
let _ = app.UseMiddleware<WebLogMiddleware> ()
|
||||
@ -172,7 +177,8 @@ let rec main args =
|
||||
let _ = app.UseSession ()
|
||||
let _ = app.UseGiraffe Handlers.Routes.endpoint
|
||||
|
||||
Task.FromResult (app.Run ())
|
||||
app.Run ()
|
||||
}
|
||||
|> Async.AwaitTask |> Async.RunSynchronously
|
||||
|
||||
0 // Exit code
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"Generator": "myWebLog 2.0-beta05",
|
||||
"Generator": "myWebLog 2.0-rc1",
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"MyWebLog.Handlers": "Information"
|
||||
|
@ -1,2 +1,2 @@
|
||||
myWebLog Admin
|
||||
2.0.0-beta05
|
||||
2.0.0-rc1
|
@ -1,10 +1,7 @@
|
||||
{%- if is_category or is_tag %}
|
||||
<h1 class="index-title">{{ page_title }}</h1>
|
||||
{%- if is_category %}
|
||||
{%- assign cat = categories | where: "slug", slug | first -%}
|
||||
{%- if cat.description %}<h4 class="text-muted">{{ cat.description.value }}</h4>{% endif -%}
|
||||
{%- endif %}
|
||||
{%- endif %}
|
||||
{%- if subtitle %}<h4 class="text-muted">{{ subtitle }}</h4>{% endif -%}
|
||||
{% endif %}
|
||||
<section class="container mt-3" aria-label="The posts for the page">
|
||||
{% for post in model.posts %}
|
||||
<article>
|
||||
@ -27,7 +24,7 @@
|
||||
{%- if category_count > 0 -%}
|
||||
Categorized under:
|
||||
{% for cat in post.category_ids -%}
|
||||
{%- assign this_cat = categories | where: "id", cat | first -%}
|
||||
{%- assign this_cat = categories | where: "Id", cat | first -%}
|
||||
{{ this_cat.name }}{% unless forloop.last %}, {% endunless %}
|
||||
{%- assign cat_names = this_cat.name | concat: cat_names -%}
|
||||
{%- endfor -%}
|
||||
|
@ -20,7 +20,7 @@
|
||||
<h4 class="item-meta text-muted">
|
||||
Categorized under
|
||||
{% for cat_id in post.category_ids -%}
|
||||
{% assign cat = categories | where: "id", cat_id | first %}
|
||||
{% assign cat = categories | where: "Id", cat_id | first %}
|
||||
<span class="text-nowrap">
|
||||
<a href="{{ cat | category_link }}" title="Categorized under “{{ cat.name | escape }}”">
|
||||
{{ cat.name }}
|
||||
|
@ -1,2 +1,2 @@
|
||||
myWebLog Default Theme
|
||||
2.0.0-alpha36
|
||||
2.0.0-rc1
|
Loading…
Reference in New Issue
Block a user