---
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}
{kind === 'studio'
?
: body ? :
}
{showConsult && (
)}
{(newer || older) && (
)}
{nearby.length > 0 && (
)}
{filesIndex && (
)}