@@ -48,7 +48,8 @@ object Main extends CommandIOApp("build", "builds the site") {
4848
4949 def main = opts.map {
5050 case Subcommand .Build (destination) =>
51- Files [IO ].deleteRecursively(destination).voidError *>
51+ IO .println(s " Building to: $destination" ) *>
52+ Files [IO ].deleteRecursively(destination).voidError *>
5253 LaikaBuild .build(FilePath .fromFS2Path(destination)).as(ExitCode .Success )
5354
5455 case Subcommand .Serve (port) =>
@@ -85,12 +86,23 @@ object LaikaBuild {
8586 " https://raw.githubusercontent.com/typelevel/.github/refs/heads/main/SECURITY.md"
8687 ).toURL()
8788
89+ val metadataContent = scala.util
90+ .Try (
91+ java.nio.file.Files .readString(
92+ java.nio.file.Paths .get(" target/search/metadata.json" )
93+ )
94+ )
95+ .getOrElse(" {}" )
8896 InputTree [IO ]
8997 .addDirectory(" src" )
9098 // Laika skips .dotfiles by default
9199 .addDirectory(" src/.well-known" , Path .Root / " .well-known" )
100+ .addString(metadataContent, Path .Root / " search" / " metadata.json" )
92101 .addInputStream(
93- IO .blocking(securityPolicy.openStream()),
102+ IO .blocking(securityPolicy.openStream()).handleErrorWith { _ =>
103+ IO .println(" fetching security.md failed, skipping." ) *>
104+ IO .pure(new java.io.ByteArrayInputStream (Array .emptyByteArray))
105+ },
94106 Path .Root / " security.md"
95107 )
96108 .addClassResource[this .type ](
@@ -163,10 +175,72 @@ object LaikaBuild {
163175 index
164176 .from(tree)
165177 .toFile(destination / " search" / " searchIndex.idx" )
166- .render
178+ .render *>
179+ writeMetadata(tree.root, destination)
167180 }
168181 }
169182 }
183+
184+ private def writeMetadata (
185+ tree : DocumentTreeRoot ,
186+ destination : FilePath
187+ ): IO [Unit ] = {
188+ import java .time .OffsetDateTime
189+
190+ case class PostMetadata (
191+ path : String ,
192+ authors : Seq [String ],
193+ tags : Seq [String ],
194+ date : String
195+ )
196+
197+ // walk all documents, keep only ones with a date field (blog posts + events)
198+ val posts : Seq [PostMetadata ] = tree.allDocuments.flatMap { doc =>
199+ doc.config.get[OffsetDateTime ](" date" ).toOption.map { date =>
200+ val path = " /" + doc.path.toString
201+ .stripPrefix(" /" )
202+ .stripSuffix(" .md" ) + " .html"
203+ val authors = doc.config.get[Seq [String ]](" author.github" ) match {
204+ case Right (list) => list
205+ case _ =>
206+ doc.config.get[String ](" author.github" ) match {
207+ case Right (single) => Seq (single)
208+ case _ => Nil
209+ }
210+ }
211+ val tags = doc.config
212+ .get[Seq [String ]](" tags" )
213+ .getOrElse(Nil )
214+ PostMetadata (path, authors, tags, date.toLocalDate.toString)
215+ }
216+ }
217+
218+ // build JSON manually — no extra dependency needed
219+ def jsonStr (s : String ) = s """ " $s" """
220+ def jsonArr (items : Seq [String ]) =
221+ items.map(jsonStr).mkString(" [" , " , " , " ]" )
222+
223+ val entries = posts.map { p =>
224+ s """ ${jsonStr(p.path)}: { """ +
225+ s """ "authors": ${jsonArr(p.authors)}, """ +
226+ s """ "tags": ${jsonArr(p.tags)}, """ +
227+ s """ "date": ${jsonStr(p.date)}} """
228+ }
229+
230+ val json = entries.mkString(" {\n " , " ,\n " , " \n }" )
231+
232+ // write to target/search/metadata.json
233+ val outPath = java.nio.file.Paths .get(
234+ destination.toString,
235+ " search" ,
236+ " metadata.json"
237+ )
238+
239+ IO .blocking {
240+ java.nio.file.Files .createDirectories(outPath.getParent)
241+ java.nio.file.Files .writeString(outPath, json)
242+ }.void
243+ }
170244}
171245
172246object LaikaCustomizations {
@@ -180,6 +254,7 @@ object LaikaCustomizations {
180254 import laika .ast .*
181255 import laika .format .*
182256 import laika .theme .*
257+ // import ciris.{Config, ConfigValue, ObjectValue, ArrayValue, Field}
183258
184259 def addAnchorLinks (fmt : TagFormatter , h : Header ) = {
185260 val link = h.options.id.map { id =>
0 commit comments