@@ -6,21 +6,33 @@ | |||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> | <meta name="viewport" content="width=device-width, initial-scale=1.0" /> | ||||
<meta name="description" content="ぼざクリ ぼざろクリーチャーシリーズ 伊地知ニジカ リンク集 Wiki ニコニコ" /> | <meta name="description" content="ぼざクリ ぼざろクリーチャーシリーズ 伊地知ニジカ リンク集 Wiki ニコニコ" /> | ||||
<title>ぼざクリ タグ広場 〜 ぼざろクリーチャーシリーズ綜合リンク集</title> | <title>ぼざクリ タグ広場 〜 ぼざろクリーチャーシリーズ綜合リンク集</title> | ||||
<script type="application/ld+json"> | |||||
{ | |||||
"@context": "http://schema.org", | |||||
"@type": "WebSite", | |||||
"url": "https://hub.nizika.monster", | |||||
"potentialAction": { | |||||
"@type": "SearchAction", | |||||
"target": "https://hub.nizika.monster/posts?tags={search_term}", | |||||
"query-input": "required name=search_term" | |||||
} | |||||
} | |||||
</script> | |||||
<!-- helmet --> | |||||
</head> | </head> | ||||
<body> | <body> | ||||
<div id="root"></div> | |||||
<div id="root"> | |||||
<div class="flex flex-col h-screen w-screen"> | |||||
<nav class="px-3 flex justify-between items-center w-full min-h-[48px] | |||||
bg-yellow-200 dark:bg-red-975 md:bg-yellow-50"> | |||||
<div class="flex items-center gap-2 h-full"> | |||||
<a class="mx-4 text-xl font-bold text-pink-600 hover:text-pink-400 | |||||
dark:text-pink-300 dark:hover:text-pink-100" | |||||
href="/"> | |||||
ぼざクリ タグ広場 | |||||
</a> | |||||
<a class="hidden md:flex h-full items-center px-2" href="/posts">広場</a> | |||||
<a class="hidden md:flex h-full items-center px-2" href="/tags">タグ</a> | |||||
<a class="hidden md:flex h-full items-center px-2" href="/wiki/ヘルプ:ホーム">Wiki</a> | |||||
<a class="hidden md:flex h-full items-center px-2" href="/users">ユーザ</a> | |||||
</div> | |||||
<a href="#" class="md:hidden ml-auto pr-4 | |||||
text-pink-600 hover:text-pink-400 | |||||
dark:text-pink-300 dark:hover:text-pink-100"> | |||||
Menu | |||||
</a> | |||||
</nav> | |||||
</div> | |||||
<!-- outlet --> | |||||
</div> | |||||
<script type="module" src="/src/main.tsx"></script> | <script type="module" src="/src/main.tsx"></script> | ||||
</body> | </body> | ||||
</html> | </html> |
@@ -8,6 +8,8 @@ | |||||
RewriteEngine On | RewriteEngine On | ||||
RewriteBase / | RewriteBase / | ||||
# routes # | |||||
RewriteCond %{REQUEST_URI} !^/api | RewriteCond %{REQUEST_URI} !^/api | ||||
RewriteCond %{REQUEST_URI} !^/sitemap\.xml | RewriteCond %{REQUEST_URI} !^/sitemap\.xml | ||||
RewriteCond %{REQUEST_FILENAME} !-f | RewriteCond %{REQUEST_FILENAME} !-f | ||||
@@ -1,4 +1,5 @@ | |||||
import axios from 'axios' | import axios from 'axios' | ||||
import crypto from 'crypto' | |||||
import fs from 'fs' | import fs from 'fs' | ||||
import path from 'path' | import path from 'path' | ||||
@@ -14,7 +15,10 @@ const SITE_TITLE = 'ぼざクリ タグ広場' | |||||
const DOMAIN = 'https://hub.nizika.monster' | const DOMAIN = 'https://hub.nizika.monster' | ||||
const API_BASE_URL = 'https://hub.nizika.monster/api' | const API_BASE_URL = 'https://hub.nizika.monster/api' | ||||
const fetchPosts = async () => (await axios.get (`${ API_BASE_URL }/posts`)).data.posts | |||||
const fetchPosts = async tagName => (await axios.get (`${ API_BASE_URL }/posts`, | |||||
{ params: { ...(tagName && { tags: tagName, | |||||
match: 'all', | |||||
limit: '20' }) } })).data.posts | |||||
const fetchPostIds = async () => (await fetchPosts ()).map (post => post.id) | const fetchPostIds = async () => (await fetchPosts ()).map (post => post.id) | ||||
const fetchTags = async () => (await axios.get (`${ API_BASE_URL }/tags`)).data | const fetchTags = async () => (await axios.get (`${ API_BASE_URL }/tags`)).data | ||||
@@ -23,20 +27,82 @@ const fetchTagNames = async () => (await fetchTags ()).map (tag => tag.name) | |||||
const fetchWikiPages = async () => (await axios.get (`${ API_BASE_URL }/wiki`)).data | const fetchWikiPages = async () => (await axios.get (`${ API_BASE_URL }/wiki`)).data | ||||
const fetchWikiTitles = async () => (await fetchWikiPages ()).map (page => page.title) | const fetchWikiTitles = async () => (await fetchWikiPages ()).map (page => page.title) | ||||
const createPostListOutlet = async tagName => ` | |||||
<main class="flex-1 overflow-y-auto p-4"> | |||||
<div class="mt-4"> | |||||
<div class="flex gap-4"><a href="#" class="font-bold">広場</a></div> | |||||
<div class="mt-2"> | |||||
<div class="flex flex-wrap gap-6 p-4"> | |||||
${ (await fetchPosts (tagName)).map (post => ` | |||||
<a class="w-40 h-40 overflow-hidden rounded-lg shadow-md hover:shadow-lg" | |||||
href="/posts/${ post.id }"> | |||||
<img alt="${ post.title }" | |||||
title="${ post.title }" | |||||
loading="eager" | |||||
fetchpriority="high" | |||||
decoding="async" | |||||
class="object-none w-full h-full" | |||||
src="${ post.url }" /> | |||||
</a>`).join ('') } | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</main>` | |||||
const createPostDetailOutlet = post => ` | |||||
<div class="md:flex md:flex-1"> | |||||
<div> | |||||
<div> | |||||
${ ['deerjikist', 'meme', 'character', 'general', 'material', 'meta', 'nico'] | |||||
.map (cat => '<ul>' + post.tags.filter (tag => tag.category === cat).map (tag => ` | |||||
<li> | |||||
<span> | |||||
<a href="/wiki/${ encodeURIComponent (tag.name) }">?</a> | |||||
</span> | |||||
<a href="/posts?${ new URLSearchParams ({ tags: tag.name }) }"> | |||||
${ tag.name } | |||||
</a> | |||||
<span>${ tag['post_count'] }</span> | |||||
</li>`).join ('') + '</ul>').join ('') } | |||||
</div> | |||||
</div> | |||||
<main class="flex-1 overflow-y-auto p-4"> | |||||
<img src="${ post.thumbnail }" alt="${ post.url }" /> | |||||
</main> | |||||
</div>` | |||||
const posts = (await fetchPosts ()).map (post => [ | const posts = (await fetchPosts ()).map (post => [ | ||||
`/posts/${ post.id }`, | `/posts/${ post.id }`, | ||||
`${ post.title || post.url } | ${ SITE_TITLE }` ]) | |||||
`${ post.title || post.url } | ${ SITE_TITLE }`, | |||||
createPostDetailOutlet (post)]) | |||||
const tags = (await fetchTags ()).map (tag => [ | |||||
`/posts?${ new URLSearchParams ({ tags: tag.name }) }`, | |||||
`${ tag.name } | ${ SITE_TITLE }`]) | |||||
const tags = [] | |||||
for (const tag of await fetchTags ()) | |||||
{ | |||||
tags.push ([`/posts?${ new URLSearchParams ({ tags: tag.name }) }`, | |||||
`${ tag.name } | ${ SITE_TITLE }`, | |||||
await createPostListOutlet (tag.name)]) | |||||
} | |||||
const wikiPages = (await fetchWikiPages ()).map (page => [ | const wikiPages = (await fetchWikiPages ()).map (page => [ | ||||
`/wiki/${ encodeURIComponent (page.title) }`, | `/wiki/${ encodeURIComponent (page.title) }`, | ||||
`${ page.title } Wiki | ${ SITE_TITLE }`]) | `${ page.title } Wiki | ${ SITE_TITLE }`]) | ||||
const routes = [ | const routes = [ | ||||
['/', `${ SITE_TITLE } 〜 ぼざろクリーチャーシリーズ綜合リンク集サイト`], | |||||
['/', `${ SITE_TITLE } 〜 ぼざろクリーチャーシリーズ綜合リンク集`, | |||||
await createPostListOutlet (), ` | |||||
<script type="application/ld+json"> | |||||
{ | |||||
"@context": "http://schema.org", | |||||
"@type": "WebSite", | |||||
"url": "https://hub.nizika.monster", | |||||
"potentialAction": { | |||||
"@type": "SearchAction", | |||||
"target": "https://hub.nizika.monster/posts?tags={search_term}", | |||||
"query-input": "required name=search_term" | |||||
} | |||||
} | |||||
</script>`], | |||||
...tags, | ...tags, | ||||
...posts, | ...posts, | ||||
['/wiki', `Wiki | ${ SITE_TITLE }`], | ['/wiki', `Wiki | ${ SITE_TITLE }`], | ||||
@@ -70,3 +136,29 @@ ${ routes.map (([route, title]) => ` <item> | |||||
fs.writeFileSync (path.resolve ('dist/sitemap.xml'), xml) | fs.writeFileSync (path.resolve ('dist/sitemap.xml'), xml) | ||||
fs.writeFileSync (path.resolve ('dist/rss.xml'), rss) | fs.writeFileSync (path.resolve ('dist/rss.xml'), rss) | ||||
const baseHTML = fs.readFileSync (path.resolve ('dist/index.html'), 'utf8') | |||||
let htaccess = fs.readFileSync (path.resolve ('dist/.htaccess'), 'utf8') | |||||
routes.forEach (([route, title, outlet, helmet]) => { | |||||
const fileName = ( | |||||
route === '/' | |||||
? 'index.html' | |||||
: `${ crypto.createHash ('sha256').update (encodeURIComponent (route)).digest ('hex') }.html`) | |||||
let html = baseHTML.replace (/(?<=<title>).*?(?=<\/title>)/, title) | |||||
html = html.replace ('<!-- outlet -->', outlet || '') | |||||
html = html.replace ('<!-- helmet -->', helmet || '') | |||||
fs.writeFileSync (path.resolve (`dist/${ fileName }`), html) | |||||
const [routeBase, q] = route.split ('?') | |||||
if (route !== '/') | |||||
{ | |||||
htaccess = htaccess.replace ('# routes #', ` | |||||
RewriteCond %{REQUEST_URI} ^${ routeBase }$ | |||||
${ q ? `RewriteCond %{QUERY_STRING} (^|&)${ q }(&|$)` : '' } | |||||
RewriteRule ^ /${ fileName } [L] | |||||
# routes # | |||||
`) | |||||
} | |||||
}) | |||||
fs.writeFileSync (path.resolve ('dist/.htaccess'), htaccess.replace ('# routes #', '')) |
@@ -1,13 +1,14 @@ | |||||
import ReactMarkdown from 'react-markdown' | import ReactMarkdown from 'react-markdown' | ||||
import { Link } from 'react-router-dom' | import { Link } from 'react-router-dom' | ||||
type Props = { body: string } | |||||
type Props = { title: string | |||||
body?: string } | |||||
export default ({ body }: Props) => ( | |||||
export default ({ title, body }: Props) => ( | |||||
<ReactMarkdown components={{ a: ( | <ReactMarkdown components={{ a: ( | ||||
({ href, children }) => (['/', '.'].some (e => href?.startsWith (e)) | |||||
? <Link to={href!}>{children}</Link> | |||||
: <a href={href} target="_blank" rel="noopener noreferrer">{children}</a>)) }}> | |||||
{body} | |||||
({ href, children }) => (['/', '.'].some (e => href?.startsWith (e)) | |||||
? <Link to={href!}>{children}</Link> | |||||
: <a href={href} target="_blank" rel="noopener noreferrer">{children}</a>)) }}> | |||||
{body || `このページは存在しません。[新規作成してください](/wiki/new?title=${ encodeURIComponent (title) })。`} | |||||
</ReactMarkdown>) | </ReactMarkdown>) |
@@ -2,8 +2,8 @@ import React, { useState } from 'react' | |||||
import { cn } from '@/lib/utils' | import { cn } from '@/lib/utils' | ||||
type TabProps = { name: string | type TabProps = { name: string | ||||
init?: boolean | |||||
children: React.ReactNode } | |||||
init?: boolean | |||||
children: React.ReactNode } | |||||
type Props = { children: React.ReactNode } | type Props = { children: React.ReactNode } | ||||
@@ -20,20 +20,20 @@ export default ({ children }: Props) => { | |||||
return ( | return ( | ||||
<div className="mt-4"> | <div className="mt-4"> | ||||
<div className="flex gap-4"> | |||||
{tabs.map ((tab, i) => ( | |||||
<a key={i} | |||||
href="#" | |||||
className={cn (i === current && 'font-bold')} | |||||
onClick={ev => { | |||||
ev.preventDefault () | |||||
setCurrent (i) | |||||
}}> | |||||
{tab.props.name} | |||||
</a>))} | |||||
</div> | |||||
<div className="mt-2"> | |||||
{tabs[current]} | |||||
</div> | |||||
<div className="flex gap-4"> | |||||
{tabs.map ((tab, i) => ( | |||||
<a key={i} | |||||
href="#" | |||||
className={cn (i === current && 'font-bold')} | |||||
onClick={ev => { | |||||
ev.preventDefault () | |||||
setCurrent (i) | |||||
}}> | |||||
{tab.props.name} | |||||
</a>))} | |||||
</div> | |||||
<div className="mt-2"> | |||||
{tabs[current]} | |||||
</div> | |||||
</div>) | </div>) | ||||
} | } |
@@ -129,11 +129,11 @@ export default () => { | |||||
{loading && 'Loading...'} | {loading && 'Loading...'} | ||||
<div ref={loaderRef} className="h-12"></div> | <div ref={loaderRef} className="h-12"></div> | ||||
</Tab> | </Tab> | ||||
{(wikiPage && wikiPage.body) && ( | |||||
<Tab name="Wiki" init={!(posts.length)}> | |||||
<WikiBody body={wikiPage.body} /> | |||||
{tags.length === 1 && ( | |||||
<Tab name="Wiki"> | |||||
<WikiBody body={wikiPage?.body} title={tags[0]} /> | |||||
<div className="my-2"> | <div className="my-2"> | ||||
<Link to={`/wiki/${ encodeURIComponent (wikiPage.title) }`}> | |||||
<Link to={`/wiki/${ encodeURIComponent (tags[0]) }`}> | |||||
Wiki を見る | Wiki を見る | ||||
</Link> | </Link> | ||||
</div> | </div> | ||||
@@ -123,7 +123,7 @@ export default () => { | |||||
<div className="prose mx-auto p-4"> | <div className="prose mx-auto p-4"> | ||||
{wikiPage === undefined | {wikiPage === undefined | ||||
? 'Loading...' | ? 'Loading...' | ||||
: <WikiBody body={wikiPage?.body || `このページは存在しません。[新規作成してください](/wiki/new?title=${ encodeURIComponent (title) })。`} />} | |||||
: <WikiBody body={wikiPage?.body} title={title} />} | |||||
</div> | </div> | ||||
{(!(version) && posts.length > 0) && ( | {(!(version) && posts.length > 0) && ( | ||||