--- import Base from '../../layouts/Base.astro'; import { ymd } from '../../lib/format'; import { mediaHtml } from '../../lib/media'; import { fixBody, textOf, abs, AUTHOR, PUBLISHER, SITE, SITE_NAME } from '../../lib/seo'; import { SERIES_PAGES, allCards, withDeepDiveThumb } from '../../lib/records'; import RecordCard from '../../components/RecordCard.astro'; import { getCollection, render } from 'astro:content'; import { existsSync, readFileSync } from 'node:fs'; import aiConsult from '../../data/aiConsult.json'; import { linkifyHtml, emphasizeHtml, linkLogsHtml } from '../../lib/links.mjs'; import { roomyTables } from '../../lib/tables.mjs'; import emphasis from '../../data/emphasis.json'; // 記事ページは2種類から作る // - Studio から移した記事:CMS の本文 HTML(src/data/records/。tools/import_all.py で作る) // - これから Claude が書く記事:Markdown(src/content/records/) export async function getStaticPaths() { const studio = Object.values(import.meta.glob('../../data/records/*.json', { eager: true, import: 'default' })) as any[]; const md = await getCollection('records'); return [ ...studio.map((r) => ({ params: { slug: r.slug }, props: { kind: 'studio', rec: r } })), ...md.map((e) => ({ params: { slug: e.data.slug }, props: { kind: 'md', entry: e } })), ]; } const { kind, entry, rec } = Astro.props; const esc = (s) => s.replace(/&/g, '&').replace(/ items.length ? `` : ''; let r, body = '', Content = null, toc = ''; if (kind === 'studio') { r = rec; body = fixBody(mediaHtml(rec.body.replace('', tocOf(rec.toc))), rec.title); } else { const d = entry.data; r = { title: d.title, description: d.meta_description || '', slug: d.slug, day: `LOG${d.day}`, series: d.series, published: d.date.toISOString(), cover: d.cover ?? null, thumb: d.cover ? null : `/thumbs/LOG${d.day}.html`, tags: d.tags || [] }; const rendered = await render(entry); Content = rendered.Content; toc = tocOf(rendered.headings.filter((h) => h.depth === 2).map((h) => ({ id: h.slug, text: h.text }))); // 目次は最初の見出しの直前に置く(冒頭の書き出しを先に読んでもらうため。article_rules.md「冒頭の書き出し」) const html = entry.rendered?.html; if (html) { const at = toc ? html.search(/]/) : -1; body = !toc ? html : at >= 0 ? html.slice(0, at) + toc + html.slice(at) : toc + html; } } // 本文の URL・サイトの住所をリンクにする(中の住所は同じタブ、外のサイトは新しいタブ。src/lib/links.mjs) // 記事のいちばん大事な一文を太字にする(src/data/emphasis.json) // 本文の記事番号(LOG334 など)を、その記事へのリンクにする(同じ記事へは最初の1か所だけ・自分へはつながない)。 // 古い呼び方の DAY200 は LOG200 に直す。LOG343(DAY から LOG への呼び方の変更を書いた記事)は DAY を例として書いているので直さない const LOGS = Object.fromEntries((await allCards()).map((c) => [c.day, c.slug])); const isEnglish = /-en$/.test(r.slug); body = linkifyHtml(linkLogsHtml(emphasizeHtml(body, (emphasis as Record)[r.slug] || []), LOGS, r.slug, isEnglish, r.slug !== 'studio-shutdown-cloudflare-redirect', r.slug === 'day39')); // 表を窮屈にしない(本文より広い表は横スクロール・短いセルは折り返さない・長いセルは最小の幅を持たせる。src/lib/tables.mjs) body = roomyTables(body); // 検索エンジン向けの情報 // 考察記事は、問いを文字で組んだサムネイルを記事の上と SNS の画像にも使う(Gemini の絵より優先) r = withDeepDiveThumb(r); // SNS・検索結果に出す画像:表紙の画像 → サムネイルを書き出した画像(public/og/。tools/render_og.py)→ サイト共通の画像 const ogFromThumb = r.thumb ? r.thumb.replace(/^\/thumbs\//, '/og/').replace(/\.html$/, '.jpg') : null; const ogImage = r.cover || (ogFromThumb && existsSync(`public${ogFromThumb}`) ? ogFromThumb : undefined); const isEn = rec?.lang === 'EN' || /-en$/.test(r.slug); const description = r.description || textOf(kind === 'studio' ? rec.body : entry.body, 120); const allSlugs = new Set([ ...Object.values(import.meta.glob('../../data/records/*.json', { eager: true, import: 'default' })).map((x: any) => x.slug), ...(await getCollection('records')).map((e) => e.data.slug), ]); const pair = isEn ? r.slug.replace(/-en$/, '') : `${r.slug}-en`; const alternates = allSlugs.has(pair) ? [{ lang: isEn ? 'ja' : 'en', path: `/record/${pair}` }] : []; // 英語の考察記事(Consideration)は、考察記事のシリーズページ(series5)につなぐ const seriesKey = r.series === 'Consideration' ? '考察記事' : r.series; const seriesPage = Object.entries(SERIES_PAGES).find(([, c]) => c.series === seriesKey); const seriesName = seriesPage ? seriesPage[1].title.split(' | ').pop() : r.series; // 同じシリーズの記事(一覧と同じく LOG 番号の大きい順)。前後の記事と、この記事の近くの6本 const sameSeries = (await allCards()).filter((c) => c.series === r.series); const pos = sameSeries.findIndex((c) => c.slug === r.slug); const newer = pos > 0 ? sameSeries[pos - 1] : null; const older = pos >= 0 && pos < sameSeries.length - 1 ? sameSeries[pos + 1] : null; const others = sameSeries.filter((c) => c.slug !== r.slug); const start = Math.max(0, Math.min((pos < 0 ? 0 : pos) - 3, others.length - 6)); const nearby = others.slice(start, start + 6); // AI活用の記事(研究を回すために作った仕組みの記録)の末尾に、相談の案内を出す。 // Markdown の記事は先頭の項目 ai_consult: true、Studio から移した記事は src/data/aiConsult.json の slug で決める const showConsult = kind === 'md' ? entry.data.ai_consult === true : (aiConsult as string[]).includes(r.slug); const url = `${SITE}/record/${r.slug}`; // 長いコード・ログ・データを外に出した記事(LOG364 から。tools/split_files.py)は、 // 全文(Markdown。Studio から移した記事は HTML)を head に、ファイルの一覧を構造化データ(hasPart)に出して、AI がたどれるようにする const filesIndexPath = `public/files/${r.day}/index.json`; const filesIndex = existsSync(filesIndexPath) ? JSON.parse(readFileSync(filesIndexPath, 'utf-8')) : null; const codeLang = { csharp: 'C#', json: 'JSON', yaml: 'YAML', markdown: 'Markdown', python: 'Python', text: 'Text' }; const jsonLd = [ { '@context': 'https://schema.org', '@type': 'BlogPosting', headline: r.title, description, url, mainEntityOfPage: url, inLanguage: isEn ? 'en' : 'ja', datePublished: r.published, dateModified: rec?.updated || r.published, image: ogImage ? [abs(ogImage)] : undefined, articleSection: r.series, keywords: r.tags?.length ? r.tags.join(', ') : undefined, author: AUTHOR, publisher: PUBLISHER, isPartOf: { '@type': 'Blog', name: SITE_NAME, url: SITE + '/' }, hasPart: filesIndex ? [ { '@type': 'DigitalDocument', name: `${r.day} 記事の全文(${filesIndex.article_full_type === 'text/html' ? 'HTML' : 'Markdown'})`, encodingFormat: filesIndex.article_full_type || 'text/markdown', url: abs(filesIndex.article_full || filesIndex.article_full_markdown) }, ...filesIndex.files.map((f) => f.lang === 'csharp' || f.lang === 'yaml' || /\.(cs|prefab|asset|mat)\.txt$/.test(f.file) ? { '@type': 'SoftwareSourceCode', name: f.subheading || f.heading || f.file, programmingLanguage: codeLang[f.lang] || f.lang, url: abs(f.url) } : { '@type': 'Dataset', name: [f.heading, f.subheading].filter(Boolean).join(' / ') || f.file, url: abs(f.url) }), ] : undefined, }, { '@context': 'https://schema.org', '@type': 'BreadcrumbList', itemListElement: [ { '@type': 'ListItem', position: 1, name: SITE_NAME, item: SITE + '/' }, ...(seriesPage ? [{ '@type': 'ListItem', position: 2, name: seriesPage[1].title, item: `${SITE}/${seriesPage[0]}` }] : []), { '@type': 'ListItem', position: seriesPage ? 3 : 2, name: r.title, item: url }, ], }, ]; --- {filesIndex && }
{r.thumb ?
: r.cover && {r.title}}
{r.day}{seriesPage ? {r.series} : {r.series}}{ymd(r.published)}

{r.title}

{kind === 'studio' ?
: body ?
:
} {showConsult && ( )} {(newer || older) && ( )}
{nearby.length > 0 && ( )} {filesIndex && ( )}