Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: seed docs script #231

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,658 changes: 1,658 additions & 0 deletions config/datocms/package-lock.json

Large diffs are not rendered by default.

11 changes: 10 additions & 1 deletion config/datocms/package.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
{
"type": "commonjs"
"type": "commonjs",
"dependencies": {
"@datocms/cma-client-node": "^3.4.0",
"datocms-html-to-structured-text": "^4.0.1",
"datocms-structured-text-utils": "^4.0.1",
"dotenv-safe": "^9.1.0",
"mdast-util-from-markdown": "^2.0.2",
"mdast-util-to-hast": "^13.2.0",
"unist-util-visit": "^5.0.0"
}
}
214 changes: 214 additions & 0 deletions config/datocms/scripts/seed-docs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
import { readdir, readFile } from 'node:fs/promises';
import path from 'node:path';
import dotenv from 'dotenv-safe';
import { buildClient } from '@datocms/cma-client-node';
import { fromMarkdown } from 'mdast-util-from-markdown';
import type { Root } from 'mdast';
import { toHast } from 'mdast-util-to-hast';
import { hastToStructuredText, type HastRootNode } from 'datocms-html-to-structured-text';
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Astro check in the CI lint job fails on this module. Not sure why it's not found. Locally I can verify it exists. Maybe it has to do with the "type": "commonjs" in package.json? But I can't change that to "module" as then running DatoCMS migrations will fail 🤷 .

It's a bit of a non-issue. Astro check shouldn't even lint this file and just stick to src/. But I'm unable to configure that. What I could do as a workaround is in the CI lint job, delete config/datocms/scripts/ before running npm run lint:astro. What do you think?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

patch available as PR: #241

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Think it fails in CI because the packages in config/datocms are not installed. Locally I get the same error unless I cd into that directory and run npm install.

import { validate } from 'datocms-structured-text-utils';
import { visit } from 'unist-util-visit';
import { datocmsEnvironment } from '../../../datocms-environment';

dotenv.config({
allowEmptyValues: Boolean(process.env.CI),
});

const client = buildClient({
apiToken: process.env.DATOCMS_API_TOKEN!,
environment: datocmsEnvironment,
});
const docExtension = '.md';
const docDirectory = path.resolve(__dirname,'../../../docs');
const modelType = 'page';
const documentationSlug = 'documentation';
const mainBranchUrl = 'https://github.com/voorhoede/head-start/tree/main/';

async function listDocs() {
const filenames = await readdir(docDirectory);
return filenames.filter(file => file.endsWith(docExtension));
}

async function readDoc(filename: string) {
const filepath = path.join(docDirectory, filename);
const contents = await readFile(filepath, 'utf-8');
const titlePattern = /^# .*\n/;
const title = contents.match(titlePattern)?.[0].replace(/^# /, '').trim() ?? '';
const text = contents.replace(titlePattern, '')?.trim() ?? '';
const slug = path.basename(filename, docExtension);
return { slug, title, text };
}

type Model = {
id: string;
}
type Document = {
slug: string;
title: string;
text: string;
}
type Page = {
id: string;
[key: string]: unknown;
}
async function upsertRecord ({ model, document, parent }: { model: Model, document: Document, parent?: Page }) {
const record = await findRecordBySlug(document.slug);
const note = `!Note: this page is auto-generated from [docs/${document.slug}.md](${mainBranchUrl}docs/${document.slug}.md).`;
const markdown = `${note}\n\n${document.text}`;
const structuredText = await markdownToStructuredText(markdown);
const textBlockItemType = await client.itemTypes.find('text_block');

const data = {
item_type: { type: 'item_type' as const, id: model.id },
title: { en: document.title },
slug: { en: document.slug },
parent_page: parent?.id,
body_blocks: {
en: [
{
type: 'item',
attributes: {
text: structuredText,
},
relationships: {
item_type: {
data: {
type: 'item_type',
id: textBlockItemType.id
}
},
}
}
],
}
};

const newRecord = record
? await client.items.update(record.id, { ...record, ...data })
: await client.items.create(data);
await client.items.publish(newRecord.id);
console.log('✨', record ? 'updated' : 'created', document.title);
return newRecord;
}

async function findRecordBySlug (slug: string) {
const items = await client.items.list({
nested: true,
filter: {
type: modelType,
fields: {
slug: { eq: slug },
}
},
});
return items[0];
}

async function upsertDocumentationPartialRecord(documents: Document[]) {
const title = 'Documentation index';
const itemType = 'page_partial';
const model = await client.itemTypes.find(itemType);
const items = await client.items.list({
nested: true,
filter: {
type: itemType,
fields: {
title: { eq: title },
}
},
});
const record = items[0];

const markdown = documents.map(doc => {
return `* [${doc.title}](/en/${documentationSlug}/${doc.slug}/)`;
}).join('\n');
const structuredText = await markdownToStructuredText(markdown);
const textBlockItemType = await client.itemTypes.find('text_block');

const data = {
item_type: { type: 'item_type' as const, id: model.id },
title: { en: title },
blocks: {
en: [
{
type: 'item',
attributes: {
text: structuredText,
},
relationships: {
item_type: {
data: {
type: 'item_type',
id: textBlockItemType.id
}
},
}
}
],
}
};

record
? await client.items.update(record.id, { ...record, ...data })
: await client.items.create(data);
console.log('✨', record ? 'updated' : 'created', 'page partial:', title);
}

async function getDocumentationRecord() {
const page = await findRecordBySlug(documentationSlug);
if (page) {
console.log('✅ documentation page already exists');
return page;
}

console.log('✨ creating new documentation page');
const model = await client.itemTypes.find(modelType);
return await upsertRecord({ model, document: {
slug: documentationSlug,
title: 'Documentation',
text: ''
} });
}

/**
* adapted from https://www.datocms.com/docs/structured-text/migrating-content-to-structured-text#migrating-markdown-content
*/
async function markdownToStructuredText(markdown: string) {
const mdast = fromMarkdown(markdown);
resolveLinks(mdast);
const hast = toHast(mdast) as HastRootNode;
const structuredText = await hastToStructuredText(hast);
const validationResult = validate(structuredText);
if (!validationResult.valid) {
throw new Error(validationResult.message);
}
return structuredText;
}

function resolveLinks (mdast: Root) {
visit(mdast, 'link', (node) => {
if (node.url.startsWith('./') && node.url.includes('.md')) {
node.url = node.url
.replace('./', '../')
.replace('.md', '/');
} else if (node.url.startsWith('../')) {
node.url = node.url.replace('../', mainBranchUrl);
}
});
}

async function seedDocs() {
const filenames = await listDocs();
const model = await client.itemTypes.find(modelType);
const parent = await getDocumentationRecord();
const documents: Document[] = [];
for (const filename of filenames) {
const document = await readDoc(filename);
documents.push(document);
await upsertRecord({ model, document, parent });
}
await upsertDocumentationPartialRecord(documents);
}

seedDocs()
.then(() => console.log('✅ Docs seeded'));
2 changes: 1 addition & 1 deletion datocms-environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@
* @see docs/getting-started.md on how to use this file
* @see docs/decision-log/2023-10-24-datocms-env-file.md on why file is preferred over env vars
*/
export const datocmsEnvironment = 'action-block';
export const datocmsEnvironment = 'seed-docs';
export const datocmsBuildTriggerId = '30535';
2 changes: 1 addition & 1 deletion docs/blocks-and-components.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ See [CMS Data Loading](./cms-data-loading.md) for documentation on the use of Gr

## Block templates

Head Start uses the same convention for props and types for every Block: the `Props` interface always contains a `block` property containing the type based on the CMS model. This `block` type is automatically generated based on a Block's GraphQL Fragment file (see [CMS Data Loading](cms-data-loading.md#graphql-files)). This means a basic Block template looks like this:
Head Start uses the same convention for props and types for every Block: the `Props` interface always contains a `block` property containing the type based on the CMS model. This `block` type is automatically generated based on a Block's GraphQL Fragment file (see [CMS Data Loading](./cms-data-loading.md#graphql-files)). This means a basic Block template looks like this:

```astro
---
Expand Down
2 changes: 1 addition & 1 deletion docs/cms-data-loading.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

## Configuration

Head Start supports the use of [primary and sandbox environments in DatoCMS](https://www.datocms.com/docs/scripting-migrations/introduction). This enables feature branches to use a different environment than the main branch. You need to set the DatoCMS environment where content should be fetched from in [`/datocms-environment.ts`](/datocms-environment.ts):
Head Start supports the use of [primary and sandbox environments in DatoCMS](https://www.datocms.com/docs/scripting-migrations/introduction). This enables feature branches to use a different environment than the main branch. You need to set the DatoCMS environment where content should be fetched from in [`/datocms-environment.ts`](../datocms-environment.ts):

```ts
export const datocmsEnvironment = 'your-environment-name';
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"lint:astro": "astro check",
"lint:eslint": "eslint . --ext .js,.ts,.astro",
"lint:html": "html-validate --config config/htmlvalidate/config.json dist/",
"seed:docs": "jiti config/datocms/scripts/seed-docs.ts",
"pretest": "npm run prep",
"test": "run-s test:*",
"test:unit": "vitest run",
Expand Down
3 changes: 3 additions & 0 deletions src/layouts/Default.astro
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,9 @@ const mainContentId = 'content';
figure {
margin: 0;
}
pre {
overflow-x: scroll;
}
</style>
<style>
/* Sticky footer. @see https://css-tricks.com/a-clever-sticky-footer-technique/ */
Expand Down
Loading