ぼざクリ タグ広場 https://hub.nizika.monster
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

165 lines
6.0 KiB

  1. import axios from 'axios'
  2. import crypto from 'crypto'
  3. import fs from 'fs'
  4. import path from 'path'
  5. const formatDate = (date = new Date) => {
  6. const year = date.getFullYear ()
  7. const month = (date.getMonth () + 1).toString ().padStart (2, '0')
  8. const day = date.getDate ().toString ().padStart (2, '0')
  9. return `${ year }-${ month }-${ day }`
  10. }
  11. const SITE_TITLE = 'ぼざクリ タグ広場'
  12. const DOMAIN = 'https://hub.nizika.monster'
  13. const API_BASE_URL = 'https://hub.nizika.monster/api'
  14. const fetchPosts = async tagName => (await axios.get (`${ API_BASE_URL }/posts`,
  15. { params: { ...(tagName && { tags: tagName,
  16. match: 'all',
  17. limit: '20' }) } })).data.posts
  18. const fetchPostIds = async () => (await fetchPosts ()).map (post => post.id)
  19. const fetchTags = async () => (await axios.get (`${ API_BASE_URL }/tags`)).data
  20. const fetchTagNames = async () => (await fetchTags ()).map (tag => tag.name)
  21. const fetchWikiPages = async () => (await axios.get (`${ API_BASE_URL }/wiki`)).data
  22. const fetchWikiTitles = async () => (await fetchWikiPages ()).map (page => page.title)
  23. const createPostListOutlet = async tagName => `
  24. <main class="flex-1 overflow-y-auto p-4">
  25. <div class="mt-4">
  26. <div class="flex gap-4"><a href="#" class="font-bold">広場</a></div>
  27. <div class="mt-2">
  28. <div class="flex flex-wrap gap-6 p-4">
  29. ${ (await fetchPosts (tagName)).map (post => `
  30. <a class="w-40 h-40 overflow-hidden rounded-lg shadow-md hover:shadow-lg"
  31. href="/posts/${ post.id }">
  32. <img alt="${ post.title }"
  33. title="${ post.title }"
  34. loading="eager"
  35. fetchpriority="high"
  36. decoding="async"
  37. class="object-none w-full h-full"
  38. src="${ post.url }" />
  39. </a>`).join ('') }
  40. </div>
  41. </div>
  42. </div>
  43. </main>`
  44. const createPostDetailOutlet = post => `
  45. <div class="md:flex md:flex-1">
  46. <div>
  47. <div>
  48. ${ ['deerjikist', 'meme', 'character', 'general', 'material', 'meta', 'nico']
  49. .map (cat => '<ul>' + post.tags.filter (tag => tag.category === cat).map (tag => `
  50. <li>
  51. <span>
  52. <a href="/wiki/${ encodeURIComponent (tag.name) }">?</a>
  53. </span>
  54. <a href="/posts?${ new URLSearchParams ({ tags: tag.name }) }">
  55. ${ tag.name }
  56. </a>
  57. <span>${ tag['post_count'] }</span>
  58. </li>`).join ('') + '</ul>').join ('') }
  59. </div>
  60. </div>
  61. <main class="flex-1 overflow-y-auto p-4">
  62. <img src="${ post.thumbnail }" alt="${ post.url }" />
  63. </main>
  64. </div>`
  65. const posts = (await fetchPosts ()).map (post => [
  66. `/posts/${ post.id }`,
  67. `${ post.title || post.url } | ${ SITE_TITLE }`,
  68. createPostDetailOutlet (post)])
  69. const tags = []
  70. for (const tag of await fetchTags ())
  71. {
  72. tags.push ([`/posts?${ new URLSearchParams ({ tags: tag.name }) }`,
  73. `${ tag.name } | ${ SITE_TITLE }`,
  74. await createPostListOutlet (tag.name)])
  75. }
  76. const wikiPages = (await fetchWikiPages ()).map (page => [
  77. `/wiki/${ encodeURIComponent (page.title) }`,
  78. `${ page.title } Wiki | ${ SITE_TITLE }`])
  79. const routes = [
  80. ['/', `${ SITE_TITLE } 〜 ぼざろクリーチャーシリーズ綜合リンク集`,
  81. await createPostListOutlet (), `
  82. <script type="application/ld+json">
  83. {
  84. "@context": "http://schema.org",
  85. "@type": "WebSite",
  86. "url": "https://hub.nizika.monster",
  87. "potentialAction": {
  88. "@type": "SearchAction",
  89. "target": "https://hub.nizika.monster/posts?tags={search_term}",
  90. "query-input": "required name=search_term"
  91. }
  92. }
  93. </script>`],
  94. ...tags,
  95. ...posts,
  96. ['/wiki', `Wiki | ${ SITE_TITLE }`],
  97. ...wikiPages]
  98. const xml = `<?xml version="1.0" encoding="UTF-8"?>
  99. <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  100. ${ routes.map (([route]) => ` <url>
  101. <loc>${ DOMAIN }${ route }</loc>
  102. <lastmod>${ formatDate () }</lastmod>
  103. <changefreq>daily</changefreq>
  104. </url>`).join ('\n') }
  105. </urlset>`
  106. const rss = `<?xml version="1.0" encoding="UTF-8"?>
  107. <rss version="2.0">
  108. <channel>
  109. <title>The Series of Bocchi the Rock! Creatures Hub Feed</title>
  110. <link>${ DOMAIN }/</link>
  111. <description>最新の投稿、タグ、Wiki ページの一覧</description>
  112. <language>ja</language>
  113. <pubDate>${ (new Date).toUTCString () }</pubDate>
  114. ${ routes.map (([route, title]) => ` <item>
  115. <title>${ title }</title>
  116. <link>${ DOMAIN }${ route }</link>
  117. <guid>${ DOMAIN }${ route }</guid>
  118. <pubDate>${ (new Date).toUTCString () }</pubDate>
  119. </item>`).join ('\n') }
  120. </channel>
  121. </rss>`
  122. fs.writeFileSync (path.resolve ('dist/sitemap.xml'), xml)
  123. fs.writeFileSync (path.resolve ('dist/rss.xml'), rss)
  124. const baseHTML = fs.readFileSync (path.resolve ('dist/index.html'), 'utf8')
  125. let htaccess = fs.readFileSync (path.resolve ('dist/.htaccess'), 'utf8')
  126. routes.forEach (([route, title, outlet, helmet]) => {
  127. const fileName = (
  128. route === '/'
  129. ? 'index.html'
  130. : `${ crypto.createHash ('sha256').update (encodeURIComponent (route)).digest ('hex') }.html`)
  131. let html = baseHTML.replace (/(?<=<title>).*?(?=<\/title>)/, title)
  132. html = html.replace ('<!-- outlet -->', outlet || '')
  133. html = html.replace ('<!-- helmet -->', helmet || '')
  134. fs.writeFileSync (path.resolve (`dist/${ fileName }`), html)
  135. const [routeBase, q] = route.split ('?')
  136. if (route !== '/')
  137. {
  138. htaccess = htaccess.replace ('# routes #', `
  139. RewriteCond %{REQUEST_URI} ^${ routeBase }$
  140. ${ q ? `RewriteCond %{QUERY_STRING} (^|&)${ q }(&|$)` : '' }
  141. RewriteRule ^ /${ fileName } [L]
  142. # routes #
  143. `)
  144. }
  145. })
  146. fs.writeFileSync (path.resolve ('dist/.htaccess'), htaccess.replace ('# routes #', ''))