Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 4 additions & 3 deletions apps/admin/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,16 @@ import { connection } from "next/server";
import { BrowserTelemetry } from "@devfeed/telemetry/browser";
import { browserSettings } from "@devfeed/telemetry/receiver";
import type { Metadata } from "next";
import brandMark from "@devfeed/theme/assets/devfeed-mark.png";
import browserIcon from "@devfeed/theme/assets/devfeed-icon-32.png";
import appleIcon from "@devfeed/theme/assets/devfeed-icon-180.png";
import { Toaster } from "@/components/atoms/sonner";
import { adminSiteTitle } from "@/lib/page-titles";
import "./globals.css";

export const metadata: Metadata = {
icons: {
icon: { url: brandMark.src, type: "image/png" },
apple: brandMark.src,
icon: { url: browserIcon.src, type: "image/png", sizes: "32x32" },
apple: { url: appleIcon.src, type: "image/png", sizes: "180x180" },
},
title: { default: adminSiteTitle, template: `%s · ${adminSiteTitle}` },
description: "DevFeed site administration",
Expand Down
2 changes: 2 additions & 0 deletions apps/admin/tests/browser/overview.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { mkdir, writeFile } from "node:fs/promises";
import { fileURLToPath } from "node:url";
import { build } from "esbuild";
import { chromium } from "playwright";
import { checkBrowserIcons } from "../../../web/tests/browser/article-first.mjs";

// Run after npm run admin:build. All API traffic stays on disposable localhost servers.
const root = fileURLToPath(new URL("../../../../", import.meta.url));
Expand Down Expand Up @@ -236,6 +237,7 @@ try {
);
console.log("Opening", origin);
await page.goto(origin);
await checkBrowserIcons(page, true);
const response = await page.request.get(origin);
assert.match(response.headers()["content-security-policy"], /script-src [^;]*'nonce-[^']+'/);
assert.doesNotMatch(
Expand Down
21 changes: 21 additions & 0 deletions apps/admin/tests/layout-metadata.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { expect, it, vi } from "vitest";
import { metadata } from "@/app/layout";

vi.mock("@devfeed/theme/assets/devfeed-icon-32.png", () => ({
default: { src: "/assets/favicon-32.png" },
}));
vi.mock("@devfeed/theme/assets/devfeed-icon-180.png", () => ({
default: { src: "/assets/apple-180.png" },
}));

it("uses compact icons while keeping admin pages out of search results", () => {
expect(metadata.icons).toEqual({
icon: { url: "/assets/favicon-32.png", type: "image/png", sizes: "32x32" },
apple: { url: "/assets/apple-180.png", type: "image/png", sizes: "180x180" },
});
expect(metadata.robots).toEqual({ index: false, follow: false });
expect(metadata.title).toEqual({
default: "DevFeed Admin",
template: "%s · DevFeed Admin",
});
});
89 changes: 49 additions & 40 deletions apps/extensions/build.mjs
Original file line number Diff line number Diff line change
@@ -1,45 +1,54 @@
import { build } from "esbuild";
import { cp, mkdir } from "node:fs/promises";
import { fileURLToPath } from "node:url";
import { fileURLToPath, pathToFileURL } from "node:url";

const root = fileURLToPath(new URL("../../", import.meta.url)).replace(/\/$/, "");
const browser = process.argv[2] ?? "chrome";
if (!["chrome", "edge"].includes(browser)) throw new Error(`Unsupported browser: ${browser}`);
const output = `${root}/apps/extensions/dist/${browser}`;
await mkdir(output, { recursive: true });
await cp(`${root}/apps/extensions/chrome`, output, { recursive: true });
await cp(`${root}/packages/theme/assets/devfeed-mark.png`, `${output}/icon.png`);
await cp(`${root}/apps/web/public/tool-icons`, `${output}/tool-icons`, { recursive: true });
await build({
absWorkingDir: root,
entryPoints: { newtab: "apps/extensions/src/entry.tsx" },
bundle: true,
outdir: output,
target: browser === "edge" ? "edge120" : "chrome120",
jsx: "automatic",
legalComments: "eof",
define: { "process.env.NODE_ENV": '"production"', __DEVFEED_BROWSER__: JSON.stringify(browser) },
alias: {
"@": `${root}/apps/web/src`,
"next/link": `${root}/apps/extensions/src/link.tsx`,
"next/image": `${root}/apps/extensions/src/image.tsx`,
"next/navigation": `${root}/apps/extensions/src/navigation.tsx`,
},
plugins: [
{
name: "bundled-brand-mark",
setup(build) {
build.onResolve({ filter: /^@devfeed\/theme\/assets\/devfeed-mark\.png$/ }, () => ({
path: "icon.png",
namespace: "brand",
}));
build.onLoad({ filter: /.*/, namespace: "brand" }, () => ({
contents: 'export default { src: "icon.png" };',
loader: "js",
}));
},
export async function buildExtension(browser = "chrome") {
if (!["chrome", "edge"].includes(browser)) throw new Error(`Unsupported browser: ${browser}`);
const output = `${root}/apps/extensions/dist/${browser}`;
await mkdir(output, { recursive: true });
await cp(`${root}/apps/extensions/chrome`, output, { recursive: true });
await cp(`${root}/packages/theme/assets/devfeed-icon-128.png`, `${output}/icon.png`);
await cp(`${root}/packages/theme/assets/devfeed-icon-32.png`, `${output}/favicon.png`);
await cp(`${root}/apps/web/public/tool-icons`, `${output}/tool-icons`, { recursive: true });
await build({
absWorkingDir: root,
entryPoints: { newtab: "apps/extensions/src/entry.tsx" },
bundle: true,
outdir: output,
target: browser === "edge" ? "edge120" : "chrome120",
jsx: "automatic",
legalComments: "eof",
define: {
"process.env.NODE_ENV": '"production"',
__DEVFEED_BROWSER__: JSON.stringify(browser),
},
alias: {
"@": `${root}/apps/web/src`,
"next/link": `${root}/apps/extensions/src/link.tsx`,
"next/image": `${root}/apps/extensions/src/image.tsx`,
"next/navigation": `${root}/apps/extensions/src/navigation.tsx`,
},
],
minify: true,
});
console.log(`Load unpacked: ${output}`);
plugins: [
{
name: "bundled-brand-mark",
setup(build) {
build.onResolve({ filter: /^@devfeed\/theme\/assets\/devfeed-mark\.png$/ }, () => ({
path: "icon.png",
namespace: "brand",
}));
build.onLoad({ filter: /.*/, namespace: "brand" }, () => ({
contents: 'export default { src: "icon.png" };',
loader: "js",
}));
},
},
],
minify: true,
});
console.log(`Load unpacked: ${output}`);
}

if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
await buildExtension(process.argv[2]);
}
1 change: 1 addition & 0 deletions apps/extensions/chrome/newtab.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>DevFeed — New Tab</title>
<link rel="icon" type="image/png" sizes="32x32" href="favicon.png" />
<link rel="stylesheet" href="newtab.css" />
<script src="newtab.js" defer></script>
</head>
Expand Down
27 changes: 22 additions & 5 deletions apps/extensions/tests/reader.browser.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
checkEngagementPagination,
} from "../../../scripts/testing/engagement-pagination.mjs";
import { checkPreviewBackground } from "../../../scripts/testing/preview-background.mjs";
import { blockedFeed, checkArticleFirst } from "../../web/tests/browser/article-first.mjs";
import {
searchFixture,
checkSearchFilters,
Expand Down Expand Up @@ -89,6 +90,7 @@ test(
let engagementPagination = false;
let failNextPage = true;
let failArticle = true;
let articleFeedGate;
await context.route("https://identity.example/authorize?**", (route) =>
route.fulfill({ contentType: "text/html", body: "<p>Sign-in provider</p>" }),
);
Expand All @@ -100,6 +102,10 @@ test(
);
await context.route("https://devfeed.tech/api/**", async (route) => {
const url = new URL(route.request().url());
const pendingFeed = articleFeedGate;
if (pendingFeed && (await pendingFeed.wait(url.pathname)) && pendingFeed.fail) {
return route.fulfill({ status: 503, json: {} });
}
requests.push(url);
if (
engagementPagination &&
Expand Down Expand Up @@ -298,11 +304,22 @@ test(
await retryPage.getByRole("button", { name: "Try again", exact: true }).click();
await retryPage.locator("#article-preview-title").waitFor();
await retryPage.close();
const direct = await context.newPage();
await direct.goto(page.url().split("#")[0] + "#/articles/direct-article");
await direct.locator("#article-preview-title").waitFor();
assert.ok(direct.url().endsWith("#/articles/direct-article"));
await direct.close();
for (const fail of [false, true]) {
const direct = await context.newPage();
articleFeedGate = blockedFeed(fail);
try {
await checkArticleFirst(
direct,
page.url().split("#")[0] + "#/articles/direct-article",
articleFeedGate,
);
if (fail) assert.ok(direct.url().endsWith("#/articles/direct-article"));
} finally {
articleFeedGate.release();
articleFeedGate = undefined;
await direct.close();
}
}
await page.bringToFront();
await page.goto(page.url().split("#")[0] + "#/latest");
await page.locator(".article-card").first().waitFor();
Expand Down
1 change: 1 addition & 0 deletions apps/web/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ COPY packages/telemetry packages/telemetry
COPY apps/web apps/web
# Reader type checks include the shared extension analytics regression test.
COPY apps/extensions apps/extensions
COPY scripts/assets/browser-icons.mjs scripts/assets/browser-icons.mjs
RUN mkdir -p apps/web/public && npm run web:build && node packages/telemetry/scripts/private-sourcemaps.mjs apps/web/.next

FROM ${DOCKERHUB_PREFIX}alpine:3.24@sha256:28bd5fe8b56d1bd048e5babf5b10710ebe0bae67db86916198a6eec434943f8b AS runtime
Expand Down
13 changes: 12 additions & 1 deletion apps/web/src/app/articles/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import { ArticleModal } from "@/components/article-modal";
import { parseFilters } from "@/lib/feed-query";
import { ArticlePreview } from "@/components/article-preview";
import { articleMetadata } from "@/lib/metadata";
import { Suspense } from "react";
import { UserShell } from "@/components/user-shell";
import { LoadingSkeleton } from "@/components/loading-skeleton";
export const dynamic = "force-dynamic";
type Props = { params: Promise<{ slug: string }> };
export async function generateMetadata({ params }: Props): Promise<Metadata> {
Expand All @@ -28,7 +31,15 @@ export default async function ArticlePage({ params }: Props) {
);
return (
<>
{await FeedView({ filters: parseFilters({}), structuredData: false })}
<Suspense
fallback={
<UserShell>
<LoadingSkeleton />
</UserShell>
}
>
<FeedView filters={parseFilters({})} structuredData={false} />
</Suspense>
<ArticleModal direct slug={article.slug}>
<ArticlePreview article={article} />
</ArticleModal>
Expand Down
6 changes: 4 additions & 2 deletions apps/web/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import { connection } from "next/server";
import { headers } from "next/headers";
import { analyticsMeasurementId, clarityProjectId, xPixelEnabled } from "@/lib/server/config";
import brandMark from "@devfeed/theme/assets/devfeed-mark.png";
import browserIcon from "@devfeed/theme/assets/devfeed-icon-32.png";
import appleIcon from "@devfeed/theme/assets/devfeed-icon-180.png";
import "./globals.css";
import "./reader-motion.css";
import "./article-share.css";
Expand All @@ -31,8 +33,8 @@ export async function generateMetadata(): Promise<Metadata> {
metadataBase: new URL(canonicalUrl("/")),
...socialMetadata("DevFeed — Developer news", SITE_DESCRIPTION),
icons: {
icon: { url: brandMark.src, type: "image/png" },
apple: brandMark.src,
icon: { url: browserIcon.src, type: "image/png", sizes: "32x32" },
apple: { url: appleIcon.src, type: "image/png", sizes: "180x180" },
},
title: {
default: "DevFeed — Developer news",
Expand Down
Loading