sanity-plugin-sgntech-interview 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 SGNTech
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,173 @@
1
+ <!-- Social preview / OpenGraph image -->
2
+ <p align="center">
3
+ <img src="https://raw.githubusercontent.com/netz-sg/sanity-plugin-sgntech-interview/main/assets/og-image.png" alt="sanity-plugin-sgntech-interview — question and answer blocks for Sanity Studio" width="640" />
4
+ </p>
5
+
6
+ <h1 align="center">sanity-plugin-sgntech-interview</h1>
7
+
8
+ <p align="center">
9
+ Interviews as Portable Text: question and answer pairs with speakers, timestamps and a note when an answer was translated.
10
+ </p>
11
+
12
+ <p align="center">
13
+ <a href="https://www.npmjs.com/package/sanity-plugin-sgntech-interview"><img alt="npm version" src="https://img.shields.io/npm/v/sanity-plugin-sgntech-interview.svg?style=flat-square" /></a>
14
+ <a href="./LICENSE"><img alt="license" src="https://img.shields.io/npm/l/sanity-plugin-sgntech-interview.svg?style=flat-square" /></a>
15
+ </p>
16
+
17
+ ---
18
+
19
+ ## What you get
20
+
21
+ - **One block per turn**, so intros, photos and section headings sit between the questions the way a printed interview does.
22
+ - **Speakers defined once** on the document. Blocks pick from the list, so renaming somebody fixes every turn at once.
23
+ - **Answers take bold, italic and links** — enough for the album someone is talking about, without an editor inside the editor.
24
+ - **Timestamps** into the recording, turned into a link that starts at the right second on YouTube and Vimeo.
25
+ - **A translation note.** A translated answer is not a verbatim quote, and the original wording can be kept alongside it.
26
+ - **A renderer that carries no design** — two columns by default, one CSS variable away from the classic `NAME: text` form.
27
+
28
+ ## Installation
29
+
30
+ ```sh
31
+ npm install sanity-plugin-sgntech-interview
32
+ ```
33
+
34
+ Sanity Studio v5 or v6, React 18 or 19. `@portabletext/react` is an optional peer — only the
35
+ renderer needs it, and only when the site does not pass its own.
36
+
37
+ ## Usage
38
+
39
+ ```ts
40
+ // sanity.config.ts
41
+ import {defineArrayMember, defineConfig, defineField, defineType} from 'sanity'
42
+ import {createSpeakersField, interview} from 'sanity-plugin-sgntech-interview'
43
+
44
+ export default defineConfig({
45
+ plugins: [interview()],
46
+ schema: {
47
+ types: [
48
+ defineType({
49
+ type: 'document',
50
+ name: 'post',
51
+ fields: [
52
+ defineField({type: 'string', name: 'title'}),
53
+ createSpeakersField(),
54
+ defineField({
55
+ type: 'array',
56
+ name: 'body',
57
+ of: [
58
+ defineArrayMember({type: 'block'}),
59
+ defineArrayMember({type: 'interviewBlock'}),
60
+ ],
61
+ }),
62
+ ],
63
+ }),
64
+ ],
65
+ },
66
+ })
67
+ ```
68
+
69
+ `createSpeakersField()` adds the list the blocks read from. Fill it in first — name, a short id
70
+ like `till`, an optional role, and a tick on whoever asks the questions. Blocks without their own
71
+ asker fall back to the first one ticked.
72
+
73
+ ### Options
74
+
75
+ ```ts
76
+ interview({
77
+ name: 'interviewBlock', // block type name
78
+ title: 'Interview',
79
+ speakersField: 'interviewSpeakers', // where the speakers live
80
+ speakerTypes: [], // e.g. ['author', 'bandMember'] to link real documents
81
+ answerMarks: {decorators: ['strong', 'em'], link: true},
82
+ timestamps: true,
83
+ translation: true,
84
+ })
85
+ ```
86
+
87
+ `createSpeakersField()` takes the same `speakersField` and `speakerTypes`, so pass the config to
88
+ both when you change them.
89
+
90
+ ## Rendering
91
+
92
+ ```tsx
93
+ import {PortableText} from '@portabletext/react'
94
+ import {
95
+ createInterviewPortableTextComponents,
96
+ ensureInterviewStyles,
97
+ } from 'sanity-plugin-sgntech-interview/render'
98
+
99
+ ensureInterviewStyles() // or paste `interviewStyles` into your own CSS
100
+
101
+ export function Article({post}) {
102
+ return (
103
+ <PortableText
104
+ value={post.body}
105
+ components={createInterviewPortableTextComponents({
106
+ speakers: post.interviewSpeakers,
107
+ source: post.videoUrl, // makes the timestamps clickable
108
+ })}
109
+ />
110
+ )
111
+ }
112
+ ```
113
+
114
+ The speakers are passed in because they live on the document, not on the block — one GROQ
115
+ projection away:
116
+
117
+ ```groq
118
+ *[_type == "post"][0]{..., interviewSpeakers}
119
+ ```
120
+
121
+ ### Styling
122
+
123
+ Two columns by default: speaker left, text right, stacking below 30rem. Every value is a custom
124
+ property with its default at the point of use, so setting one on any ancestor works:
125
+
126
+ ```css
127
+ .article {
128
+ --sgn-interview-speaker-width: 6rem;
129
+ --sgn-interview-pair-gap: 2.5rem;
130
+ --sgn-interview-question-weight: 700;
131
+ }
132
+ ```
133
+
134
+ The full list is in [`src/render/styles.ts`](./src/render/styles.ts).
135
+
136
+ ## What is stored
137
+
138
+ ```json
139
+ {
140
+ "_type": "interviewBlock",
141
+ "speaker": "till",
142
+ "askedBy": "seb",
143
+ "question": "How did the album come about?",
144
+ "answer": [{"_type": "block", "children": [{"_type": "span", "text": "It took a while…"}]}],
145
+ "timestamp": "12:34",
146
+ "translated": true,
147
+ "originalAnswer": "Das hat gedauert…"
148
+ }
149
+ ```
150
+
151
+ ## No JSON-LD
152
+
153
+ There is no correct schema.org type for a journalistic interview. `QAPage` describes pages where
154
+ users submit answers, and marking an interview up as one would be a false declaration with a real
155
+ risk in search results. So this plugin emits none — unlike the
156
+ [review plugin](https://github.com/netz-sg/sanity-plugin-sgntech-review), where a correct type
157
+ exists.
158
+
159
+ ## Develop
160
+
161
+ ```sh
162
+ npm install
163
+ npm test # speaker resolution and timestamp parsing
164
+ npm run lint
165
+ npm run build
166
+ npm run link-watch
167
+ ```
168
+
169
+ Built with [@sanity/plugin-kit](https://github.com/sanity-io/plugins/tree/main/packages/@sanity/plugin-kit).
170
+
171
+ ## License
172
+
173
+ MIT © SGNTech