Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
b3312a1
Restrict my_accounts
maparent Oct 9, 2026
33a2949
eng-1837-add-public-status-in-db
maparent Jun 5, 2026
331cc52
corrections
maparent Oct 8, 2026
6b46d34
allow access to storage
maparent Oct 9, 2026
79f2750
Do not give account info to anonymous user
maparent Oct 9, 2026
d79cd98
Routes, with minimalist conversion
maparent Jun 1, 2026
33f370e
hackish use of schema
maparent Jun 1, 2026
b62566a
add mira to jsonld
maparent Jun 1, 2026
bba7ea7
LDO shapes, v1
maparent Jun 3, 2026
c92383a
Correction: Check that the resource is in the right space
maparent Jun 3, 2026
1e409ce
AbstractRelationDef
maparent Jun 4, 2026
8ad6b73
First version of post (untested)
maparent Jun 4, 2026
89b9879
test (failing)
maparent Jun 4, 2026
f745156
wip
maparent Jun 4, 2026
3a5ffea
wip
maparent Jun 5, 2026
ef9f7a7
wip
maparent Jun 5, 2026
7bbfa25
wip
maparent Jun 5, 2026
fafe150
replace dc elements by dcterms
maparent Jun 5, 2026
d2d174d
jsdom external
maparent Jun 5, 2026
47a427a
older jsdom
maparent Jun 5, 2026
b4cb27b
separate mira and dg contexts. Align dct:date->dct:created
maparent Jun 5, 2026
e9755aa
extend to mira
maparent Jun 5, 2026
2ce660c
another date->created change
maparent Jun 6, 2026
ac5e2b7
Add user account as part of upsert logic
maparent Jun 6, 2026
abdf513
output creator correctly
maparent Jun 6, 2026
078d94a
Creator as object
maparent Jun 6, 2026
c8f8b1b
Expose public content
maparent Jun 6, 2026
cbd19bc
OpenGraph data
maparent Jun 6, 2026
1727d9f
ts is really bad at array indices
maparent Jun 6, 2026
303779b
forgot a log
maparent Jun 6, 2026
36610cb
iriToCurie
maparent Jun 7, 2026
db68b6f
Add extra json attributes to literal_content
maparent Jun 7, 2026
d024e2f
check-types
maparent Jun 7, 2026
a675789
jsonld
maparent Jun 9, 2026
32244b8
accountName
maparent Jun 9, 2026
f0cad54
multiple creators
maparent Jun 9, 2026
42100b6
indirect description; description containment
maparent Jun 9, 2026
0c3830b
partOf info
maparent Jun 9, 2026
d5f8dde
Repair content reading
maparent Jun 9, 2026
cd7ae68
update mira jsonld
maparent Jun 10, 2026
a34610c
commited by mistake
maparent Jun 10, 2026
9a9903c
more jsonld adjustments
maparent Jun 11, 2026
1ecc612
more jsonld adjustments
maparent Jun 11, 2026
c7bf201
mira:Study correction
maparent Jun 22, 2026
77c7ad5
rebase errors
maparent Jun 22, 2026
bf9577d
add test (still failing)
maparent Jun 23, 2026
fb34f4d
Swallow error when anon fails to access authors
maparent Aug 26, 2026
d0ac563
corrections: use is_relation, accept roam
maparent Aug 27, 2026
20862c7
mira synonyms
maparent Aug 27, 2026
7caa148
add mira to context
maparent Aug 27, 2026
8dfc522
more changes to jsonld
maparent Oct 4, 2026
a784bae
Corrections to jsonld
maparent Oct 10, 2026
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
214 changes: 214 additions & 0 deletions apps/website/app/api/content/[space_id]/[resource_id]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
import { NextResponse, NextRequest } from "next/server";
import { createClient } from "~/utils/supabase/server";
import { asPostgrestFailure } from "@repo/database/lib/contextFunctions";
import {
defaultOptionsHandler,
createApiResponse,
} from "~/utils/supabase/apiUtils";
import {
asJsonLD,
conceptName,
CONTENT_PAGE_MARKER,
} from "~/utils/conversion/jsonld";
import { Tables, Enums } from "@repo/database/dbTypes";
import { convert, MIMETYPES, type DocType } from "~/utils/conversion/convert";

type Concept = Tables<"Concept">;
type Content = Tables<"Content">;
type PlatformAccount = Tables<"PlatformAccount">;
type Platform = Enums<"Platform">;

export type SegmentDataType = { params: Promise<Record<string, string>> };

export const GET = async (
request: NextRequest,
segmentData: SegmentDataType,
): Promise<NextResponse> => {
const { space_id, resource_id } = await segmentData.params;
let targetFormat: DocType = (request.nextUrl.searchParams.get("format") ??
"html") as DocType;
if (MIMETYPES[targetFormat] === undefined) {
targetFormat = "html";
}
const targetMimetype = MIMETYPES[targetFormat];
if (!targetMimetype) {
return createApiResponse(
request,
asPostgrestFailure("Unsupported format", "404", 404),
);
}
const includeData =
targetFormat === "html" &&
request.nextUrl.searchParams.get("data") !== "false";
const spaceIdN = Number.parseInt(space_id || "NaN");
if (isNaN(spaceIdN)) {
return createApiResponse(
request,
asPostgrestFailure(`${space_id} is not a number`, "type"),
);
}
const resourceIdN = Number.parseInt(resource_id || "NaN");
if (isNaN(resourceIdN)) {
return createApiResponse(
request,
asPostgrestFailure(`${resource_id} is not a number`, "type"),
);
}
const supabase = await createClient();
const spaceResponse = await supabase
.from("Space")
.select()
.eq("id", spaceIdN)
.maybeSingle();
if (spaceResponse.error) {
return createApiResponse(request, spaceResponse);
}
let platform: Platform = "Obsidian";
if (spaceResponse.data) {
platform = spaceResponse.data.platform;
} else {
// consideration: We may not see it because we don't have access,
// We should find a way to check its platform otherwise.
// Let's just keep the Obsidian guess for MIRA demo.
// return createApiResponse(
// request,
// asPostgrestFailure("Space not found", "401", 401),
// );
}
const conceptResponse = await supabase
.from("Concept")
.select()
.eq("id", resourceIdN)
.eq("space_id", spaceIdN)
.maybeSingle();
if (conceptResponse.error) {
return createApiResponse(request, conceptResponse);
}
const concept = conceptResponse.data;
if (!concept) {
return createApiResponse(
request,
asPostgrestFailure("Resource not found", "401", 401),
);
}
const contentResponse = await supabase
.from("Content")
.select()
.eq("source_local_id", concept.source_local_id!);
if (contentResponse.error) {
return createApiResponse(request, conceptResponse);
}
const contents: Content[] = contentResponse.data;
const requestUrlParts = request.url.split("/");
console.log("requestUrlParts", requestUrlParts);
const pageUrl = requestUrlParts
.slice(0, requestUrlParts.length - 1)
.join("/");
console.log("pageUrl", pageUrl);
const rootUrl = pageUrl.split("/").slice(0, 3).join("/");
console.log("rootUrl", rootUrl);
const baseUrl = `${rootUrl}/api/data/${pageUrl.split("/")[5]}`;

const fullContentsArray = contents.filter((c) => c.variant === "full");
const fullContents = fullContentsArray.length
? fullContentsArray[0]
: undefined;
const titleArray = contents.filter((c) => c.variant === "direct");
const title = titleArray.length ? titleArray[0] : undefined;

if (!fullContents) {
return createApiResponse(
request,
asPostgrestFailure("Resource not found", "401", 401),
);
}

// await initRT(rootUrl);
const source: DocType | undefined =
platform === "Obsidian"
? "obsidian"
: platform === "Roam"
? "roam"
: undefined;
let text =
source && source !== targetFormat
? convert(fullContents.text, source, targetFormat)
: fullContents.text;
const isSchema = concept.is_schema;
let schema: Concept | undefined = undefined;
if (!isSchema && concept.schema_id) {
const schemaResponse = await supabase
.from("Concept")
.select()
.eq("id", concept.schema_id)
.maybeSingle();
if (schemaResponse.error) {
return createApiResponse(request, schemaResponse);
}
if (!schemaResponse.data) {
return createApiResponse(
request,
asPostgrestFailure("Resource schema not found", "401", 401),
);
}
schema = schemaResponse.data;
}
const schemaName = conceptName(concept, schema);
let author: PlatformAccount | undefined = undefined;

if (includeData) {
const authorId = concept.author_id ?? (contents ?? [{}])[0]?.author_id;
if (authorId) {
try {
const authorResponse = await supabase
.from("PlatformAccount")
.select()
.eq("id", authorId)
.maybeSingle();
if (authorResponse.data) author = authorResponse.data;
} catch (error) {
console.error(error);
// if anonymous, we do not have access.
}
}
text = `<div id="content">\n${text}\n</div>`;
}
const jsonLdData = asJsonLD({
platform,
concept,
baseUrl,
title,
schema,
content: CONTENT_PAGE_MARKER,
author,
targetFormat,
wrap: true,
});

const divideFM = fullContents.text.split("---\n");
const firstTextFragment =
divideFM.length > 2 && divideFM[0]!.trim().length == 0
? divideFM[2]!
: fullContents.text;

const wrap = `<html><head>
${title ? '<meta property="og:title" content="' + title.text + '" />' : ""}
<meta property="og:type" content="${schemaName}" />
<meta property="og:url" content="${request.url}" />
<meta property="og:description" content="${firstTextFragment.substring(0, 80)}" />
<script type="application/ld+json">${JSON.stringify(jsonLdData)}</script>
</head>
<body>
<main>
${text}
</main>
</body>
</html>
`;

return new NextResponse(wrap, {
headers: { "Content-Type": targetMimetype },
});
};

export const OPTIONS = defaultOptionsHandler;
Loading
Loading