feat(blog): add file-based blog with dynamic slugs, MDX content and layout shell

- Introduced blog routing using Next.js App Router
- Implemented dynamic [slug] pages for blog posts
- Added MDX-based content loading via lib/posts
- Integrated shared TopBar layout with navigation
- Established clear content, lib and component separation
This commit is contained in:
PascalSchattenburg
2026-01-22 14:14:15 +01:00
parent b717952234
commit d147843c76
10412 changed files with 2475583 additions and 0 deletions

View File

@@ -0,0 +1,132 @@
import { z } from 'next/dist/compiled/zod';
import { formatZodError } from '../../../shared/lib/zod';
const CookieSchema = z.object({
name: z.string(),
value: z.string(),
httpOnly: z.boolean().optional(),
path: z.string().optional()
}).strict();
const RuntimeSampleSchema = z.object({
cookies: z.array(CookieSchema).optional(),
headers: z.array(z.tuple([
z.string(),
z.string()
])).optional(),
params: z.record(z.union([
z.string(),
z.array(z.string())
])).optional(),
searchParams: z.record(z.union([
z.string(),
z.array(z.string()),
z.undefined()
])).optional()
}).strict();
const StaticPrefetchSchema = z.object({
mode: z.literal('static'),
from: z.array(z.string()).optional(),
expectUnableToVerify: z.boolean().optional()
}).strict();
const RuntimePrefetchSchema = z.object({
mode: z.literal('runtime'),
samples: z.array(RuntimeSampleSchema).min(1),
from: z.array(z.string()).optional(),
expectUnableToVerify: z.boolean().optional()
}).strict();
const PrefetchSchema = z.discriminatedUnion('mode', [
StaticPrefetchSchema,
RuntimePrefetchSchema
]);
/**
* The schema for configuration for a page.
*/ const AppSegmentConfigSchema = z.object({
/**
* The number of seconds to revalidate the page or false to disable revalidation.
*/ revalidate: z.union([
z.number().int().nonnegative(),
z.literal(false)
]).optional(),
/**
* Whether the page supports dynamic parameters.
*/ dynamicParams: z.boolean().optional(),
/**
* The dynamic behavior of the page.
*/ dynamic: z.enum([
'auto',
'error',
'force-static',
'force-dynamic'
]).optional(),
/**
* The caching behavior of the page.
*/ fetchCache: z.enum([
'auto',
'default-cache',
'only-cache',
'force-cache',
'force-no-store',
'default-no-store',
'only-no-store'
]).optional(),
/**
* How this segment should be prefetched.
*/ unstable_prefetch: PrefetchSchema.optional(),
/**
* The preferred region for the page.
*/ preferredRegion: z.union([
z.string(),
z.array(z.string())
]).optional(),
/**
* The runtime to use for the page.
*/ runtime: z.enum([
'edge',
'nodejs'
]).optional(),
/**
* The maximum duration for the page in seconds.
*/ maxDuration: z.number().int().nonnegative().optional()
});
/**
* Parse the app segment config.
* @param data - The data to parse.
* @param route - The route of the app.
* @returns The parsed app segment config.
*/ export function parseAppSegmentConfig(data, route) {
const parsed = AppSegmentConfigSchema.safeParse(data, {
errorMap: (issue, ctx)=>{
if (issue.path.length === 1) {
switch(issue.path[0]){
case 'revalidate':
{
return {
message: `Invalid revalidate value ${JSON.stringify(ctx.data)} on "${route}", must be a non-negative number or false`
};
}
case 'unstable_prefetch':
{
return {
// @TODO replace this link with a link to the docs when they are written
message: `Invalid unstable_prefetch value ${JSON.stringify(ctx.data)} on "${route}", must be an object with a mode of "static" or "runtime". Read more at https://nextjs.org/docs/messages/invalid-prefetch-configuration`
};
}
default:
}
}
return {
message: ctx.defaultError
};
}
});
if (!parsed.success) {
throw formatZodError(`Invalid segment configuration options detected for "${route}". Read more at https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config`, parsed.error);
}
return parsed.data;
}
/**
* The keys of the configuration for a page.
*
* @internal - required to exclude zod types from the build
*/ export const AppSegmentConfigSchemaKeys = AppSegmentConfigSchema.keyof().options;
//# sourceMappingURL=app-segment-config.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,132 @@
import { parseAppSegmentConfig } from './app-segment-config';
import { InvariantError } from '../../../shared/lib/invariant-error';
import { isAppRouteRouteModule, isAppPageRouteModule } from '../../../server/route-modules/checks';
import { isClientReference } from '../../../lib/client-and-server-references';
import { getSegmentParam } from '../../../shared/lib/router/utils/get-segment-param';
import { getLayoutOrPageModule } from '../../../server/lib/app-dir-module';
/**
* Parses the app config and attaches it to the segment.
*/ function attach(segment, userland, route) {
// If the userland is not an object, then we can't do anything with it.
if (typeof userland !== 'object' || userland === null) {
return;
}
// Try to parse the application configuration.
const config = parseAppSegmentConfig(userland, route);
// If there was any keys on the config, then attach it to the segment.
if (Object.keys(config).length > 0) {
segment.config = config;
}
if ('generateStaticParams' in userland && typeof userland.generateStaticParams === 'function') {
var _segment_config;
segment.generateStaticParams = userland.generateStaticParams;
// Validate that `generateStaticParams` makes sense in this context.
if (((_segment_config = segment.config) == null ? void 0 : _segment_config.runtime) === 'edge') {
throw Object.defineProperty(new Error('Edge runtime is not supported with `generateStaticParams`.'), "__NEXT_ERROR_CODE", {
value: "E502",
enumerable: false,
configurable: true
});
}
}
}
/**
* Walks the loader tree and collects the generate parameters for each segment.
*
* @param routeModule the app page route module
* @returns the segments for the app page route module
*/ async function collectAppPageSegments(routeModule) {
// We keep track of unique segments, since with parallel routes, it's possible
// to see the same segment multiple times.
const segments = [];
// Queue will store loader trees.
const queue = [
routeModule.userland.loaderTree
];
while(queue.length > 0){
const loaderTree = queue.shift();
const [name, parallelRoutes] = loaderTree;
// Process current node
const { mod: userland, filePath } = await getLayoutOrPageModule(loaderTree);
const isClientComponent = userland && isClientReference(userland);
const param = getSegmentParam(name);
const segment = {
name,
paramName: param == null ? void 0 : param.paramName,
paramType: param == null ? void 0 : param.paramType,
filePath,
config: undefined,
generateStaticParams: undefined
};
// Only server components can have app segment configurations
if (!isClientComponent) {
attach(segment, userland, routeModule.definition.pathname);
}
// If this segment doesn't already exist, then add it to the segments array.
// The list of segments is short so we just use a list traversal to check
// for duplicates and spare us needing to maintain the string key.
if (segments.every((s)=>s.name !== segment.name || s.paramName !== segment.paramName || s.paramType !== segment.paramType || s.filePath !== segment.filePath)) {
segments.push(segment);
}
// Add all parallel routes to the queue
for (const parallelRoute of Object.values(parallelRoutes)){
queue.push(parallelRoute);
}
}
return segments;
}
/**
* Collects the segments for a given app route module.
*
* @param routeModule the app route module
* @returns the segments for the app route module
*/ function collectAppRouteSegments(routeModule) {
// Get the pathname parts, slice off the first element (which is empty).
const parts = routeModule.definition.pathname.split('/').slice(1);
if (parts.length === 0) {
throw Object.defineProperty(new InvariantError('Expected at least one segment'), "__NEXT_ERROR_CODE", {
value: "E580",
enumerable: false,
configurable: true
});
}
// Generate all the segments.
const segments = parts.map((name)=>{
const param = getSegmentParam(name);
return {
name,
paramName: param == null ? void 0 : param.paramName,
paramType: param == null ? void 0 : param.paramType,
filePath: undefined,
config: undefined,
generateStaticParams: undefined
};
});
// We know we have at least one, we verified this above. We should get the
// last segment which represents the root route module.
const segment = segments[segments.length - 1];
segment.filePath = routeModule.definition.filename;
// Extract the segment config from the userland module.
attach(segment, routeModule.userland, routeModule.definition.pathname);
return segments;
}
/**
* Collects the segments for a given route module.
*
* @param components the loaded components
* @returns the segments for the route module
*/ export function collectSegments(routeModule) {
if (isAppRouteRouteModule(routeModule)) {
return collectAppRouteSegments(routeModule);
}
if (isAppPageRouteModule(routeModule)) {
return collectAppPageSegments(routeModule);
}
throw Object.defineProperty(new InvariantError('Expected a route module to be one of app route or page'), "__NEXT_ERROR_CODE", {
value: "E568",
enumerable: false,
configurable: true
});
}
//# sourceMappingURL=app-segments.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,47 @@
import { getSegmentParam } from '../../../shared/lib/router/utils/get-segment-param';
import { isAppPageRouteModule, isAppRouteRouteModule } from '../../../server/route-modules/checks';
import { InvariantError } from '../../../shared/lib/invariant-error';
function collectAppPageRootParamKeys(routeModule) {
let rootParams = [];
let current = routeModule.userland.loaderTree;
while(current){
var _getSegmentParam;
const [name, parallelRoutes, modules] = current;
// If this is a dynamic segment, then we collect the param.
const paramName = (_getSegmentParam = getSegmentParam(name)) == null ? void 0 : _getSegmentParam.paramName;
if (paramName) {
rootParams.push(paramName);
}
// If this has a layout module, then we've found the root layout because
// we return once we found the first layout.
if (typeof modules.layout !== 'undefined') {
return rootParams;
}
// This didn't include a root layout, so we need to continue. We don't need
// to collect from other parallel routes because we can't have a parallel
// route above a root layout.
current = parallelRoutes.children;
}
// If we didn't find a root layout, then we don't have any params.
return [];
}
/**
* Collects the segments for a given route module.
*
* @param components the loaded components
* @returns the segments for the route module
*/ export function collectRootParamKeys(routeModule) {
if (isAppRouteRouteModule(routeModule)) {
return [];
}
if (isAppPageRouteModule(routeModule)) {
return collectAppPageRootParamKeys(routeModule);
}
throw Object.defineProperty(new InvariantError('Expected a route module to be one of app route or page'), "__NEXT_ERROR_CODE", {
value: "E568",
enumerable: false,
configurable: true
});
}
//# sourceMappingURL=collect-root-param-keys.js.map

View File

@@ -0,0 +1 @@
{"version":3,"sources":["../../../../../src/build/segment-config/app/collect-root-param-keys.ts"],"sourcesContent":["import { getSegmentParam } from '../../../shared/lib/router/utils/get-segment-param'\nimport type AppPageRouteModule from '../../../server/route-modules/app-page/module'\nimport {\n isAppPageRouteModule,\n isAppRouteRouteModule,\n} from '../../../server/route-modules/checks'\nimport type { RouteModule } from '../../../server/route-modules/route-module'\nimport { InvariantError } from '../../../shared/lib/invariant-error'\n\nfunction collectAppPageRootParamKeys(\n routeModule: AppPageRouteModule\n): readonly string[] {\n let rootParams: string[] = []\n\n let current = routeModule.userland.loaderTree\n while (current) {\n const [name, parallelRoutes, modules] = current\n\n // If this is a dynamic segment, then we collect the param.\n const paramName = getSegmentParam(name)?.paramName\n if (paramName) {\n rootParams.push(paramName)\n }\n\n // If this has a layout module, then we've found the root layout because\n // we return once we found the first layout.\n if (typeof modules.layout !== 'undefined') {\n return rootParams\n }\n\n // This didn't include a root layout, so we need to continue. We don't need\n // to collect from other parallel routes because we can't have a parallel\n // route above a root layout.\n current = parallelRoutes.children\n }\n\n // If we didn't find a root layout, then we don't have any params.\n return []\n}\n\n/**\n * Collects the segments for a given route module.\n *\n * @param components the loaded components\n * @returns the segments for the route module\n */\nexport function collectRootParamKeys(\n routeModule: RouteModule\n): readonly string[] {\n if (isAppRouteRouteModule(routeModule)) {\n return []\n }\n\n if (isAppPageRouteModule(routeModule)) {\n return collectAppPageRootParamKeys(routeModule)\n }\n\n throw new InvariantError(\n 'Expected a route module to be one of app route or page'\n )\n}\n"],"names":["getSegmentParam","isAppPageRouteModule","isAppRouteRouteModule","InvariantError","collectAppPageRootParamKeys","routeModule","rootParams","current","userland","loaderTree","name","parallelRoutes","modules","paramName","push","layout","children","collectRootParamKeys"],"mappings":"AAAA,SAASA,eAAe,QAAQ,qDAAoD;AAEpF,SACEC,oBAAoB,EACpBC,qBAAqB,QAChB,uCAAsC;AAE7C,SAASC,cAAc,QAAQ,sCAAqC;AAEpE,SAASC,4BACPC,WAA+B;IAE/B,IAAIC,aAAuB,EAAE;IAE7B,IAAIC,UAAUF,YAAYG,QAAQ,CAACC,UAAU;IAC7C,MAAOF,QAAS;YAIIP;QAHlB,MAAM,CAACU,MAAMC,gBAAgBC,QAAQ,GAAGL;QAExC,2DAA2D;QAC3D,MAAMM,aAAYb,mBAAAA,gBAAgBU,0BAAhBV,iBAAuBa,SAAS;QAClD,IAAIA,WAAW;YACbP,WAAWQ,IAAI,CAACD;QAClB;QAEA,wEAAwE;QACxE,4CAA4C;QAC5C,IAAI,OAAOD,QAAQG,MAAM,KAAK,aAAa;YACzC,OAAOT;QACT;QAEA,2EAA2E;QAC3E,yEAAyE;QACzE,6BAA6B;QAC7BC,UAAUI,eAAeK,QAAQ;IACnC;IAEA,kEAAkE;IAClE,OAAO,EAAE;AACX;AAEA;;;;;CAKC,GACD,OAAO,SAASC,qBACdZ,WAAwB;IAExB,IAAIH,sBAAsBG,cAAc;QACtC,OAAO,EAAE;IACX;IAEA,IAAIJ,qBAAqBI,cAAc;QACrC,OAAOD,4BAA4BC;IACrC;IAEA,MAAM,qBAEL,CAFK,IAAIF,eACR,2DADI,qBAAA;eAAA;oBAAA;sBAAA;IAEN;AACF","ignoreList":[0]}

View File

@@ -0,0 +1,98 @@
import picomatch from 'next/dist/compiled/picomatch';
import { z } from 'next/dist/compiled/zod';
import { tryToParsePath } from '../../../lib/try-to-parse-path';
const RouteHasSchema = z.discriminatedUnion('type', [
z.object({
type: z.enum([
'header',
'query',
'cookie'
]),
key: z.string({
required_error: 'key is required when type is header, query or cookie'
}),
value: z.string({
invalid_type_error: 'value must be a string'
}).optional()
}).strict(),
z.object({
type: z.literal('host'),
value: z.string({
required_error: 'host must have a value'
})
}).strict()
]);
/**
* @internal - required to exclude zod types from the build
*/ export const SourceSchema = z.string({
required_error: 'source is required'
}).max(4096, 'exceeds max built length of 4096 for route').superRefine((val, ctx)=>{
if (!val.startsWith('/')) {
return ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `source must start with /`
});
}
const { error, regexStr } = tryToParsePath(val);
if (error || !regexStr) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `Invalid source '${val}': ${error.message}`
});
}
});
const MiddlewareMatcherInputSchema = z.object({
locale: z.union([
z.literal(false),
z.undefined()
]).optional(),
has: z.array(RouteHasSchema).optional(),
missing: z.array(RouteHasSchema).optional(),
source: SourceSchema
}).strict();
const MiddlewareConfigMatcherInputSchema = z.union([
SourceSchema,
z.array(z.union([
SourceSchema,
MiddlewareMatcherInputSchema
], {
invalid_type_error: 'must be an array of strings or middleware matchers'
}))
]);
const GlobSchema = z.string().superRefine((val, ctx)=>{
try {
picomatch(val);
} catch (err) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: `Invalid glob pattern '${val}': ${err.message}`
});
}
});
/**
* @internal - required to exclude zod types from the build
*/ export const MiddlewareConfigInputSchema = z.object({
/**
* The matcher for the middleware.
*/ matcher: MiddlewareConfigMatcherInputSchema.optional(),
/**
* The regions that the middleware should run in.
*/ regions: z.union([
z.string(),
z.array(z.string())
]).optional(),
/**
* A glob, or an array of globs, ignoring dynamic code evaluation for specific
* files. The globs are relative to your application root folder.
*/ unstable_allowDynamic: z.union([
GlobSchema,
z.array(GlobSchema)
]).optional()
});
/**
* The keys of the configuration for a middleware.
*
* @internal - required to exclude zod types from the build
*/ export const MiddlewareConfigInputSchemaKeys = MiddlewareConfigInputSchema.keyof().options;
//# sourceMappingURL=middleware-config.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,49 @@
import { z } from 'next/dist/compiled/zod';
import { formatZodError } from '../../../shared/lib/zod';
/**
* The schema for the page segment config.
*/ const PagesSegmentConfigSchema = z.object({
/**
* The runtime to use for the page.
*/ runtime: z.enum([
'edge',
'experimental-edge',
'nodejs'
]).optional(),
/**
* The maximum duration for the page render.
*/ maxDuration: z.number().optional(),
/**
* The exported config object for the page.
*/ config: z.object({
/**
* The runtime to use for the page.
*/ runtime: z.enum([
'edge',
'experimental-edge',
'nodejs'
]).optional(),
/**
* The maximum duration for the page render.
*/ maxDuration: z.number().optional()
}).optional()
});
/**
* Parse the page segment config.
* @param data - The data to parse.
* @param route - The route of the page.
* @returns The parsed page segment config.
*/ export function parsePagesSegmentConfig(data, route) {
const parsed = PagesSegmentConfigSchema.safeParse(data, {});
if (!parsed.success) {
throw formatZodError(`Invalid segment configuration options detected for "${route}". Read more at https://nextjs.org/docs/messages/invalid-page-config`, parsed.error);
}
return parsed.data;
}
/**
* The keys of the configuration for a page.
*
* @internal - required to exclude zod types from the build
*/ export const PagesSegmentConfigSchemaKeys = PagesSegmentConfigSchema.keyof().options;
//# sourceMappingURL=pages-segment-config.js.map

View File

@@ -0,0 +1 @@
{"version":3,"sources":["../../../../../src/build/segment-config/pages/pages-segment-config.ts"],"sourcesContent":["import { z } from 'next/dist/compiled/zod'\nimport { formatZodError } from '../../../shared/lib/zod'\n\n/**\n * The schema for the page segment config.\n */\nconst PagesSegmentConfigSchema = z.object({\n /**\n * The runtime to use for the page.\n */\n runtime: z.enum(['edge', 'experimental-edge', 'nodejs']).optional(),\n\n /**\n * The maximum duration for the page render.\n */\n maxDuration: z.number().optional(),\n\n /**\n * The exported config object for the page.\n */\n config: z\n .object({\n /**\n * The runtime to use for the page.\n */\n runtime: z.enum(['edge', 'experimental-edge', 'nodejs']).optional(),\n\n /**\n * The maximum duration for the page render.\n */\n maxDuration: z.number().optional(),\n })\n .optional(),\n})\n\n/**\n * Parse the page segment config.\n * @param data - The data to parse.\n * @param route - The route of the page.\n * @returns The parsed page segment config.\n */\nexport function parsePagesSegmentConfig(\n data: unknown,\n route: string\n): PagesSegmentConfig {\n const parsed = PagesSegmentConfigSchema.safeParse(data, {})\n if (!parsed.success) {\n throw formatZodError(\n `Invalid segment configuration options detected for \"${route}\". Read more at https://nextjs.org/docs/messages/invalid-page-config`,\n parsed.error\n )\n }\n\n return parsed.data\n}\n\n/**\n * The keys of the configuration for a page.\n *\n * @internal - required to exclude zod types from the build\n */\nexport const PagesSegmentConfigSchemaKeys =\n PagesSegmentConfigSchema.keyof().options\n\nexport type PagesSegmentConfigConfig = {\n /**\n * The maximum duration for the page render.\n */\n maxDuration?: number\n\n /**\n * The runtime to use for the page.\n */\n runtime?: 'edge' | 'experimental-edge' | 'nodejs'\n\n /**\n * The preferred region for the page.\n */\n regions?: string[]\n}\n\nexport type PagesSegmentConfig = {\n /**\n * The runtime to use for the page.\n */\n runtime?: 'edge' | 'experimental-edge' | 'nodejs'\n\n /**\n * The maximum duration for the page render.\n */\n maxDuration?: number\n\n /**\n * The exported config object for the page.\n */\n config?: PagesSegmentConfigConfig\n}\n"],"names":["z","formatZodError","PagesSegmentConfigSchema","object","runtime","enum","optional","maxDuration","number","config","parsePagesSegmentConfig","data","route","parsed","safeParse","success","error","PagesSegmentConfigSchemaKeys","keyof","options"],"mappings":"AAAA,SAASA,CAAC,QAAQ,yBAAwB;AAC1C,SAASC,cAAc,QAAQ,0BAAyB;AAExD;;CAEC,GACD,MAAMC,2BAA2BF,EAAEG,MAAM,CAAC;IACxC;;GAEC,GACDC,SAASJ,EAAEK,IAAI,CAAC;QAAC;QAAQ;QAAqB;KAAS,EAAEC,QAAQ;IAEjE;;GAEC,GACDC,aAAaP,EAAEQ,MAAM,GAAGF,QAAQ;IAEhC;;GAEC,GACDG,QAAQT,EACLG,MAAM,CAAC;QACN;;OAEC,GACDC,SAASJ,EAAEK,IAAI,CAAC;YAAC;YAAQ;YAAqB;SAAS,EAAEC,QAAQ;QAEjE;;OAEC,GACDC,aAAaP,EAAEQ,MAAM,GAAGF,QAAQ;IAClC,GACCA,QAAQ;AACb;AAEA;;;;;CAKC,GACD,OAAO,SAASI,wBACdC,IAAa,EACbC,KAAa;IAEb,MAAMC,SAASX,yBAAyBY,SAAS,CAACH,MAAM,CAAC;IACzD,IAAI,CAACE,OAAOE,OAAO,EAAE;QACnB,MAAMd,eACJ,CAAC,oDAAoD,EAAEW,MAAM,oEAAoE,CAAC,EAClIC,OAAOG,KAAK;IAEhB;IAEA,OAAOH,OAAOF,IAAI;AACpB;AAEA;;;;CAIC,GACD,OAAO,MAAMM,+BACXf,yBAAyBgB,KAAK,GAAGC,OAAO,CAAA","ignoreList":[0]}