diff --git a/api-docs/openapi/v3_0/aggregated.json b/api-docs/openapi/v3_0/aggregated.json
index 908167ed3..52399b4ce 100644
--- a/api-docs/openapi/v3_0/aggregated.json
+++ b/api-docs/openapi/v3_0/aggregated.json
@@ -13499,6 +13499,10 @@
"minLength": 1,
"type": "string"
},
+ "postTemplate": {
+ "maxLength": 255,
+ "type": "string"
+ },
"priority": {
"type": "integer",
"format": "int32",
@@ -13509,6 +13513,7 @@
"type": "string"
},
"template": {
+ "maxLength": 255,
"type": "string"
}
}
@@ -17446,12 +17451,12 @@
},
"visible": {
"type": "string",
+ "default": "PUBLIC",
"enum": [
"PUBLIC",
"INTERNAL",
"PRIVATE"
- ],
- "default": "PUBLIC"
+ ]
}
}
},
@@ -19101,12 +19106,12 @@
},
"visible": {
"type": "string",
+ "default": "PUBLIC",
"enum": [
"PUBLIC",
"INTERNAL",
"PRIVATE"
- ],
- "default": "PUBLIC"
+ ]
}
}
},
diff --git a/api/src/main/java/run/halo/app/core/extension/content/Category.java b/api/src/main/java/run/halo/app/core/extension/content/Category.java
index bc5dfadf1..76df38a84 100644
--- a/api/src/main/java/run/halo/app/core/extension/content/Category.java
+++ b/api/src/main/java/run/halo/app/core/extension/content/Category.java
@@ -1,5 +1,6 @@
package run.halo.app.core.extension.content;
+import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.NOT_REQUIRED;
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
import static run.halo.app.core.extension.content.Category.KIND;
@@ -53,8 +54,17 @@ public class Category extends AbstractExtension {
private String cover;
+ @Schema(requiredMode = NOT_REQUIRED, maxLength = 255)
private String template;
+ /**
+ *
Used to specify the template for the posts associated with the category.
+ * The priority is not as high as that of the post.
+ * If the post also specifies a template, the post's template will prevail.
+ */
+ @Schema(requiredMode = NOT_REQUIRED, maxLength = 255)
+ private String postTemplate;
+
@Schema(requiredMode = REQUIRED, defaultValue = "0")
private Integer priority;
diff --git a/application/src/main/java/run/halo/app/theme/router/factories/PostRouteFactory.java b/application/src/main/java/run/halo/app/theme/router/factories/PostRouteFactory.java
index a13f58575..fd7b4c2b4 100644
--- a/application/src/main/java/run/halo/app/theme/router/factories/PostRouteFactory.java
+++ b/application/src/main/java/run/halo/app/theme/router/factories/PostRouteFactory.java
@@ -1,11 +1,14 @@
package run.halo.app.theme.router.factories;
+import static org.apache.commons.lang3.ObjectUtils.defaultIfNull;
+import static org.apache.commons.lang3.StringUtils.isNotBlank;
import static org.springframework.web.reactive.function.server.RequestPredicates.GET;
import static org.springframework.web.reactive.function.server.RequestPredicates.accept;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import java.util.LinkedHashMap;
+import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
@@ -120,13 +123,24 @@ public class PostRouteFactory implements RouteFactory {
})
.flatMap(postVo -> {
Map model = ModelMapUtils.postModel(postVo);
- String template = postVo.getSpec().getTemplate();
- return viewNameResolver.resolveViewNameOrDefault(request, template,
- DefaultTemplateEnum.POST.getValue())
+ return determineTemplate(request, postVo)
.flatMap(templateName -> ServerResponse.ok().render(templateName, model));
});
}
+ Mono determineTemplate(ServerRequest request, PostVo postVo) {
+ return Flux.fromIterable(defaultIfNull(postVo.getCategories(), List.of()))
+ .filter(category -> isNotBlank(category.getSpec().getPostTemplate()))
+ .concatMap(category -> viewNameResolver.resolveViewNameOrDefault(request,
+ category.getSpec().getPostTemplate(), null)
+ )
+ .next()
+ .switchIfEmpty(Mono.defer(() -> viewNameResolver.resolveViewNameOrDefault(request,
+ postVo.getSpec().getTemplate(),
+ DefaultTemplateEnum.POST.getValue())
+ ));
+ }
+
Mono bestMatchPost(PostPatternVariable variable) {
return postsByPredicates(variable)
.filter(post -> {
@@ -143,10 +157,10 @@ public class PostRouteFactory implements RouteFactory {
}
Flux postsByPredicates(PostPatternVariable patternVariable) {
- if (StringUtils.isNotBlank(patternVariable.getName())) {
+ if (isNotBlank(patternVariable.getName())) {
return fetchPostsByName(patternVariable.getName());
}
- if (StringUtils.isNotBlank(patternVariable.getSlug())) {
+ if (isNotBlank(patternVariable.getSlug())) {
return fetchPostsBySlug(patternVariable.getSlug());
}
return Flux.empty();
@@ -163,7 +177,7 @@ public class PostRouteFactory implements RouteFactory {
private Flux fetchPostsBySlug(String slug) {
return queryPostPredicateResolver.getListOptions()
.flatMapMany(listOptions -> {
- if (StringUtils.isNotBlank(slug)) {
+ if (isNotBlank(slug)) {
var other = QueryFactory.equal("spec.slug", slug);
listOptions.setFieldSelector(listOptions.getFieldSelector().andQuery(other));
}
diff --git a/ui/console-src/modules/contents/posts/categories/components/CategoryEditingModal.vue b/ui/console-src/modules/contents/posts/categories/components/CategoryEditingModal.vue
index f2e222b70..72d468d9b 100644
--- a/ui/console-src/modules/contents/posts/categories/components/CategoryEditingModal.vue
+++ b/ui/console-src/modules/contents/posts/categories/components/CategoryEditingModal.vue
@@ -51,6 +51,7 @@ const formState = ref({
description: "",
cover: "",
template: "",
+ postTemplate: "",
priority: 0,
children: [],
},
@@ -160,6 +161,7 @@ onMounted(() => {
// custom templates
const { templates } = useThemeCustomTemplates("category");
+const { templates: postTemplates } = useThemeCustomTemplates("post");
// slug
const { handleGenerateSlug } = useSlugify(
@@ -243,9 +245,26 @@ const { handleGenerateSlug } = useSlugify(
:label="
$t('core.post_category.editing_modal.fields.template.label')
"
+ :help="
+ $t('core.post_category.editing_modal.fields.template.help')
+ "
type="select"
name="template"
>
+
-
+ Customize the rendering template of the category archive page, which
+ requires support from the theme
cover:
label: Cover
help: Theme adaptation is required to support
description:
label: Description
help: Theme adaptation is required to support
+ post_template:
+ label: Custom post template
+ help: >-
+ Customize the rendering template of posts in the current category,
+ which requires support from the theme
page:
title: Pages
actions:
@@ -728,9 +736,9 @@ core:
version_mismatch:
title: Version mismatch
description: >-
- The imported configuration file version does not match the
- current theme version, which may lead to compatibility issues.
- Do you want to continue importing?
+ The imported configuration file version does not match the current
+ theme version, which may lead to compatibility issues. Do you want
+ to continue importing?
invalid_format: Invalid theme configuration file
mismatched_theme: Configuration file does not match the selected theme
list_modal:
diff --git a/ui/src/locales/zh-CN.yaml b/ui/src/locales/zh-CN.yaml
index 3284a164e..ba7e54e84 100644
--- a/ui/src/locales/zh-CN.yaml
+++ b/ui/src/locales/zh-CN.yaml
@@ -366,12 +366,16 @@ core:
refresh_message: 根据名称重新生成别名
template:
label: 自定义模板
+ help: 自定义分类归档页面的渲染模版,需要主题提供支持
cover:
label: 封面图
help: 需要主题适配以支持
description:
label: 描述
help: 需要主题适配以支持
+ post_template:
+ label: 自定义文章模板
+ help: 自定义当前分类下文章的渲染模版,需要主题提供支持
page:
title: 页面
actions:
diff --git a/ui/src/locales/zh-TW.yaml b/ui/src/locales/zh-TW.yaml
index 643931bb5..c68a1f847 100644
--- a/ui/src/locales/zh-TW.yaml
+++ b/ui/src/locales/zh-TW.yaml
@@ -346,12 +346,16 @@ core:
refresh_message: 根據名稱重新生成別名
template:
label: 自定義模板
+ help: 自定義分類歸檔頁面的渲染模板,需要主題提供支持
cover:
label: 封面圖
help: 需要主題適配以支持
description:
label: 描述
help: 需要主題適配以支持
+ post_template:
+ label: 自定義文章模板
+ help: 自定義當前分類下文章的渲染模板,需要主題提供支持
page:
title: 頁面
actions: