mirror of https://github.com/halo-dev/halo
Add generator meta into head (#4821)
#### What type of PR is this? /kind feature /area core /milestone 2.11.x #### What this PR does / why we need it: Please see https://html.spec.whatwg.org/multipage/semantics.html#meta-generator for more. This PR add the generator meta into head, so that we can know what sites are using Halo. ```bash http localhost:8090/ | grep generator <meta name="generator" content="Halo 2.11.0-SNAPSHOT"/> ``` If someone want to disable the generator meta, they can configure the property `halo.theme.generator-meta-disabled` to `true`. #### Does this PR introduce a user-facing change? ```release-note 添加 Generator 元数据标识 ```pull/4844/head
parent
caa4d44907
commit
4ea20142f5
|
@ -9,6 +9,11 @@ public class ThemeProperties {
|
|||
@Valid
|
||||
private final Initializer initializer = new Initializer();
|
||||
|
||||
/**
|
||||
* Indicates whether the generator meta needs to be disabled.
|
||||
*/
|
||||
private boolean generatorMetaDisabled;
|
||||
|
||||
@Data
|
||||
public static class Initializer {
|
||||
|
||||
|
|
|
@ -8,7 +8,10 @@ import java.io.IOException;
|
|||
import java.nio.file.Path;
|
||||
import java.time.Instant;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.autoconfigure.web.WebProperties;
|
||||
import org.springframework.boot.info.BuildProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.io.FileSystemResource;
|
||||
|
@ -21,8 +24,10 @@ import org.thymeleaf.extras.springsecurity6.dialect.SpringSecurityDialect;
|
|||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.infra.ThemeRootGetter;
|
||||
import run.halo.app.infra.utils.FileUtils;
|
||||
import run.halo.app.theme.dialect.GeneratorMetaProcessor;
|
||||
import run.halo.app.theme.dialect.HaloSpringSecurityDialect;
|
||||
import run.halo.app.theme.dialect.LinkExpressionObjectDialect;
|
||||
import run.halo.app.theme.dialect.TemplateHeadProcessor;
|
||||
|
||||
/**
|
||||
* @author guqing
|
||||
|
@ -86,4 +91,12 @@ public class ThemeConfiguration {
|
|||
ServerSecurityContextRepository securityContextRepository) {
|
||||
return new HaloSpringSecurityDialect(securityContextRepository);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnProperty(name = "halo.theme.generator-meta-disabled",
|
||||
havingValue = "false",
|
||||
matchIfMissing = true)
|
||||
TemplateHeadProcessor generatorMetaProcessor(ObjectProvider<BuildProperties> buildProperties) {
|
||||
return new GeneratorMetaProcessor(buildProperties);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
package run.halo.app.theme.dialect;
|
||||
|
||||
import static org.thymeleaf.model.AttributeValueQuotes.DOUBLE;
|
||||
|
||||
import java.util.Map;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.boot.info.BuildProperties;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.thymeleaf.context.ITemplateContext;
|
||||
import org.thymeleaf.model.IModel;
|
||||
import org.thymeleaf.processor.element.IElementModelStructureHandler;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/**
|
||||
* Processor for generating generator meta.
|
||||
* Set the order to 0 for removing the meta in later TemplateHeadProcessor.
|
||||
*
|
||||
* @author johnniang
|
||||
*/
|
||||
@Order(0)
|
||||
public class GeneratorMetaProcessor implements TemplateHeadProcessor {
|
||||
|
||||
private final String generatorValue;
|
||||
|
||||
public GeneratorMetaProcessor(ObjectProvider<BuildProperties> buildProperties) {
|
||||
this.generatorValue = "Halo " + buildProperties.stream().findFirst()
|
||||
.map(BuildProperties::getVersion)
|
||||
.orElse("Unknown");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> process(ITemplateContext context, IModel model,
|
||||
IElementModelStructureHandler structureHandler) {
|
||||
return Mono.fromRunnable(() -> {
|
||||
var modelFactory = context.getModelFactory();
|
||||
var generatorMeta = modelFactory.createStandaloneElementTag("meta",
|
||||
Map.of("name", "generator", "content", generatorValue),
|
||||
DOUBLE, false, true);
|
||||
model.add(generatorMeta);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
package run.halo.app.theme.dialect;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.file.Path;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||
import org.springframework.util.ResourceUtils;
|
||||
import org.springframework.web.server.ServerWebExchange;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.infra.InitializationStateGetter;
|
||||
import run.halo.app.theme.ThemeContext;
|
||||
import run.halo.app.theme.ThemeResolver;
|
||||
|
||||
@SpringBootTest
|
||||
@AutoConfigureWebTestClient
|
||||
class GeneratorMetaProcessorTest {
|
||||
|
||||
@Autowired
|
||||
WebTestClient webClient;
|
||||
|
||||
@MockBean
|
||||
InitializationStateGetter initializationStateGetter;
|
||||
|
||||
@MockBean
|
||||
ThemeResolver themeResolver;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws FileNotFoundException, URISyntaxException {
|
||||
when(initializationStateGetter.userInitialized()).thenReturn(Mono.just(true));
|
||||
var themeContext = ThemeContext.builder()
|
||||
.name("default")
|
||||
.path(Path.of(ResourceUtils.getURL("classpath:themes/default").toURI()))
|
||||
.active(true)
|
||||
.build();
|
||||
when(themeResolver.getTheme(any(ServerWebExchange.class)))
|
||||
.thenReturn(Mono.just(themeContext));
|
||||
}
|
||||
|
||||
@Test
|
||||
void requestIndexPage() {
|
||||
webClient.get().uri("/")
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody()
|
||||
.consumeWith(System.out::println)
|
||||
.xpath("/html/head/meta[@name=\"generator\"][starts-with(@content, \"Halo \")]")
|
||||
.exists();
|
||||
}
|
||||
|
||||
}
|
|
@ -68,21 +68,9 @@ public class ThemeMessageResolverIntegrationTest {
|
|||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk()
|
||||
.expectBody(String.class)
|
||||
.isEqualTo("""
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Title</title>
|
||||
</head>
|
||||
<body>
|
||||
index
|
||||
<div>zh</div>
|
||||
<div>欢迎来到首页</div>
|
||||
</body>
|
||||
</html>
|
||||
""");
|
||||
.expectBody()
|
||||
.xpath("/html/body/div[1]").isEqualTo("zh")
|
||||
.xpath("/html/body/div[2]").isEqualTo("欢迎来到首页");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -92,21 +80,9 @@ public class ThemeMessageResolverIntegrationTest {
|
|||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk()
|
||||
.expectBody(String.class)
|
||||
.isEqualTo("""
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Title</title>
|
||||
</head>
|
||||
<body>
|
||||
index
|
||||
<div>en</div>
|
||||
<div>Welcome to the index</div>
|
||||
</body>
|
||||
</html>
|
||||
""");
|
||||
.expectBody()
|
||||
.xpath("/html/body/div[1]").isEqualTo("en")
|
||||
.xpath("/html/body/div[2]").isEqualTo("Welcome to the index");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -116,21 +92,9 @@ public class ThemeMessageResolverIntegrationTest {
|
|||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk()
|
||||
.expectBody(String.class)
|
||||
.isEqualTo("""
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Title</title>
|
||||
</head>
|
||||
<body>
|
||||
index
|
||||
<div>foo</div>
|
||||
<div>欢迎来到首页</div>
|
||||
</body>
|
||||
</html>
|
||||
""");
|
||||
.expectBody()
|
||||
.xpath("/html/body/div[1]").isEqualTo("foo")
|
||||
.xpath("/html/body/div[2]").isEqualTo("欢迎来到首页");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -140,21 +104,11 @@ public class ThemeMessageResolverIntegrationTest {
|
|||
.exchange()
|
||||
.expectStatus()
|
||||
.isOk()
|
||||
.expectBody(String.class)
|
||||
.isEqualTo("""
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Title</title>
|
||||
</head>
|
||||
<body>
|
||||
index
|
||||
<div>zh</div>
|
||||
<div>欢迎来到首页</div>
|
||||
</body>
|
||||
</html>
|
||||
""");
|
||||
.expectBody()
|
||||
.xpath("/html/head/title").isEqualTo("Title")
|
||||
.xpath("/html/body/div[1]").isEqualTo("zh")
|
||||
.xpath("/html/body/div[2]").isEqualTo("欢迎来到首页")
|
||||
;
|
||||
|
||||
// For other theme
|
||||
when(themeResolver.getTheme(any(ServerWebExchange.class)))
|
||||
|
@ -162,35 +116,16 @@ public class ThemeMessageResolverIntegrationTest {
|
|||
webTestClient.get()
|
||||
.uri("/index?language=zh")
|
||||
.exchange()
|
||||
.expectBody(String.class)
|
||||
.isEqualTo("""
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Other theme title</title>
|
||||
</head>
|
||||
<body>
|
||||
<p>Other 首页</p>
|
||||
</body>
|
||||
</html>
|
||||
""");
|
||||
.expectBody()
|
||||
.xpath("/html/head/title").isEqualTo("Other theme title")
|
||||
.xpath("/html/body/p").isEqualTo("Other 首页");
|
||||
|
||||
webTestClient.get()
|
||||
.uri("/index?language=en")
|
||||
.exchange()
|
||||
.expectBody(String.class)
|
||||
.isEqualTo("""
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Other theme title</title>
|
||||
</head>
|
||||
<body>
|
||||
<p>other index</p>
|
||||
</body>
|
||||
</html>
|
||||
""");
|
||||
.expectBody()
|
||||
.xpath("/html/head/title").isEqualTo("Other theme title")
|
||||
.xpath("/html/body/p").isEqualTo("other index");
|
||||
}
|
||||
|
||||
ThemeContext createDefaultContext() throws URISyntaxException {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta charset="UTF-8"/>
|
||||
<title>Title</title>
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta charset="UTF-8"/>
|
||||
<title>Other theme title</title>
|
||||
</head>
|
||||
<body>
|
||||
|
|
Loading…
Reference in New Issue