Skip to content

Commit

Permalink
refactor: content page meta tags now override global injected (#4069)
Browse files Browse the repository at this point in the history
#### 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 标签重复问题
```
  • Loading branch information
guqing committed Jun 28, 2023
1 parent 8db4cec commit 972ebed
Show file tree
Hide file tree
Showing 9 changed files with 321 additions and 39 deletions.
Expand Up @@ -8,6 +8,9 @@

/**
* 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
Expand Down
Expand Up @@ -9,6 +9,7 @@
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;
Expand All @@ -29,6 +30,7 @@
* @since 2.0.0
*/
@Component
@Order(1)
@AllArgsConstructor
public class ContentTemplateHeadProcessor implements TemplateHeadProcessor {
private static final String POST_NAME_VARIABLE = "name";
Expand Down
@@ -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) {
}
}
Expand Up @@ -2,9 +2,10 @@

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;
Expand Down Expand Up @@ -51,14 +52,23 @@ protected void doProcess(ITemplateContext context, IModel model,
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 =
Expand All @@ -69,14 +79,20 @@ protected void doProcess(ITemplateContext context, IModel model,
.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();
}
}
Expand Up @@ -3,6 +3,7 @@
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;
Expand All @@ -20,7 +21,7 @@
* @see SystemSetting.Seo
* @since 2.0.0
*/
@Order
@Order(Ordered.HIGHEST_PRECEDENCE + 1)
@Component
@AllArgsConstructor
public class GlobalSeoProcessor implements TemplateHeadProcessor {
Expand Down
@@ -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;
Expand All @@ -14,10 +16,12 @@

/**
* <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 {

Expand Down

0 comments on commit 972ebed

Please sign in to comment.