@commonpub/layer 0.57.0 → 0.58.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/components/LayoutRow.vue +8 -8
- package/components/LayoutSection.vue +8 -8
- package/components/LayoutSlot.vue +3 -3
- package/components/MirrorDetailModal.vue +3 -3
- package/components/MirrorRequestApproveModal.vue +3 -3
- package/components/PollDisplay.vue +1 -1
- package/components/RegistryDirectory.vue +2 -2
- package/components/admin/layouts/AdminLayoutsAutoForm.vue +1 -1
- package/components/admin/layouts/AdminLayoutsCanvas.vue +2 -2
- package/components/admin/layouts/AdminLayoutsConflictModal.vue +1 -1
- package/components/admin/layouts/AdminLayoutsHelpOverlay.vue +1 -1
- package/components/admin/layouts/AdminLayoutsInspectorPage.vue +1 -1
- package/components/admin/layouts/AdminLayoutsToolbar.vue +5 -5
- package/components/admin/theme/AdminThemeSceneGallery.vue +3 -3
- package/components/admin/theme/AdminThemeSceneProse.vue +3 -3
- package/components/admin/theme/AdminThemeTokenInput.vue +1 -1
- package/components/blocks/BlockCodeView.vue +2 -2
- package/components/blocks/BlockDividerView.vue +1 -1
- package/components/blocks/BlockPartsListView.vue +1 -1
- package/components/blocks/BlockQuizView.vue +1 -1
- package/components/blocks/BlockQuoteView.vue +1 -1
- package/components/contest/ContestHero.vue +2 -2
- package/components/contest/ContestStagesEditor.vue +4 -4
- package/components/editors/ArticleEditor.vue +1 -1
- package/components/editors/ExplainerEditor.vue +1 -1
- package/components/sections/SectionLearning.vue +1 -1
- package/components/views/ArticleView.vue +2 -2
- package/components/views/ProjectView.vue +3 -3
- package/composables/useAdminSidebar.ts +3 -3
- package/composables/useLayoutEditor.ts +1 -1
- package/composables/useLayoutHotkeys.ts +1 -1
- package/composables/useLayoutResize.ts +1 -1
- package/composables/usePublishValidation.ts +1 -1
- package/composables/useThemeAdmin.ts +2 -2
- package/error.vue +1 -1
- package/layouts/admin.vue +2 -2
- package/layouts/default.vue +2 -2
- package/package.json +8 -8
- package/pages/[type]/index.vue +1 -1
- package/pages/about.vue +3 -3
- package/pages/admin/api-keys.vue +5 -5
- package/pages/admin/audit.vue +2 -2
- package/pages/admin/categories.vue +1 -1
- package/pages/admin/content.vue +2 -2
- package/pages/admin/features.vue +1 -1
- package/pages/admin/federation.vue +9 -9
- package/pages/admin/homepage.vue +4 -4
- package/pages/admin/index.vue +1 -1
- package/pages/admin/layouts/[id].vue +18 -18
- package/pages/admin/layouts/index.vue +4 -4
- package/pages/admin/navigation.vue +1 -1
- package/pages/admin/reports.vue +1 -1
- package/pages/admin/settings.vue +2 -2
- package/pages/admin/theme/edit/[id].vue +2 -2
- package/pages/admin/theme/index.vue +5 -5
- package/pages/admin/users.vue +1 -1
- package/pages/auth/forgot-password.vue +1 -1
- package/pages/auth/login.vue +3 -3
- package/pages/auth/register.vue +1 -1
- package/pages/auth/reset-password.vue +1 -1
- package/pages/auth/verify-email.vue +1 -1
- package/pages/cert/[code].vue +1 -1
- package/pages/contests/[slug]/edit.vue +13 -12
- package/pages/contests/[slug]/index.vue +7 -7
- package/pages/contests/[slug]/judge.vue +15 -3
- package/pages/contests/[slug]/results.vue +5 -5
- package/pages/contests/create.vue +15 -15
- package/pages/contests/index.vue +2 -2
- package/pages/cookies.vue +1 -1
- package/pages/create.vue +2 -2
- package/pages/dashboard.vue +1 -1
- package/pages/docs/[siteSlug]/[...pagePath].vue +1 -1
- package/pages/docs/[siteSlug]/edit.vue +1 -1
- package/pages/docs/[siteSlug]/index.vue +1 -1
- package/pages/docs/create.vue +1 -1
- package/pages/docs/index.vue +1 -1
- package/pages/events/[slug]/edit.vue +1 -1
- package/pages/events/[slug]/index.vue +2 -2
- package/pages/events/create.vue +1 -1
- package/pages/events/index.vue +1 -1
- package/pages/explore.vue +1 -1
- package/pages/federated-hubs/[id]/index.vue +3 -3
- package/pages/federated-hubs/[id]/posts/[postId].vue +1 -1
- package/pages/federation/search.vue +1 -1
- package/pages/feed.vue +1 -1
- package/pages/hubs/[slug]/members.vue +1 -1
- package/pages/hubs/[slug]/posts/[postId].vue +1 -1
- package/pages/hubs/[slug]/settings.vue +5 -5
- package/pages/hubs/create.vue +6 -6
- package/pages/hubs/index.vue +1 -1
- package/pages/index.vue +2 -2
- package/pages/learn/[slug]/[lessonSlug]/edit.vue +1 -1
- package/pages/learn/[slug]/[lessonSlug]/index.vue +4 -4
- package/pages/learn/[slug]/edit.vue +1 -1
- package/pages/learn/[slug]/index.vue +1 -1
- package/pages/learn/create.vue +1 -1
- package/pages/learn/index.vue +2 -2
- package/pages/messages/[conversationId].vue +1 -1
- package/pages/messages/index.vue +1 -1
- package/pages/notifications.vue +1 -1
- package/pages/privacy.vue +5 -5
- package/pages/products/[slug].vue +1 -1
- package/pages/products/index.vue +1 -1
- package/pages/search.vue +1 -1
- package/pages/settings/profile.vue +1 -1
- package/pages/settings.vue +1 -1
- package/pages/tags/[slug].vue +1 -1
- package/pages/tags/index.vue +1 -1
- package/pages/terms.vue +1 -1
- package/pages/u/[username]/[type]/[slug]/edit.vue +3 -3
- package/pages/u/[username]/[type]/[slug]/index.vue +1 -1
- package/pages/u/[username]/followers.vue +1 -1
- package/pages/u/[username]/following.vue +1 -1
- package/pages/u/[username]/index.vue +3 -3
- package/pages/videos/[id].vue +1 -1
- package/pages/videos/index.vue +1 -1
- package/pages/videos/submit.vue +2 -2
- package/sections/builtin/hero.ts +1 -1
- package/sections/builtin/markdown.ts +1 -1
- package/server/api/admin/homepage/sections.put.ts +1 -1
- package/server/api/admin/layouts/[id].put.ts +1 -1
- package/server/api/contests/[slug]/entries.post.ts +3 -3
- package/server/api/hubs/[slug]/feed.xml.get.ts +1 -1
- package/server/api/users/[username]/feed.xml.get.ts +1 -1
- package/server/middleware/content-redirect.ts +1 -1
- package/server/plugins/federation-delivery.ts +1 -1
- package/server/plugins/federation-hub-sync.ts +1 -1
- package/server/plugins/registry-heartbeat.ts +3 -3
- package/server/plugins/search-index.ts +1 -1
- package/server/utils/email.ts +3 -3
- package/server/utils/instanceTheme.ts +1 -1
- package/utils/contestStages.ts +3 -3
|
@@ -23,7 +23,7 @@ const { data: members } = useLazyFetch<FederatedMember[]>(`/api/federated-hubs/$
|
|
|
23
23
|
});
|
|
24
24
|
|
|
25
25
|
useSeoMeta({
|
|
26
|
-
title: () => hub.value ? `${hub.value.name}
|
|
26
|
+
title: () => hub.value ? `${hub.value.name}, ${useSiteName()}` : 'Federated Hub',
|
|
27
27
|
description: () => hub.value?.description || '',
|
|
28
28
|
});
|
|
29
29
|
|
|
@@ -168,7 +168,7 @@ async function handlePost(): Promise<void> {
|
|
|
168
168
|
toast.success('Post sent to hub via federation');
|
|
169
169
|
await Promise.all([refreshHub(), refreshPosts()]);
|
|
170
170
|
} catch {
|
|
171
|
-
toast.error('Failed to post
|
|
171
|
+
toast.error('Failed to post, the remote hub may not accept posts from this instance');
|
|
172
172
|
} finally {
|
|
173
173
|
posting.value = false;
|
|
174
174
|
}
|
|
@@ -234,7 +234,7 @@ async function handleJoinHub(): Promise<void> {
|
|
|
234
234
|
method: 'POST',
|
|
235
235
|
body: { federatedHubId: hub.value.id },
|
|
236
236
|
});
|
|
237
|
-
toast.success(result.status === 'joined' ? 'Now following this hub' : 'Follow request sent
|
|
237
|
+
toast.success(result.status === 'joined' ? 'Now following this hub' : 'Follow request sent, it may take a moment to be accepted');
|
|
238
238
|
await Promise.all([refreshHub(), refreshFollowState()]);
|
|
239
239
|
} catch (err: unknown) {
|
|
240
240
|
const msg = err instanceof Error ? err.message : 'Failed to follow hub';
|
|
@@ -97,7 +97,7 @@ function stripHtml(html: string): string {
|
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
useSeoMeta({
|
|
100
|
-
title: () => post.value ? `${stripHtml(post.value.content || '').slice(0, 60)}
|
|
100
|
+
title: () => post.value ? `${stripHtml(post.value.content || '').slice(0, 60)}, ${hub.value?.name ?? 'Hub'}` : 'Post',
|
|
101
101
|
description: () => stripHtml(post.value?.content ?? '').slice(0, 160),
|
|
102
102
|
});
|
|
103
103
|
|
package/pages/feed.vue
CHANGED
|
@@ -11,7 +11,7 @@ const { user } = useAuth();
|
|
|
11
11
|
const currentUserRole = computed(() => hub.value?.currentUserRole ?? null);
|
|
12
12
|
const canManage = computed(() => currentUserRole.value === 'owner' || currentUserRole.value === 'admin');
|
|
13
13
|
|
|
14
|
-
useSeoMeta({ title: () => `Members
|
|
14
|
+
useSeoMeta({ title: () => `Members, ${hub.value?.name ?? 'Hub'}, ${useSiteName()}` });
|
|
15
15
|
|
|
16
16
|
const roles = ['member', 'moderator', 'admin'] as const;
|
|
17
17
|
|
|
@@ -137,7 +137,7 @@ function formatDate(d: string | Date): string {
|
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
useSeoMeta({
|
|
140
|
-
title: () => post.value ? `${post.value.content?.slice(0, 60)}
|
|
140
|
+
title: () => post.value ? `${post.value.content?.slice(0, 60)}, ${hub.value?.name ?? 'Hub'}` : 'Post',
|
|
141
141
|
});
|
|
142
142
|
</script>
|
|
143
143
|
|
|
@@ -10,7 +10,7 @@ import type { Serialized, HubDetail } from '@commonpub/server';
|
|
|
10
10
|
const { data: hub } = useLazyFetch<Serialized<HubDetail>>(() => `/api/hubs/${slug.value}`);
|
|
11
11
|
|
|
12
12
|
useSeoMeta({
|
|
13
|
-
title: () => `Settings
|
|
13
|
+
title: () => `Settings, ${hub.value?.name ?? 'Hub'}, ${useSiteName()}`,
|
|
14
14
|
});
|
|
15
15
|
|
|
16
16
|
const form = reactive({
|
|
@@ -134,8 +134,8 @@ async function handleSave(): Promise<void> {
|
|
|
134
134
|
<div class="cpub-field">
|
|
135
135
|
<label for="hub-join" class="cpub-field-label">Join Policy</label>
|
|
136
136
|
<select id="hub-join" v-model="form.joinPolicy" class="cpub-field-input">
|
|
137
|
-
<option value="open">Open
|
|
138
|
-
<option value="approval">Approval
|
|
137
|
+
<option value="open">Open, anyone can join</option>
|
|
138
|
+
<option value="approval">Approval, requests must be approved</option>
|
|
139
139
|
<option value="invite">Invite Only</option>
|
|
140
140
|
</select>
|
|
141
141
|
</div>
|
|
@@ -143,8 +143,8 @@ async function handleSave(): Promise<void> {
|
|
|
143
143
|
<div class="cpub-field">
|
|
144
144
|
<label for="hub-privacy" class="cpub-field-label">Privacy</label>
|
|
145
145
|
<select id="hub-privacy" v-model="form.privacy" class="cpub-field-input">
|
|
146
|
-
<option value="public">Public
|
|
147
|
-
<option value="private">Private
|
|
146
|
+
<option value="public">Public, visible to everyone</option>
|
|
147
|
+
<option value="private">Private, members only</option>
|
|
148
148
|
</select>
|
|
149
149
|
</div>
|
|
150
150
|
</div>
|
package/pages/hubs/create.vue
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
definePageMeta({ middleware: 'auth' });
|
|
3
3
|
|
|
4
4
|
useSeoMeta({
|
|
5
|
-
title: `Create Hub
|
|
5
|
+
title: `Create Hub, ${useSiteName()}`,
|
|
6
6
|
description: 'Create a new maker hub.',
|
|
7
7
|
});
|
|
8
8
|
|
|
@@ -73,17 +73,17 @@ async function handleCreate(): Promise<void> {
|
|
|
73
73
|
<div class="cpub-field">
|
|
74
74
|
<label for="hub-type" class="cpub-field-label">Hub Type</label>
|
|
75
75
|
<select id="hub-type" v-model="hubType" class="cpub-field-input">
|
|
76
|
-
<option value="community">Community
|
|
77
|
-
<option value="product">Product
|
|
78
|
-
<option value="company">Company
|
|
76
|
+
<option value="community">Community, maker group / topic space</option>
|
|
77
|
+
<option value="product">Product, product or platform page</option>
|
|
78
|
+
<option value="company">Company, organization page</option>
|
|
79
79
|
</select>
|
|
80
80
|
</div>
|
|
81
81
|
|
|
82
82
|
<div class="cpub-field">
|
|
83
83
|
<label for="hub-join" class="cpub-field-label">Join Policy</label>
|
|
84
84
|
<select id="hub-join" v-model="joinPolicy" class="cpub-field-input">
|
|
85
|
-
<option value="open">Open
|
|
86
|
-
<option value="approval">Approval
|
|
85
|
+
<option value="open">Open, anyone can join</option>
|
|
86
|
+
<option value="approval">Approval, requests must be approved</option>
|
|
87
87
|
<option value="invite">Invite Only</option>
|
|
88
88
|
</select>
|
|
89
89
|
</div>
|
package/pages/hubs/index.vue
CHANGED
package/pages/index.vue
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import type { Serialized, ContentListItem, PaginatedResponse, HomepageSection } from '@commonpub/server';
|
|
3
3
|
|
|
4
4
|
useSeoMeta({
|
|
5
|
-
title: `${useSiteName()}
|
|
5
|
+
title: `${useSiteName()}, Open Maker Platform`,
|
|
6
6
|
description: 'Build, document, and share your projects with a community of makers.',
|
|
7
7
|
});
|
|
8
8
|
|
|
@@ -126,7 +126,7 @@ async function handleHubJoin(hubSlug: string): Promise<void> {
|
|
|
126
126
|
Renders DB-driven layout via <LayoutSlot> zones ONLY when BOTH
|
|
127
127
|
(a) features.layoutEngine is ON AND (b) a layout actually exists
|
|
128
128
|
at scope ('route', '/'). Falls through to the configurable or
|
|
129
|
-
legacy renderer otherwise
|
|
129
|
+
legacy renderer otherwise, including when the flag's ON but
|
|
130
130
|
no layout has been seeded yet (the "blank page" trap reported
|
|
131
131
|
in session 158 follow-up).
|
|
132
132
|
|
|
@@ -19,7 +19,7 @@ const { data: lessonData, pending: lessonPending, error: lessonError, refresh }
|
|
|
19
19
|
);
|
|
20
20
|
|
|
21
21
|
useSeoMeta({
|
|
22
|
-
title: () => lessonData.value ? `Edit: ${lessonData.value.lesson.title}
|
|
22
|
+
title: () => lessonData.value ? `Edit: ${lessonData.value.lesson.title}, ${useSiteName()}` : `Edit Lesson, ${useSiteName()}`,
|
|
23
23
|
});
|
|
24
24
|
|
|
25
25
|
const lesson = computed(() => lessonData.value?.lesson);
|
|
@@ -28,7 +28,7 @@ const lesson = computed(() => lessonData.value?.lesson);
|
|
|
28
28
|
const lessonModule = computed(() => lessonData.value?.module);
|
|
29
29
|
|
|
30
30
|
useSeoMeta({
|
|
31
|
-
title: () => lesson.value ? `${lesson.value.title}
|
|
31
|
+
title: () => lesson.value ? `${lesson.value.title}, ${useSiteName()}` : `Lesson, ${useSiteName()}`,
|
|
32
32
|
});
|
|
33
33
|
|
|
34
34
|
const { isAuthenticated, user } = useAuth();
|
|
@@ -165,9 +165,9 @@ async function submitQuiz(): Promise<void> {
|
|
|
165
165
|
quizGrade.value = res.quiz;
|
|
166
166
|
if (res.quiz.passed) {
|
|
167
167
|
completed.value = true;
|
|
168
|
-
toast.success(`Passed
|
|
168
|
+
toast.success(`Passed, ${res.quiz.score}%`);
|
|
169
169
|
} else {
|
|
170
|
-
toast.error(`Scored ${res.quiz.score}
|
|
170
|
+
toast.error(`Scored ${res.quiz.score}%, below passing. Try again.`);
|
|
171
171
|
}
|
|
172
172
|
}
|
|
173
173
|
} catch (err: unknown) {
|
|
@@ -339,7 +339,7 @@ const isOwner = computed(() => user.value?.id === path.value?.author?.id);
|
|
|
339
339
|
<div v-if="quizGrade" class="quiz-score" :class="{ passed: quizGrade.passed, failed: !quizGrade.passed }">
|
|
340
340
|
<div class="quiz-score-value">{{ quizGrade.correct }} / {{ quizGrade.total }}</div>
|
|
341
341
|
<div class="quiz-score-label">
|
|
342
|
-
{{ quizGrade.score }}
|
|
342
|
+
{{ quizGrade.score }}%, {{ quizGrade.passed ? 'Passed' : 'Did not pass' }}
|
|
343
343
|
</div>
|
|
344
344
|
<button v-if="!quizGrade.passed" class="quiz-retry-btn" @click="retryQuiz">
|
|
345
345
|
<i class="fa-solid fa-rotate-right"></i> Try Again
|
|
@@ -17,7 +17,7 @@ type PathModule = Omit<NonNullable<Serialized<LearningPathDetail>['modules']>[nu
|
|
|
17
17
|
|
|
18
18
|
const { data: path, pending: pathPending, error: pathError, refresh } = useLazyFetch<Serialized<LearningPathDetail>>(() => `/api/learn/${slug.value}`);
|
|
19
19
|
|
|
20
|
-
useSeoMeta({ title: () => `Edit ${path.value?.title ?? 'Path'}
|
|
20
|
+
useSeoMeta({ title: () => `Edit ${path.value?.title ?? 'Path'}, ${useSiteName()}` });
|
|
21
21
|
|
|
22
22
|
const saving = ref(false);
|
|
23
23
|
const publishing = ref(false);
|
|
@@ -7,7 +7,7 @@ import type { Serialized, LearningPathDetail } from '@commonpub/server';
|
|
|
7
7
|
const { data: path, pending: pathPending, error: pathError, refresh } = useLazyFetch<Serialized<LearningPathDetail>>(() => `/api/learn/${slug.value}`);
|
|
8
8
|
|
|
9
9
|
useSeoMeta({
|
|
10
|
-
title: () => path.value ? `${path.value.title}
|
|
10
|
+
title: () => path.value ? `${path.value.title}, Learn, ${useSiteName()}` : `Learn, ${useSiteName()}`,
|
|
11
11
|
description: () => path.value?.description || '',
|
|
12
12
|
ogImage: '/og-default.png',
|
|
13
13
|
});
|
package/pages/learn/create.vue
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
definePageMeta({ middleware: 'auth' });
|
|
3
3
|
|
|
4
|
-
useSeoMeta({ title: `Create Learning Path
|
|
4
|
+
useSeoMeta({ title: `Create Learning Path, ${useSiteName()}` });
|
|
5
5
|
|
|
6
6
|
const toast = useToast();
|
|
7
7
|
const { extract: extractError } = useApiError();
|
package/pages/learn/index.vue
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
useSeoMeta({ title: `Learn
|
|
2
|
+
useSeoMeta({ title: `Learn, ${useSiteName()}` });
|
|
3
3
|
|
|
4
4
|
const { isAuthenticated, user } = useAuth();
|
|
5
5
|
|
|
@@ -55,7 +55,7 @@ const activeDifficultyFilter = ref('');
|
|
|
55
55
|
<div class="cpub-learn-hero-inner">
|
|
56
56
|
<div class="cpub-hero-eyebrow"><i class="fa-solid fa-graduation-cap"></i> Learning Hub</div>
|
|
57
57
|
<h1 class="cpub-hero-title">Learn</h1>
|
|
58
|
-
<p class="cpub-hero-sub">Structured courses, interactive explainers, and hands-on tutorials
|
|
58
|
+
<p class="cpub-hero-sub">Structured courses, interactive explainers, and hands-on tutorials, from beginner to production-ready.</p>
|
|
59
59
|
<div class="cpub-hero-cats">
|
|
60
60
|
<div
|
|
61
61
|
v-for="c in heroCategories"
|
|
@@ -78,7 +78,7 @@ const participantLabel = computed(() => {
|
|
|
78
78
|
return others.length > 0 ? others.join(', ') : 'Conversation';
|
|
79
79
|
});
|
|
80
80
|
|
|
81
|
-
useSeoMeta({ title: () => `Message
|
|
81
|
+
useSeoMeta({ title: () => `Message, ${participantLabel.value}` });
|
|
82
82
|
|
|
83
83
|
async function handleSend(text: string): Promise<void> {
|
|
84
84
|
try {
|
package/pages/messages/index.vue
CHANGED
package/pages/notifications.vue
CHANGED
package/pages/privacy.vue
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
useSeoMeta({
|
|
3
|
-
title: `Privacy Policy
|
|
3
|
+
title: `Privacy Policy, ${useSiteName()}`,
|
|
4
4
|
description: 'How we collect, use, and protect your personal data.',
|
|
5
5
|
});
|
|
6
6
|
|
|
@@ -65,11 +65,11 @@ const { federation: federationEnabled } = useFeatures();
|
|
|
65
65
|
<h2>5. Cookies</h2>
|
|
66
66
|
<p>We use a small number of cookies to provide and improve the service:</p>
|
|
67
67
|
<ul>
|
|
68
|
-
<li><strong>Session cookie</strong> (<code>better-auth.session_token</code>): strictly necessary
|
|
69
|
-
<li><strong>Consent cookie</strong> (<code>cpub-consent</code>): strictly necessary
|
|
70
|
-
<li><strong>Color scheme</strong> (<code>cpub-color-scheme</code>): functional
|
|
68
|
+
<li><strong>Session cookie</strong> (<code>better-auth.session_token</code>): strictly necessary, authenticates your login session. HttpOnly, secure, 7-day expiry.</li>
|
|
69
|
+
<li><strong>Consent cookie</strong> (<code>cpub-consent</code>): strictly necessary, stores your cookie consent choice.</li>
|
|
70
|
+
<li><strong>Color scheme</strong> (<code>cpub-color-scheme</code>): functional, remembers your light/dark mode preference. Set only with your consent.</li>
|
|
71
71
|
</ul>
|
|
72
|
-
<p>We do not use any advertising or tracking cookies. Your instance operator may add analytics cookies
|
|
72
|
+
<p>We do not use any advertising or tracking cookies. Your instance operator may add analytics cookies, these require your explicit consent. For the full list of cookies and to manage your preferences, visit our <NuxtLink to="/cookies">Cookie Policy</NuxtLink>.</p>
|
|
73
73
|
</section>
|
|
74
74
|
|
|
75
75
|
<section v-if="federationEnabled" class="cpub-legal-section">
|
|
@@ -6,7 +6,7 @@ const { data: product, pending } = useLazyFetch(`/api/products/${slug}`) as { da
|
|
|
6
6
|
const { data: projectsUsing } = useLazyFetch(`/api/products/${slug}/content`) as { data: Ref<any[] | null> };
|
|
7
7
|
|
|
8
8
|
useSeoMeta({
|
|
9
|
-
title: () => product.value ? `${product.value.name}
|
|
9
|
+
title: () => product.value ? `${product.value.name}, ${useSiteName()}` : `Product, ${useSiteName()}`,
|
|
10
10
|
description: () => product.value?.description ?? '',
|
|
11
11
|
});
|
|
12
12
|
</script>
|
package/pages/products/index.vue
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
useSeoMeta({ title: `Products
|
|
2
|
+
useSeoMeta({ title: `Products, ${useSiteName()}`, description: 'Browse products, components, and tools.' });
|
|
3
3
|
|
|
4
4
|
const search = ref('');
|
|
5
5
|
const category = ref('');
|
package/pages/search.vue
CHANGED
package/pages/settings.vue
CHANGED
package/pages/tags/[slug].vue
CHANGED
|
@@ -5,7 +5,7 @@ const route = useRoute();
|
|
|
5
5
|
const tagSlug = computed(() => route.params.slug as string);
|
|
6
6
|
|
|
7
7
|
useSeoMeta({
|
|
8
|
-
title: () => `#${tagSlug.value}
|
|
8
|
+
title: () => `#${tagSlug.value}, ${useSiteName()}`,
|
|
9
9
|
description: () => `Content tagged with "${tagSlug.value}" on CommonPub`,
|
|
10
10
|
});
|
|
11
11
|
|
package/pages/tags/index.vue
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
useSeoMeta({ title: `Tags
|
|
2
|
+
useSeoMeta({ title: `Tags, ${useSiteName()}`, description: 'Browse content by tags.' });
|
|
3
3
|
|
|
4
4
|
const { data: trending, pending } = await useFetch<any>('/api/search/trending');
|
|
5
5
|
|
package/pages/terms.vue
CHANGED
|
@@ -30,7 +30,7 @@ if (user.value?.username && user.value.username !== username.value) {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
useSeoMeta({
|
|
33
|
-
title: () => isNew.value ? `New ${contentType.value}
|
|
33
|
+
title: () => isNew.value ? `New ${contentType.value}, ${useSiteName()}` : `Edit, ${useSiteName()}`,
|
|
34
34
|
});
|
|
35
35
|
|
|
36
36
|
const title = ref('');
|
|
@@ -62,7 +62,7 @@ const projectTemplate: [string, Record<string, unknown>][] = [
|
|
|
62
62
|
['heading', { text: 'Prerequisites', level: 2 }],
|
|
63
63
|
['paragraph', { html: '<ul><li><p>Required tools, accounts, or hardware</p></li><li><p>Link each tool/service on first mention</p></li></ul>' }],
|
|
64
64
|
['partsList', { parts: [] }],
|
|
65
|
-
['buildStep', { stepNumber: 1, title: 'Set Up', time: '', children: [['paragraph', { html: '<p>Brief context sentence
|
|
65
|
+
['buildStep', { stepNumber: 1, title: 'Set Up', time: '', children: [['paragraph', { html: '<p>Brief context sentence, why this step matters. Use code blocks for commands, inline code for filenames and values.</p>' }]] }],
|
|
66
66
|
['buildStep', { stepNumber: 2, title: 'Build', time: '', children: [['paragraph', { html: '<p>Continue the pattern. Each build step = one phase of the build. Link to external repos for full code instead of pasting walls of code.</p>' }]] }],
|
|
67
67
|
['buildStep', { stepNumber: 3, title: 'Verify', time: '', children: [['paragraph', { html: '<p>Add as many steps as needed. Each step should be a clear, actionable phase.</p>' }]] }],
|
|
68
68
|
['heading', { text: 'Testing & Verification', level: 2 }],
|
|
@@ -290,7 +290,7 @@ async function handlePublish(): Promise<void> {
|
|
|
290
290
|
try {
|
|
291
291
|
await $fetch(`/api/contests/${contestFromQuery}/entries`, { method: 'POST', body: { contentId: contentId.value } });
|
|
292
292
|
submitToast.success('Entered into the contest!');
|
|
293
|
-
} catch { /* non-blocking
|
|
293
|
+
} catch { /* non-blocking, user can submit manually from the contest page */ }
|
|
294
294
|
}
|
|
295
295
|
}
|
|
296
296
|
|
|
@@ -27,7 +27,7 @@ watch(() => content.value?.type, (type) => {
|
|
|
27
27
|
}, { immediate: true });
|
|
28
28
|
|
|
29
29
|
useSeoMeta({
|
|
30
|
-
title: () => content.value?.title ? `${content.value.title}
|
|
30
|
+
title: () => content.value?.title ? `${content.value.title}, ${useSiteName()}` : useSiteName(),
|
|
31
31
|
description: () => content.value?.seoDescription || content.value?.description || '',
|
|
32
32
|
ogImage: () => content.value?.coverImageUrl || '/og-default.png',
|
|
33
33
|
ogTitle: () => content.value?.title || useSiteName(),
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
const route = useRoute();
|
|
3
3
|
const username = route.params.username as string;
|
|
4
4
|
|
|
5
|
-
useSeoMeta({ title: `Followers
|
|
5
|
+
useSeoMeta({ title: `Followers, @${username}, ${useSiteName()}` });
|
|
6
6
|
|
|
7
7
|
const { data: followers } = useLazyFetch<Array<{ id: string; username: string; displayName: string | null; avatarUrl: string | null }>>(`/api/users/${username}/followers`);
|
|
8
8
|
const { isAuthenticated, user } = useAuth();
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
const route = useRoute();
|
|
3
3
|
const username = route.params.username as string;
|
|
4
4
|
|
|
5
|
-
useSeoMeta({ title: `Following
|
|
5
|
+
useSeoMeta({ title: `Following, @${username}, ${useSiteName()}` });
|
|
6
6
|
|
|
7
7
|
const { data: following } = useLazyFetch<Array<{ id: string; username: string; displayName: string | null; avatarUrl: string | null }>>(`/api/users/${username}/following`);
|
|
8
8
|
const { isAuthenticated, user } = useAuth();
|
|
@@ -3,8 +3,8 @@ const route = useRoute();
|
|
|
3
3
|
const username = route.params.username as string;
|
|
4
4
|
|
|
5
5
|
useSeoMeta({
|
|
6
|
-
title: `${username}
|
|
7
|
-
ogTitle: `${username}
|
|
6
|
+
title: `${username}, ${useSiteName()}`,
|
|
7
|
+
ogTitle: `${username}, ${useSiteName()}`,
|
|
8
8
|
ogImage: '/og-default.png',
|
|
9
9
|
ogType: 'profile',
|
|
10
10
|
twitterCard: 'summary',
|
|
@@ -402,7 +402,7 @@ async function handleReport(): Promise<void> {
|
|
|
402
402
|
<span v-if="exp.company" class="cpub-exp-company">{{ exp.company }}</span>
|
|
403
403
|
</div>
|
|
404
404
|
<div v-if="exp.startDate" class="cpub-exp-dates">
|
|
405
|
-
{{ exp.startDate }}{{ exp.endDate ?
|
|
405
|
+
{{ exp.startDate }}{{ exp.endDate ? `, ${exp.endDate}` : ', Present' }}
|
|
406
406
|
</div>
|
|
407
407
|
<p v-if="exp.description" class="cpub-exp-desc">{{ exp.description }}</p>
|
|
408
408
|
</div>
|
package/pages/videos/[id].vue
CHANGED
|
@@ -6,7 +6,7 @@ const videoId = computed(() => route.params.id as string);
|
|
|
6
6
|
const { data: video } = useLazyFetch<any>(() => `/api/videos/${videoId.value}`);
|
|
7
7
|
|
|
8
8
|
useSeoMeta({
|
|
9
|
-
title: () => video.value?.title ? `${video.value.title}
|
|
9
|
+
title: () => video.value?.title ? `${video.value.title}, ${useSiteName()}` : `Video, ${useSiteName()}`,
|
|
10
10
|
description: () => video.value?.description ?? '',
|
|
11
11
|
});
|
|
12
12
|
|
package/pages/videos/index.vue
CHANGED
package/pages/videos/submit.vue
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
definePageMeta({ middleware: 'auth' });
|
|
3
|
-
useSeoMeta({ title: `Submit Video
|
|
3
|
+
useSeoMeta({ title: `Submit Video, ${useSiteName()}` });
|
|
4
4
|
|
|
5
5
|
const toast = useToast();
|
|
6
6
|
const { extract: extractError } = useApiError();
|
|
@@ -78,7 +78,7 @@ async function handleSubmit(): Promise<void> {
|
|
|
78
78
|
<div class="form-field">
|
|
79
79
|
<label for="video-cat" class="form-label">Category</label>
|
|
80
80
|
<select id="video-cat" v-model="categoryId" class="form-select">
|
|
81
|
-
<option value=""
|
|
81
|
+
<option value="">- None -</option>
|
|
82
82
|
<option v-for="cat in (categories ?? [])" :key="cat.id" :value="cat.id">{{ cat.name }}</option>
|
|
83
83
|
</select>
|
|
84
84
|
</div>
|
package/sections/builtin/hero.ts
CHANGED
|
@@ -15,7 +15,7 @@ import HeroSection from '../../components/homepage/HeroSection.vue';
|
|
|
15
15
|
export const heroSection: SectionDefinition<HeroConfig> = {
|
|
16
16
|
type: 'hero',
|
|
17
17
|
name: 'Hero',
|
|
18
|
-
description: 'Top-of-page banner (uses HeroSection
|
|
18
|
+
description: 'Top-of-page banner (uses HeroSection, contest-aware)',
|
|
19
19
|
icon: 'fa-bullhorn',
|
|
20
20
|
category: 'layout',
|
|
21
21
|
status: 'stable',
|
|
@@ -13,7 +13,7 @@ import BlockMarkdownView from '../../components/blocks/BlockMarkdownView.vue';
|
|
|
13
13
|
export const markdownSection: SectionDefinition<MarkdownConfig> = {
|
|
14
14
|
type: 'markdown',
|
|
15
15
|
name: 'Markdown',
|
|
16
|
-
description: 'Markdown body
|
|
16
|
+
description: 'Markdown body, safer than custom-html (uses BlockMarkdownView)',
|
|
17
17
|
icon: 'fa-markdown',
|
|
18
18
|
category: 'content',
|
|
19
19
|
status: 'stable',
|
|
@@ -86,7 +86,7 @@ export default defineEventHandler(async (event) => {
|
|
|
86
86
|
try {
|
|
87
87
|
const result = await migrateHomepageSectionsToLayout(db, {
|
|
88
88
|
adminId: user.id,
|
|
89
|
-
force: false, // changed from true
|
|
89
|
+
force: false, // changed from true, see comment above
|
|
90
90
|
});
|
|
91
91
|
if (result.migrated) {
|
|
92
92
|
invalidateLayoutsByRouteCache();
|
|
@@ -103,7 +103,7 @@ export default defineEventHandler(async (event) => {
|
|
|
103
103
|
) {
|
|
104
104
|
throw createError({
|
|
105
105
|
statusCode: 400,
|
|
106
|
-
statusMessage: 'Cannot change layout scope via PUT
|
|
106
|
+
statusMessage: 'Cannot change layout scope via PUT, POST a new layout instead',
|
|
107
107
|
});
|
|
108
108
|
}
|
|
109
109
|
|
|
@@ -27,7 +27,7 @@ export default defineEventHandler(async (event): Promise<ContestEntryItem> => {
|
|
|
27
27
|
const detail = contest.status === 'upcoming'
|
|
28
28
|
? 'Entries open once the contest is active.'
|
|
29
29
|
: contest.status === 'judging'
|
|
30
|
-
? 'Submissions are closed
|
|
30
|
+
? 'Submissions are closed, the contest is being judged.'
|
|
31
31
|
: `The contest is ${contest.status}.`;
|
|
32
32
|
throw createError({ statusCode: 400, statusMessage: `This contest isn't accepting entries right now. ${detail}` });
|
|
33
33
|
}
|
|
@@ -38,7 +38,7 @@ export default defineEventHandler(async (event): Promise<ContestEntryItem> => {
|
|
|
38
38
|
.limit(1);
|
|
39
39
|
if (!content) throw createError({ statusCode: 400, statusMessage: 'That content no longer exists.' });
|
|
40
40
|
if (content.authorId !== user.id) throw createError({ statusCode: 403, statusMessage: 'You can only submit your own content.' });
|
|
41
|
-
if (content.status !== 'published') throw createError({ statusCode: 400, statusMessage: 'That project isn’t published yet
|
|
41
|
+
if (content.status !== 'published') throw createError({ statusCode: 400, statusMessage: 'That project isn’t published yet, publish it first, then submit.' });
|
|
42
42
|
const eligible = contest.eligibleContentTypes ?? null;
|
|
43
43
|
if (eligible && eligible.length > 0 && !eligible.includes(content.type)) {
|
|
44
44
|
throw createError({ statusCode: 400, statusMessage: `This contest only accepts: ${eligible.join(', ')}.` });
|
|
@@ -48,7 +48,7 @@ export default defineEventHandler(async (event): Promise<ContestEntryItem> => {
|
|
|
48
48
|
// cap + dedupes; a null here means already-entered or over the entry limit.
|
|
49
49
|
const entry = await submitContestEntry(db, contest.id, input.contentId, user.id);
|
|
50
50
|
if (!entry) {
|
|
51
|
-
throw createError({ statusCode: 400, statusMessage: 'Couldn’t submit
|
|
51
|
+
throw createError({ statusCode: 400, statusMessage: 'Couldn’t submit, you may have already entered this project, or reached the contest’s entry limit.' });
|
|
52
52
|
}
|
|
53
53
|
return entry;
|
|
54
54
|
});
|
|
@@ -44,7 +44,7 @@ export default defineEventHandler(async (event) => {
|
|
|
44
44
|
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
45
45
|
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
|
|
46
46
|
<channel>
|
|
47
|
-
<title>${escapeXml(hub.name)}
|
|
47
|
+
<title>${escapeXml(hub.name)}, CommonPub</title>
|
|
48
48
|
<link>${escapeXml(siteUrl)}/hubs/${escapeXml(slug)}</link>
|
|
49
49
|
<description>${escapeXml(hub.description ?? `Content from ${hub.name}`)}</description>
|
|
50
50
|
<language>en</language>
|
|
@@ -49,7 +49,7 @@ export default defineEventHandler(async (event) => {
|
|
|
49
49
|
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
50
50
|
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
|
|
51
51
|
<channel>
|
|
52
|
-
<title>${escapeXml(displayName)}
|
|
52
|
+
<title>${escapeXml(displayName)}, CommonPub</title>
|
|
53
53
|
<link>${escapeXml(siteUrl)}/u/${escapeXml(username)}</link>
|
|
54
54
|
<description>Content by ${escapeXml(displayName)}</description>
|
|
55
55
|
<language>en</language>
|
|
@@ -43,7 +43,7 @@ export default defineEventHandler(async (event) => {
|
|
|
43
43
|
.where(and(eq(contentItems.slug, slug), isNull(contentItems.deletedAt)))
|
|
44
44
|
.limit(1);
|
|
45
45
|
|
|
46
|
-
if (!row) return; // Content not found
|
|
46
|
+
if (!row) return; // Content not found, let the page handler show 404
|
|
47
47
|
|
|
48
48
|
const newPath = isEdit
|
|
49
49
|
? `/u/${row.username}/${row.type}/${row.slug}/edit`
|
|
@@ -15,7 +15,7 @@ export default defineNitroPlugin((nitro) => {
|
|
|
15
15
|
try {
|
|
16
16
|
const config = useConfig();
|
|
17
17
|
if (!config.features.federation) {
|
|
18
|
-
console.log('[federation] Federation disabled
|
|
18
|
+
console.log('[federation] Federation disabled, delivery worker not started');
|
|
19
19
|
return;
|
|
20
20
|
}
|
|
21
21
|
|
|
@@ -23,7 +23,7 @@ export default defineNitroPlugin((nitro) => {
|
|
|
23
23
|
try {
|
|
24
24
|
const config = useConfig();
|
|
25
25
|
if (!config.features.federateHubs) {
|
|
26
|
-
console.log('[hub-sync] Hub federation disabled
|
|
26
|
+
console.log('[hub-sync] Hub federation disabled, sync worker not started');
|
|
27
27
|
return;
|
|
28
28
|
}
|
|
29
29
|
|