FAQ items as custom blocks

I intend to have an FAQ page where the entries are formatted as description list (<dl>) where each question is a <dt> and each answer is a <dd>. So, I figured the block field would be appropriate for that, with a custom “FAQ item” block that contains two text fields/areas.

This is all good but the problem is that there can also be headings in between the FAQ items, so basically I need to close and open a description list each time a heading comes along. So I can’t just put a heading block in between the FAQ items.

I thought about creating an “FAQ list” block with a structure field for repeating question/answer sequences but then I’m losing the visual representation of the content in the main blueprint.
Anybody got any idea how I could solve this conundrum?

Isn’t this what you are after? Block factory: Creating your own blocks collection | Kirby CMS

Damn, this is awesome! I saw a link to that page but I didn’t even think of looking at it. :man_facepalming: This looks promising, though, thanks.

Hm, I tried to adapt this but I’m getting the error:

content.faq is undefined

I’m not sure where I’m wrong; here’s my setup:

Kirby::plugin('myproject/faq-block', [
	'blueprints' => [
		'blocks/accordion' => __DIR__ . 'blueprints/blocks/accordion.yml',
		'blocks/faq' => __DIR__ . 'blueprints/blocks/faq.yml'
	],
	'snippets' => [
		'blocks/accordion' => __DIR__ . 'blueprints/blocks/accordion.php',
		'blocks/faq' => __DIR__ . 'blueprints/blocks/faq.php'
	]
]);

accordion.yml:

name: field.blocks.accordion.name
icon: bars
fields:
	summary:
		label: Summary
		type: writer
		marks: false
		placeholder: Enter summary…
	details:
		label: Detail
		type: writer
		marks: true

faq.yml

name: field.blocks.faq.name
icon: question
fields:
	heading:
		label: Section Heading
		type: writer
		inline: true
		marks: false
	faq:
		label: FAQ
		type: blocks
		fieldsets:
			- accordion

and index.js:

panel.plugin("myproject/faq-block", {
	blocks: {
		accordion: {
			computed: {
				summaryField() {
					return this.field("summary");
				},
				detailsField() {
					return this.field("details");
				}
			},
			template: `
				<div @dblclick="open">
					<details>
						<summary>
							<k-writer
								ref="summary"
								:inline="true"
								marks="false"
								:placeholder="summaryField.placeholder || 'Add a summary…'"
								:value="content.summary"
								@input="update({ summary: $event })"
							/>
						</summary>
						<k-writer
								ref="details"
								:inline="detailsField.inline || false"
								:marks="detailsField.marks"
								:value="content.details"
								:placeholder="detailsField.placeholder || 'Add some details'"
								@input="update({ details: $event })"
							/>
					</details>
				</div>
			`
		},
		faq: {
			computed: {
				items() {
					return this.content.faq || {};
				},
				headingField() {
					return this.field("heading") || '';
				}
			},
			methods: {
				updateItem(content, index, name, value) {
					content.faq[index].content[name]= value;
					this.$emit("update", {
							...this.content,
							...content
						});
				}
			},
			template: `
				<div @dblclick="open">
					<h2 class="k-block-type-faq-heading">
						<k-writer
							ref="heading"
							:inline="headingField.inline"
							:marks="headingField.marks"
							:placeholder="headingField.placeholder || 'Add a heading'"
							:value="content.heading"
							@input="update({ heading: $event })"
						/>
					</h2>
					<div v-if="content.faq.length">
						<details
							class="k-block-type-faq-item"
							v-for="(item, index) in items"
							:key="index"
						>
						<summary>
							<k-writer
								ref="summary"
								:inline="true"
								:marks="false"
								:value="item.content.summary"
								@input="updateItem(content, index, 'summary', $event)"
							/>
						</summary>
						<div>
							<k-writer
								ref="details"
								:marks="true"
								:value="item.content.details"
								@input="updateItem(content, index, 'details', $event)"
						/>
						</div>
						</details>
					</div>
					<div v-else>No items yet</div>
				</div>
			`
		},
	}
});

I basically just copy/pasted it verbatim from the Cookbook recipe, except renaming “faq2” to “faq”.

What did I miss?

1 Like

I dont see any mistake, maybe filenames or folderstructure?

This should probably be this.field("faq")

Hmm, it seems to have something to do with the fact that there is already content on the page that I created with other fields while testing. If I create a brand new page, it shows the empty blocks placeholder. However, if I add the FAQ block, it gives me the error

e[0] is undefined

Actually, it’s two errors; the console says this:

11:37:07.643 TypeError: content.faq is undefined
VueJS 43
add http://example.loc/media/panel/8b5ea4a0bf473451a1635a801effd782/js/index.js:1
VueJS 4
add http://example.loc/media/panel/8b5ea4a0bf473451a1635a801effd782/js/index.js:1
click http://example.loc/media/panel/8b5ea4a0bf473451a1635a801effd782/js/index.js:1
VueJS 35
index.js:1:196770

and:

11:37:07.653 TypeError: e[0] is undefined
open http://example.loc/media/panel/8b5ea4a0bf473451a1635a801effd782/js/index.js:1
focusOrOpen http://example.loc/media/panel/8b5ea4a0bf473451a1635a801effd782/js/index.js:1
add http://example.loc/media/panel/8b5ea4a0bf473451a1635a801effd782/js/index.js:1
VueJS 8
add http://example.loc/media/panel/8b5ea4a0bf473451a1635a801effd782/js/index.js:1
VueJS 4
add http://example.loc/media/panel/8b5ea4a0bf473451a1635a801effd782/js/index.js:1
click http://example.loc/media/panel/8b5ea4a0bf473451a1635a801effd782/js/index.js:1
VueJS 35
index.js:1:196770

And the first error is regardless of whether I have return this.field("faq") || {}; or return this.content.faq || {};

Ok, looks like I have to test this again, will do tonight.

Are you using Kirby 3 or 4?

I’m using the latest version of Kirby 3.
Thank you very much.

Sorry, I only just got round to testing this again, but for me the cookbook still works as expected.

When you renamed faq2 to faq, did you make sure to also rename the yaml file. Please double-check everything again.

I went back and copied the index.php file from the Cookbook, and it worked with that. Turns out, when I wrote the original index.php manually (instead of copying) I forgot to add the leading slashes in the file paths (besides forgetting to change “blueprints” to “snippets” for the PHP files, as can be seen in post #4 above, but nobody has noticed :stuck_out_tongue:).

So, it should be:

'blocks/accordion' => __DIR__ . '/blueprints/blocks/accordion.yml',

not

'blocks/accordion' => __DIR__ . 'blueprints/blocks/accordion.yml',

(note the missing slash before “blueprints” in the second example).

Thanks a lot for your help and sorry for wasting your time.