Skip to content
Merged
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
3 changes: 3 additions & 0 deletions src/content/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ const posts = defineCollection({
// backward compat with existing frontmatter but no longer rendered.
topic: z.string().optional(),
topicId: z.enum(TOPIC_IDS),
// A writer-suggested topic not yet in TOPIC_IDS — surfaced for a maintainer
// (or automation) to create the topic or remap topicId. Ignored by rendering.
proposedTopic: z.string().optional(),
tags: z.array(z.string()).optional(),
updated: z.coerce.date().optional(),
cover: z.union([image(), z.string().url()]).optional(),
Expand Down
1 change: 1 addition & 0 deletions src/write/WritePortal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ function emptyMeta(): PostMeta {
tags: [],
slug: '',
coverFileName: '',
proposedTopic: '',
};
}

Expand Down
42 changes: 40 additions & 2 deletions src/write/meta/MetaForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ type Props = {
export function MetaForm({ authors, topics, meta, images, onChange }: Props) {
const [slugTouched, setSlugTouched] = useState(false);
const [tagInput, setTagInput] = useState('');
const [proposing, setProposing] = useState(!!meta.proposedTopic);

const set = (patch: Partial<PostMeta>) => onChange({ ...meta, ...patch });

Expand Down Expand Up @@ -98,10 +99,15 @@ export function MetaForm({ authors, topics, meta, images, onChange }: Props) {
<label>
Topic
<select
value={meta.topicId}
value={proposing ? '__propose__' : meta.topicId}
onChange={(e) => {
if (e.target.value === '__propose__') {
setProposing(true);
return;
}
const topic = topics.find((t) => t.id === e.target.value);
set({ topicId: topic?.id ?? '', topicName: topic?.name ?? '' });
setProposing(false);
set({ topicId: topic?.id ?? '', topicName: topic?.name ?? '', proposedTopic: '' });
}}
>
<option value="">Pick a topic…</option>
Expand All @@ -110,10 +116,42 @@ export function MetaForm({ authors, topics, meta, images, onChange }: Props) {
{t.name}
</option>
))}
<option value="__propose__">+ Propose a new topic…</option>
</select>
</label>
</div>

{proposing && (
<div className="write-meta-row">
<label>
New topic
<input
type="text"
placeholder="e.g. Memory Systems"
value={meta.proposedTopic ?? ''}
onChange={(e) => set({ proposedTopic: e.target.value })}
/>
</label>
<label>
File under for now
<select
value={meta.topicId}
onChange={(e) => {
const topic = topics.find((t) => t.id === e.target.value);
set({ topicId: topic?.id ?? '', topicName: topic?.name ?? '' });
}}
>
<option value="">Pick the closest…</option>
{topics.map((t) => (
<option key={t.id} value={t.id}>
{t.name}
</option>
))}
</select>
</label>
</div>
)}

<div className="write-meta-row">
<label>
URL slug
Expand Down
10 changes: 10 additions & 0 deletions src/write/serialize/toMdx.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -473,9 +473,19 @@ describe('frontmatter', () => {
const fm = mdx.split('---')[1];
expect(fm).not.toContain('tags:');
expect(fm).not.toContain('cover:');
expect(fm).not.toContain('proposedTopic:');
expect(fm).toContain('readMin: 1');
});

it('emits a proposedTopic when the writer suggests a new one', () => {
const { mdx } = serializePost(
{ ...meta, proposedTopic: 'Memory Systems' },
[block('paragraph', {}, [t('x')])],
{ tableVariants: {}, today },
);
expect(mdx.split('---')[1]).toContain("proposedTopic: 'Memory Systems'");
});

it('serializes multiple authors into the frontmatter array', () => {
const { mdx } = serializePost(
{ ...meta, authors: ['dinesh', 'felix'] },
Expand Down
4 changes: 4 additions & 0 deletions src/write/serialize/toMdx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ export type PostMeta = {
coverFileName: string;
// Set only when editing an existing post; preserves its original publish date.
date?: string;
// A topic the writer proposes that isn't in the list yet — a maintainer (or, later,
// automation) reads this from the frontmatter and creates it or remaps topicId.
proposedTopic?: string;
};

export type TableStyle = { border: 'rule' | 'lined' | 'plain'; zebra: boolean };
Expand Down Expand Up @@ -385,6 +388,7 @@ function buildFrontmatter(meta: PostMeta, blocks: SBlock[], opts: SerializeOptio
`topicId: ${yaml(meta.topicId)}`,
];
if (meta.tags.length > 0) lines.push(`tags: [${meta.tags.map(yaml).join(', ')}]`);
if (meta.proposedTopic?.trim()) lines.push(`proposedTopic: ${yaml(meta.proposedTopic.trim())}`);
if (meta.coverFileName) lines.push(`cover: ${yaml(`./${meta.coverFileName}`)}`);
return `---\n${lines.join('\n')}\n---`;
}
Expand Down
Loading