import matter from '@11ty/gray-matter'; import { Client, toDatetimeString, type AtUriString, type BlobRef } from '@atproto/lex'; import { PasswordSession } from '@atproto/lex-password-session'; import { lstatSync, readdirSync, readFileSync } from 'node:fs'; import { join } from 'node:path'; import z from 'zod'; import * as site from './src/lexicons/site'; const SITE_BASE_URL = 'https://clembs.com'; const PDS_SERVICE = 'https://eurosky.social'; const ATPROTO_IDENTIFIER = 'clembs.com'; const BLOG_BASE_PATH = '../src/content/blog'; // same schema defined in content.config.ts const BLOG_ZOD_SCHEMA = z.object({ title: z.string(), lede: z.string(), createdAt: z.coerce.date(), lastEditedAt: z.coerce.date().optional(), bannerPath: z.string().optional(), bannerThumbnailPath: z.string().optional(), bannerAlt: z.string().optional(), projectUrl: z.string().optional(), clientText: z.string().optional(), index: z.boolean().optional().default(true), }); type BlogArticle = z.infer; const client = new Client( await PasswordSession.login({ service: PDS_SERVICE, identifier: ATPROTO_IDENTIFIER, password: process.env.ATPROTO_APP_PASSWORD!, }), ); const { blogFR, blogEN } = await getAllPublications(); const documents = await client.list(site.standard.document).then((r) => r.records); for (const articleFilePath of getArticleFilePaths()) { const rawArticle = readFileSync(articleFilePath, 'utf-8'); const { data: rawFrontmatter } = matter(rawArticle); const frontmatter = z.parse(BLOG_ZOD_SCHEMA, rawFrontmatter); // we skip articles we don't index if (!frontmatter.index) continue; const lang = articleFilePath.match(/(?:.*)(fr|en)\.mdx/)?.[1] as 'fr' | 'en'; const slug = articleFilePath.replace(`${BLOG_BASE_PATH}/`, '').replace(`/${lang}.mdx`, ''); const document = formatFrontmatterToDocument({ path: `/${slug}`, site: lang === 'fr' ? blogFR.uri : blogEN.uri, frontmatter, }); // upsert cover image with the existing one, or uploaded blob document.coverImage = await upsertArticleBlob({ slug, frontmatter }); await upsertStandardSiteDoc(document); } function getArticleFilePaths() { return readdirSync(BLOG_BASE_PATH) .map((path) => join(BLOG_BASE_PATH, path)) .filter((fullPath) => lstatSync(fullPath).isDirectory()) .flatMap((fullPath) => readdirSync(fullPath) .filter((p) => /(.*)(?:fr|en)\.mdx/.test(p)) .map((p) => join(fullPath, p)), ); } async function getAllPublications() { const publications = await client.list(site.standard.publication); const blogFR = publications.records.find((p) => p.value.url === `${SITE_BASE_URL}/fr/blog`)!; const blogEN = publications.records.find((p) => p.value.url === `${SITE_BASE_URL}/en/blog`)!; return { blogEN, blogFR }; } function formatFrontmatterToDocument({ path, site, frontmatter, }: { path: string; site: AtUriString; frontmatter: BlogArticle; }): site.standard.document.Main { return { $type: 'site.standard.document', site, path, title: frontmatter.title, description: frontmatter.lede, publishedAt: toDatetimeString(frontmatter.createdAt), updatedAt: frontmatter.lastEditedAt ? toDatetimeString(frontmatter.lastEditedAt) : undefined, }; } async function upsertArticleBlob({ slug, frontmatter, }: { slug: string; frontmatter: BlogArticle; }): Promise { if (frontmatter.bannerPath) { const bannerLocalUrl = `${BLOG_BASE_PATH}/${slug}/${frontmatter.bannerPath.replace('./', '')}`; const bannerBlob = readFileSync(bannerLocalUrl); return await client.uploadBlob(bannerBlob).then((r) => r.body.blob); } else { // get the existing document's cover image const existingDoc = documents.find((d) => d.value.title === frontmatter.title); return existingDoc?.value.coverImage as BlobRef | undefined; } } async function upsertStandardSiteDoc(document: site.standard.document.Main) { const existingDoc = documents.find((d) => d.value.title === document.title); // if the article is already on my PDS, update it if (existingDoc) { const rkey = existingDoc.uri.split('/').at(-1)!; const editedDoc = await client.put(site.standard.document, document, { rkey, }); console.log(`📝 Article "${document.title}" updated with AT URI: ${editedDoc.uri}`); } else { const publishedDoc = await client.create(site.standard.document, document); console.log(`✅ Article "${document.title}" published with AT URI: ${publishedDoc.uri}`); } }