@morphemecms/cms 0.7.1 → 0.8.0-next-20261105074335
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/CHANGELOG.md +11 -0
- package/defaults/collections.ts +56 -0
- package/defaults/company.ts +13 -4
- package/defaults/texts.ts +17 -0
- package/icons/lucide/LICENSE-phosphor +21 -0
- package/icons/lucide/facebook-logo.svg +1 -0
- package/icons/lucide/linkedin-logo.svg +1 -0
- package/icons/lucide/pack.json +16 -1
- package/icons/lucide/pinterest-logo.svg +1 -0
- package/icons/lucide/x-logo.svg +1 -0
- package/icons/material-symbols/LICENSE-phosphor +21 -0
- package/icons/material-symbols/facebook-logo.svg +1 -0
- package/icons/material-symbols/linkedin-logo.svg +1 -0
- package/icons/material-symbols/pack.json +15 -0
- package/icons/material-symbols/pinterest-logo.svg +1 -0
- package/icons/material-symbols/x-logo.svg +1 -0
- package/icons/phosphor/facebook-logo.svg +1 -0
- package/icons/phosphor/linkedin-logo.svg +1 -0
- package/icons/phosphor/pinterest-logo.svg +1 -0
- package/icons/phosphor/x-logo.svg +1 -0
- package/lib/lead.d.mts +5 -0
- package/lib/lead.mjs +10 -0
- package/lib/lead.test.mjs +65 -0
- package/package.json +1 -1
- package/scripts/icon-packs.mjs +51 -13
- package/site/icons.d.mts +13 -0
- package/site/icons.mjs +25 -3
- package/site/schema.ts +2 -0
- package/site/site.ts +9 -2
- package/site/social.d.mts +37 -0
- package/site/social.mjs +92 -0
- package/site/social.test.mjs +80 -0
- package/site/theme.mjs +5 -1
- package/src/EntryForm.tsx +21 -58
- package/src/FormFrame.tsx +5 -5
- package/src/Parts.tsx +8 -30
- package/src/Screen.tsx +12 -5
- package/src/editor.css +70 -5
- package/src/i18n/da-DK/parts.ts +1 -1
- package/src/i18n/da-DK/theme.ts +4 -0
- package/src/i18n/en-US/parts.ts +1 -1
- package/src/i18n/en-US/theme.ts +4 -0
- package/src/icon.tsx +1 -1
- package/src/pagePreview.tsx +21 -1
- package/src/postLead.d.mts +7 -0
- package/src/postLead.mjs +9 -0
- package/src/postLead.test.mjs +28 -0
- package/src/ui.tsx +41 -96
- package/starter/src/data/social.json +14 -0
- package/theme/components/PostAuthor.astro +20 -0
- package/theme/components/PostCard.tsx +26 -0
- package/theme/components/PostHeader.astro +36 -0
- package/theme/components/PostMeta.tsx +27 -2
- package/theme/components/RelatedPosts.astro +29 -0
- package/theme/components/ShareRow.astro +48 -0
- package/theme/layouts/PostLayout.astro +28 -54
- package/theme/styles/base.css +12 -0
- package/theme/ui/copyLink.script.ts +17 -0
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
---
|
|
2
|
-
// Blog post template. A narrow reading column, the site's owner as author
|
|
2
|
+
// Blog post template. A narrow reading column, the site's owner as author. Order: PostHeader (back link, title, author,
|
|
3
|
+
// byline with tags, share row), the lead (the text's first paragraph), the featured picture, the rest of the text, the
|
|
4
|
+
// end block, RelatedPosts. Without a lead the picture comes before the text.
|
|
3
5
|
// data-editor marks what the editor's preview fills in with what is in its form ("Forhåndsvis",
|
|
4
|
-
// packages/editor/src/pagePreview.tsx): the heading, the picture and the text. The text goes through RichText (blocks={false}: a post draws no blocks, but its coloured sections are panels).
|
|
6
|
+
// packages/editor/src/pagePreview.tsx): the heading, the lead, the picture and the text. The text goes through RichText (blocks={false}: a post draws no blocks, but its coloured sections are panels).
|
|
7
|
+
import { splitLead } from '@morphemecms/cms/lib/lead.mjs';
|
|
5
8
|
import { firstImage, heroImage, picturesOf } from '@morphemecms/cms/site/images.mjs';
|
|
6
9
|
import Base from '@theme/layouts/Base.astro';
|
|
7
|
-
import Icon from '@theme/components/Icon.astro';
|
|
8
10
|
import Img from '@theme/components/Img.astro';
|
|
9
11
|
import EndBlock from '@theme/components/EndBlock.astro';
|
|
10
12
|
import RichText from '@theme/components/RichText.astro';
|
|
11
13
|
import { categoryName, getPosts, postPath, stateOf, type Post } from '@morphemecms/cms/site/content';
|
|
12
|
-
import
|
|
13
|
-
import
|
|
14
|
+
import PostHeader from '@theme/components/PostHeader.astro';
|
|
15
|
+
import RelatedPosts from '@theme/components/RelatedPosts.astro';
|
|
14
16
|
import { postViewOf } from '@theme/components/loadPosts';
|
|
15
17
|
import { site } from '@morphemecms/cms/site/site';
|
|
16
18
|
import { isBlogCategory } from '@morphemecms/cms/site/categories';
|
|
@@ -24,11 +26,20 @@ const { post } = Astro.props;
|
|
|
24
26
|
const { title, description, date, updated, categories } = post.data;
|
|
25
27
|
// A decorative picture (imageDecorative) has no alternative text.
|
|
26
28
|
const imageAlt = post.data.imageDecorative ? undefined : post.data.imageAlt;
|
|
27
|
-
// The
|
|
29
|
+
// The tags in the byline: only the blog categories (the others have no page).
|
|
28
30
|
const onBlog = categories.filter(isBlogCategory);
|
|
29
31
|
// Some posts also have their featured image (or another version of it) inside the text; never show it twice.
|
|
30
32
|
const image = heroImage(post.data.image, post.body);
|
|
31
33
|
|
|
34
|
+
// The picture a share to Pinterest pins: the one chosen for sharing, else the post's own, with the site's address in front.
|
|
35
|
+
const sharePicture = post.data.shareImage || post.data.image || firstImage(post.body);
|
|
36
|
+
const picture = sharePicture?.startsWith('/') ? site.url + sharePicture : sharePicture || undefined;
|
|
37
|
+
|
|
38
|
+
// The text's first paragraph is the lead, drawn before the featured picture; without one the picture comes first.
|
|
39
|
+
const { lead, rest } = splitLead(await Astro.slots.render('default'));
|
|
40
|
+
// The lead has the text's class too (the editor's preview copies the text's class to the lead) and post-lead.
|
|
41
|
+
const textClass = 'prose mx-auto mt-10 max-w-3xl px-5 medium:mt-14 medium:px-8';
|
|
42
|
+
|
|
32
43
|
// Related: most shared categories first, then newest.
|
|
33
44
|
const related = (await getPosts())
|
|
34
45
|
.filter((p) => p.id !== post.id)
|
|
@@ -69,23 +80,14 @@ const related = (await getPosts())
|
|
|
69
80
|
]}
|
|
70
81
|
>
|
|
71
82
|
<article>
|
|
72
|
-
<
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
{site.portrait && (
|
|
81
|
-
<Img src={site.portrait} alt="" loading="eager" class="size-11 rounded-full object-cover object-[50%_20%]" sizes="44px" />
|
|
82
|
-
)}
|
|
83
|
-
<div class="leading-snug">
|
|
84
|
-
<span class="block font-medium">{site.name}</span>
|
|
85
|
-
<PostMeta post={postViewOf(post)} />
|
|
86
|
-
</div>
|
|
87
|
-
</div>
|
|
88
|
-
</header>
|
|
83
|
+
<PostHeader
|
|
84
|
+
title={title}
|
|
85
|
+
post={postViewOf(post)}
|
|
86
|
+
tags={onBlog.map((c) => ({ label: categoryName(c), href: `/blog/kategori/${c}/` }))}
|
|
87
|
+
share={{ url: site.url + postPath(post), title, picture, share: site.social.share }}
|
|
88
|
+
/>
|
|
89
|
+
|
|
90
|
+
{lead && <div data-editor="lead" class={`${textClass} post-lead`} set:html={lead} />}
|
|
89
91
|
|
|
90
92
|
{image && (
|
|
91
93
|
<div data-editor="image" class="mx-auto mt-8 max-w-5xl px-5 medium:mt-12 medium:px-8">
|
|
@@ -99,40 +101,12 @@ const related = (await getPosts())
|
|
|
99
101
|
</div>
|
|
100
102
|
)}
|
|
101
103
|
|
|
102
|
-
<div data-editor="text" class=
|
|
103
|
-
<RichText html={
|
|
104
|
+
<div data-editor="text" class={textClass}>
|
|
105
|
+
<RichText html={rest} blocks={false} />
|
|
104
106
|
</div>
|
|
105
|
-
|
|
106
|
-
<footer class="mx-auto mt-12 max-w-3xl px-5 medium:px-8">
|
|
107
|
-
{onBlog.length > 0 && (
|
|
108
|
-
<ul class="flex flex-wrap gap-2" aria-label="Tags">
|
|
109
|
-
{onBlog.map((c) => (
|
|
110
|
-
<li>
|
|
111
|
-
<Chip href={`/blog/kategori/${c}/`} label={categoryName(c)} />
|
|
112
|
-
</li>
|
|
113
|
-
))}
|
|
114
|
-
</ul>
|
|
115
|
-
)}
|
|
116
|
-
</footer>
|
|
117
107
|
</article>
|
|
118
108
|
|
|
119
109
|
{site.postEnd && <EndBlock type={site.postEnd} />}
|
|
120
110
|
|
|
121
|
-
{related.
|
|
122
|
-
<Fragment slot="footer-column">
|
|
123
|
-
<p class="footer-heading font-medium">{siteT.theme.relatedPosts}</p>
|
|
124
|
-
<ul class="mt-3 space-y-2.5 text-muted">
|
|
125
|
-
{related.map((p) => (
|
|
126
|
-
<li>
|
|
127
|
-
<a href={postPath(p)} class="hover:text-accent">
|
|
128
|
-
{p.data.title}
|
|
129
|
-
</a>
|
|
130
|
-
</li>
|
|
131
|
-
))}
|
|
132
|
-
</ul>
|
|
133
|
-
<a href="/blog/" class="mt-4 inline-flex items-center gap-1.5 font-medium text-accent">
|
|
134
|
-
{siteT.theme.seeAll} <Icon name="arrow-right" size={16} />
|
|
135
|
-
</a>
|
|
136
|
-
</Fragment>
|
|
137
|
-
)}
|
|
111
|
+
<RelatedPosts posts={related.map(postViewOf)} />
|
|
138
112
|
</Base>
|
package/theme/styles/base.css
CHANGED
|
@@ -181,6 +181,18 @@
|
|
|
181
181
|
font-weight: var(--type-body-large-weight);
|
|
182
182
|
letter-spacing: var(--type-body-large-tracking);
|
|
183
183
|
}
|
|
184
|
+
/* A blog post's lead: its first paragraph, one step larger than the text (the title-large size, with the body weight). */
|
|
185
|
+
.prose.post-lead {
|
|
186
|
+
font-size: var(--type-title-large-size);
|
|
187
|
+
line-height: var(--type-title-large-line-height);
|
|
188
|
+
font-weight: var(--type-body-large-weight);
|
|
189
|
+
letter-spacing: var(--type-title-large-tracking);
|
|
190
|
+
}
|
|
191
|
+
/* The text right after the lead (no picture between): the space the prose puts between paragraphs (1.25em), not the
|
|
192
|
+
larger space of a text under a picture. Unlayered on purpose: it must beat the mt-10 / medium:mt-14 utilities (a layer). */
|
|
193
|
+
.prose.post-lead + .prose {
|
|
194
|
+
margin-top: 1.25em;
|
|
195
|
+
}
|
|
184
196
|
/* The text's headings: a headline, a title under it and a smaller title under that (the type scale, ui.css). */
|
|
185
197
|
.prose :where(h2) {
|
|
186
198
|
font-size: var(--type-headline-medium-size);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// What a copy-link button (ShareRow.astro, [data-copy-link]) does on the site: it writes its address to the clipboard and
|
|
2
|
+
// says so in a snackbar (its text in data-copied). Run on every page by Base.astro (blockScripts, by `ui/copyLink`);
|
|
3
|
+
// without such a button on the page it does nothing.
|
|
4
|
+
import { snackbar } from '@morphemecms/cms/lib/snackbar';
|
|
5
|
+
|
|
6
|
+
export default function copyLinks() {
|
|
7
|
+
document.querySelectorAll<HTMLButtonElement>('button[data-copy-link]').forEach((button) => {
|
|
8
|
+
button.addEventListener('click', async () => {
|
|
9
|
+
try {
|
|
10
|
+
await navigator.clipboard.writeText(button.dataset.copyLink ?? '');
|
|
11
|
+
} catch {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
snackbar({ text: button.dataset.copied ?? '' });
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
}
|