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,62 @@
const timeUnits = [
{
label: 'y',
seconds: 31536000
},
{
label: 'w',
seconds: 604800
},
{
label: 'd',
seconds: 86400
},
{
label: 'h',
seconds: 3600
},
{
label: 'm',
seconds: 60
},
{
label: 's',
seconds: 1
}
];
function humanReadableTimeRounded(seconds) {
// Find the largest fitting unit.
let candidateIndex = timeUnits.length - 1;
for(let i = 0; i < timeUnits.length; i++){
if (seconds >= timeUnits[i].seconds) {
candidateIndex = i;
break;
}
}
const candidate = timeUnits[candidateIndex];
const value = seconds / candidate.seconds;
const isExact = Number.isInteger(value);
// For days and weeks only, check if using the next smaller unit yields an
// exact result.
if (!isExact && (candidate.label === 'd' || candidate.label === 'w')) {
const nextUnit = timeUnits[candidateIndex + 1];
const nextValue = seconds / nextUnit.seconds;
if (Number.isInteger(nextValue)) {
return `${nextValue}${nextUnit.label}`;
}
}
if (isExact) {
return `${value}${candidate.label}`;
}
return `${Math.round(value)}${candidate.label}`;
}
export function formatRevalidate(cacheControl) {
const { revalidate } = cacheControl;
return revalidate ? humanReadableTimeRounded(revalidate) : '';
}
export function formatExpire(cacheControl) {
const { expire } = cacheControl;
return expire ? humanReadableTimeRounded(expire) : '';
}
//# sourceMappingURL=format.js.map

View File

@@ -0,0 +1 @@
{"version":3,"sources":["../../../../src/build/output/format.ts"],"sourcesContent":["import type { CacheControl } from '../../server/lib/cache-control'\n\nconst timeUnits = [\n { label: 'y', seconds: 31536000 },\n { label: 'w', seconds: 604800 },\n { label: 'd', seconds: 86400 },\n { label: 'h', seconds: 3600 },\n { label: 'm', seconds: 60 },\n { label: 's', seconds: 1 },\n]\n\nfunction humanReadableTimeRounded(seconds: number): string {\n // Find the largest fitting unit.\n let candidateIndex = timeUnits.length - 1\n for (let i = 0; i < timeUnits.length; i++) {\n if (seconds >= timeUnits[i].seconds) {\n candidateIndex = i\n break\n }\n }\n\n const candidate = timeUnits[candidateIndex]\n const value = seconds / candidate.seconds\n const isExact = Number.isInteger(value)\n\n // For days and weeks only, check if using the next smaller unit yields an\n // exact result.\n if (!isExact && (candidate.label === 'd' || candidate.label === 'w')) {\n const nextUnit = timeUnits[candidateIndex + 1]\n const nextValue = seconds / nextUnit.seconds\n\n if (Number.isInteger(nextValue)) {\n return `${nextValue}${nextUnit.label}`\n }\n }\n\n if (isExact) {\n return `${value}${candidate.label}`\n }\n\n return `≈${Math.round(value)}${candidate.label}`\n}\n\nexport function formatRevalidate(cacheControl: CacheControl): string {\n const { revalidate } = cacheControl\n\n return revalidate ? humanReadableTimeRounded(revalidate) : ''\n}\n\nexport function formatExpire(cacheControl: CacheControl): string {\n const { expire } = cacheControl\n\n return expire ? humanReadableTimeRounded(expire) : ''\n}\n"],"names":["timeUnits","label","seconds","humanReadableTimeRounded","candidateIndex","length","i","candidate","value","isExact","Number","isInteger","nextUnit","nextValue","Math","round","formatRevalidate","cacheControl","revalidate","formatExpire","expire"],"mappings":"AAEA,MAAMA,YAAY;IAChB;QAAEC,OAAO;QAAKC,SAAS;IAAS;IAChC;QAAED,OAAO;QAAKC,SAAS;IAAO;IAC9B;QAAED,OAAO;QAAKC,SAAS;IAAM;IAC7B;QAAED,OAAO;QAAKC,SAAS;IAAK;IAC5B;QAAED,OAAO;QAAKC,SAAS;IAAG;IAC1B;QAAED,OAAO;QAAKC,SAAS;IAAE;CAC1B;AAED,SAASC,yBAAyBD,OAAe;IAC/C,iCAAiC;IACjC,IAAIE,iBAAiBJ,UAAUK,MAAM,GAAG;IACxC,IAAK,IAAIC,IAAI,GAAGA,IAAIN,UAAUK,MAAM,EAAEC,IAAK;QACzC,IAAIJ,WAAWF,SAAS,CAACM,EAAE,CAACJ,OAAO,EAAE;YACnCE,iBAAiBE;YACjB;QACF;IACF;IAEA,MAAMC,YAAYP,SAAS,CAACI,eAAe;IAC3C,MAAMI,QAAQN,UAAUK,UAAUL,OAAO;IACzC,MAAMO,UAAUC,OAAOC,SAAS,CAACH;IAEjC,0EAA0E;IAC1E,gBAAgB;IAChB,IAAI,CAACC,WAAYF,CAAAA,UAAUN,KAAK,KAAK,OAAOM,UAAUN,KAAK,KAAK,GAAE,GAAI;QACpE,MAAMW,WAAWZ,SAAS,CAACI,iBAAiB,EAAE;QAC9C,MAAMS,YAAYX,UAAUU,SAASV,OAAO;QAE5C,IAAIQ,OAAOC,SAAS,CAACE,YAAY;YAC/B,OAAO,GAAGA,YAAYD,SAASX,KAAK,EAAE;QACxC;IACF;IAEA,IAAIQ,SAAS;QACX,OAAO,GAAGD,QAAQD,UAAUN,KAAK,EAAE;IACrC;IAEA,OAAO,CAAC,CAAC,EAAEa,KAAKC,KAAK,CAACP,SAASD,UAAUN,KAAK,EAAE;AAClD;AAEA,OAAO,SAASe,iBAAiBC,YAA0B;IACzD,MAAM,EAAEC,UAAU,EAAE,GAAGD;IAEvB,OAAOC,aAAaf,yBAAyBe,cAAc;AAC7D;AAEA,OAAO,SAASC,aAAaF,YAA0B;IACrD,MAAM,EAAEG,MAAM,EAAE,GAAGH;IAEnB,OAAOG,SAASjB,yBAAyBiB,UAAU;AACrD","ignoreList":[0]}

View File

@@ -0,0 +1,159 @@
import createStore from 'next/dist/compiled/unistore';
import formatWebpackMessages from '../../shared/lib/format-webpack-messages';
import { store as consoleStore } from './store';
import { COMPILER_NAMES } from '../../shared/lib/constants';
const buildStore = createStore({
// @ts-expect-error initial value
client: {},
// @ts-expect-error initial value
server: {},
// @ts-expect-error initial value
edgeServer: {}
});
let buildWasDone = false;
let clientWasLoading = true;
let serverWasLoading = true;
let edgeServerWasLoading = false;
buildStore.subscribe((state)=>{
const { client, server, edgeServer, trigger, url } = state;
const { appUrl } = consoleStore.getState();
if (client.loading || server.loading || (edgeServer == null ? void 0 : edgeServer.loading)) {
consoleStore.setState({
bootstrap: false,
appUrl: appUrl,
// If it takes more than 3 seconds to compile, mark it as loading status
loading: true,
trigger,
url
}, true);
clientWasLoading = !buildWasDone && clientWasLoading || client.loading;
serverWasLoading = !buildWasDone && serverWasLoading || server.loading;
edgeServerWasLoading = !buildWasDone && edgeServerWasLoading || edgeServer.loading;
buildWasDone = false;
return;
}
buildWasDone = true;
let partialState = {
bootstrap: false,
appUrl: appUrl,
loading: false,
typeChecking: false,
totalModulesCount: (clientWasLoading ? client.totalModulesCount : 0) + (serverWasLoading ? server.totalModulesCount : 0) + (edgeServerWasLoading ? (edgeServer == null ? void 0 : edgeServer.totalModulesCount) || 0 : 0),
hasEdgeServer: !!edgeServer
};
if (client.errors && clientWasLoading) {
// Show only client errors
consoleStore.setState({
...partialState,
errors: client.errors,
warnings: null
}, true);
} else if (server.errors && serverWasLoading) {
consoleStore.setState({
...partialState,
errors: server.errors,
warnings: null
}, true);
} else if (edgeServer.errors && edgeServerWasLoading) {
consoleStore.setState({
...partialState,
errors: edgeServer.errors,
warnings: null
}, true);
} else {
// Show warnings from all of them
const warnings = [
...client.warnings || [],
...server.warnings || [],
...edgeServer.warnings || []
];
consoleStore.setState({
...partialState,
errors: null,
warnings: warnings.length === 0 ? null : warnings
}, true);
}
});
export function watchCompilers(client, server, edgeServer) {
buildStore.setState({
client: {
loading: true
},
server: {
loading: true
},
edgeServer: {
loading: true
},
trigger: 'initial',
url: undefined
});
function tapCompiler(key, compiler, onEvent) {
compiler.hooks.invalid.tap(`NextJsInvalid-${key}`, ()=>{
onEvent({
loading: true
});
});
compiler.hooks.done.tap(`NextJsDone-${key}`, (stats)=>{
const { errors, warnings } = formatWebpackMessages(stats.toJson({
preset: 'errors-warnings',
moduleTrace: true
}));
const hasErrors = !!(errors == null ? void 0 : errors.length);
const hasWarnings = !!(warnings == null ? void 0 : warnings.length);
onEvent({
loading: false,
totalModulesCount: stats.compilation.modules.size,
errors: hasErrors ? errors : null,
warnings: hasWarnings ? warnings : null
});
});
}
tapCompiler(COMPILER_NAMES.client, client, (status)=>{
if (!status.loading && !buildStore.getState().server.loading && !buildStore.getState().edgeServer.loading && status.totalModulesCount > 0) {
buildStore.setState({
client: status,
trigger: undefined,
url: undefined
});
} else {
buildStore.setState({
client: status
});
}
});
tapCompiler(COMPILER_NAMES.server, server, (status)=>{
if (!status.loading && !buildStore.getState().client.loading && !buildStore.getState().edgeServer.loading && status.totalModulesCount > 0) {
buildStore.setState({
server: status,
trigger: undefined,
url: undefined
});
} else {
buildStore.setState({
server: status
});
}
});
tapCompiler(COMPILER_NAMES.edgeServer, edgeServer, (status)=>{
if (!status.loading && !buildStore.getState().client.loading && !buildStore.getState().server.loading && status.totalModulesCount > 0) {
buildStore.setState({
edgeServer: status,
trigger: undefined,
url: undefined
});
} else {
buildStore.setState({
edgeServer: status
});
}
});
}
export function reportTrigger(trigger, url) {
buildStore.setState({
trigger,
url
});
}
//# sourceMappingURL=index.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,77 @@
import { bold, green, magenta, red, yellow, white } from '../../lib/picocolors';
import { LRUCache } from '../../server/lib/lru-cache';
export const prefixes = {
wait: white(bold('○')),
error: red(bold('')),
warn: yellow(bold('⚠')),
ready: '▲',
info: white(bold(' ')),
event: green(bold('✓')),
trace: magenta(bold('»'))
};
const LOGGING_METHOD = {
log: 'log',
warn: 'warn',
error: 'error'
};
function prefixedLog(prefixType, ...message) {
if ((message[0] === '' || message[0] === undefined) && message.length === 1) {
message.shift();
}
const consoleMethod = prefixType in LOGGING_METHOD ? LOGGING_METHOD[prefixType] : 'log';
const prefix = prefixes[prefixType];
// If there's no message, don't print the prefix but a new line
if (message.length === 0) {
console[consoleMethod]('');
} else {
// Ensure if there's ANSI escape codes it's concatenated into one string.
// Chrome DevTool can only handle color if it's in one string.
if (message.length === 1 && typeof message[0] === 'string') {
console[consoleMethod](prefix + ' ' + message[0]);
} else {
console[consoleMethod](prefix, ...message);
}
}
}
export function bootstrap(message) {
console.log(message);
}
export function wait(...message) {
prefixedLog('wait', ...message);
}
export function error(...message) {
prefixedLog('error', ...message);
}
export function warn(...message) {
prefixedLog('warn', ...message);
}
export function ready(...message) {
prefixedLog('ready', ...message);
}
export function info(...message) {
prefixedLog('info', ...message);
}
export function event(...message) {
prefixedLog('event', ...message);
}
export function trace(...message) {
prefixedLog('trace', ...message);
}
const warnOnceCache = new LRUCache(10000, (value)=>value.length);
export function warnOnce(...message) {
const key = message.join(' ');
if (!warnOnceCache.has(key)) {
warnOnceCache.set(key, key);
warn(...message);
}
}
const errorOnceCache = new LRUCache(10000, (value)=>value.length);
export function errorOnce(...message) {
const key = message.join(' ');
if (!errorOnceCache.has(key)) {
errorOnceCache.set(key, key);
error(...message);
}
}
//# sourceMappingURL=log.js.map

View File

@@ -0,0 +1 @@
{"version":3,"sources":["../../../../src/build/output/log.ts"],"sourcesContent":["import { bold, green, magenta, red, yellow, white } from '../../lib/picocolors'\nimport { LRUCache } from '../../server/lib/lru-cache'\n\nexport const prefixes = {\n wait: white(bold('○')),\n error: red(bold('')),\n warn: yellow(bold('⚠')),\n ready: '▲', // no color\n info: white(bold(' ')),\n event: green(bold('✓')),\n trace: magenta(bold('»')),\n} as const\n\nconst LOGGING_METHOD = {\n log: 'log',\n warn: 'warn',\n error: 'error',\n} as const\n\nfunction prefixedLog(prefixType: keyof typeof prefixes, ...message: any[]) {\n if ((message[0] === '' || message[0] === undefined) && message.length === 1) {\n message.shift()\n }\n\n const consoleMethod: keyof typeof LOGGING_METHOD =\n prefixType in LOGGING_METHOD\n ? LOGGING_METHOD[prefixType as keyof typeof LOGGING_METHOD]\n : 'log'\n\n const prefix = prefixes[prefixType]\n // If there's no message, don't print the prefix but a new line\n if (message.length === 0) {\n console[consoleMethod]('')\n } else {\n // Ensure if there's ANSI escape codes it's concatenated into one string.\n // Chrome DevTool can only handle color if it's in one string.\n if (message.length === 1 && typeof message[0] === 'string') {\n console[consoleMethod](prefix + ' ' + message[0])\n } else {\n console[consoleMethod](prefix, ...message)\n }\n }\n}\n\nexport function bootstrap(message: string) {\n console.log(message)\n}\n\nexport function wait(...message: any[]) {\n prefixedLog('wait', ...message)\n}\n\nexport function error(...message: any[]) {\n prefixedLog('error', ...message)\n}\n\nexport function warn(...message: any[]) {\n prefixedLog('warn', ...message)\n}\n\nexport function ready(...message: any[]) {\n prefixedLog('ready', ...message)\n}\n\nexport function info(...message: any[]) {\n prefixedLog('info', ...message)\n}\n\nexport function event(...message: any[]) {\n prefixedLog('event', ...message)\n}\n\nexport function trace(...message: any[]) {\n prefixedLog('trace', ...message)\n}\n\nconst warnOnceCache = new LRUCache<string>(10_000, (value) => value.length)\nexport function warnOnce(...message: any[]) {\n const key = message.join(' ')\n if (!warnOnceCache.has(key)) {\n warnOnceCache.set(key, key)\n warn(...message)\n }\n}\n\nconst errorOnceCache = new LRUCache<string>(10_000, (value) => value.length)\nexport function errorOnce(...message: any[]) {\n const key = message.join(' ')\n if (!errorOnceCache.has(key)) {\n errorOnceCache.set(key, key)\n error(...message)\n }\n}\n"],"names":["bold","green","magenta","red","yellow","white","LRUCache","prefixes","wait","error","warn","ready","info","event","trace","LOGGING_METHOD","log","prefixedLog","prefixType","message","undefined","length","shift","consoleMethod","prefix","console","bootstrap","warnOnceCache","value","warnOnce","key","join","has","set","errorOnceCache","errorOnce"],"mappings":"AAAA,SAASA,IAAI,EAAEC,KAAK,EAAEC,OAAO,EAAEC,GAAG,EAAEC,MAAM,EAAEC,KAAK,QAAQ,uBAAsB;AAC/E,SAASC,QAAQ,QAAQ,6BAA4B;AAErD,OAAO,MAAMC,WAAW;IACtBC,MAAMH,MAAML,KAAK;IACjBS,OAAON,IAAIH,KAAK;IAChBU,MAAMN,OAAOJ,KAAK;IAClBW,OAAO;IACPC,MAAMP,MAAML,KAAK;IACjBa,OAAOZ,MAAMD,KAAK;IAClBc,OAAOZ,QAAQF,KAAK;AACtB,EAAU;AAEV,MAAMe,iBAAiB;IACrBC,KAAK;IACLN,MAAM;IACND,OAAO;AACT;AAEA,SAASQ,YAAYC,UAAiC,EAAE,GAAGC,OAAc;IACvE,IAAI,AAACA,CAAAA,OAAO,CAAC,EAAE,KAAK,MAAMA,OAAO,CAAC,EAAE,KAAKC,SAAQ,KAAMD,QAAQE,MAAM,KAAK,GAAG;QAC3EF,QAAQG,KAAK;IACf;IAEA,MAAMC,gBACJL,cAAcH,iBACVA,cAAc,CAACG,WAA0C,GACzD;IAEN,MAAMM,SAASjB,QAAQ,CAACW,WAAW;IACnC,+DAA+D;IAC/D,IAAIC,QAAQE,MAAM,KAAK,GAAG;QACxBI,OAAO,CAACF,cAAc,CAAC;IACzB,OAAO;QACL,yEAAyE;QACzE,8DAA8D;QAC9D,IAAIJ,QAAQE,MAAM,KAAK,KAAK,OAAOF,OAAO,CAAC,EAAE,KAAK,UAAU;YAC1DM,OAAO,CAACF,cAAc,CAACC,SAAS,MAAML,OAAO,CAAC,EAAE;QAClD,OAAO;YACLM,OAAO,CAACF,cAAc,CAACC,WAAWL;QACpC;IACF;AACF;AAEA,OAAO,SAASO,UAAUP,OAAe;IACvCM,QAAQT,GAAG,CAACG;AACd;AAEA,OAAO,SAASX,KAAK,GAAGW,OAAc;IACpCF,YAAY,WAAWE;AACzB;AAEA,OAAO,SAASV,MAAM,GAAGU,OAAc;IACrCF,YAAY,YAAYE;AAC1B;AAEA,OAAO,SAAST,KAAK,GAAGS,OAAc;IACpCF,YAAY,WAAWE;AACzB;AAEA,OAAO,SAASR,MAAM,GAAGQ,OAAc;IACrCF,YAAY,YAAYE;AAC1B;AAEA,OAAO,SAASP,KAAK,GAAGO,OAAc;IACpCF,YAAY,WAAWE;AACzB;AAEA,OAAO,SAASN,MAAM,GAAGM,OAAc;IACrCF,YAAY,YAAYE;AAC1B;AAEA,OAAO,SAASL,MAAM,GAAGK,OAAc;IACrCF,YAAY,YAAYE;AAC1B;AAEA,MAAMQ,gBAAgB,IAAIrB,SAAiB,OAAQ,CAACsB,QAAUA,MAAMP,MAAM;AAC1E,OAAO,SAASQ,SAAS,GAAGV,OAAc;IACxC,MAAMW,MAAMX,QAAQY,IAAI,CAAC;IACzB,IAAI,CAACJ,cAAcK,GAAG,CAACF,MAAM;QAC3BH,cAAcM,GAAG,CAACH,KAAKA;QACvBpB,QAAQS;IACV;AACF;AAEA,MAAMe,iBAAiB,IAAI5B,SAAiB,OAAQ,CAACsB,QAAUA,MAAMP,MAAM;AAC3E,OAAO,SAASc,UAAU,GAAGhB,OAAc;IACzC,MAAMW,MAAMX,QAAQY,IAAI,CAAC;IACzB,IAAI,CAACG,eAAeF,GAAG,CAACF,MAAM;QAC5BI,eAAeD,GAAG,CAACH,KAAKA;QACxBrB,SAASU;IACX;AACF","ignoreList":[0]}

View File

@@ -0,0 +1,136 @@
import createStore from 'next/dist/compiled/unistore';
import { flushAllTraces, trace } from '../../trace';
import { teardownTraceSubscriber } from '../swc';
import * as Log from './log';
const MAX_LOG_SKIP_DURATION_MS = 3000;
export function formatTrigger(trigger) {
// Format dynamic sitemap routes to simpler file path
// e.g., /sitemap.xml[] -> /sitemap.xml
if (trigger.includes('[__metadata_id__]')) {
trigger = trigger.replace('/[__metadata_id__]', '/[id]');
}
if (trigger.length > 1 && trigger.endsWith('/')) {
trigger = trigger.slice(0, -1);
}
return trigger;
}
export const store = createStore({
appUrl: null,
bindAddr: null,
bootstrap: true,
logging: true
});
let lastStore = {
appUrl: null,
bindAddr: null,
bootstrap: true,
logging: true
};
function hasStoreChanged(nextStore) {
if ([
...new Set([
...Object.keys(lastStore),
...Object.keys(nextStore)
])
].every((key)=>Object.is(lastStore[key], nextStore[key]))) {
return false;
}
lastStore = nextStore;
return true;
}
let startTime = 0;
let trigger = '' // default, use empty string for trigger
;
let triggerUrl = undefined;
let loadingLogTimer = null;
let traceSpan = null;
let logging = true;
store.subscribe((state)=>{
// Update persisted logging state
if ('logging' in state) {
logging = state.logging;
}
// If logging is disabled, do not log
if (!logging) {
return;
}
if (!hasStoreChanged(state)) {
return;
}
if (state.bootstrap) {
return;
}
if (state.loading) {
if (state.trigger) {
trigger = formatTrigger(state.trigger);
triggerUrl = state.url;
if (trigger !== 'initial') {
traceSpan = trace('compile-path', undefined, {
trigger: trigger
});
if (!loadingLogTimer) {
// Only log compiling if compiled is not finished quickly
loadingLogTimer = setTimeout(()=>{
if (triggerUrl && triggerUrl !== trigger && process.env.NEXT_TRIGGER_URL) {
Log.wait(`Compiling ${trigger} (${triggerUrl}) ...`);
} else {
Log.wait(`Compiling ${trigger} ...`);
}
}, MAX_LOG_SKIP_DURATION_MS);
}
}
}
if (startTime === 0) {
startTime = Date.now();
}
return;
}
if (state.errors) {
// Log compilation errors
Log.error(state.errors[0]);
startTime = 0;
// Ensure traces are flushed after each compile in development mode
flushAllTraces();
teardownTraceSubscriber();
return;
}
let timeMessage = '';
if (startTime) {
const time = Date.now() - startTime;
startTime = 0;
timeMessage = ' ' + (time > 2000 ? `in ${Math.round(time / 100) / 10}s` : `in ${time}ms`);
}
let modulesMessage = '';
if (state.totalModulesCount) {
modulesMessage = ` (${state.totalModulesCount} modules)`;
}
if (state.warnings) {
Log.warn(state.warnings.join('\n\n'));
// Ensure traces are flushed after each compile in development mode
flushAllTraces();
teardownTraceSubscriber();
return;
}
if (state.typeChecking) {
Log.info(`bundled ${trigger}${timeMessage}${modulesMessage}, type checking...`);
return;
}
if (trigger === 'initial') {
trigger = '';
} else {
if (loadingLogTimer) {
clearTimeout(loadingLogTimer);
loadingLogTimer = null;
}
if (traceSpan) {
traceSpan.stop();
traceSpan = null;
}
trigger = '';
}
// Ensure traces are flushed after each compile in development mode
flushAllTraces();
teardownTraceSubscriber();
});
//# sourceMappingURL=store.js.map

File diff suppressed because one or more lines are too long