mirror of https://github.com/halo-dev/halo
feat: support route pattern /categories/{categorySlug}/{postSlug} for post access (#7331)
#### What type of PR is this? /kind feature /milestone 2.20.x #### What this PR does / why we need it: 文章访问路径支持设置 `/categories/{categorySlug}/{postSlug}` 的形式 #### Which issue(s) this PR fixes: Fixes #7330 #### Does this PR introduce a user-facing change? ```release-note 文章访问路径支持设置 `/categories/{categorySlug}/{postSlug}` 的形式 ```pull/7353/head^2
parent
9225668f73
commit
482436b2d0
|
@ -1,8 +1,10 @@
|
|||
package run.halo.app.content;
|
||||
|
||||
import java.util.List;
|
||||
import org.springframework.lang.NonNull;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.core.extension.content.Category;
|
||||
import run.halo.app.core.extension.content.Post;
|
||||
import run.halo.app.extension.ListResult;
|
||||
|
||||
|
@ -52,4 +54,6 @@ public interface PostService {
|
|||
Mono<ContentWrapper> deleteContent(String postName, String snapshotName);
|
||||
|
||||
Mono<Post> recycleBy(String postName, String username);
|
||||
|
||||
Flux<Category> listCategories(List<String> categories);
|
||||
}
|
||||
|
|
|
@ -4,9 +4,11 @@ import static run.halo.app.extension.index.query.QueryFactory.in;
|
|||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.ToIntFunction;
|
||||
import java.util.function.UnaryOperator;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
@ -161,13 +163,17 @@ public class PostServiceImpl extends AbstractContentService implements PostServi
|
|||
return client.listAll(Tag.class, listOptions, Sort.by("metadata.creationTimestamp"));
|
||||
}
|
||||
|
||||
private Flux<Category> listCategories(List<String> categoryNames) {
|
||||
@Override
|
||||
public Flux<Category> listCategories(List<String> categoryNames) {
|
||||
if (categoryNames == null) {
|
||||
return Flux.empty();
|
||||
}
|
||||
ToIntFunction<Category> comparator =
|
||||
category -> categoryNames.indexOf(category.getMetadata().getName());
|
||||
var listOptions = new ListOptions();
|
||||
listOptions.setFieldSelector(FieldSelector.of(in("metadata.name", categoryNames)));
|
||||
return client.listAll(Category.class, listOptions, Sort.by("metadata.creationTimestamp"));
|
||||
return client.listAll(Category.class, listOptions, Sort.unsorted())
|
||||
.sort(Comparator.comparingInt(comparator));
|
||||
}
|
||||
|
||||
private Flux<Contributor> listContributors(List<String> usernames) {
|
||||
|
|
|
@ -12,6 +12,7 @@ import java.util.Map;
|
|||
import java.util.Properties;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Component;
|
||||
import run.halo.app.content.PostService;
|
||||
import run.halo.app.core.extension.content.Constant;
|
||||
import run.halo.app.core.extension.content.Post;
|
||||
import run.halo.app.extension.MetadataUtil;
|
||||
|
@ -27,12 +28,14 @@ import run.halo.app.infra.utils.PathUtils;
|
|||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class PostPermalinkPolicy implements PermalinkPolicy<Post> {
|
||||
public static final String DEFAULT_CATEGORY = "default";
|
||||
public static final String DEFAULT_PERMALINK_PATTERN =
|
||||
SystemSetting.ThemeRouteRules.empty().getPost();
|
||||
private static final NumberFormat NUMBER_FORMAT = new DecimalFormat("00");
|
||||
|
||||
private final SystemConfigurableEnvironmentFetcher environmentFetcher;
|
||||
private final ExternalUrlSupplier externalUrlSupplier;
|
||||
private final PostService postService;
|
||||
|
||||
@Override
|
||||
public String permalink(Post post) {
|
||||
|
@ -62,6 +65,13 @@ public class PostPermalinkPolicy implements PermalinkPolicy<Post> {
|
|||
properties.put("month", NUMBER_FORMAT.format(zonedDateTime.getMonthValue()));
|
||||
properties.put("day", NUMBER_FORMAT.format(zonedDateTime.getDayOfMonth()));
|
||||
|
||||
var categorySlug = postService.listCategories(post.getSpec().getCategories())
|
||||
.next()
|
||||
.blockOptional()
|
||||
.map(category -> category.getSpec().getSlug())
|
||||
.orElse(DEFAULT_CATEGORY);
|
||||
properties.put("categorySlug", categorySlug);
|
||||
|
||||
String simplifiedPattern = PathUtils.simplifyPathPattern(pattern);
|
||||
String permalink =
|
||||
PROPERTY_PLACEHOLDER_HELPER.replacePlaceholders(simplifiedPattern, properties);
|
||||
|
|
|
@ -8,18 +8,22 @@ import java.util.Map;
|
|||
import java.util.Set;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import run.halo.app.content.CategoryService;
|
||||
import run.halo.app.content.permalinks.CategoryPermalinkPolicy;
|
||||
import run.halo.app.core.extension.content.Category;
|
||||
import run.halo.app.core.extension.content.Constant;
|
||||
import run.halo.app.core.extension.content.Post;
|
||||
import run.halo.app.event.post.CategoryHiddenStateChangeEvent;
|
||||
import run.halo.app.extension.ExtensionClient;
|
||||
import run.halo.app.extension.ExtensionUtil;
|
||||
import run.halo.app.extension.ListOptions;
|
||||
import run.halo.app.extension.controller.Controller;
|
||||
import run.halo.app.extension.controller.ControllerBuilder;
|
||||
import run.halo.app.extension.controller.Reconciler;
|
||||
import run.halo.app.extension.index.query.QueryFactory;
|
||||
|
||||
/**
|
||||
* Reconciler for {@link Category}.
|
||||
|
@ -43,6 +47,7 @@ public class CategoryReconciler implements Reconciler<Reconciler.Request> {
|
|||
if (ExtensionUtil.isDeleted(category)) {
|
||||
if (removeFinalizers(category.getMetadata(), Set.of(FINALIZER_NAME))) {
|
||||
refreshHiddenState(category, false);
|
||||
updateCategoryForPost(category.getMetadata().getName());
|
||||
client.update(category);
|
||||
}
|
||||
return;
|
||||
|
@ -118,4 +123,18 @@ public class CategoryReconciler implements Reconciler<Reconciler.Request> {
|
|||
category.getStatusOrDefault()
|
||||
.setPermalink(categoryPermalinkPolicy.permalink(category));
|
||||
}
|
||||
|
||||
private void updateCategoryForPost(String categoryName) {
|
||||
var posts = client.listAll(Post.class, ListOptions.builder()
|
||||
.fieldQuery(QueryFactory.equal("spec.categories", categoryName))
|
||||
.build(), Sort.by("metadata.creationTimestamp", "metadata.name")
|
||||
);
|
||||
for (Post post : posts) {
|
||||
var categoryNames = post.getSpec().getCategories();
|
||||
if (!CollectionUtils.isEmpty(categoryNames)) {
|
||||
categoryNames.remove(categoryName);
|
||||
}
|
||||
client.update(post);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -55,7 +55,9 @@ public class ThemeCompositeRouterFunction implements RouterFunction<ServerRespon
|
|||
@NonNull
|
||||
public Mono<HandlerFunction<ServerResponse>> route(@NonNull ServerRequest request) {
|
||||
return Flux.fromIterable(cachedRouters)
|
||||
.concatMap(routerFunction -> routerFunction.route(request))
|
||||
.concatMap(routerFunction -> routerFunction.route(request)
|
||||
.filterWhen(handle -> handle.handle(request).hasElement())
|
||||
)
|
||||
.next();
|
||||
}
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ 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 static run.halo.app.content.permalinks.PostPermalinkPolicy.DEFAULT_CATEGORY;
|
||||
|
||||
import com.google.common.cache.Cache;
|
||||
import com.google.common.cache.CacheBuilder;
|
||||
|
@ -33,11 +34,11 @@ import org.springframework.web.reactive.function.server.ServerResponse;
|
|||
import org.springframework.web.server.i18n.LocaleContextResolver;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.content.PostService;
|
||||
import run.halo.app.core.extension.content.Post;
|
||||
import run.halo.app.extension.MetadataUtil;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
import run.halo.app.extension.index.query.QueryFactory;
|
||||
import run.halo.app.infra.exception.NotFoundException;
|
||||
import run.halo.app.infra.utils.JsonUtils;
|
||||
import run.halo.app.theme.DefaultTemplateEnum;
|
||||
import run.halo.app.theme.ViewNameResolver;
|
||||
|
@ -69,6 +70,7 @@ public class PostRouteFactory implements RouteFactory {
|
|||
private final TitleVisibilityIdentifyCalculator titleVisibilityIdentifyCalculator;
|
||||
|
||||
private final LocaleContextResolver localeContextResolver;
|
||||
private final PostService postService;
|
||||
|
||||
@Override
|
||||
public RouterFunction<ServerResponse> create(String pattern) {
|
||||
|
@ -151,9 +153,27 @@ public class PostRouteFactory implements RouteFactory {
|
|||
&& matchIfPresent(variable.getMonth(), labels.get(Post.ARCHIVE_MONTH_LABEL))
|
||||
&& matchIfPresent(variable.getDay(), labels.get(Post.ARCHIVE_DAY_LABEL));
|
||||
})
|
||||
.filterWhen(post -> {
|
||||
if (isNotBlank(variable.getCategorySlug())) {
|
||||
var categoryNames = post.getSpec().getCategories();
|
||||
return postService.listCategories(categoryNames)
|
||||
.next()
|
||||
.filter(category -> category.getSpec().getSlug()
|
||||
.equals(variable.getCategorySlug())
|
||||
)
|
||||
.map(category -> category.getSpec().getSlug())
|
||||
.switchIfEmpty(Mono.defer(() -> {
|
||||
if (DEFAULT_CATEGORY.equals(variable.getCategorySlug())) {
|
||||
return Mono.just(DEFAULT_CATEGORY);
|
||||
}
|
||||
return Mono.empty();
|
||||
}))
|
||||
.hasElement();
|
||||
}
|
||||
return Mono.just(true);
|
||||
})
|
||||
.next()
|
||||
.flatMap(post -> postFinder.getByName(post.getMetadata().getName()))
|
||||
.switchIfEmpty(Mono.error(new NotFoundException("Post not found")));
|
||||
.flatMap(post -> postFinder.getByName(post.getMetadata().getName()));
|
||||
}
|
||||
|
||||
Flux<Post> postsByPredicates(PostPatternVariable patternVariable) {
|
||||
|
@ -196,6 +216,7 @@ public class PostRouteFactory implements RouteFactory {
|
|||
String year;
|
||||
String month;
|
||||
String day;
|
||||
String categorySlug;
|
||||
|
||||
static PostPatternVariable from(ServerRequest request) {
|
||||
Map<String, String> variables = mergedVariables(request);
|
||||
|
|
|
@ -180,6 +180,8 @@ spec:
|
|||
value: '/{year:\d{4}}/{month:\d{2}}/{slug}'
|
||||
- label: '/{year}/{month}/{day}/{slug}'
|
||||
value: '/{year:\d{4}}/{month:\d{2}}/{day:\d{2}}/{slug}'
|
||||
- label: '/categories/{categorySlug}/{slug}'
|
||||
value: '/categories/{categorySlug}/{slug}'
|
||||
name: post
|
||||
validation: required
|
||||
- group: codeInjection
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package run.halo.app.content.permalinks;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
|
@ -10,6 +11,7 @@ import java.text.NumberFormat;
|
|||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
@ -17,9 +19,13 @@ import org.junit.jupiter.api.extension.ExtendWith;
|
|||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import reactor.core.publisher.Flux;
|
||||
import run.halo.app.content.PostService;
|
||||
import run.halo.app.content.TestPost;
|
||||
import run.halo.app.core.extension.content.Category;
|
||||
import run.halo.app.core.extension.content.Constant;
|
||||
import run.halo.app.core.extension.content.Post;
|
||||
import run.halo.app.extension.Metadata;
|
||||
import run.halo.app.extension.MetadataUtil;
|
||||
import run.halo.app.infra.ExternalUrlSupplier;
|
||||
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
|
||||
|
@ -44,12 +50,17 @@ class PostPermalinkPolicyTest {
|
|||
@Mock
|
||||
private SystemConfigurableEnvironmentFetcher environmentFetcher;
|
||||
|
||||
@Mock
|
||||
private PostService postService;
|
||||
|
||||
private PostPermalinkPolicy postPermalinkPolicy;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
lenient().when(externalUrlSupplier.get()).thenReturn(URI.create(""));
|
||||
postPermalinkPolicy = new PostPermalinkPolicy(environmentFetcher, externalUrlSupplier);
|
||||
lenient().when(postService.listCategories(any())).thenReturn(Flux.empty());
|
||||
postPermalinkPolicy =
|
||||
new PostPermalinkPolicy(environmentFetcher, externalUrlSupplier, postService);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -93,6 +104,24 @@ class PostPermalinkPolicyTest {
|
|||
assertThat(permalink).isEqualTo("/posts/test-post");
|
||||
}
|
||||
|
||||
@Test
|
||||
void permalinkForCategory() {
|
||||
Post post = TestPost.postV1();
|
||||
post.getSpec().setCategories(List.of("test-category"));
|
||||
Map<String, String> annotations = MetadataUtil.nullSafeAnnotations(post);
|
||||
annotations.put(Constant.PERMALINK_PATTERN_ANNO, "/{categorySlug}/{slug}");
|
||||
post.getMetadata().setName("test-post");
|
||||
post.getSpec().setSlug("test-post-slug");
|
||||
Instant now = Instant.now();
|
||||
post.getSpec().setPublishTime(now);
|
||||
|
||||
var category = createCategory("test-category", "test-category-slug");
|
||||
when(postService.listCategories(post.getSpec().getCategories()))
|
||||
.thenReturn(Flux.just(category));
|
||||
var permalink = postPermalinkPolicy.permalink(post);
|
||||
assertThat(permalink).isEqualTo("/test-category-slug/test-post-slug");
|
||||
}
|
||||
|
||||
@Test
|
||||
void permalinkWithExternalUrl() {
|
||||
Post post = TestPost.postV1();
|
||||
|
@ -112,4 +141,18 @@ class PostPermalinkPolicyTest {
|
|||
permalink = postPermalinkPolicy.permalink(post);
|
||||
assertThat(permalink).isEqualTo("http://example.com/2022/11/01/%E4%B8%AD%E6%96%87%20slug");
|
||||
}
|
||||
|
||||
private Category createCategory(String name, String slug) {
|
||||
Category category = new Category();
|
||||
Metadata metadata = new Metadata();
|
||||
metadata.setName(name);
|
||||
category.setMetadata(metadata);
|
||||
category.setSpec(new Category.CategorySpec());
|
||||
category.setStatus(new Category.CategoryStatus());
|
||||
|
||||
category.getSpec().setDisplayName("display-name");
|
||||
category.getSpec().setSlug(slug);
|
||||
category.getSpec().setPriority(0);
|
||||
return category;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue