1. 为什么单文件 application.yml 迟早会变成维护噩梦刚接手一个跑了两年多的 Spring Boot 项目时我打开src/main/resources/application.yml的第一反应是——这文件怎么比我的工位还乱。三百多行配置开发环境的数据库连接、测试环境的 Redis 地址、生产环境的消息队列参数全挤在一起靠注释和spring.profiles.active来回切换。每次发版前团队里总有人忘记改回正确的配置或者把测试环境的密钥误提交到主干分支然后一群人围着日志排查半天。这个场景太典型了。单文件配置在项目初期确实省事一个文件搞定所有环境改起来也直观。但项目一旦进入多环境并行阶段——本地开发、联调测试、预发布验证、正式生产——单文件的弊端就会集中爆发。最核心的问题不是配置多而是配置的边界模糊了哪些是公共配置、哪些是环境专属、哪些是敏感信息全混在一个文件里没有任何物理隔离。人脑不是编译器靠肉眼区分这些边界出错只是时间问题。我后来梳理了一下单文件配置主要带来四类麻烦。第一类是误改风险开发同学本地调试时顺手改了个端口提交时没注意直接把测试环境的端口带偏了。第二类是合并冲突多人同时维护不同环境的配置Git 合并时经常出现大段冲突解决起来极其痛苦。第三类是敏感信息泄露生产环境的数据库密码写在主配置文件里跟着代码一起进了版本库这几乎是安全审计的必查项。第四类是可读性下降新人接手项目时面对几百行配置根本分不清哪些生效、哪些是历史遗留。所以拆分配置不是为了优雅而优雅而是项目规模上来之后的必然选择。Spring Boot 本身对多环境配置是有原生支持的application-{profile}.yml这套命名约定就是官方给的方案。但光靠 Spring Boot 的 profile 机制还不够因为哪个 profile 生效这件事在打包阶段和运行阶段是两个不同的决策点。Maven 的 profile 机制恰好能补上打包阶段这一环两者配合才能做到打包时决定环境、运行时零手动干预。这篇内容适合两类人看一类是正在被单文件配置折磨、想动手拆分但不知道从哪下手的开发者另一类是想搞清楚 Maven profile 和 Spring profile 到底怎么协作、为什么这么设计的中级工程师。我会从文件结构设计讲到 Maven 配置细节再到实际踩过的坑尽量把每一步的为什么说透而不是只丢一堆 XML 让你抄。2. 拆分的核心思路Maven profile 管打包Spring profile 管运行在动手改文件之前得先把一个概念理清楚否则后面配置起来会一直犯迷糊Maven 的 profile 和 Spring Boot 的 profile 是两个层面的东西各管各的不能混为一谈。Maven 的 profile 是构建期的概念。它决定的是打包时把哪些文件放进最终的 jar 包里。比如你打一个测试环境的包Maven 会根据激活的 profile把application-test.yml复制到target/classes目录下同时可能替换掉一些占位符。这个动作发生在mvn package阶段产物一旦生成环境就固定了。Spring Boot 的 profile 是运行期的概念。它决定的是应用启动时加载哪份配置。通过spring.profiles.active指定Spring 会去找对应的application-{profile}.yml并加载。这个动作发生在java -jar启动阶段。两者配合的逻辑是这样的Maven 在打包时根据你指定的 profile把对应环境的配置文件塞进 jar 包同时通过资源过滤把spring.profiles.active的值写死到主配置里。这样打出来的包启动时不需要任何额外参数Spring 自动就知道该加载哪份配置。这就是所谓的多环境动态启用——动态体现在打包时可以选择启用体现在运行时自动生效。我见过不少人把这两层搞混结果配出来的东西要么打包时没生效要么启动时报找不到 profile。举个常见的错误有人在pom.xml里配了 Maven profile也在application.yml里写了spring.profiles.active: profiles.active但忘了在build里开启资源过滤结果profiles.active这个占位符原封不动地留在了 jar 包里启动时 Spring 把它当成一个叫profiles.active的 profile 去找自然找不到。这个坑后面会详细讲。理解了分层文件结构的设计就顺理成章了。我的做法是把配置拆成三类文件文件类型命名示例职责是否进版本库主配置文件application.yml公共配置 profile 激活占位符是环境配置文件application-dev.yml、application-test.yml、application-prod.yml各环境专属配置是敏感值用占位符本地覆盖文件application-local.yml个人本地调试用否加入 .gitignore主配置文件只放所有环境共用的东西比如应用名称、编码格式、MyBatis 的通用配置。环境配置文件放数据库地址、Redis 地址、日志级别这类随环境变化的项。本地覆盖文件给每个人自己用比如你本地想连一个特殊的数据库或者想开 debug 日志就写在application-local.yml里这个文件不提交避免污染团队配置。这里有个细节值得说一下为什么主配置文件里要放spring.profiles.active的占位符而不是直接写死因为如果写死成dev那打生产包的时候还得手动改这个文件又回到了手动改配置的老路。用 Maven 的占位符profiles.active打包时由 Maven 根据激活的 profile 自动替换才能真正做到打包即定环境。这个占位符的写法也有讲究Spring Boot 2.4 之后推荐用...而不是${...}因为${...}会和 Spring 自己的属性占位符冲突导致解析顺序混乱。3. pom.xml 里的关键配置资源过滤与 profile 定义文件结构定好了接下来是pom.xml的配置。这块是整个方案的技术核心配错一个标签整个链路就断了。我把它拆成两个部分来讲profile 的定义以及资源过滤的开启。3.1 Maven profile 的定义与激活方式在pom.xml的profiles节点下为每个环境定义一个 profile。每个 profile 里最关键的是properties中的自定义属性比如profiles.active它的值就是最终要写进spring.profiles.active的环境标识。profiles profile iddev/id properties profiles.activedev/profiles.active /properties activation activeByDefaulttrue/activeByDefault /activation /profile profile idtest/id properties profiles.activetest/profiles.active /properties /profile profile idprod/id properties profiles.activeprod/profiles.active /properties /profile /profiles这里有几个设计决策需要解释。第一id的值我建议和环境标识保持一致比如dev、test、prod这样mvn package -P dev和spring.profiles.activedev用的是同一个词减少心智负担。第二activeByDefaulttrue/activeByDefault只放在dev上意思是不指定 profile 时默认打开发包。这个设置很实用因为日常开发中大部分打包都是开发环境省得每次都加-P参数。但要注意一旦你显式指定了任何其他 profileactiveByDefault就会失效这是 Maven 的既定行为不是 bug。第三profiles.active这个属性名可以自定义但建议别用spring.profiles.active这种带点的名字虽然技术上可行但容易和 Spring 的属性解析混淆用profiles.active这种简洁的命名更清晰。激活方式有三种实际项目中都会用到命令行激活mvn clean package -P prod这是 CI/CD 流水线里最常用的方式明确指定环境。默认激活不指定-P时走activeByDefault的dev适合本地快速打包。设置激活在settings.xml里配activeProfiles适合固定工作机的场景但团队协作时不推荐因为每个人的settings.xml不一样容易造成我这里能打包你那里不行的问题。提示如果同时激活了多个 profileMaven 会按声明顺序合并属性后面的覆盖前面的。所以别让dev和prod同时激活否则结果不可预期。CI 脚本里务必显式指定-P不要依赖默认值。3.2 资源过滤让占位符真正被替换profile 定义好了但 Maven 默认不会去替换application.yml里的...占位符。必须显式开启资源过滤告诉 Maven这个目录下的文件需要做变量替换。build resources resource directorysrc/main/resources/directory filteringtrue/filtering includes includeapplication.yml/include includeapplication-*.yml/include /includes /resource /resources /build这段配置的意图很明确只对application.yml和application-*.yml开启过滤其他资源文件比如静态资源、字体文件不处理。为什么要限制范围因为资源过滤本质上是把文件当文本读进来做字符串替换如果对二进制文件如图片、字体开启过滤Maven 会尝试用文本方式读取可能导致文件损坏。我早期图省事直接对整个resources目录开filtering结果项目里的一个.woff字体文件被打包后加载失败排查了大半天才发现是过滤把二进制内容搞坏了。这个教训值得记一下。另外includes里用application-*.yml通配符能覆盖所有环境配置文件。如果你还有bootstrap.yml用了配置中心的项目会有也要加进去。过滤开启后application.yml里的profiles.active就会在打包时被替换成当前激活 profile 的profiles.active属性值。这里补充一个容易忽略的点Spring Boot 的spring-boot-starter-parent默认对src/main/resources开启了过滤但它用的是${...}语法。如果你用的是...语法需要在properties里加一行resource.delimiter/resource.delimiter把分隔符改成。不加这行的话profiles.active不会被识别。这个配置在 Spring Boot 2.x 的 parent 里是默认配好的但如果你没继承 parent比如自己管理依赖版本就得手动加。4. 配置文件的实际写法与占位符陷阱pom 配好之后回到配置文件本身。主配置文件application.yml的写法直接决定了运行时能不能正确加载环境配置。spring: application: name: demo-service profiles: active: profiles.active jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8 server: servlet: encoding: charset: UTF-8 force: true注意spring.profiles.active的值是profiles.active这个占位符在打包时会被 Maven 替换。比如打prod包最终 jar 里的application.yml这一行就变成了active: prod。启动时 Spring 读到prod就会去加载application-prod.yml。环境配置文件application-prod.yml里只放该环境专属的配置spring: datasource: url: jdbc:mysql://prod-db-host:3306/demo?useUnicodetruecharacterEncodingutf8 username: ${DB_USERNAME} password: ${DB_PASSWORD} redis: host: prod-redis-host port: 6379 logging: level: root: warn com.example.demo: info这里我特意把数据库的用户名和密码写成了${DB_USERNAME}这种形式而不是明文。这是第二层防护即使配置文件进了版本库敏感信息也不在里面实际值通过环境变量或启动参数注入。生产环境的密码管理是另一个话题但至少配置文件层面要做到不含明文密钥。开发环境的application-dev.yml就可以宽松一些直接写本地地址方便新人拉下来就能跑。4.1 占位符不生效的三类典型原因占位符这块是踩坑重灾区我整理了三类最常见的问题基本覆盖了 90% 的配置没生效场景。第一类是资源过滤没开或范围不对。表现是 jar 包里的application.yml里还留着profiles.active原样。排查方法很简单打完包后解压 jarunzip -p target/demo.jar BOOT-INF/classes/application.yml直接看里面的内容。如果占位符还在就是过滤没生效。检查filteringtrue/filtering有没有加、includes有没有漏掉主配置文件。第二类是分隔符不匹配。用了...语法但没配resource.delimiter或者反过来用了${...}但和 Spring 的属性占位符打架。判断方法如果打包时报Failed to replace properties之类的警告或者替换后值不对先检查分隔符配置。Spring Boot 项目继承 parent 的话默认是自己管依赖的话要手动加。第三类是profile 没激活对。命令行-P prod拼写错了或者 CI 脚本里变量没传进去导致实际激活的是默认的dev。表现是打出来的包连的是开发库。排查时在pom.xml里临时加个maven-antrun-plugin打印${profiles.active}的值或者直接看打包日志里Building demo 1.0.0后面跟的 profile 列表。注意IDEA 里直接点运行按钮启动时走的是 IDEA 自己的编译流程不一定触发 Maven 的资源过滤。所以本地用 IDEA 跑的时候profiles.active可能不会被替换导致启动失败。解决办法是在 IDEA 的 Run Configuration 里加 VM 参数-Dspring.profiles.activedev或者装一个能触发 Maven 处理的插件。这个坑我踩过不止一次每次换新同事都要解释一遍。4.2 环境配置文件的加载顺序与覆盖规则Spring Boot 加载配置是有优先级的理解这个顺序能帮你避免明明配了却不生效的困惑。默认情况下application.yml主配置先加载然后application-{profile}.yml后加载后者会覆盖前者的同名属性。所以公共配置放主文件环境差异放 profile 文件这个分层是符合加载顺序的。但有个细节如果你在application-dev.yml里也写了spring.profiles.active它会覆盖主文件里的值。这有时候会被用来做profile 嵌套比如dev环境再激活一个local子 profile。但这种用法容易让配置链路变得难以追踪我的建议是只在主配置文件里写一次spring.profiles.active环境文件里不要再碰这个属性保持单一入口。另外application-local.yml这种本地覆盖文件的加载需要在启动时额外指定-Dspring.profiles.activedev,local用逗号分隔多个 profile后面的覆盖前面的。这样团队共享的dev配置和你个人的local覆盖就能叠加生效互不干扰。5. 从踩坑到跑通一次完整的排查链路复盘配置这东西看文档觉得挺简单真动手时总会在某个环节卡住。我把自己第一次实施这套方案时踩的坑完整复盘一遍你可以对照着排查自己的问题。当时的场景是项目从单文件拆成多文件pom 里配了三个 profile主配置写了profiles.active资源过滤也开了。本地用mvn clean package -P test打包一切正常。但推到 CI 流水线后打出来的包启动就报错日志里写着No active profile set, falling back to default profiles: default然后连的是本地地址直接连不上数据库。第一步排查先确认包里的配置长什么样。解压 jar 看application.yml发现spring.profiles.active的值是空的占位符被替换成了空字符串。这说明资源过滤生效了占位符被处理了但profiles.active这个属性的值是空的。第二步怀疑 profile 没激活。看 CI 日志打包命令是mvn clean package -Ptest注意这里-P和test之间没有空格。Maven 对-P test和-Ptest都支持理论上没问题。但日志里没有打印激活的 profile 列表无法确认。第三步在 pom 里临时加了一段打印逻辑用maven-help-plugin的active-profilesgoal输出当前激活的 profile。结果发现激活的是dev默认 profile不是test。问题定位到了CI 脚本里的-Ptest没生效。第四步深挖为什么没生效。查 CI 配置发现打包命令被包在一个 shell 脚本里脚本里写的是mvn clean package -P ${PROFILE}而${PROFILE}这个环境变量在 CI 的某个阶段被清空了导致实际执行的是mvn clean package -P-P后面没参数Maven 就回退到默认 profile。这就是典型的变量没传进去问题。修复方案很简单在脚本里加个默认值兜底PROFILE${PROFILE:-dev}并且在打包前echo Building with profile: $PROFILE打印出来方便后续排查。改完之后重新跑流水线包里的spring.profiles.active正确变成了test启动正常。这次排查给我的经验是配置链路越长越要在中间环节加探针。从命令行参数到 Maven 属性再到资源过滤最后到 Spring 加载每一环都可能断。与其猜不如在关键节点打印实际值。打包日志里打印激活的 profile、解压 jar 看最终配置内容这两个动作花不了两分钟但能省下大量瞎猜的时间。还有一个衍生坑值得提有次打包一切正常但启动后某些配置没生效。查下来是application-prod.yml里有个 YAML 缩进错误导致某个配置项被解析到了错误的层级。YAML 对缩进极其敏感多一个空格少一个空格结果完全不同。我的习惯是改完 YAML 后用在线校验工具过一遍或者本地先mvn spring-boot:run -Dspring-boot.run.profilesprod跑一下确认能正常启动再提交。别等到 CI 上才发现那时候排查成本高得多。6. 多环境方案落地后的团队协作约定技术方案跑通只是第一步真正让这套东西发挥价值的是团队协作层面的约定。我见过不少团队技术上配好了多环境但实际用起来还是一团乱根子在于没有约定谁能改什么、什么能进版本库。第一条约定环境配置文件里的敏感值一律用占位符明文只存在于部署环境。application-prod.yml里数据库密码写${DB_PASSWORD}实际值由部署平台的环境变量注入。这条约定需要配合代码审查提交时看到明文密码直接打回。我建议在 CI 里加一个简单的 grep 检查扫描配置文件里有没有疑似密码的明文比如匹配password:后面跟非占位符的值命中就阻断构建。这个检查写起来就几行 shell但能挡住大部分低级失误。第二条约定application-local.yml必须进.gitignore。这个文件是个人调试用的每个人的内容都不一样提交上去只会制造冲突。新人入职时在 README 里放一份application-local.yml.example模板复制改名就能用。这个模板里把常用的本地配置项列出来注释说明每项的作用降低上手成本。第三条约定新增配置项时先想清楚归属。是公共配置还是环境专属公共的放主文件专属的放对应环境文件。如果拿不准问自己一句这个值在生产和测试环境会不一样吗会就放环境文件不会就放主文件。这个判断标准简单粗暴但有效能避免大部分配置放错位置的问题。第四条约定打包命令统一走脚本不手敲。团队里每个人手敲mvn package -P xxx迟早有人敲错。我习惯在项目根目录放一个build.sh封装好打包逻辑参数校验、profile 打印、产物路径提示都做进去。CI 和本地都用同一个脚本行为一致减少我这里能打你那里不能打的扯皮。#!/bin/bash PROFILE${1:-dev} case $PROFILE in dev|test|prod) ;; *) echo Invalid profile: $PROFILE. Use dev|test|prod; exit 1 ;; esac echo Building with profile: $PROFILE mvn clean package -P $PROFILE -DskipTests echo Artifact: target/*.jar这个脚本没什么技术含量但把参数校验和过程可见两件事做了实际用起来省心不少。尤其是参数校验那几行能挡住手滑敲错 profile 名的情况——敲错了直接报错退出而不是默默用默认 profile 打出一个错误的包。最后分享一个我在实际项目中体会比较深的小技巧给每个环境的配置文件加一行注释头写明该文件的用途和修改注意事项。比如application-prod.yml开头写清楚生产环境配置敏感值用环境变量注入修改需走变更流程。这行注释不参与任何逻辑但能在有人想直接改这个文件时起到提醒作用。配置文件是团队共享的资产多写一句说明就少一次误操作。这套方案落地大半年后我们团队因为配置问题导致的线上事故基本归零发版前的配置检查也从原来的十几分钟缩短到几分钟收益是实打实的。
企业数字化 ERP 产品动态
相关推荐
Paperclip:面向前端的轻量级AI Agent运行时框架 1. 这不是回形针,是AI时代前端工程师的“新扳手”你搜“paperclip”,第一反应可能是办公桌抽屉里那个银色小金属件——但最近半年,在GitHub趋势榜、React社区讨论帖、Node.js技术分享会现场,这个词频繁出现在“AI agents”“开源框… · 2026/11/3 7:02:52
ClickHouse Schema修改引发的Merge阻塞问题解析 1. ClickHouse Schema修改引发的Merge阻塞问题深度解析作为一名长期与ClickHouse打交道的工程师,我最近遇到一个由Schema修改引发的Merge阻塞问题,整个过程堪称教科书级的分布式系统故障排查案例。本文将详细记录从问题发现到最终解决的完整过程… · 2026/11/3 7:02:52
风机状态测试系统:从传感器到效率曲线的自动换算与避坑指南 简介:这份资源是面向机电一体化、自动化及相关专业学生与工程技术人员的毕业设计文档,聚焦风机状态测试系统的总体设计,解决传统手工检测手段落后、劳动量大、读数与计算误差偏高等问题。文档围绕虚拟仪器技术展开,涵盖风机性能测… · 2026/11/3 7:43:00
OpenClaw:Windows平台轻量级自动化工具使用指南 1. OpenClaw 项目概述OpenClaw 是一款基于 Windows 平台的轻量级自动化工具集,主要用于简化日常重复性任务的操作流程。它通过模块化设计将常见操作封装为可复用的"爪子"(Claw),让用户能够像搭积木一样组合各种功能单元… · 2026/11/3 7:43:00
编程Agent评测复测:六种假通过陷阱与流程重建 1. 那场让我重新审视评测方法的复测去年年底,我所在的团队接了一个内部任务:给一批编程 Agent 做一轮能力摸底,看看它们在实际代码仓库里到底能不能干活。任务本身不复杂,选几个主流模型,跑一套标准化的编程任务集&… · 2026/11/3 7:43:00
Decoder-only Transformer:为什么AI圈都押注“只剩解码器”的架构 Decode-Only Transformer:为什么整个AI圈最后都押注在"只剩解码器"的架构上2020年我刚接触Transformer时,周围人讨论最多的是BERT,面试官最爱追问"CLS向量到底该怎么用"。风向变得比想象中快,如今GPT系列、LL… · 2026/11/3 7:43:00
CNN与DCNN本质关系:深度卷积网络的技术演进逻辑 1. 这不是两套并列模型,而是演进关系里的“同一支笔的两种写法”很多人第一次看到“卷积神经网络(CNN)”和“深度卷积神经网络(DCNN)”这两个词并列出现时,下意识会以为是两种不同架构、像ResNet和VGG那样可… · 2026/11/3 7:43:00
GENCODE基因注释:生物信息学分析的基准标准与实战指南 1. 项目概述:为什么“gencode”不是另一个基因数据库,而是生物信息学里的“标准尺子”如果你刚接触转录组分析、RNA-seq比对、差异表达或功能富集,大概率已经在某个配置文件里见过GRCh38.p14和gencode.v44.primary_assembly.annotation.gtf这… · 2026/11/3 7:42:00
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 13:10:28
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 6:04:24
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 5:52:46
STM32F411RE与MAX14431实现SPI隔离:工业现场抗共模干扰实战 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 1:01:59
ESP32+大模型AI硬件开发:从Demo到产品的8个工程问题 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 1:01:59
基于MAX14431BAEE+与PIC18F85J10的SPI隔离方案设计与实践 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 1:01:59
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/2 13:10:28
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 6:04:24
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/3 5:52:46