@commonpub/layer 0.15.5 → 0.15.7
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@commonpub/layer",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.7",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./nuxt.config.ts",
|
|
6
6
|
"files": [
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@aws-sdk/client-s3": "^3.1010.0",
|
|
31
|
-
"@commonpub/explainer": "^0.7.
|
|
31
|
+
"@commonpub/explainer": "^0.7.12",
|
|
32
32
|
"@commonpub/schema": "^0.13.0",
|
|
33
33
|
"@commonpub/server": "^2.43.0",
|
|
34
34
|
"@tiptap/core": "^2.11.0",
|
|
@@ -53,12 +53,12 @@
|
|
|
53
53
|
"vue": "^3.4.0",
|
|
54
54
|
"vue-router": "^4.3.0",
|
|
55
55
|
"zod": "^4.3.6",
|
|
56
|
-
"@commonpub/docs": "0.6.2",
|
|
57
|
-
"@commonpub/config": "0.10.0",
|
|
58
56
|
"@commonpub/auth": "0.5.1",
|
|
57
|
+
"@commonpub/config": "0.10.0",
|
|
58
|
+
"@commonpub/docs": "0.6.2",
|
|
59
59
|
"@commonpub/editor": "0.7.9",
|
|
60
|
-
"@commonpub/protocol": "0.9.9",
|
|
61
60
|
"@commonpub/learning": "0.5.0",
|
|
61
|
+
"@commonpub/protocol": "0.9.9",
|
|
62
62
|
"@commonpub/ui": "0.8.5"
|
|
63
63
|
},
|
|
64
64
|
"devDependencies": {
|
package/pages/[type]/index.vue
CHANGED
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import type { Serialized, ContentListItem, PaginatedResponse } from '@commonpub/server';
|
|
3
|
+
import type { ContentType } from '../../composables/useContentTypes';
|
|
3
4
|
|
|
4
5
|
const route = useRoute();
|
|
5
6
|
const contentType = computed(() => route.params.type as string);
|
|
6
7
|
const siteName = useSiteName();
|
|
7
8
|
const { user } = useAuth();
|
|
9
|
+
const { isTypeEnabled } = useContentTypes();
|
|
10
|
+
|
|
11
|
+
// Hard 404 for any path that isn't an enabled content type — this catch-all
|
|
12
|
+
// route would otherwise match /foo, /@username, /wp-admin, /.env, etc. and
|
|
13
|
+
// render a broken empty listing with the URL segment as the "type".
|
|
14
|
+
if (!isTypeEnabled(contentType.value as ContentType)) {
|
|
15
|
+
throw createError({ statusCode: 404, statusMessage: 'Not Found' });
|
|
16
|
+
}
|
|
8
17
|
|
|
9
18
|
useSeoMeta({
|
|
10
19
|
title: () => `${contentType.value} — ${siteName}`,
|
|
@@ -21,7 +21,10 @@ export default defineEventHandler(async (event) => {
|
|
|
21
21
|
if (!isAPRequest) return;
|
|
22
22
|
|
|
23
23
|
const path = getRequestURL(event).pathname;
|
|
24
|
-
|
|
24
|
+
// postId must be a UUID — anything else short-circuits before the drizzle
|
|
25
|
+
// query, which would otherwise throw on invalid UUID input and 500 for
|
|
26
|
+
// federation peers that probe bad URIs.
|
|
27
|
+
const match = path.match(/^\/hubs\/([a-z0-9][a-z0-9_-]*)\/posts\/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/);
|
|
25
28
|
if (!match) return;
|
|
26
29
|
|
|
27
30
|
const config = useConfig();
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { users, hubs } from '@commonpub/schema';
|
|
2
|
+
import { eq, and, isNull } from 'drizzle-orm';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Redirect /@handle → canonical profile URL.
|
|
6
|
+
*
|
|
7
|
+
* WebFinger advertises `/@username` as the `profile-page` (Mastodon/Fediverse
|
|
8
|
+
* convention) — without this middleware, that URL falls through to the
|
|
9
|
+
* `[type]/index.vue` catchall and renders a broken content listing for
|
|
10
|
+
* "@usernames". Federated users clicking a CommonPub member's profile from
|
|
11
|
+
* Mastodon end up on a broken page.
|
|
12
|
+
*
|
|
13
|
+
* Matches /@{handle} exactly (no sub-paths). Looks up users first, then hubs
|
|
14
|
+
* (both are advertised via WebFinger). Misses 404.
|
|
15
|
+
*/
|
|
16
|
+
export default defineEventHandler(async (event) => {
|
|
17
|
+
const path = getRequestURL(event).pathname;
|
|
18
|
+
const match = path.match(/^\/@([a-zA-Z0-9._-]+)$/);
|
|
19
|
+
if (!match) return;
|
|
20
|
+
|
|
21
|
+
const handle = match[1]!;
|
|
22
|
+
const db = useDB();
|
|
23
|
+
|
|
24
|
+
const [user] = await db
|
|
25
|
+
.select({ username: users.username })
|
|
26
|
+
.from(users)
|
|
27
|
+
.where(and(eq(users.username, handle), isNull(users.deletedAt)))
|
|
28
|
+
.limit(1);
|
|
29
|
+
|
|
30
|
+
if (user) {
|
|
31
|
+
return sendRedirect(event, `/u/${user.username}`, 301);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const [hub] = await db
|
|
35
|
+
.select({ slug: hubs.slug })
|
|
36
|
+
.from(hubs)
|
|
37
|
+
.where(and(eq(hubs.slug, handle), isNull(hubs.deletedAt)))
|
|
38
|
+
.limit(1);
|
|
39
|
+
|
|
40
|
+
if (hub) {
|
|
41
|
+
return sendRedirect(event, `/hubs/${hub.slug}`, 301);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
throw createError({ statusCode: 404, statusMessage: 'Not Found' });
|
|
45
|
+
});
|
|
@@ -3,12 +3,15 @@ import { contentItems, users } from '@commonpub/schema';
|
|
|
3
3
|
import { eq, and, isNull } from 'drizzle-orm';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
6
|
+
* Legacy content URI: /content/:slug
|
|
7
|
+
*
|
|
8
|
+
* AP clients (Accept: application/activity+json) get Article JSON-LD —
|
|
9
|
+
* remote instances still dereference this legacy URI when processing old
|
|
10
|
+
* Create/Announce/Like activities created before the URL restructure.
|
|
11
|
+
*
|
|
12
|
+
* Browsers get a 301 redirect to the canonical /u/:author/:type/:slug URL.
|
|
13
|
+
* Anything else would be a dead 204 (this is a server route, not middleware,
|
|
14
|
+
* and there's no Nuxt page at /content/:slug to fall through to).
|
|
12
15
|
*/
|
|
13
16
|
export default defineEventHandler(async (event) => {
|
|
14
17
|
const accept = getRequestHeader(event, 'accept') ?? '';
|
|
@@ -16,14 +19,12 @@ export default defineEventHandler(async (event) => {
|
|
|
16
19
|
accept.includes('application/activity+json') ||
|
|
17
20
|
accept.includes('application/ld+json');
|
|
18
21
|
|
|
19
|
-
if (!isAPRequest) return;
|
|
20
|
-
|
|
21
|
-
const config = useConfig();
|
|
22
|
-
if (!config.features.federation) return;
|
|
23
|
-
|
|
24
22
|
const slug = getRouterParam(event, 'slug');
|
|
25
|
-
if (!slug)
|
|
23
|
+
if (!slug) {
|
|
24
|
+
throw createError({ statusCode: 404, statusMessage: 'Not Found' });
|
|
25
|
+
}
|
|
26
26
|
|
|
27
|
+
const config = useConfig();
|
|
27
28
|
const db = useDB();
|
|
28
29
|
const domain = config.instance.domain;
|
|
29
30
|
|
|
@@ -44,12 +45,23 @@ export default defineEventHandler(async (event) => {
|
|
|
44
45
|
))
|
|
45
46
|
.limit(1);
|
|
46
47
|
|
|
47
|
-
if (!row)
|
|
48
|
+
if (!row) {
|
|
49
|
+
throw createError({ statusCode: 404, statusMessage: 'Content not found' });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Browser: redirect to canonical URL.
|
|
53
|
+
if (!isAPRequest) {
|
|
54
|
+
return sendRedirect(event, `/u/${row.author.username}/${row.content.type}/${row.content.slug}`, 301);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// AP: serve Article JSON-LD.
|
|
58
|
+
if (!config.features.federation) {
|
|
59
|
+
throw createError({ statusCode: 404, statusMessage: 'Federation disabled' });
|
|
60
|
+
}
|
|
48
61
|
|
|
49
62
|
setResponseHeader(event, 'content-type', 'application/activity+json');
|
|
50
63
|
|
|
51
|
-
|
|
52
|
-
const article = contentToArticle(
|
|
64
|
+
return contentToArticle(
|
|
53
65
|
{
|
|
54
66
|
id: row.content.id,
|
|
55
67
|
type: row.content.type,
|
|
@@ -66,6 +78,4 @@ export default defineEventHandler(async (event) => {
|
|
|
66
78
|
{ username: row.author.username, displayName: row.author.displayName ?? row.author.username },
|
|
67
79
|
domain,
|
|
68
80
|
);
|
|
69
|
-
|
|
70
|
-
return article;
|
|
71
81
|
});
|