From 9d60b8ae06a534ba095dc39095589f9e9c5fab4a Mon Sep 17 00:00:00 2001 From: guqing <38999863+guqing@users.noreply.github.com> Date: Wed, 30 Nov 2022 10:31:46 +0800 Subject: [PATCH] feat: theme templates support global seo settings (#2801) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #### What type of PR is this? /kind improvement /area core /milestone 2.0.0-rc.2 #### What this PR does / why we need it: 适配 SEO 系统设置,此功能之前遗漏 #### Which issue(s) this PR fixes: Fixes #2797 #### Special notes for your reviewer: how to test it? 1. seo 设置禁用搜索引擎,会在所有页面的 head 添加 `` 2. seo 设置填写`关键词`和`描述`会在所有页面的 head 标签填充 `` /cc @halo-dev/sig-halo #### Does this PR introduce a user-facing change? ```release-note 支持 SEO 全局系统设置 ``` --- ...java => ContentTemplateHeadProcessor.java} | 37 +++++--- .../app/theme/dialect/GlobalSeoProcessor.java | 59 ++++++++++++ .../dialect/HaloProcessorDialectTest.java | 95 ++++++++++++++++++- 3 files changed, 175 insertions(+), 16 deletions(-) rename src/main/java/run/halo/app/theme/dialect/{PostTemplateHeadProcessor.java => ContentTemplateHeadProcessor.java} (63%) create mode 100644 src/main/java/run/halo/app/theme/dialect/GlobalSeoProcessor.java diff --git a/src/main/java/run/halo/app/theme/dialect/PostTemplateHeadProcessor.java b/src/main/java/run/halo/app/theme/dialect/ContentTemplateHeadProcessor.java similarity index 63% rename from src/main/java/run/halo/app/theme/dialect/PostTemplateHeadProcessor.java rename to src/main/java/run/halo/app/theme/dialect/ContentTemplateHeadProcessor.java index 9c26c9f47..294a80f42 100644 --- a/src/main/java/run/halo/app/theme/dialect/PostTemplateHeadProcessor.java +++ b/src/main/java/run/halo/app/theme/dialect/ContentTemplateHeadProcessor.java @@ -2,6 +2,7 @@ package run.halo.app.theme.dialect; import java.util.List; import java.util.Map; +import lombok.AllArgsConstructor; import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Component; import org.thymeleaf.context.ITemplateContext; @@ -11,34 +12,39 @@ import org.thymeleaf.processor.element.IElementModelStructureHandler; import reactor.core.publisher.Mono; import run.halo.app.theme.DefaultTemplateEnum; import run.halo.app.theme.finders.PostFinder; +import run.halo.app.theme.finders.SinglePageFinder; import run.halo.app.theme.router.strategy.ModelConst; /** - *

The head html snippet injection processor for post template.

+ *

The head html snippet injection processor for content template such as post + * and page.

* * @author guqing * @since 2.0.0 */ @Component -public class PostTemplateHeadProcessor implements TemplateHeadProcessor { +@AllArgsConstructor +public class ContentTemplateHeadProcessor implements TemplateHeadProcessor { private static final String POST_NAME_VARIABLE = "name"; private final PostFinder postFinder; - - - public PostTemplateHeadProcessor(PostFinder postFinder) { - this.postFinder = postFinder; - } + private final SinglePageFinder singlePageFinder; @Override public Mono process(ITemplateContext context, IModel model, IElementModelStructureHandler structureHandler) { - if (!isPostTemplate(context)) { - return Mono.empty(); + Mono nameMono = Mono.justOrEmpty((String) context.getVariable(POST_NAME_VARIABLE)); + + Mono>> htmlMetasMono = Mono.empty(); + if (isPostTemplate(context)) { + htmlMetasMono = nameMono.flatMap(postFinder::getByName) + .map(post -> post.getSpec().getHtmlMetas()); + } else if (isPageTemplate(context)) { + htmlMetasMono = nameMono.flatMap(singlePageFinder::getByName) + .map(page -> page.getSpec().getHtmlMetas()); } - return Mono.justOrEmpty((String) context.getVariable(POST_NAME_VARIABLE)) - .flatMap(postFinder::getByName) - .doOnNext(postVo -> { - List> htmlMetas = postVo.getSpec().getHtmlMetas(); + + return htmlMetasMono + .doOnNext(htmlMetas -> { String metaHtml = headMetaBuilder(htmlMetas); IModelFactory modelFactory = context.getModelFactory(); model.add(modelFactory.createText(metaHtml)); @@ -65,4 +71,9 @@ public class PostTemplateHeadProcessor implements TemplateHeadProcessor { return DefaultTemplateEnum.POST.getValue() .equals(context.getVariable(ModelConst.TEMPLATE_ID)); } + + private boolean isPageTemplate(ITemplateContext context) { + return DefaultTemplateEnum.SINGLE_PAGE.getValue() + .equals(context.getVariable(ModelConst.TEMPLATE_ID)); + } } \ No newline at end of file diff --git a/src/main/java/run/halo/app/theme/dialect/GlobalSeoProcessor.java b/src/main/java/run/halo/app/theme/dialect/GlobalSeoProcessor.java new file mode 100644 index 000000000..afacfa03e --- /dev/null +++ b/src/main/java/run/halo/app/theme/dialect/GlobalSeoProcessor.java @@ -0,0 +1,59 @@ +package run.halo.app.theme.dialect; + +import lombok.AllArgsConstructor; +import org.apache.commons.lang3.BooleanUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; +import org.thymeleaf.context.ITemplateContext; +import org.thymeleaf.model.IModel; +import org.thymeleaf.model.IModelFactory; +import org.thymeleaf.processor.element.IElementModelStructureHandler; +import reactor.core.publisher.Mono; +import run.halo.app.infra.SystemConfigurableEnvironmentFetcher; +import run.halo.app.infra.SystemSetting; + +/** + * Inject code to the template head tag according to the global seo settings. + * + * @author guqing + * @see SystemSetting.Seo + * @since 2.0.0 + */ +@Order +@Component +@AllArgsConstructor +public class GlobalSeoProcessor implements TemplateHeadProcessor { + + private final SystemConfigurableEnvironmentFetcher environmentFetcher; + + @Override + public Mono process(ITemplateContext context, IModel model, + IElementModelStructureHandler structureHandler) { + return environmentFetcher.fetch(SystemSetting.Seo.GROUP, SystemSetting.Seo.class) + .map(seo -> { + boolean blockSpiders = BooleanUtils.isTrue(seo.getBlockSpiders()); + IModelFactory modelFactory = context.getModelFactory(); + if (blockSpiders) { + String noIndexMeta = "\n"; + model.add(modelFactory.createText(noIndexMeta)); + return model; + } + + String keywords = seo.getKeywords(); + if (StringUtils.isNotBlank(keywords)) { + String keywordsMeta = + "\n"; + model.add(modelFactory.createText(keywordsMeta)); + } + + if (StringUtils.isNotBlank(seo.getDescription())) { + String descriptionMeta = + "\n"; + model.add(modelFactory.createText(descriptionMeta)); + } + return model; + }) + .then(); + } +} diff --git a/src/test/java/run/halo/app/theme/dialect/HaloProcessorDialectTest.java b/src/test/java/run/halo/app/theme/dialect/HaloProcessorDialectTest.java index 14df794ee..c69558699 100644 --- a/src/test/java/run/halo/app/theme/dialect/HaloProcessorDialectTest.java +++ b/src/test/java/run/halo/app/theme/dialect/HaloProcessorDialectTest.java @@ -33,6 +33,7 @@ import run.halo.app.infra.SystemSetting; import run.halo.app.plugin.ExtensionComponentsFinder; import run.halo.app.theme.DefaultTemplateEnum; import run.halo.app.theme.finders.PostFinder; +import run.halo.app.theme.finders.SinglePageFinder; import run.halo.app.theme.finders.vo.PostVo; import run.halo.app.theme.router.strategy.ModelConst; @@ -42,7 +43,7 @@ import run.halo.app.theme.router.strategy.ModelConst; * @author guqing * @see HaloProcessorDialect * @see GlobalHeadInjectionProcessor - * @see PostTemplateHeadProcessor + * @see ContentTemplateHeadProcessor * @see TemplateHeadProcessor * @see TemplateGlobalHeadProcessor * @see TemplateFooterElementTagProcessor @@ -56,6 +57,9 @@ class HaloProcessorDialectTest { @Mock private PostFinder postFinder; + @Mock + private SinglePageFinder singlePageFinder; + @Mock private SystemConfigurableEnvironmentFetcher fetcher; @@ -72,9 +76,11 @@ class HaloProcessorDialectTest { templateEngine.addTemplateResolver(new TestTemplateResolver()); Map map = new HashMap<>(); - map.put("postTemplateHeadProcessor", new PostTemplateHeadProcessor(postFinder)); + map.put("postTemplateHeadProcessor", + new ContentTemplateHeadProcessor(postFinder, singlePageFinder)); map.put("templateGlobalHeadProcessor", new TemplateGlobalHeadProcessor(fetcher)); map.put("faviconHeadProcessor", new DefaultFaviconHeadProcessor(fetcher)); + map.put("globalSeoProcessor", new GlobalSeoProcessor(fetcher)); lenient().when(applicationContext.getBeansOfType(eq(TemplateHeadProcessor.class))) .thenReturn(map); @@ -85,8 +91,10 @@ class HaloProcessorDialectTest { when(fetcher.fetch(eq(SystemSetting.CodeInjection.GROUP), eq(SystemSetting.CodeInjection.class))).thenReturn(Mono.just(codeInjection)); - when(applicationContext.getBean(eq(SystemConfigurableEnvironmentFetcher.class))) + lenient().when(applicationContext.getBean(eq(SystemConfigurableEnvironmentFetcher.class))) .thenReturn(fetcher); + lenient().when(fetcher.fetch(eq(SystemSetting.Seo.GROUP), + eq(SystemSetting.Seo.class))).thenReturn(Mono.empty()); when(applicationContext.getBean(eq(ExtensionComponentsFinder.class))) .thenReturn(extensionComponentsFinder); @@ -172,6 +180,68 @@ class HaloProcessorDialectTest { """); } + @Test + void blockSeo() { + final Context context = getContext(); + SystemSetting.Seo seo = new SystemSetting.Seo(); + seo.setBlockSpiders(true); + when(fetcher.fetch(eq(SystemSetting.Seo.GROUP), + eq(SystemSetting.Seo.class))).thenReturn(Mono.just(seo)); + SystemSetting.Basic basic = new SystemSetting.Basic(); + basic.setFavicon("favicon.ico"); + when(fetcher.fetch(eq(SystemSetting.Basic.GROUP), + eq(SystemSetting.Basic.class))).thenReturn(Mono.just(basic)); + + String result = templateEngine.process("seo", context); + assertThat(result).isEqualTo(""" + + + + + Seo Test + + + + + + seo setting test. + + + """); + } + + @Test + void seoWithKeywordsAndDescription() { + final Context context = getContext(); + SystemSetting.Seo seo = new SystemSetting.Seo(); + seo.setKeywords("K1, K2, K3"); + seo.setDescription("This is a description."); + when(fetcher.fetch(eq(SystemSetting.Seo.GROUP), + eq(SystemSetting.Seo.class))).thenReturn(Mono.just(seo)); + SystemSetting.Basic basic = new SystemSetting.Basic(); + basic.setFavicon("favicon.ico"); + when(fetcher.fetch(eq(SystemSetting.Basic.GROUP), + eq(SystemSetting.Basic.class))).thenReturn(Mono.just(basic)); + + String result = templateEngine.process("seo", context); + assertThat(result).isEqualTo(""" + + + + + Seo Test + + + + + + + seo setting test. + + + """); + } + private Context getContext() { Context context = new Context(); context.setVariable( @@ -192,6 +262,10 @@ class HaloProcessorDialectTest { if (template.equals(DefaultTemplateEnum.POST.getValue())) { return new StringTemplateResource(postTemplate()); } + + if (template.equals("seo")) { + return new StringTemplateResource(seoTemplate()); + } return null; } @@ -227,5 +301,20 @@ class HaloProcessorDialectTest { """; } + + private String seoTemplate() { + return """ + + + + + Seo Test + + + seo setting test. + + + """; + } } } \ No newline at end of file