From f2df762725a3313200fd01abf564a2e1916ab8e3 Mon Sep 17 00:00:00 2001 From: Jop Molenaar Date: Fri, 24 Jul 2026 15:51:26 +0200 Subject: [PATCH 1/5] feat(faqBlock): adds faq block and model that reuses accordion block --- datocms-environment.ts | 2 +- src/blocks/Blocks.d.ts | 2 + src/blocks/FaqBlock/FaqBlock.astro | 20 ++++++ src/blocks/FaqBlock/FaqBlock.fragment.graphql | 17 +++++ src/blocks/FaqBlock/FaqBlock.preview.txt | 1 + src/blocks/FaqBlock/FaqBlock.test.ts | 64 +++++++++++++++++++ src/blocks/FaqBlock/README.md | 5 ++ src/blocks/blocksByTypename.ts | 2 + .../Pages/CollectionEntry.query.graphql | 4 ++ src/pages/[locale]/_index.query.graphql | 4 ++ 10 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 src/blocks/FaqBlock/FaqBlock.astro create mode 100644 src/blocks/FaqBlock/FaqBlock.fragment.graphql create mode 100644 src/blocks/FaqBlock/FaqBlock.preview.txt create mode 100644 src/blocks/FaqBlock/FaqBlock.test.ts create mode 100644 src/blocks/FaqBlock/README.md diff --git a/datocms-environment.ts b/datocms-environment.ts index fc67c6d8..112c8fc5 100644 --- a/datocms-environment.ts +++ b/datocms-environment.ts @@ -2,5 +2,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 = 'deploy-restructure-grouping-block'; +export const datocmsEnvironment = 'dev-sandbox-21-05'; export const datocmsBuildTriggerId = '34548'; diff --git a/src/blocks/Blocks.d.ts b/src/blocks/Blocks.d.ts index 34b52205..eee0f07d 100644 --- a/src/blocks/Blocks.d.ts +++ b/src/blocks/Blocks.d.ts @@ -18,6 +18,7 @@ import type { TabsBlockFragment, AccordionBlockFragment, ColumnBlockFragment, + FaqBlockFragment, } from '~/lib/datocms/types'; import type { VariableBlockRecord } from '~/lib/datocms/schema'; @@ -42,5 +43,6 @@ export type AnyBlock = Omit< | TabsBlockFragment | AccordionBlockFragment | ColumnBlockFragment + | FaqBlockFragment ), '__typename' // Allow for any __typename so that missing blocks can be reported on. > & { __typename: string }; diff --git a/src/blocks/FaqBlock/FaqBlock.astro b/src/blocks/FaqBlock/FaqBlock.astro new file mode 100644 index 00000000..4f97c0b3 --- /dev/null +++ b/src/blocks/FaqBlock/FaqBlock.astro @@ -0,0 +1,20 @@ +--- +import AccordionBlock from '~/blocks/AccordionBlock/AccordionBlock.astro'; +import type { AccordionBlockFragment, FaqBlockFragment } from '~/lib/datocms/types'; + +export interface Props { + block: FaqBlockFragment +} +const { block } = Astro.props; + +const items = block.items + .map((faq) => faq.item) + .filter((item) => item !== undefined && item !== null) as AccordionBlockFragment['items']; +--- +{ block.groupTitle &&

{ block.groupTitle }

} + + diff --git a/src/blocks/FaqBlock/FaqBlock.fragment.graphql b/src/blocks/FaqBlock/FaqBlock.fragment.graphql new file mode 100644 index 00000000..862b7197 --- /dev/null +++ b/src/blocks/FaqBlock/FaqBlock.fragment.graphql @@ -0,0 +1,17 @@ +#import '~/blocks/AccordionBlock/AccordionBlock.fragment.graphql' + +fragment FaqBlock on FaqBlockRecord { + __typename + id + groupTitle + items { + id + slug + item { + __typename + ... on AccordionItemRecord { + ...AccordionItem + } + } + } +} diff --git a/src/blocks/FaqBlock/FaqBlock.preview.txt b/src/blocks/FaqBlock/FaqBlock.preview.txt new file mode 100644 index 00000000..778ab39c --- /dev/null +++ b/src/blocks/FaqBlock/FaqBlock.preview.txt @@ -0,0 +1 @@ +Renders reusable FAQ records as an accordion of questions and answers, with an optional group heading. diff --git a/src/blocks/FaqBlock/FaqBlock.test.ts b/src/blocks/FaqBlock/FaqBlock.test.ts new file mode 100644 index 00000000..b59a0058 --- /dev/null +++ b/src/blocks/FaqBlock/FaqBlock.test.ts @@ -0,0 +1,64 @@ +import { renderToFragment } from '~/lib/renderer'; +import { describe, expect, test } from 'vitest'; +import FaqBlock, { type Props } from './FaqBlock.astro'; + +const ACCORDION_ITEM_RECORD = 'AccordionItemRecord' as const; +const TEXT_BLOCK_RECORD = 'TextBlockRecord' as const; + +const textBlock = (value: string) => ({ + __typename: TEXT_BLOCK_RECORD, + text: { + blocks: [], + inlineBlocks: [], + links: [], + value: { + schema: 'dast', + document: { + type: 'root', + children: [ + { type: 'paragraph', children: [{ type: 'span', value }] }, + ], + }, + }, + }, +}); + +const faqItem = (slug: string, question: string, answer: string) => ({ + id: slug, + slug, + item: { __typename: ACCORDION_ITEM_RECORD, title: question, blocks: [textBlock(answer)] }, +}); + +describe('FaqBlock', () => { + const renderBlock = () => renderToFragment(FaqBlock, { + props: { + block: { + __typename: 'FaqBlockRecord', + id: 'faq-block-1', + groupTitle: 'Frequently asked questions', + items: [ + faqItem('question-a', 'Question A', 'Answer A'), + faqItem('question-b', 'Question B', 'Answer B'), + ], + }, + }, + }); + + test('renders the group title as a heading', async () => { + const fragment = await renderBlock(); + expect(fragment.querySelector('h2')?.textContent).toBe('Frequently asked questions'); + }); + + test('renders each linked question as an accordion item', async () => { + const fragment = await renderBlock(); + const summaries = [...fragment.querySelectorAll('summary')].map(s => s.textContent?.trim()); + expect(fragment.querySelectorAll('details').length).toBe(2); + expect(summaries?.[0]).toContain('Question A'); + expect(summaries?.[1]).toContain('Question B'); + }); + + test('renders the answer body of a linked question', async () => { + const fragment = await renderBlock(); + expect(fragment.textContent).toContain('Answer A'); + }); +}); diff --git a/src/blocks/FaqBlock/README.md b/src/blocks/FaqBlock/README.md new file mode 100644 index 00000000..949b9d9d --- /dev/null +++ b/src/blocks/FaqBlock/README.md @@ -0,0 +1,5 @@ +# FAQ Block + +**Renders a set of reusable FAQ records as an accordion of questions and answers.** + +Each FAQ is a shared record in the `Faq` model, holding a single Accordion Item (its title is the question, its blocks are the answer). The FAQ Block links to the specific FAQ records you want and renders them as an accordion, so the same question can be reused across multiple pages. Use the `groupTitle` field to add a heading above the questions. diff --git a/src/blocks/blocksByTypename.ts b/src/blocks/blocksByTypename.ts index e9ac3d25..3a020195 100644 --- a/src/blocks/blocksByTypename.ts +++ b/src/blocks/blocksByTypename.ts @@ -14,6 +14,7 @@ import VideoBlock from './VideoBlock/VideoBlock.astro'; import VideoEmbedBlock from './VideoEmbedBlock/VideoEmbedBlock.astro'; import SearchFormBlock from './SearchFormBlock/SearchFormBlock.astro'; import AccordionBlock from './AccordionBlock/AccordionBlock.astro'; +import FaqBlock from './FaqBlock/FaqBlock.astro'; import StackBlock from './StackBlock/StackBlock.astro'; import TabsBlock from './TabsBlock/TabsBlock.astro'; import ColumnBlock from './ColumnBlock/ColumnBlock.astro'; @@ -34,6 +35,7 @@ export const blocksByTypename = { VideoBlockRecord: VideoBlock, VideoEmbedBlockRecord: VideoEmbedBlock, AccordionBlockRecord: AccordionBlock, + FaqBlockRecord: FaqBlock, StackBlockRecord: StackBlock, TabsBlockRecord: TabsBlock, SearchFormBlockRecord: SearchFormBlock, diff --git a/src/content/Pages/CollectionEntry.query.graphql b/src/content/Pages/CollectionEntry.query.graphql index f4ccab25..9fef14fa 100644 --- a/src/content/Pages/CollectionEntry.query.graphql +++ b/src/content/Pages/CollectionEntry.query.graphql @@ -9,6 +9,7 @@ #import '~/blocks/VideoBlock/VideoBlock.fragment.graphql' #import '~/blocks/VideoEmbedBlock/VideoEmbedBlock.fragment.graphql' #import '~/blocks/AccordionBlock/AccordionBlock.fragment.graphql' +#import '~/blocks/FaqBlock/FaqBlock.fragment.graphql' #import '~/blocks/GroupingBlock/GroupingBlock.fragment.graphql' #import '~/blocks/ListBlock/ListBlockFragment.graphql' #import '~/blocks/SearchFormBlock/SearchFormBlock.fragment.graphql' @@ -33,6 +34,9 @@ query PageCollectionEntry($locale: SiteLocale!, $slug: String!) { ... on AccordionBlockRecord { ...AccordionBlock } + ... on FaqBlockRecord { + ...FaqBlock + } ... on ColumnBlockRecord { ...ColumnBlock } diff --git a/src/pages/[locale]/_index.query.graphql b/src/pages/[locale]/_index.query.graphql index 511299d8..b5a93ac0 100644 --- a/src/pages/[locale]/_index.query.graphql +++ b/src/pages/[locale]/_index.query.graphql @@ -8,6 +8,7 @@ #import '~/blocks/VideoBlock/VideoBlock.fragment.graphql' #import '~/blocks/VideoEmbedBlock/VideoEmbedBlock.fragment.graphql' #import '~/blocks/AccordionBlock/AccordionBlock.fragment.graphql' +#import '~/blocks/FaqBlock/FaqBlock.fragment.graphql' #import '~/blocks/GroupingBlock/GroupingBlock.fragment.graphql' #import '~/blocks/SearchFormBlock/SearchFormBlock.fragment.graphql' #import '~/blocks/StackBlock/StackBlock.fragment.graphql' @@ -32,6 +33,9 @@ query HomePage($locale: SiteLocale!) { ... on AccordionBlockRecord { ...AccordionBlock } + ... on FaqBlockRecord { + ...FaqBlock + } ... on ColumnBlockRecord { ...ColumnBlock } From 4b19f79fe55bf5f90eb184e231b510a87fb4c0da Mon Sep 17 00:00:00 2001 From: Jop Molenaar Date: Fri, 24 Jul 2026 16:20:52 +0200 Subject: [PATCH 2/5] fix(): changes item and items to a clear name. Adds migration file --- .../datocms/migrations/1784902160_faqBlock.ts | 167 ++++++++++++++++++ src/blocks/FaqBlock/FaqBlock.astro | 4 +- src/blocks/FaqBlock/FaqBlock.fragment.graphql | 4 +- src/blocks/FaqBlock/FaqBlock.test.ts | 4 +- 4 files changed, 173 insertions(+), 6 deletions(-) create mode 100644 config/datocms/migrations/1784902160_faqBlock.ts diff --git a/config/datocms/migrations/1784902160_faqBlock.ts b/config/datocms/migrations/1784902160_faqBlock.ts new file mode 100644 index 00000000..e053a001 --- /dev/null +++ b/config/datocms/migrations/1784902160_faqBlock.ts @@ -0,0 +1,167 @@ +import { Client } from '@datocms/lib/cma-client-node'; + +export default async function (client: Client) { + console.log('Create new models/block models'); + + console.log('Create block model "\u2753 FAQ Block" (`faq_block`)'); + await client.itemTypes.create( + { + id: 'bwnEedmZRLCodyteEudcIQ', + name: '\u2753 FAQ Block', + api_key: 'faq_block', + modular_block: true, + draft_saving_active: false, + hint: '', + inverse_relationships_enabled: false, + }, + { + skip_menu_item_creation: true, + schema_menu_item_id: 'PQtosMOrR_2nUjIwyGI5-w', + }, + ); + + console.log('Create model "\u2753 FAQ" (`faq`)'); + await client.itemTypes.create( + { + id: 'Wp8-9-o4Tc2uFPn01WwxYQ', + name: '\u2753 FAQ', + api_key: 'faq', + draft_mode_active: true, + draft_saving_active: false, + collection_appearance: 'table', + inverse_relationships_enabled: false, + }, + { + skip_menu_item_creation: true, + schema_menu_item_id: 'XAz-FlSfTumPrm_vuq20lQ', + }, + ); + + console.log('Creating new fields/fieldsets'); + + console.log( + 'Create Single-line string field "Group title " (`group_title`) in block model "\u2753 FAQ Block" (`faq_block`)', + ); + await client.fields.create('bwnEedmZRLCodyteEudcIQ', { + id: 'FuciaLmuRR6sm_G3eZ1kaA', + label: 'Group title ', + field_type: 'string', + api_key: 'group_title', + appearance: { + addons: [], + editor: 'single_line', + parameters: { heading: false, placeholder: null }, + }, + default_value: null, + content_link_enabled: true, + }); + + console.log( + 'Create Multiple links field "Question and Answers" (`question_and_answers`) in block model "\u2753 FAQ Block" (`faq_block`)', + ); + await client.fields.create('bwnEedmZRLCodyteEudcIQ', { + id: 'DmOR-2ehTOyylvEt0S6NJg', + label: 'Question and Answers', + field_type: 'links', + api_key: 'question_and_answers', + validators: { + items_item_type: { + on_publish_with_unpublished_references_strategy: 'fail', + on_reference_unpublish_strategy: 'delete_references', + on_reference_delete_strategy: 'delete_references', + item_types: ['Wp8-9-o4Tc2uFPn01WwxYQ'], + }, + }, + appearance: { + addons: [], + editor: 'links_embed', + parameters: { filters: [] }, + }, + default_value: null, + content_link_enabled: true, + }); + + console.log( + 'Create Modular Content (Single block) field "Question and Answer" (`question_and_answer`) in model "\u2753 FAQ" (`faq`)', + ); + // NOTE: `item_types` below references the "Accordion Item" block model. + // This id must match the Accordion Item block in the environment this + // migration runs against. Verify it before applying. + await client.fields.create('Wp8-9-o4Tc2uFPn01WwxYQ', { + id: 'ObBfAPRKQKqulCO-p4ZiAw', + label: 'Question and Answer', + field_type: 'single_block', + api_key: 'question_and_answer', + validators: { + single_block_blocks: { item_types: ['NzOqAAh2Ra2DfyEMrsF-DQ'] }, + required: {}, + }, + appearance: { + addons: [], + editor: 'framed_single_block', + parameters: { start_collapsed: false }, + }, + default_value: null, + content_link_enabled: true, + }); + + console.log( + 'Create Slug field "Slug" (`slug`) in model "\u2753 FAQ" (`faq`)', + ); + await client.fields.create('Wp8-9-o4Tc2uFPn01WwxYQ', { + id: 'Cs6T765xTlanqEV6ZB01uQ', + label: 'Slug', + field_type: 'slug', + api_key: 'slug', + validators: { + slug_format: { predefined_pattern: 'webpage_slug' }, + required: {}, + unique: {}, + }, + appearance: { + addons: [], + editor: 'slug', + parameters: { url_prefix: null, placeholder: null }, + }, + default_value: null, + content_link_enabled: true, + }); + + console.log('Finalize models/block models'); + + console.log('Update block model "\u2753 FAQ Block" (`faq_block`)'); + await client.itemTypes.update('bwnEedmZRLCodyteEudcIQ', { + presentation_title_field: { id: 'FuciaLmuRR6sm_G3eZ1kaA', type: 'field' }, + }); + + console.log('Update model "\u2753 FAQ" (`faq`)'); + await client.itemTypes.update('Wp8-9-o4Tc2uFPn01WwxYQ', { + presentation_title_field: { id: 'ObBfAPRKQKqulCO-p4ZiAw', type: 'field' }, + }); + + console.log('Manage menu items'); + + console.log('Create menu item "\u2753 FAQ"'); + await client.menuItems.create({ + id: 'c5jZf1KyTsCmRfzpTIwYAA', + label: '\u2753 FAQ', + item_type: { id: 'Wp8-9-o4Tc2uFPn01WwxYQ', type: 'item_type' }, + }); + + console.log('Update menu item "\u2753 FAQ"'); + await client.menuItems.update('c5jZf1KyTsCmRfzpTIwYAA', { position: 10 }); + + console.log('Manage schema menu items'); + + console.log('Update model schema menu item for model "\u2753 FAQ" (`faq`)'); + await client.schemaMenuItems.update('XAz-FlSfTumPrm_vuq20lQ', { + position: 52, + }); + + console.log( + 'Update block schema menu item for block model "\u2753 FAQ Block" (`faq_block`)', + ); + await client.schemaMenuItems.update('PQtosMOrR_2nUjIwyGI5-w', { + position: 51, + }); +} diff --git a/src/blocks/FaqBlock/FaqBlock.astro b/src/blocks/FaqBlock/FaqBlock.astro index 4f97c0b3..ea5f8a59 100644 --- a/src/blocks/FaqBlock/FaqBlock.astro +++ b/src/blocks/FaqBlock/FaqBlock.astro @@ -7,8 +7,8 @@ export interface Props { } const { block } = Astro.props; -const items = block.items - .map((faq) => faq.item) +const items = block.questionAndAnswers + .map((faq) => faq.questionAndAnswer) .filter((item) => item !== undefined && item !== null) as AccordionBlockFragment['items']; --- { block.groupTitle &&

{ block.groupTitle }

} diff --git a/src/blocks/FaqBlock/FaqBlock.fragment.graphql b/src/blocks/FaqBlock/FaqBlock.fragment.graphql index 862b7197..6b04c3d2 100644 --- a/src/blocks/FaqBlock/FaqBlock.fragment.graphql +++ b/src/blocks/FaqBlock/FaqBlock.fragment.graphql @@ -4,10 +4,10 @@ fragment FaqBlock on FaqBlockRecord { __typename id groupTitle - items { + questionAndAnswers { id slug - item { + questionAndAnswer { __typename ... on AccordionItemRecord { ...AccordionItem diff --git a/src/blocks/FaqBlock/FaqBlock.test.ts b/src/blocks/FaqBlock/FaqBlock.test.ts index b59a0058..14a80e63 100644 --- a/src/blocks/FaqBlock/FaqBlock.test.ts +++ b/src/blocks/FaqBlock/FaqBlock.test.ts @@ -26,7 +26,7 @@ const textBlock = (value: string) => ({ const faqItem = (slug: string, question: string, answer: string) => ({ id: slug, slug, - item: { __typename: ACCORDION_ITEM_RECORD, title: question, blocks: [textBlock(answer)] }, + questionAndAnswer: { __typename: ACCORDION_ITEM_RECORD, title: question, blocks: [textBlock(answer)] }, }); describe('FaqBlock', () => { @@ -36,7 +36,7 @@ describe('FaqBlock', () => { __typename: 'FaqBlockRecord', id: 'faq-block-1', groupTitle: 'Frequently asked questions', - items: [ + questionAndAnswers: [ faqItem('question-a', 'Question A', 'Answer A'), faqItem('question-b', 'Question B', 'Answer B'), ], From 7c98e137deabe19a82a578dad11ce6da38dcbac4 Mon Sep 17 00:00:00 2001 From: Jop Molenaar Date: Fri, 24 Jul 2026 16:37:46 +0200 Subject: [PATCH 3/5] fix(): modify migration so it will make the faq block correctly --- .../datocms/migrations/1784902160_faqBlock.ts | 80 +++++++++++++------ 1 file changed, 56 insertions(+), 24 deletions(-) diff --git a/config/datocms/migrations/1784902160_faqBlock.ts b/config/datocms/migrations/1784902160_faqBlock.ts index e053a001..f0b5a08e 100644 --- a/config/datocms/migrations/1784902160_faqBlock.ts +++ b/config/datocms/migrations/1784902160_faqBlock.ts @@ -1,10 +1,15 @@ -import { Client } from '@datocms/lib/cma-client-node'; +import { Client, SimpleSchemaTypes } from '@datocms/cli/lib/cma-client-node'; export default async function (client: Client) { + //@ts-expect-error rich_text_blocks is only available on Modular Content fields + const homeBodyBlocks = (await client.fields.find('home_page::body_blocks')).validators.rich_text_blocks?.item_types as string[]; + //@ts-expect-error rich_text_blocks is only available on Modular Content fields + const pageBodyBlocks = (await client.fields.find('page::body_blocks')).validators.rich_text_blocks?.item_types as string[]; + console.log('Create new models/block models'); console.log('Create block model "\u2753 FAQ Block" (`faq_block`)'); - await client.itemTypes.create( + const faqBlock = await client.itemTypes.create( { id: 'bwnEedmZRLCodyteEudcIQ', name: '\u2753 FAQ Block', @@ -53,7 +58,6 @@ export default async function (client: Client) { parameters: { heading: false, placeholder: null }, }, default_value: null, - content_link_enabled: true, }); console.log( @@ -78,22 +82,49 @@ export default async function (client: Client) { parameters: { filters: [] }, }, default_value: null, - content_link_enabled: true, + }); + + console.log( + 'Create Slug field "Slug" (`slug`) in model "\u2753 FAQ" (`faq`)', + ); + await client.fields.create('Wp8-9-o4Tc2uFPn01WwxYQ', { + id: 'Cs6T765xTlanqEV6ZB01uQ', + label: 'Slug', + field_type: 'slug', + api_key: 'slug', + validators: { + slug_format: { predefined_pattern: 'webpage_slug' }, + required: {}, + unique: {}, + }, + appearance: { + addons: [], + editor: 'slug', + parameters: { url_prefix: null, placeholder: null }, + }, + default_value: null, }); console.log( 'Create Modular Content (Single block) field "Question and Answer" (`question_and_answer`) in model "\u2753 FAQ" (`faq`)', ); - // NOTE: `item_types` below references the "Accordion Item" block model. - // This id must match the Accordion Item block in the environment this - // migration runs against. Verify it before applying. + // The answer links to the existing "Accordion Item" block model, whose id + // differs per environment. Resolve it by api_key instead of hardcoding. + const accordionItem = (await client.itemTypes.list()).find( + (itemType: SimpleSchemaTypes.ItemType) => itemType.api_key === 'accordion_item', + ); + if (!accordionItem) { + throw new Error( + 'Could not find the "accordion_item" block model required by the FAQ "Question and Answer" field.', + ); + } await client.fields.create('Wp8-9-o4Tc2uFPn01WwxYQ', { id: 'ObBfAPRKQKqulCO-p4ZiAw', label: 'Question and Answer', field_type: 'single_block', api_key: 'question_and_answer', validators: { - single_block_blocks: { item_types: ['NzOqAAh2Ra2DfyEMrsF-DQ'] }, + single_block_blocks: { item_types: [accordionItem.id] }, required: {}, }, appearance: { @@ -102,29 +133,30 @@ export default async function (client: Client) { parameters: { start_collapsed: false }, }, default_value: null, - content_link_enabled: true, }); + console.log('Update existing fields/fieldsets'); + console.log( - 'Create Slug field "Slug" (`slug`) in model "\u2753 FAQ" (`faq`)', + 'Update Modular Content (Multiple blocks) field "Body" (`body_blocks`) in model "\ud83d\udcd1 Page" (`page`)', ); - await client.fields.create('Wp8-9-o4Tc2uFPn01WwxYQ', { - id: 'Cs6T765xTlanqEV6ZB01uQ', - label: 'Slug', - field_type: 'slug', - api_key: 'slug', + await client.fields.update('Q-z1nyMsQtC8Sr6w6J2oGw', { validators: { - slug_format: { predefined_pattern: 'webpage_slug' }, - required: {}, - unique: {}, + rich_text_blocks: { + item_types: [...pageBodyBlocks, faqBlock.id], + }, }, - appearance: { - addons: [], - editor: 'slug', - parameters: { url_prefix: null, placeholder: null }, + }); + + console.log( + 'Update Modular Content (Multiple blocks) field "Body" (`body_blocks`) in model "\ud83c\udfe0 Home" (`home_page`)', + ); + await client.fields.update('pUj2PObgTyC-8X4lvZLMBA', { + validators: { + rich_text_blocks: { + item_types: [...homeBodyBlocks, faqBlock.id], + }, }, - default_value: null, - content_link_enabled: true, }); console.log('Finalize models/block models'); From 0a2e7479b16e1d4fc3b152484e158542091e2846 Mon Sep 17 00:00:00 2001 From: Jop Molenaar Date: Fri, 24 Jul 2026 17:04:18 +0200 Subject: [PATCH 4/5] fix(): adds deep linking --- src/blocks/FaqBlock/FaqBlock.astro | 26 ++++++++++++-------- src/blocks/FaqBlock/FaqBlock.client.ts | 21 ++++++++++++++++ src/blocks/FaqBlock/FaqBlock.test.ts | 6 +++++ src/components/Accordion/AccordionItem.astro | 7 +++--- 4 files changed, 47 insertions(+), 13 deletions(-) create mode 100644 src/blocks/FaqBlock/FaqBlock.client.ts diff --git a/src/blocks/FaqBlock/FaqBlock.astro b/src/blocks/FaqBlock/FaqBlock.astro index ea5f8a59..f180e357 100644 --- a/src/blocks/FaqBlock/FaqBlock.astro +++ b/src/blocks/FaqBlock/FaqBlock.astro @@ -1,20 +1,26 @@ --- -import AccordionBlock from '~/blocks/AccordionBlock/AccordionBlock.astro'; -import type { AccordionBlockFragment, FaqBlockFragment } from '~/lib/datocms/types'; +import Blocks from '~/blocks/Blocks.astro'; +import { Accordion, AccordionItem } from '~/components/Accordion'; +import type { FaqBlockFragment } from '~/lib/datocms/types'; export interface Props { block: FaqBlockFragment } const { block } = Astro.props; -const items = block.questionAndAnswers - .map((faq) => faq.questionAndAnswer) - .filter((item) => item !== undefined && item !== null) as AccordionBlockFragment['items']; +const faqs = block.questionAndAnswers.filter((faq) => faq.questionAndAnswer); --- { block.groupTitle &&

{ block.groupTitle }

} - + + { faqs.map((faq) => ( + + { faq.questionAndAnswer.title } + + + + + )) } + + + diff --git a/src/blocks/FaqBlock/FaqBlock.client.ts b/src/blocks/FaqBlock/FaqBlock.client.ts new file mode 100644 index 00000000..1d78d94b --- /dev/null +++ b/src/blocks/FaqBlock/FaqBlock.client.ts @@ -0,0 +1,21 @@ +function openTargetedQuestion() { + const { hash } = window.location; + if (!hash) { + return; + } + + let target: Element | null; + try { + target = document.querySelector(hash); + } catch { + return; + } + + if (target instanceof HTMLDetailsElement) { + target.open = true; + target.scrollIntoView(); + } +} + +openTargetedQuestion(); +window.addEventListener('hashchange', openTargetedQuestion); diff --git a/src/blocks/FaqBlock/FaqBlock.test.ts b/src/blocks/FaqBlock/FaqBlock.test.ts index 14a80e63..b50526be 100644 --- a/src/blocks/FaqBlock/FaqBlock.test.ts +++ b/src/blocks/FaqBlock/FaqBlock.test.ts @@ -61,4 +61,10 @@ describe('FaqBlock', () => { const fragment = await renderBlock(); expect(fragment.textContent).toContain('Answer A'); }); + + test('exposes each question by its slug for deep-linking', async () => { + const fragment = await renderBlock(); + expect(fragment.querySelector('details#question-a')).toBeTruthy(); + expect(fragment.querySelector('details#question-b')).toBeTruthy(); + }); }); diff --git a/src/components/Accordion/AccordionItem.astro b/src/components/Accordion/AccordionItem.astro index 2a592692..0b80743e 100644 --- a/src/components/Accordion/AccordionItem.astro +++ b/src/components/Accordion/AccordionItem.astro @@ -2,12 +2,13 @@ import Icon from '~/components/Icon'; export interface Props { - name: string; + name?: string; open?: boolean; + id?: string; } -const { name, open } = Astro.props; -const attributes = { name, open }; +const { name, open, id } = Astro.props; +const attributes = { name, open, id }; --- { /* HTMLDetailsElement name attribute is new, using spread to bypass valiation error */ }
From a3e5b98456a6d33c9329a09dfd1d9698e4639ac9 Mon Sep 17 00:00:00 2001 From: Jop Molenaar Date: Wed, 29 Jul 2026 11:16:36 +0200 Subject: [PATCH 5/5] fix(): typos, removed name from accordion in faq block, --- config/datocms/migrations/1784902160_faqBlock.ts | 4 ++-- src/blocks/FaqBlock/FaqBlock.astro | 2 +- src/components/Accordion/AccordionItem.astro | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/config/datocms/migrations/1784902160_faqBlock.ts b/config/datocms/migrations/1784902160_faqBlock.ts index f0b5a08e..701fbf0e 100644 --- a/config/datocms/migrations/1784902160_faqBlock.ts +++ b/config/datocms/migrations/1784902160_faqBlock.ts @@ -45,11 +45,11 @@ export default async function (client: Client) { console.log('Creating new fields/fieldsets'); console.log( - 'Create Single-line string field "Group title " (`group_title`) in block model "\u2753 FAQ Block" (`faq_block`)', + 'Create Single-line string field "Group title" (`group_title`) in block model "\u2753 FAQ Block" (`faq_block`)', ); await client.fields.create('bwnEedmZRLCodyteEudcIQ', { id: 'FuciaLmuRR6sm_G3eZ1kaA', - label: 'Group title ', + label: 'Group title', field_type: 'string', api_key: 'group_title', appearance: { diff --git a/src/blocks/FaqBlock/FaqBlock.astro b/src/blocks/FaqBlock/FaqBlock.astro index f180e357..4f89b2df 100644 --- a/src/blocks/FaqBlock/FaqBlock.astro +++ b/src/blocks/FaqBlock/FaqBlock.astro @@ -14,7 +14,7 @@ const faqs = block.questionAndAnswers.filter((faq) => faq.questionAndAnswer); { faqs.map((faq) => ( - + { faq.questionAndAnswer.title } diff --git a/src/components/Accordion/AccordionItem.astro b/src/components/Accordion/AccordionItem.astro index 0b80743e..6a0d70ae 100644 --- a/src/components/Accordion/AccordionItem.astro +++ b/src/components/Accordion/AccordionItem.astro @@ -10,7 +10,7 @@ export interface Props { const { name, open, id } = Astro.props; const attributes = { name, open, id }; --- -{ /* HTMLDetailsElement name attribute is new, using spread to bypass valiation error */ } +{ /* HTMLDetailsElement name attribute is new, using spread to bypass validation error */ }