Head Start distinguishes between CMS content related Blocks and regular UI Components.
Head Start is based on the Astro project structure (see Project Structure). Head Start extends the default structure to group blocks next to the regular components:
src/
├── blocks/
│ ├── Blocks.astro
│ ├── Blocks.d.ts
│ └── SomeContentBlock/
│ ├── SomeContentBlock.astro
│ ├── SomeContentBlock.fragment.graphql
│ ├── SomeContentBlock.client.ts
│ └── SomeContentBlock.test.ts
│
└── components/
└── SomeUiComponent/
├── SomeUiComponent.astro
│ ├── SomeContentBlock.client.ts
│ └── SomeContentBlock.test.ts
- Components are the UI elements the website is composed of. This can be Astro and framework specific components.
- Blocks are a specific set of components which have a complementary content Block in DatoCMS and therefore have a paired GraphQL Fragment file.
- Optionally blocks and components have a complementary
*.client.ts
file for client-side scripts and a*.test.ts
file for related unit tests.
Note
You can use npm run create:block
and npm run create:component
to quickly scaffold a new block or component with their associated files.
See CMS Data Loading for documentation on the use of GraphQL Fragment files.
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). This means a basic Block template looks like this:
---
import type { SomeContentBlockFragment } from '@lib/datocms/types';
interface Props {
block: SomeContentBlockFragment
}
const { block } = Astro.props;
---
{ block.someContentBlockField }
Within the CMS the Blocks are used in Modular Content and Structured Text to create dynamic layouts.
Pages and other templates can use the <Blocks />
component to render a Modular Content or Structured Text field. For example:
---
import { datocmsRequest } from '@lib/datocms';
import Blocks from '@blocks/Blocks.astro';
import query from './_index.query.graphql';
const { page } = await datocmsRequest({ query, variables: { locale: Astro.params.locale } });
---
<Blocks blocks={page.bodyBlocks} />
When you create a new Block, you need to register it in blocks/Blocks.astro
(using the CMS model's typename) and add its type to blocks/Blocks.d.ts
:
Register a new Block's template:
---
// src/Blocks/Blocks.astro:
import type { AnyBlock } from './Blocks';
import ImageBlock from './ImageBlock/ImageBlock.astro';
// import your new Block template (order imports A to Z):
import SomeContentBlock from './SomeContentBlock/SomeContentBlock.astro';
import TextBlock from './TextBlock/TextBlock.astro';
const blocksByTypename = {
ImageBlockRecord: ImageBlock,
// register your new Block using its CMS model's typename (order A to Z):
SomeContentBlockRecord: SomeContentBlock,
TextBlockRecord: TextBlock,
};
interface Props {
blocks: AnyBlock[];
}
const { blocks } = Astro.props;
Register a new Block's type:
// src/Blocks/Blocks.d.ts:
import {
ImageBlockFragment,
// import new Block's Fragment:
SomeContentBlockFragment,
TextBlockFragment,
} from '@lib/datocms/types';
export type AnyBlock =
| ImageBlockFragment
| SomeContentBlockFragment // and add it here (order A to Z)
| TextBlockFragment;
Astro supports client-side scripts inside components. Head Start uses the convention to include these as external scripts for better TypeScript intellisense and linting. To distinguish server-side files (most in Astro) from client-side scripts we use a .client.ts
extension. So blocks and components can include these as <script src="./SomeComponent.client.ts"></script>
.
Head Start provides a testing setup. This includes helpers to make component testing easier. Astro renders components to string. The renderToFragment
helper allows you to test components as document fragments providing most familiar DOM methods like .querySelector
and .getAttribute
:
// SomeComponent.test.ts
import { describe, expect, test } from 'vitest';
import { renderToFragment } from '@lib/renderer';
import SomeComponent, { type Props } from './SomeComponent.astro';
describe('Some Component', () => {
const fragment = await renderToFragment<Props>(SomeComponent, {
someProp: 'some value',
});
test('uses some prop as attribute', () => {
const value = fragment.querySelector('.someSelector')?.getAttribute('some-attribute');
expect(value).toBe('some value');
});
// Add more tests here
});
Note: test files must use the .test.ts
extension to run.