Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
ac45012
eng-1837-add-public-status-in-db
maparent Jun 5, 2026
8c22b20
Routes, with minimalist conversion
maparent Jun 1, 2026
640411e
hackish use of schema
maparent Jun 1, 2026
0ade16e
add mira to jsonld
maparent Jun 1, 2026
4c79cba
LDO shapes, v1
maparent Jun 3, 2026
95567ab
Correction: Check that the resource is in the right space
maparent Jun 3, 2026
ef09f87
AbstractRelationDef
maparent Jun 4, 2026
9f2f0a5
First version of post (untested)
maparent Jun 4, 2026
b5a1593
test (failing)
maparent Jun 4, 2026
b7b840f
wip
maparent Jun 4, 2026
a2efe85
wip
maparent Jun 5, 2026
7fd9a1c
wip
maparent Jun 5, 2026
dd686ad
wip
maparent Jun 5, 2026
4914634
replace dc elements by dcterms
maparent Jun 5, 2026
a7b9492
jsdom external
maparent Jun 5, 2026
ec2a29a
older jsdom
maparent Jun 5, 2026
eacdc23
separate mira and dg contexts. Align dct:date->dct:created
maparent Jun 5, 2026
d5b4925
extend to mira
maparent Jun 5, 2026
ada267b
another date->created change
maparent Jun 6, 2026
0435850
Add user account as part of upsert logic
maparent Jun 6, 2026
70df79c
output creator correctly
maparent Jun 6, 2026
bb40ec5
Creator as object
maparent Jun 6, 2026
bc5242c
Expose public content
maparent Jun 6, 2026
1934786
OpenGraph data
maparent Jun 6, 2026
139438d
ts is really bad at array indices
maparent Jun 6, 2026
ae51015
forgot a log
maparent Jun 6, 2026
d21b732
iriToCurie
maparent Jun 7, 2026
f125720
Add extra json attributes to literal_content
maparent Jun 7, 2026
dd05192
check-types
maparent Jun 7, 2026
100e60e
jsonld
maparent Jun 9, 2026
569cca5
accountName
maparent Jun 9, 2026
ef28bcc
multiple creators
maparent Jun 9, 2026
a7a782f
indirect description; description containment
maparent Jun 9, 2026
ed703e0
partOf info
maparent Jun 9, 2026
706f839
Repair content reading
maparent Jun 9, 2026
059a699
update mira jsonld
maparent Jun 10, 2026
f6efebf
commited by mistake
maparent Jun 10, 2026
173094d
more jsonld adjustments
maparent Jun 11, 2026
9781655
more jsonld adjustments
maparent Jun 11, 2026
6c1ef01
mira:Study correction
maparent Jun 22, 2026
1fe51c1
rebase errors
maparent Jun 22, 2026
d137c05
add test (still failing)
maparent Jun 23, 2026
3a0d058
Swallow error when anon fails to access authors
maparent Aug 26, 2026
c751670
corrections: use is_relation, accept roam
maparent Aug 27, 2026
7db598c
mira synonyms
maparent Aug 27, 2026
c2aeff9
add mira to context
maparent Aug 27, 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
203 changes: 203 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,203 @@
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 } 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("/");
const baseUrl = requestUrlParts
.slice(0, requestUrlParts.length - 1)
.join("/");
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);

if (includeData) {
const authorId = concept.author_id ?? (contents ?? [{}])[0]?.author_id;
let author: PlatformAccount | undefined = undefined;
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.
}
}

const jsonLdData = asJsonLD({
platform,
concept,
baseUrl,
title,
schema,
content: undefined,
author,
targetFormat,
wrap: true,
});
text = `<div id="content">\n<script type="application/ld+json">${JSON.stringify(jsonLdData)}</script>\n${text}\n</div>`;
}
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)}" />
</head>
<body>
<main>
${text}
</main>
</body>
</html>
`;

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

export const OPTIONS = defaultOptionsHandler;
Loading
Loading