mirror of https://github.com/halo-dev/halo
				
				
				
			refactor: content page meta tags now override global injected (#4069)
#### What type of PR is this? /kind improvement /area core /milestone 2.7.x #### What this PR does / why we need it: 修复文章页 HTML Meta 标签重复问题 see #4049 for more details. #### Which issue(s) this PR fixes: Fixes #4049 #### Does this PR introduce a user-facing change? ```release-note 修复文章页 Meta Description 标签重复问题 ```pull/4080/head
							parent
							
								
									8db4cec91e
								
							
						
					
					
						commit
						972ebed03a
					
				| 
						 | 
				
			
			@ -8,6 +8,9 @@ import reactor.core.publisher.Mono;
 | 
			
		|||
 | 
			
		||||
/**
 | 
			
		||||
 * Theme template <code>head</code> tag snippet injection processor.
 | 
			
		||||
 * <p>Head processor is processed order by {@link org.springframework.core.annotation.Order}
 | 
			
		||||
 * annotation, Higher order will be processed first and so that low-priority processor can be
 | 
			
		||||
 * overwritten head tag written by high-priority processor.</p>
 | 
			
		||||
 *
 | 
			
		||||
 * @author guqing
 | 
			
		||||
 * @since 2.0.0
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,6 +9,7 @@ import java.util.List;
 | 
			
		|||
import java.util.Map;
 | 
			
		||||
import lombok.AllArgsConstructor;
 | 
			
		||||
import org.apache.commons.lang3.StringUtils;
 | 
			
		||||
import org.springframework.core.annotation.Order;
 | 
			
		||||
import org.springframework.stereotype.Component;
 | 
			
		||||
import org.springframework.web.util.HtmlUtils;
 | 
			
		||||
import org.thymeleaf.context.ITemplateContext;
 | 
			
		||||
| 
						 | 
				
			
			@ -29,6 +30,7 @@ import run.halo.app.theme.router.ModelConst;
 | 
			
		|||
 * @since 2.0.0
 | 
			
		||||
 */
 | 
			
		||||
@Component
 | 
			
		||||
@Order(1)
 | 
			
		||||
@AllArgsConstructor
 | 
			
		||||
public class ContentTemplateHeadProcessor implements TemplateHeadProcessor {
 | 
			
		||||
    private static final String POST_NAME_VARIABLE = "name";
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,78 @@
 | 
			
		|||
package run.halo.app.theme.dialect;
 | 
			
		||||
 | 
			
		||||
import java.util.ArrayList;
 | 
			
		||||
import java.util.Comparator;
 | 
			
		||||
import java.util.LinkedHashMap;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
import java.util.regex.Matcher;
 | 
			
		||||
import java.util.regex.Pattern;
 | 
			
		||||
import lombok.AllArgsConstructor;
 | 
			
		||||
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.ITemplateEvent;
 | 
			
		||||
import org.thymeleaf.model.IText;
 | 
			
		||||
import org.thymeleaf.processor.element.IElementModelStructureHandler;
 | 
			
		||||
import reactor.core.publisher.Mono;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * <p>This processor will remove the duplicate meta tag with the same name in head tag and only
 | 
			
		||||
 * keep the last one.</p>
 | 
			
		||||
 * <p>This processor will be executed last.</p>
 | 
			
		||||
 *
 | 
			
		||||
 * @author guqing
 | 
			
		||||
 * @since 2.0.0
 | 
			
		||||
 */
 | 
			
		||||
@Order
 | 
			
		||||
@Component
 | 
			
		||||
@AllArgsConstructor
 | 
			
		||||
public class DuplicateMetaTagProcessor implements TemplateHeadProcessor {
 | 
			
		||||
    static final Pattern META_PATTERN = Pattern.compile("<meta\\s+name=\"(\\w+)\"(.*?)>");
 | 
			
		||||
 | 
			
		||||
    @Override
 | 
			
		||||
    public Mono<Void> process(ITemplateContext context, IModel model,
 | 
			
		||||
        IElementModelStructureHandler structureHandler) {
 | 
			
		||||
        IModel newModel = context.getModelFactory().createModel();
 | 
			
		||||
 | 
			
		||||
        Map<String, IndexedModel> uniqueMetaTags = new LinkedHashMap<>();
 | 
			
		||||
        List<IndexedModel> otherModel = new ArrayList<>();
 | 
			
		||||
        for (int i = 0; i < model.size(); i++) {
 | 
			
		||||
            ITemplateEvent templateEvent = model.get(i);
 | 
			
		||||
            // If the current node is a text node, it is processed separately.
 | 
			
		||||
            // Because the text node may contain multiple meta tags.
 | 
			
		||||
            if (templateEvent instanceof IText textNode) {
 | 
			
		||||
                String text = textNode.getText();
 | 
			
		||||
                Matcher matcher = META_PATTERN.matcher(text);
 | 
			
		||||
                while (matcher.find()) {
 | 
			
		||||
                    String tagLine = matcher.group(0);
 | 
			
		||||
                    String nameAttribute = matcher.group(1);
 | 
			
		||||
                    IText metaTagNode = context.getModelFactory().createText(tagLine);
 | 
			
		||||
                    uniqueMetaTags.put(nameAttribute, new IndexedModel(i, metaTagNode));
 | 
			
		||||
                    text = text.replace(tagLine, "");
 | 
			
		||||
                }
 | 
			
		||||
                if (StringUtils.isNotBlank(text)) {
 | 
			
		||||
                    IText otherText = context.getModelFactory()
 | 
			
		||||
                        .createText(text);
 | 
			
		||||
                    otherModel.add(new IndexedModel(i, otherText));
 | 
			
		||||
                }
 | 
			
		||||
            } else {
 | 
			
		||||
                otherModel.add(new IndexedModel(i, templateEvent));
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        otherModel.addAll(uniqueMetaTags.values());
 | 
			
		||||
        otherModel.stream().sorted(Comparator.comparing(IndexedModel::index))
 | 
			
		||||
            .map(IndexedModel::templateEvent)
 | 
			
		||||
            .forEach(newModel::add);
 | 
			
		||||
 | 
			
		||||
        model.reset();
 | 
			
		||||
        model.addModel(newModel);
 | 
			
		||||
        return Mono.empty();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    record IndexedModel(int index, ITemplateEvent templateEvent) {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -2,9 +2,10 @@ package run.halo.app.theme.dialect;
 | 
			
		|||
 | 
			
		||||
import java.util.Collection;
 | 
			
		||||
import org.springframework.context.ApplicationContext;
 | 
			
		||||
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
 | 
			
		||||
import org.thymeleaf.context.ITemplateContext;
 | 
			
		||||
import org.thymeleaf.model.IModel;
 | 
			
		||||
import org.thymeleaf.model.IModelFactory;
 | 
			
		||||
import org.thymeleaf.model.ITemplateEvent;
 | 
			
		||||
import org.thymeleaf.processor.element.AbstractElementModelProcessor;
 | 
			
		||||
import org.thymeleaf.processor.element.IElementModelStructureHandler;
 | 
			
		||||
import org.thymeleaf.spring6.context.SpringContextUtils;
 | 
			
		||||
| 
						 | 
				
			
			@ -51,14 +52,23 @@ public class GlobalHeadInjectionProcessor extends AbstractElementModelProcessor
 | 
			
		|||
        structureHandler.setLocalVariable(PROCESS_FLAG, true);
 | 
			
		||||
 | 
			
		||||
        // handle <head> tag
 | 
			
		||||
        if (model.size() < 2) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /*
 | 
			
		||||
         * Create the DOM structure that will be substituting our custom tag.
 | 
			
		||||
         * The headline will be shown inside a '<div>' tag, and so this must
 | 
			
		||||
         * be created first and then a Text node must be added to it.
 | 
			
		||||
         */
 | 
			
		||||
        final IModelFactory modelFactory = context.getModelFactory();
 | 
			
		||||
        IModel modelToInsert = modelFactory.createModel();
 | 
			
		||||
        IModel modelToInsert = model.cloneModel();
 | 
			
		||||
        // close </head> tag
 | 
			
		||||
        final ITemplateEvent closeHeadTag = modelToInsert.get(modelToInsert.size() - 1);
 | 
			
		||||
        modelToInsert.remove(modelToInsert.size() - 1);
 | 
			
		||||
 | 
			
		||||
        // open <head> tag
 | 
			
		||||
        final ITemplateEvent openHeadTag = modelToInsert.get(0);
 | 
			
		||||
        modelToInsert.remove(0);
 | 
			
		||||
 | 
			
		||||
        // apply processors to modelToInsert
 | 
			
		||||
        Collection<TemplateHeadProcessor> templateHeadProcessors =
 | 
			
		||||
| 
						 | 
				
			
			@ -69,14 +79,20 @@ public class GlobalHeadInjectionProcessor extends AbstractElementModelProcessor
 | 
			
		|||
                .block();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // add to target model
 | 
			
		||||
        model.insertModel(model.size() - 1, modelToInsert);
 | 
			
		||||
        // reset model to insert
 | 
			
		||||
        model.reset();
 | 
			
		||||
        model.add(openHeadTag);
 | 
			
		||||
        model.addModel(modelToInsert);
 | 
			
		||||
        model.add(closeHeadTag);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private Collection<TemplateHeadProcessor> getTemplateHeadProcessors(ITemplateContext context) {
 | 
			
		||||
        ApplicationContext appCtx = SpringContextUtils.getApplicationContext(context);
 | 
			
		||||
        ExtensionComponentsFinder componentsFinder =
 | 
			
		||||
            appCtx.getBean(ExtensionComponentsFinder.class);
 | 
			
		||||
        return componentsFinder.getExtensions(TemplateHeadProcessor.class);
 | 
			
		||||
        return componentsFinder.getExtensions(TemplateHeadProcessor.class)
 | 
			
		||||
            .stream()
 | 
			
		||||
            .sorted(AnnotationAwareOrderComparator.INSTANCE)
 | 
			
		||||
            .toList();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,6 +3,7 @@ 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.Ordered;
 | 
			
		||||
import org.springframework.core.annotation.Order;
 | 
			
		||||
import org.springframework.stereotype.Component;
 | 
			
		||||
import org.thymeleaf.context.ITemplateContext;
 | 
			
		||||
| 
						 | 
				
			
			@ -20,7 +21,7 @@ import run.halo.app.infra.SystemSetting;
 | 
			
		|||
 * @see SystemSetting.Seo
 | 
			
		||||
 * @since 2.0.0
 | 
			
		||||
 */
 | 
			
		||||
@Order
 | 
			
		||||
@Order(Ordered.HIGHEST_PRECEDENCE + 1)
 | 
			
		||||
@Component
 | 
			
		||||
@AllArgsConstructor
 | 
			
		||||
public class GlobalSeoProcessor implements TemplateHeadProcessor {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,6 +1,8 @@
 | 
			
		|||
package run.halo.app.theme.dialect;
 | 
			
		||||
 | 
			
		||||
import org.apache.commons.lang3.StringUtils;
 | 
			
		||||
import org.springframework.core.Ordered;
 | 
			
		||||
import org.springframework.core.annotation.Order;
 | 
			
		||||
import org.springframework.stereotype.Component;
 | 
			
		||||
import org.thymeleaf.context.ITemplateContext;
 | 
			
		||||
import org.thymeleaf.model.IModel;
 | 
			
		||||
| 
						 | 
				
			
			@ -14,10 +16,12 @@ import run.halo.app.theme.router.ModelConst;
 | 
			
		|||
 | 
			
		||||
/**
 | 
			
		||||
 * <p>Global custom head snippet injection for theme global setting.</p>
 | 
			
		||||
 * <p>Globally injected head snippet can be overridden by content template.</p>
 | 
			
		||||
 *
 | 
			
		||||
 * @author guqing
 | 
			
		||||
 * @since 2.0.0
 | 
			
		||||
 */
 | 
			
		||||
@Order(Ordered.HIGHEST_PRECEDENCE + 2)
 | 
			
		||||
@Component
 | 
			
		||||
public class TemplateGlobalHeadProcessor implements TemplateHeadProcessor {
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,196 @@
 | 
			
		|||
package run.halo.app.theme.dialect;
 | 
			
		||||
 | 
			
		||||
import static org.assertj.core.api.Assertions.assertThat;
 | 
			
		||||
import static org.mockito.ArgumentMatchers.eq;
 | 
			
		||||
import static org.mockito.Mockito.lenient;
 | 
			
		||||
import static org.mockito.Mockito.when;
 | 
			
		||||
 | 
			
		||||
import java.util.ArrayList;
 | 
			
		||||
import java.util.HashMap;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.Map;
 | 
			
		||||
import java.util.Set;
 | 
			
		||||
import org.jsoup.Jsoup;
 | 
			
		||||
import org.junit.jupiter.api.BeforeEach;
 | 
			
		||||
import org.junit.jupiter.api.Test;
 | 
			
		||||
import org.junit.jupiter.api.extension.ExtendWith;
 | 
			
		||||
import org.mockito.Mock;
 | 
			
		||||
import org.mockito.junit.jupiter.MockitoExtension;
 | 
			
		||||
import org.springframework.context.ApplicationContext;
 | 
			
		||||
import org.thymeleaf.IEngineConfiguration;
 | 
			
		||||
import org.thymeleaf.TemplateEngine;
 | 
			
		||||
import org.thymeleaf.context.Context;
 | 
			
		||||
import org.thymeleaf.spring6.dialect.SpringStandardDialect;
 | 
			
		||||
import org.thymeleaf.spring6.expression.ThymeleafEvaluationContext;
 | 
			
		||||
import org.thymeleaf.templateresolver.StringTemplateResolver;
 | 
			
		||||
import org.thymeleaf.templateresource.ITemplateResource;
 | 
			
		||||
import org.thymeleaf.templateresource.StringTemplateResource;
 | 
			
		||||
import reactor.core.publisher.Mono;
 | 
			
		||||
import run.halo.app.core.extension.content.Post;
 | 
			
		||||
import run.halo.app.extension.Metadata;
 | 
			
		||||
import run.halo.app.infra.SystemConfigurableEnvironmentFetcher;
 | 
			
		||||
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.ModelConst;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Integration tests for {@link ContentTemplateHeadProcessor}.
 | 
			
		||||
 *
 | 
			
		||||
 * @author guqing
 | 
			
		||||
 * @see HaloProcessorDialect
 | 
			
		||||
 * @see GlobalHeadInjectionProcessor
 | 
			
		||||
 * @see ContentTemplateHeadProcessor
 | 
			
		||||
 * @see TemplateHeadProcessor
 | 
			
		||||
 * @see TemplateGlobalHeadProcessor
 | 
			
		||||
 * @see TemplateFooterElementTagProcessor
 | 
			
		||||
 * @since 2.7.0
 | 
			
		||||
 */
 | 
			
		||||
@ExtendWith(MockitoExtension.class)
 | 
			
		||||
class ContentTemplateHeadProcessorIntegrationTest {
 | 
			
		||||
    @Mock
 | 
			
		||||
    private ApplicationContext applicationContext;
 | 
			
		||||
 | 
			
		||||
    @Mock
 | 
			
		||||
    private PostFinder postFinder;
 | 
			
		||||
 | 
			
		||||
    @Mock
 | 
			
		||||
    private SinglePageFinder singlePageFinder;
 | 
			
		||||
 | 
			
		||||
    @Mock
 | 
			
		||||
    private SystemConfigurableEnvironmentFetcher fetcher;
 | 
			
		||||
 | 
			
		||||
    @Mock
 | 
			
		||||
    private ExtensionComponentsFinder extensionComponentsFinder;
 | 
			
		||||
 | 
			
		||||
    private TemplateEngine templateEngine;
 | 
			
		||||
 | 
			
		||||
    @BeforeEach
 | 
			
		||||
    void setUp() {
 | 
			
		||||
        HaloProcessorDialect haloProcessorDialect = new HaloProcessorDialect();
 | 
			
		||||
        templateEngine = new TemplateEngine();
 | 
			
		||||
        templateEngine.setDialects(Set.of(haloProcessorDialect, new SpringStandardDialect()));
 | 
			
		||||
        templateEngine.addTemplateResolver(new TestTemplateResolver());
 | 
			
		||||
 | 
			
		||||
        Map<String, TemplateHeadProcessor> map = new HashMap<>();
 | 
			
		||||
        map.put("postTemplateHeadProcessor",
 | 
			
		||||
            new ContentTemplateHeadProcessor(postFinder, singlePageFinder));
 | 
			
		||||
        map.put("templateGlobalHeadProcessor", new TemplateGlobalHeadProcessor(fetcher));
 | 
			
		||||
        map.put("seoProcessor", new GlobalSeoProcessor(fetcher));
 | 
			
		||||
        map.put("duplicateMetaTagProcessor", new DuplicateMetaTagProcessor());
 | 
			
		||||
        lenient().when(applicationContext.getBeansOfType(eq(TemplateHeadProcessor.class)))
 | 
			
		||||
            .thenReturn(map);
 | 
			
		||||
 | 
			
		||||
        SystemSetting.Seo seo = new SystemSetting.Seo();
 | 
			
		||||
        seo.setKeywords("global keywords");
 | 
			
		||||
        seo.setDescription("global description");
 | 
			
		||||
        lenient().when(fetcher.fetch(eq(SystemSetting.Seo.GROUP), eq(SystemSetting.Seo.class)))
 | 
			
		||||
            .thenReturn(Mono.just(seo));
 | 
			
		||||
 | 
			
		||||
        SystemSetting.CodeInjection codeInjection = new SystemSetting.CodeInjection();
 | 
			
		||||
        codeInjection.setGlobalHead(
 | 
			
		||||
            "<meta name=\"description\" content=\"global-head-description\"/>");
 | 
			
		||||
        codeInjection.setContentHead(
 | 
			
		||||
            "<meta name=\"description\" content=\"content-head-description\"/>");
 | 
			
		||||
        lenient().when(fetcher.fetch(eq(SystemSetting.CodeInjection.GROUP),
 | 
			
		||||
            eq(SystemSetting.CodeInjection.class))).thenReturn(Mono.just(codeInjection));
 | 
			
		||||
 | 
			
		||||
        lenient().when(applicationContext.getBean(eq(SystemConfigurableEnvironmentFetcher.class)))
 | 
			
		||||
            .thenReturn(fetcher);
 | 
			
		||||
        lenient().when(fetcher.fetch(eq(SystemSetting.Seo.GROUP), eq(SystemSetting.Seo.class)))
 | 
			
		||||
            .thenReturn(Mono.empty());
 | 
			
		||||
 | 
			
		||||
        lenient().when(applicationContext.getBean(eq(ExtensionComponentsFinder.class)))
 | 
			
		||||
            .thenReturn(extensionComponentsFinder);
 | 
			
		||||
 | 
			
		||||
        lenient().when(extensionComponentsFinder.getExtensions(eq(TemplateHeadProcessor.class)))
 | 
			
		||||
            .thenReturn(new ArrayList<>(map.values()));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    @Test
 | 
			
		||||
    void overrideGlobalMetaTest() {
 | 
			
		||||
        Context context = getContext();
 | 
			
		||||
        context.setVariable("name", "fake-post");
 | 
			
		||||
        // template id flag is used by TemplateGlobalHeadProcessor
 | 
			
		||||
        context.setVariable(ModelConst.TEMPLATE_ID, DefaultTemplateEnum.POST.getValue());
 | 
			
		||||
 | 
			
		||||
        List<Map<String, String>> htmlMetas = new ArrayList<>();
 | 
			
		||||
        htmlMetas.add(mutableMetaMap("keyword", "postK1,postK2"));
 | 
			
		||||
        htmlMetas.add(mutableMetaMap("description", "post-description"));
 | 
			
		||||
        htmlMetas.add(mutableMetaMap("other", "post-other-meta"));
 | 
			
		||||
        Post.PostSpec postSpec = new Post.PostSpec();
 | 
			
		||||
        postSpec.setHtmlMetas(htmlMetas);
 | 
			
		||||
        Metadata metadata = new Metadata();
 | 
			
		||||
        metadata.setName("fake-post");
 | 
			
		||||
        PostVo postVo = PostVo.builder().spec(postSpec).metadata(metadata).build();
 | 
			
		||||
        when(postFinder.getByName(eq("fake-post"))).thenReturn(Mono.just(postVo));
 | 
			
		||||
 | 
			
		||||
        String result = templateEngine.process("post", context);
 | 
			
		||||
        /*
 | 
			
		||||
          this test case shows:
 | 
			
		||||
            1. global seo meta keywords and description is overridden by content head meta
 | 
			
		||||
            2. global head meta is overridden by content head meta
 | 
			
		||||
            3. but global head meta is not overridden by global seo meta
 | 
			
		||||
         */
 | 
			
		||||
        assertThat(Jsoup.parse(result).html()).isEqualTo("""
 | 
			
		||||
           <!doctype html>
 | 
			
		||||
           <html lang="en">
 | 
			
		||||
            <head>
 | 
			
		||||
             <meta charset="UTF-8">
 | 
			
		||||
             <title>Post detail</title>
 | 
			
		||||
             <meta name="description" content="post-description">
 | 
			
		||||
             <meta name="keyword" content="postK1,postK2">
 | 
			
		||||
             <meta name="other" content="post-other-meta">
 | 
			
		||||
            </head>
 | 
			
		||||
            <body>
 | 
			
		||||
             this is body
 | 
			
		||||
            </body>
 | 
			
		||||
           </html>""");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    Map<String, String> mutableMetaMap(String nameValue, String contentValue) {
 | 
			
		||||
        Map<String, String> map = new HashMap<>();
 | 
			
		||||
        map.put("name", nameValue);
 | 
			
		||||
        map.put("content", contentValue);
 | 
			
		||||
        return map;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private Context getContext() {
 | 
			
		||||
        Context context = new Context();
 | 
			
		||||
        context.setVariable(
 | 
			
		||||
            ThymeleafEvaluationContext.THYMELEAF_EVALUATION_CONTEXT_CONTEXT_VARIABLE_NAME,
 | 
			
		||||
            new ThymeleafEvaluationContext(applicationContext, null));
 | 
			
		||||
        return context;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    static class TestTemplateResolver extends StringTemplateResolver {
 | 
			
		||||
        @Override
 | 
			
		||||
        protected ITemplateResource computeTemplateResource(IEngineConfiguration configuration,
 | 
			
		||||
            String ownerTemplate, String template,
 | 
			
		||||
            Map<String, Object> templateResolutionAttributes) {
 | 
			
		||||
            if (template.equals("post")) {
 | 
			
		||||
                return new StringTemplateResource(postTemplate());
 | 
			
		||||
            }
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private String postTemplate() {
 | 
			
		||||
            return """
 | 
			
		||||
                <!DOCTYPE html>
 | 
			
		||||
                <html lang="en" xmlns:th="http://www.thymeleaf.org">
 | 
			
		||||
                  <head>
 | 
			
		||||
                    <meta charset="UTF-8" />
 | 
			
		||||
                    <title>Post detail</title>
 | 
			
		||||
                  </head>
 | 
			
		||||
                  <body>
 | 
			
		||||
                    this is body
 | 
			
		||||
                  </body>
 | 
			
		||||
                </html>
 | 
			
		||||
                """;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -167,11 +167,11 @@ class HaloProcessorDialectTest {
 | 
			
		|||
              <head>
 | 
			
		||||
                <meta charset="UTF-8" />
 | 
			
		||||
                <title>Post</title>
 | 
			
		||||
              <meta content="post-meta-V1" name="post-meta-V1" />
 | 
			
		||||
              <meta name="global-head-test" content="test" />
 | 
			
		||||
            <meta name="content-head-test" content="test" />
 | 
			
		||||
            <meta content="post-meta-V1" name="post-meta-V1" />
 | 
			
		||||
            <meta content="post-meta-V2" name="post-meta-V2" />
 | 
			
		||||
            <meta name="description" content="" />
 | 
			
		||||
            <meta name="global-head-test" content="test" />
 | 
			
		||||
            <meta name="content-head-test" content="test" />
 | 
			
		||||
            </head>
 | 
			
		||||
              <body>
 | 
			
		||||
                <p>post</p>
 | 
			
		||||
| 
						 | 
				
			
			@ -203,9 +203,9 @@ class HaloProcessorDialectTest {
 | 
			
		|||
              <head>
 | 
			
		||||
                <meta charset="UTF-8" />
 | 
			
		||||
                <title>Seo Test</title>
 | 
			
		||||
              <meta name="global-head-test" content="test" />
 | 
			
		||||
              <meta name="robots" content="noindex" />
 | 
			
		||||
            <meta name="global-head-test" content="test" />
 | 
			
		||||
            <link rel="icon" href="favicon.ico" />
 | 
			
		||||
            <meta name="robots" content="noindex" />
 | 
			
		||||
            </head>
 | 
			
		||||
              <body>
 | 
			
		||||
                seo setting test.
 | 
			
		||||
| 
						 | 
				
			
			@ -234,10 +234,10 @@ class HaloProcessorDialectTest {
 | 
			
		|||
              <head>
 | 
			
		||||
                <meta charset="UTF-8" />
 | 
			
		||||
                <title>Seo Test</title>
 | 
			
		||||
              <meta name="global-head-test" content="test" />
 | 
			
		||||
            <link rel="icon" href="favicon.ico" />
 | 
			
		||||
            <meta name="keywords" content="K1, K2, K3" />
 | 
			
		||||
              <meta name="keywords" content="K1, K2, K3" />
 | 
			
		||||
            <meta name="description" content="This is a description." />
 | 
			
		||||
            <meta name="global-head-test" content="test" />
 | 
			
		||||
            <link rel="icon" href="favicon.ico" />
 | 
			
		||||
            </head>
 | 
			
		||||
              <body>
 | 
			
		||||
                seo setting test.
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -72,10 +72,7 @@ public class ThemeMessageResolverIntegrationTest {
 | 
			
		|||
            .isEqualTo("""
 | 
			
		||||
                <!DOCTYPE html>
 | 
			
		||||
                <html lang="en">
 | 
			
		||||
                <head>
 | 
			
		||||
                    <meta charset="UTF-8">
 | 
			
		||||
                    <title>Title</title>
 | 
			
		||||
                </head>
 | 
			
		||||
                <head><meta charset="UTF-8"><title>Title</title></head>
 | 
			
		||||
                <body>
 | 
			
		||||
                index
 | 
			
		||||
                <div>zh</div>
 | 
			
		||||
| 
						 | 
				
			
			@ -96,10 +93,7 @@ public class ThemeMessageResolverIntegrationTest {
 | 
			
		|||
            .isEqualTo("""
 | 
			
		||||
                <!DOCTYPE html>
 | 
			
		||||
                <html lang="en">
 | 
			
		||||
                <head>
 | 
			
		||||
                    <meta charset="UTF-8">
 | 
			
		||||
                    <title>Title</title>
 | 
			
		||||
                </head>
 | 
			
		||||
                <head><meta charset="UTF-8"><title>Title</title></head>
 | 
			
		||||
                <body>
 | 
			
		||||
                index
 | 
			
		||||
                <div>en</div>
 | 
			
		||||
| 
						 | 
				
			
			@ -120,10 +114,7 @@ public class ThemeMessageResolverIntegrationTest {
 | 
			
		|||
            .isEqualTo("""
 | 
			
		||||
                <!DOCTYPE html>
 | 
			
		||||
                <html lang="en">
 | 
			
		||||
                <head>
 | 
			
		||||
                    <meta charset="UTF-8">
 | 
			
		||||
                    <title>Title</title>
 | 
			
		||||
                </head>
 | 
			
		||||
                <head><meta charset="UTF-8"><title>Title</title></head>
 | 
			
		||||
                <body>
 | 
			
		||||
                index
 | 
			
		||||
                <div>foo</div>
 | 
			
		||||
| 
						 | 
				
			
			@ -144,10 +135,7 @@ public class ThemeMessageResolverIntegrationTest {
 | 
			
		|||
            .isEqualTo("""
 | 
			
		||||
                <!DOCTYPE html>
 | 
			
		||||
                <html lang="en">
 | 
			
		||||
                <head>
 | 
			
		||||
                    <meta charset="UTF-8">
 | 
			
		||||
                    <title>Title</title>
 | 
			
		||||
                </head>
 | 
			
		||||
                <head><meta charset="UTF-8"><title>Title</title></head>
 | 
			
		||||
                <body>
 | 
			
		||||
                index
 | 
			
		||||
                <div>zh</div>
 | 
			
		||||
| 
						 | 
				
			
			@ -166,10 +154,7 @@ public class ThemeMessageResolverIntegrationTest {
 | 
			
		|||
            .isEqualTo("""
 | 
			
		||||
                <!DOCTYPE html>
 | 
			
		||||
                <html lang="en">
 | 
			
		||||
                <head>
 | 
			
		||||
                    <meta charset="UTF-8">
 | 
			
		||||
                    <title>Other theme title</title>
 | 
			
		||||
                </head>
 | 
			
		||||
                <head><meta charset="UTF-8"><title>Other theme title</title></head>
 | 
			
		||||
                <body>
 | 
			
		||||
                <p>Other 首页</p>
 | 
			
		||||
                </body>
 | 
			
		||||
| 
						 | 
				
			
			@ -182,10 +167,7 @@ public class ThemeMessageResolverIntegrationTest {
 | 
			
		|||
            .isEqualTo("""
 | 
			
		||||
                <!DOCTYPE html>
 | 
			
		||||
                <html lang="en">
 | 
			
		||||
                <head>
 | 
			
		||||
                    <meta charset="UTF-8">
 | 
			
		||||
                    <title>Other theme title</title>
 | 
			
		||||
                </head>
 | 
			
		||||
                <head><meta charset="UTF-8"><title>Other theme title</title></head>
 | 
			
		||||
                <body>
 | 
			
		||||
                <p>other index</p>
 | 
			
		||||
                </body>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue