mixc
This commit is contained in:
1 parent
00edb9dfc2
commit
3865c45642
18 files changed
+439
-87
No files matched your search
@@ -54,6 +54,35 @@ npm run check # Astro + TypeScript check
|
|||||||
- **Matomo.** The privacy page links to the existing opt-out on `cipher.uber.space`. The tracking
|
- **Matomo.** The privacy page links to the existing opt-out on `cipher.uber.space`. The tracking
|
||||||
snippet itself is not included; add it in `src/layouts/Base.astro` if analytics should continue.
|
snippet itself is not included; add it in `src/layouts/Base.astro` if analytics should continue.
|
||||||
|
|
||||||
|
## Insights filter
|
||||||
|
|
||||||
|
The Insights index can be filtered by type: Essays (posts), LinkedIn (mentions with
|
||||||
|
`platform: 'LinkedIn'`) and Other (mentions from any other platform). A filter only appears once it
|
||||||
|
has an entry, and the bar only when there are at least two types. The choice is kept in
|
||||||
|
`?type=essay|linkedin|other`, so a filtered view can be linked. The types are in `src/data/insights.ts`.
|
||||||
|
|
||||||
|
## Adding a mention
|
||||||
|
|
||||||
|
A mention is a short pointer to a post elsewhere (LinkedIn) that CipherCell took part in. It appears
|
||||||
|
in the Insights list and on the home page, sorted by date with the essays, and has a short page at
|
||||||
|
`/insights/<slug>`: headline, summary, a link out, and the post embedded on request. The embed is
|
||||||
|
two-click: nothing loads from LinkedIn until the visitor presses the button (see the privacy page).
|
||||||
|
The embed URL is derived from a LinkedIn post link; set `embed` by hand only if that fails, and
|
||||||
|
`embedHeight` (pixels, default 720) if the post is cut off or leaves a gap. Text below the front
|
||||||
|
matter is shown as our own notes. A slug used by both an essay and a mention fails the build. Create `src/content/mentions/<slug>.md` with front matter only:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
title: 'Component security testing at the xG-ALOE Plugfest 2026.' # our headline, sentence case
|
||||||
|
summary: 'One or two sentences in our words on what the post says.'
|
||||||
|
url: 'https://www.linkedin.com/posts/...' # paste the share link; query and fragment are stripped
|
||||||
|
source: 'xG-ALOE' # who posted it
|
||||||
|
platform: 'LinkedIn' # optional, defaults to LinkedIn
|
||||||
|
date: 2026-09-17 # the post's publication date
|
||||||
|
lang: en # language of the post itself
|
||||||
|
```
|
||||||
|
|
||||||
|
Summarise only what the post says.
|
||||||
|
|
||||||
## Adding a post
|
## Adding a post
|
||||||
|
|
||||||
Create `src/content/insights/<slug>.md` with the front matter used in `xg-aloe-user-forum.md`.
|
Create `src/content/insights/<slug>.md` with the front matter used in `xg-aloe-user-forum.md`.
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
---
|
---
|
||||||
import { getCollection } from 'astro:content';
|
import { getFeed } from '../../data/insights';
|
||||||
import { Picture } from 'astro:assets';
|
import { Picture } from 'astro:assets';
|
||||||
import { localePath, t, type Lang } from '../../i18n/ui';
|
import { localePath, t, type Lang } from '../../i18n/ui';
|
||||||
interface Props { lang: Lang }
|
interface Props { lang: Lang }
|
||||||
const { lang } = Astro.props;
|
const { lang } = Astro.props;
|
||||||
const ui = t(lang);
|
const ui = t(lang);
|
||||||
const posts = (await getCollection('insights'))
|
const feed = (await getFeed()).slice(0, 2);
|
||||||
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf())
|
const m = ui.insights.mention;
|
||||||
.slice(0, 2);
|
|
||||||
---
|
---
|
||||||
<section class="section insights" aria-labelledby="insights-title">
|
<section class="section insights" aria-labelledby="insights-title">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
@@ -17,23 +16,31 @@ const posts = (await getCollection('insights'))
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ul class="insights__list" role="list">
|
<ul class="insights__list" role="list">
|
||||||
{posts.map((post) => (
|
{feed.map((item) => (
|
||||||
<li class="insights__item">
|
<li class="insights__item">
|
||||||
<a class="insights__card" href={localePath(lang, `/insights/${post.id}`)} lang={post.data.lang !== lang ? post.data.lang : undefined}>
|
{item.kind === 'post' ? (
|
||||||
<Picture
|
<a class="insights__card" href={localePath(lang, `/insights/${item.entry.id}`)} lang={item.entry.data.lang !== lang ? item.entry.data.lang : undefined}>
|
||||||
src={post.data.cover}
|
<Picture
|
||||||
alt={post.data.coverAlt}
|
src={item.entry.data.cover}
|
||||||
formats={['avif']}
|
alt={item.entry.data.coverAlt}
|
||||||
fallbackFormat="webp"
|
formats={['avif']}
|
||||||
widths={[400, 640, 960]}
|
fallbackFormat="webp"
|
||||||
sizes="(min-width: 64rem) 30vw, (min-width: 48rem) 45vw, 100vw"
|
widths={[400, 640, 960]}
|
||||||
loading="lazy"
|
sizes="(min-width: 64rem) 30vw, (min-width: 48rem) 45vw, 100vw"
|
||||||
decoding="async"
|
loading="lazy"
|
||||||
class="insights__img img-doc"
|
decoding="async"
|
||||||
/>
|
class="insights__img img-doc"
|
||||||
<p class="small muted">{post.data.category}</p>
|
/>
|
||||||
<h3 class="h3">{post.data.title}</h3>
|
<p class="small muted">{item.entry.data.category}</p>
|
||||||
</a>
|
<h3 class="h3">{item.entry.data.title}</h3>
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<a class="insights__card" href={localePath(lang, `/insights/${item.entry.id}`)} lang={item.entry.data.lang !== lang ? item.entry.data.lang : undefined}>
|
||||||
|
<p class="insights__panel" aria-hidden="true">{item.entry.data.source}</p>
|
||||||
|
<p class="small muted">{m.category(item.entry.data.platform)}</p>
|
||||||
|
<h3 class="h3">{item.entry.data.title}</h3>
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
<li class="insights__item">
|
<li class="insights__item">
|
||||||
@@ -70,7 +77,7 @@ const posts = (await getCollection('insights'))
|
|||||||
.insights__card:hover h3 { text-decoration: underline; text-decoration-thickness: var(--hairline); text-underline-offset: 0.12em; }
|
.insights__card:hover h3 { text-decoration: underline; text-decoration-thickness: var(--hairline); text-underline-offset: 0.12em; }
|
||||||
.insights__img, :global(.insights__img) { width: 100%; height: auto; aspect-ratio: 3 / 2; object-fit: cover; margin-bottom: var(--space-2); }
|
.insights__img, :global(.insights__img) { width: 100%; height: auto; aspect-ratio: 3 / 2; object-fit: cover; margin-bottom: var(--space-2); }
|
||||||
.insights__card h3 { max-width: 18em; }
|
.insights__card h3 { max-width: 18em; }
|
||||||
.insights__event-when {
|
.insights__event-when, .insights__panel {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
aspect-ratio: 3 / 2;
|
aspect-ratio: 3 / 2;
|
||||||
|
|||||||
@@ -34,28 +34,28 @@ const fieldStyle = (paper: (typeof papers)[number]) =>
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="research__controls" data-research-controls hidden>
|
<div class="research__controls" data-research-controls hidden>
|
||||||
<fieldset class="research__group">
|
<fieldset class="choice-group">
|
||||||
<legend class="research__legend">{r.sortLabel}</legend>
|
<legend class="choice-group__legend">{r.sortLabel}</legend>
|
||||||
<div class="research__options">
|
<div class="choices">
|
||||||
{(['cites', 'newest', 'oldest'] as const).map((key, i) => (
|
{(['cites', 'newest', 'oldest'] as const).map((key, i) => (
|
||||||
<label class="research__option">
|
<label class="choice">
|
||||||
<input type="radio" name="research-sort" value={key} checked={i === 0} />
|
<input type="radio" name="research-sort" value={key} checked={i === 0} />
|
||||||
<span>{r.sort[key]}</span>
|
<span>{r.sort[key]}</span>
|
||||||
</label>
|
</label>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset class="research__group">
|
<fieldset class="choice-group">
|
||||||
<legend class="research__legend">{r.filterLabel}</legend>
|
<legend class="choice-group__legend">{r.filterLabel}</legend>
|
||||||
<div class="research__options research__options--wrap">
|
<div class="choices">
|
||||||
<label class="research__option">
|
<label class="choice">
|
||||||
<input type="radio" name="research-category" value="" checked />
|
<input type="radio" name="research-category" value="" checked />
|
||||||
<span>{r.all} <span class="research__n">{papers.length}</span></span>
|
<span>{r.all} <span class="choice__n">{papers.length}</span></span>
|
||||||
</label>
|
</label>
|
||||||
{categories.map((c) => (
|
{categories.map((c) => (
|
||||||
<label class="research__option">
|
<label class="choice">
|
||||||
<input type="radio" name="research-category" value={c} />
|
<input type="radio" name="research-category" value={c} />
|
||||||
<span>{r.categories[c] ?? c} <span class="research__n">{countIn(c)}</span></span>
|
<span>{r.categories[c] ?? c} <span class="choice__n">{countIn(c)}</span></span>
|
||||||
</label>
|
</label>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -209,37 +209,12 @@ const fieldStyle = (paper: (typeof papers)[number]) =>
|
|||||||
letter-spacing: 0;
|
letter-spacing: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Controls: segmented rows in the menu bar's language, hairline cells and mono caps. */
|
/* Controls: segmented rows (.choice-group, base.css). */
|
||||||
.research__controls {
|
.research__controls {
|
||||||
margin-top: var(--space-7);
|
margin-top: var(--space-7);
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: var(--space-4);
|
gap: var(--space-4);
|
||||||
}
|
}
|
||||||
.research__group { border: 0; padding: 0; margin: 0; min-width: 0; display: grid; gap: var(--space-2); }
|
|
||||||
.research__legend { padding: 0; color: var(--ink-muted); font-size: var(--step-small); }
|
|
||||||
/* Each option draws its own frame and overlaps its neighbours by one line, so a wrapped row
|
|
||||||
never leaves a rule running past its last option. */
|
|
||||||
.research__options { display: flex; flex-wrap: wrap; padding: 0 var(--hairline) var(--hairline) 0; }
|
|
||||||
.research__option { position: relative; display: flex; margin: 0 calc(-1 * var(--hairline)) calc(-1 * var(--hairline)) 0; }
|
|
||||||
.research__option input { position: absolute; opacity: 0; inset: 0; margin: 0; cursor: pointer; }
|
|
||||||
.research__option > span {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--space-2);
|
|
||||||
padding: var(--space-3) var(--space-4);
|
|
||||||
border: var(--hairline) solid var(--line);
|
|
||||||
font-family: var(--font-mono);
|
|
||||||
font-size: var(--step-micro);
|
|
||||||
font-weight: var(--weight-medium);
|
|
||||||
letter-spacing: var(--track-caps);
|
|
||||||
text-transform: uppercase;
|
|
||||||
line-height: 1;
|
|
||||||
transition: background-color var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
|
|
||||||
}
|
|
||||||
.research__option:hover > span { background: color-mix(in srgb, var(--ink) var(--tint-hover), transparent); }
|
|
||||||
.research__option input:checked + span { background: var(--ink); color: var(--paper); }
|
|
||||||
.research__option input:focus-visible + span { outline: var(--focus-width) solid var(--ink); outline-offset: calc(-1 * var(--focus-width) - 2px); }
|
|
||||||
.research__n { color: inherit; opacity: 0.6; }
|
|
||||||
|
|
||||||
.research__list { margin-top: var(--space-6); scroll-margin-top: calc(var(--header-height) + var(--space-5)); }
|
.research__list { margin-top: var(--space-6); scroll-margin-top: calc(var(--header-height) + var(--space-5)); }
|
||||||
.research__item[hidden] { display: none; }
|
.research__item[hidden] { display: none; }
|
||||||
|
|||||||
@@ -55,17 +55,16 @@ const now = 0;
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
/* Release path: one hairline, one node per phase. Numbering is earned here: it is a sequence. */
|
/* Release path: one hairline, one node per phase. */
|
||||||
.track__head { grid-column: 1 / -1; display: grid; gap: var(--space-5); align-content: start; }
|
.track__head { grid-column: 1 / -1; display: grid; gap: var(--space-5); align-content: start; }
|
||||||
.track__head p { max-width: 24em; }
|
.track__head p { max-width: 24em; }
|
||||||
.track__steps { grid-column: 1 / -1; display: grid; gap: var(--space-7); counter-reset: step; }
|
.track__steps { grid-column: 1 / -1; display: grid; gap: var(--space-7); }
|
||||||
.track__step {
|
.track__step {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: var(--space-2);
|
gap: var(--space-2);
|
||||||
align-content: start;
|
align-content: start;
|
||||||
padding-left: var(--space-7);
|
padding-left: var(--space-7);
|
||||||
counter-increment: step;
|
|
||||||
}
|
}
|
||||||
/* Vertical rail on small screens. */
|
/* Vertical rail on small screens. */
|
||||||
.track__step::before {
|
.track__step::before {
|
||||||
@@ -99,7 +98,6 @@ const now = 0;
|
|||||||
}
|
}
|
||||||
.track__step[data-state='next'] .track__node { border-style: dashed; border-color: var(--ink-muted); }
|
.track__step[data-state='next'] .track__node { border-style: dashed; border-color: var(--ink-muted); }
|
||||||
.track__state { font-size: var(--step-small); line-height: var(--leading-small); color: var(--ink-muted); }
|
.track__state { font-size: var(--step-small); line-height: var(--leading-small); color: var(--ink-muted); }
|
||||||
.track__state::before { content: counter(step, decimal-leading-zero) ' '; white-space: pre; color: var(--ink); }
|
|
||||||
.track__step[data-state='next'] .track__name { color: var(--ink-muted); }
|
.track__step[data-state='next'] .track__name { color: var(--ink-muted); }
|
||||||
.track__body { max-width: 22em; }
|
.track__body { max-width: 22em; }
|
||||||
|
|
||||||
@@ -134,7 +132,7 @@ const now = 0;
|
|||||||
.track__step { padding-left: 0; padding-top: var(--space-7); padding-right: var(--space-4); }
|
.track__step { padding-left: 0; padding-top: var(--space-7); padding-right: var(--space-4); }
|
||||||
.track__step::before {
|
.track__step::before {
|
||||||
left: 1.25rem;
|
left: 1.25rem;
|
||||||
right: calc(-1 * var(--gap) - 0rem);
|
right: calc(-1 * var(--gap));
|
||||||
top: calc(0.5rem - var(--hairline) / 2);
|
top: calc(0.5rem - var(--hairline) / 2);
|
||||||
bottom: auto;
|
bottom: auto;
|
||||||
border-left: 0;
|
border-left: 0;
|
||||||
|
|||||||
+29
-1
@@ -21,4 +21,32 @@ const insights = defineCollection({
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const collections = { insights };
|
/* Mentions: short references to posts elsewhere (LinkedIn first) that CipherCell took part in.
|
||||||
|
Each gets a short page under /insights/<slug> with the post embedded on request. Query and
|
||||||
|
fragment are dropped from the URL, so tracking and member IDs from a share link never reach
|
||||||
|
the site. `embed` is derived from a LinkedIn post URL; set it by hand only if that fails. */
|
||||||
|
const mentions = defineCollection({
|
||||||
|
loader: glob({ pattern: '**/*.md', base: './src/content/mentions' }),
|
||||||
|
schema: z.object({
|
||||||
|
title: z.string(),
|
||||||
|
summary: z.string(),
|
||||||
|
url: z.url().transform((u) => {
|
||||||
|
const clean = new URL(u);
|
||||||
|
clean.search = '';
|
||||||
|
clean.hash = '';
|
||||||
|
return clean.toString();
|
||||||
|
}),
|
||||||
|
source: z.string(),
|
||||||
|
platform: z.string().default('LinkedIn'),
|
||||||
|
embed: z.url().optional(),
|
||||||
|
embedHeight: z.number().int().positive().default(720),
|
||||||
|
date: z.coerce.date(),
|
||||||
|
lang: z.enum(['en', 'de']),
|
||||||
|
}).transform((d) => {
|
||||||
|
const activity = d.url.match(/activity[-:](\d+)/)?.[1];
|
||||||
|
const embed = d.embed ?? (activity ? `https://www.linkedin.com/embed/feed/update/urn:li:activity:${activity}` : undefined);
|
||||||
|
return { ...d, embed };
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const collections = { insights, mentions };
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
title: 'CipherCell at the Berlin 6G Conference 2026.'
|
||||||
|
summary: 'xG-Incubator announces Florian Frank, Marc Schlotzhauer and Felix Klement at StartUpConnect booth 20, with a demo that analyses software for telecommunications services statically, automatically and deterministically, air-gapped in the customer''s infrastructure. Felix Klement also joins the panel "AI for Trustworthiness".'
|
||||||
|
url: 'https://www.linkedin.com/posts/felix-klement_activity-7499368352876896256-1iCO'
|
||||||
|
source: 'xG-Incubator'
|
||||||
|
platform: 'LinkedIn'
|
||||||
|
date: 2026-08-29
|
||||||
|
lang: en
|
||||||
|
---
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
title: 'Component security testing at the xG-ALOE Plugfest 2026.'
|
||||||
|
summary: 'xG-ALOE welcomes Felix Klement and Florian Frank of the University of Passau to the Plugfest in Berlin. They show a security testing platform for xG software: deterministic vulnerability detection with targeted AI support, with a focus on O-RAN.'
|
||||||
|
url: 'https://www.linkedin.com/posts/xg-aloe_xgaloe-plugfest2026-openran-activity-7506238009315454976-HZPG'
|
||||||
|
source: 'xG-ALOE'
|
||||||
|
platform: 'LinkedIn'
|
||||||
|
date: 2026-09-17
|
||||||
|
lang: en
|
||||||
|
---
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
title: 'Two days on 5G and xG security in Nuremberg.'
|
||||||
|
summary: 'At the xG-ALOE User Forum 2026, hosted by Fraunhofer IIS, Felix Klement and Heiner Grottendieck of the BSI led the tutorial "Securing the Evolution to xG" on the key security challenges of next-generation networks.'
|
||||||
|
url: 'https://www.linkedin.com/posts/felix-klement_activity-7476365312829841408-BIsp'
|
||||||
|
source: 'CipherCell'
|
||||||
|
platform: 'LinkedIn'
|
||||||
|
date: 2026-06-26
|
||||||
|
lang: en
|
||||||
|
---
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
title: 'Tutorial B2 at the xG-ALOE User Forum 2026.'
|
||||||
|
summary: 'xG-ALOE announces the joint tutorial "Securing the Evolution to xG" by Felix Klement (University of Passau) and Heiner Grottendieck (BSI): current threat landscapes, security challenges in open telco architectures, and ways to strengthen network resilience. Fraunhofer IIS, Nuremberg, 24 and 25 June 2026.'
|
||||||
|
url: 'https://www.linkedin.com/posts/felix-klement_activity-7459949467878744064-Y3iJ'
|
||||||
|
source: 'xG-ALOE'
|
||||||
|
platform: 'LinkedIn'
|
||||||
|
date: 2026-05-12
|
||||||
|
lang: en
|
||||||
|
---
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
/* The Insights feed: essays (own pages) and mentions (links out), newest first.
|
||||||
|
One list so the index and the home page sort and slice them the same way. */
|
||||||
|
import { getCollection, type CollectionEntry } from 'astro:content';
|
||||||
|
|
||||||
|
export type FeedItem =
|
||||||
|
| { kind: 'post'; date: Date; entry: CollectionEntry<'insights'> }
|
||||||
|
| { kind: 'mention'; date: Date; entry: CollectionEntry<'mentions'> };
|
||||||
|
|
||||||
|
export async function getFeed(): Promise<FeedItem[]> {
|
||||||
|
const posts = (await getCollection('insights')).map((entry) => ({ kind: 'post' as const, date: entry.data.date, entry }));
|
||||||
|
const mentions = (await getCollection('mentions')).map((entry) => ({ kind: 'mention' as const, date: entry.data.date, entry }));
|
||||||
|
return [...posts, ...mentions].sort((a, b) => b.date.valueOf() - a.date.valueOf());
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The filter an item belongs to on the Insights index: essays, LinkedIn, or any other platform. */
|
||||||
|
export type FeedType = 'essay' | 'linkedin' | 'other';
|
||||||
|
export const feedTypes: FeedType[] = ['essay', 'linkedin', 'other'];
|
||||||
|
export function feedType(item: FeedItem): FeedType {
|
||||||
|
if (item.kind === 'post') return 'essay';
|
||||||
|
return item.entry.data.platform.toLowerCase() === 'linkedin' ? 'linkedin' : 'other';
|
||||||
|
}
|
||||||
@@ -114,6 +114,18 @@ const en = {
|
|||||||
back: 'All insights',
|
back: 'All insights',
|
||||||
by: 'By',
|
by: 'By',
|
||||||
inEnglish: 'This essay is available in English.',
|
inEnglish: 'This essay is available in English.',
|
||||||
|
filter: { label: 'Category', all: 'All', essay: 'Essays', linkedin: 'LinkedIn', other: 'Other' },
|
||||||
|
mention: {
|
||||||
|
category: (platform: string) => `On ${platform}`,
|
||||||
|
source: (source: string) => `Posted by ${source}`,
|
||||||
|
inEnglish: 'This post is in English.',
|
||||||
|
inGerman: 'This post is in German.',
|
||||||
|
open: (platform: string) => `Open on ${platform}`,
|
||||||
|
embedTitle: (source: string, platform: string) => `Post by ${source} on ${platform}`,
|
||||||
|
consent: (platform: string) => `The post is hosted by ${platform}. Loading it here connects your browser to ${platform}, which may set cookies.`,
|
||||||
|
load: (platform: string) => `Load post from ${platform}`,
|
||||||
|
privacy: 'Privacy policy',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
contact: {
|
contact: {
|
||||||
title: 'Talk to us.',
|
title: 'Talk to us.',
|
||||||
@@ -395,6 +407,18 @@ const de: typeof en = {
|
|||||||
back: 'Alle Einblicke',
|
back: 'Alle Einblicke',
|
||||||
by: 'Von',
|
by: 'Von',
|
||||||
inEnglish: 'Dieser Essay liegt auf Englisch vor.',
|
inEnglish: 'Dieser Essay liegt auf Englisch vor.',
|
||||||
|
filter: { label: 'Kategorie', all: 'Alle', essay: 'Essays', linkedin: 'LinkedIn', other: 'Weitere' },
|
||||||
|
mention: {
|
||||||
|
category: (platform: string) => `Auf ${platform}`,
|
||||||
|
source: (source: string) => `Veröffentlicht von ${source}`,
|
||||||
|
inEnglish: 'Dieser Beitrag ist auf Englisch.',
|
||||||
|
inGerman: 'Dieser Beitrag ist auf Deutsch.',
|
||||||
|
open: (platform: string) => `Auf ${platform} öffnen`,
|
||||||
|
embedTitle: (source: string, platform: string) => `Beitrag von ${source} auf ${platform}`,
|
||||||
|
consent: (platform: string) => `Der Beitrag liegt bei ${platform}. Wenn Sie ihn hier laden, verbindet sich Ihr Browser mit ${platform}; dabei können Cookies gesetzt werden.`,
|
||||||
|
load: (platform: string) => `Beitrag von ${platform} laden`,
|
||||||
|
privacy: 'Datenschutzerklärung',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
contact: {
|
contact: {
|
||||||
title: 'Sprechen Sie mit uns.',
|
title: 'Sprechen Sie mit uns.',
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
import { getCollection } from 'astro:content';
|
import { feedType, feedTypes, getFeed } from '../data/insights';
|
||||||
import { Picture } from 'astro:assets';
|
import { Picture } from 'astro:assets';
|
||||||
import Base from '../layouts/Base.astro';
|
import Base from '../layouts/Base.astro';
|
||||||
import { formatDate, localePath, t, type Lang } from '../i18n/ui';
|
import { formatDate, localePath, t, type Lang } from '../i18n/ui';
|
||||||
@@ -7,7 +7,16 @@ import { formatDate, localePath, t, type Lang } from '../i18n/ui';
|
|||||||
interface Props { lang: Lang }
|
interface Props { lang: Lang }
|
||||||
const { lang } = Astro.props;
|
const { lang } = Astro.props;
|
||||||
const ui = t(lang);
|
const ui = t(lang);
|
||||||
const posts = (await getCollection('insights')).sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf());
|
const feed = await getFeed();
|
||||||
|
const m = ui.insights.mention;
|
||||||
|
const f = ui.insights.filter;
|
||||||
|
/* Only filters that have entries; "All" first. */
|
||||||
|
const filters = [
|
||||||
|
{ key: 'all', label: f.all, count: feed.length },
|
||||||
|
...feedTypes
|
||||||
|
.map((key) => ({ key, label: f[key], count: feed.filter((item) => feedType(item) === key).length }))
|
||||||
|
.filter((x) => x.count > 0),
|
||||||
|
];
|
||||||
---
|
---
|
||||||
<Base lang={lang} title={ui.meta.insights.title} description={ui.meta.insights.description}>
|
<Base lang={lang} title={ui.meta.insights.title} description={ui.meta.insights.description}>
|
||||||
<section class="section section--intro">
|
<section class="section section--intro">
|
||||||
@@ -19,16 +28,29 @@ const posts = (await getCollection('insights')).sort((a, b) => b.data.date.value
|
|||||||
|
|
||||||
<section class="section--list" aria-label={ui.insights.title}>
|
<section class="section--list" aria-label={ui.insights.title}>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<ul class="posts" role="list">
|
{filters.length > 2 && (
|
||||||
{posts.map((post) => (
|
<fieldset class="choice-group filters" data-filters>
|
||||||
<li class="post">
|
<legend class="choice-group__legend">{f.label}</legend>
|
||||||
<a class="post__link" href={localePath(lang, `/insights/${post.id}`)} lang={post.data.lang !== lang ? post.data.lang : undefined}>
|
<div class="choices">
|
||||||
|
{filters.map((x) => (
|
||||||
|
<label class="choice">
|
||||||
|
<input type="radio" name="insights-type" value={x.key} checked={x.key === 'all'} />
|
||||||
|
<span>{x.label} <span class="choice__n">{x.count}</span></span>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
)}
|
||||||
|
<ul class="posts" role="list" data-feed>
|
||||||
|
{feed.map((item) => item.kind === 'post' ? (
|
||||||
|
<li class="post" data-type={feedType(item)}>
|
||||||
|
<a class="post__link" href={localePath(lang, `/insights/${item.entry.id}`)} lang={item.entry.data.lang !== lang ? item.entry.data.lang : undefined}>
|
||||||
<div class="post__media">
|
<div class="post__media">
|
||||||
<Picture
|
<Picture
|
||||||
src={post.data.cover}
|
src={item.entry.data.cover}
|
||||||
alt={post.data.coverAlt}
|
alt={item.entry.data.coverAlt}
|
||||||
formats={['avif']}
|
formats={['avif']}
|
||||||
fallbackFormat="webp"
|
fallbackFormat="webp"
|
||||||
widths={[400, 640, 960]}
|
widths={[400, 640, 960]}
|
||||||
sizes="(min-width: 48rem) 40vw, 100vw"
|
sizes="(min-width: 48rem) 40vw, 100vw"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
@@ -38,13 +60,32 @@ const posts = (await getCollection('insights')).sort((a, b) => b.data.date.value
|
|||||||
</div>
|
</div>
|
||||||
<div class="post__text">
|
<div class="post__text">
|
||||||
<p class="small muted post__meta">
|
<p class="small muted post__meta">
|
||||||
<span>{post.data.category}</span>
|
<span>{item.entry.data.category}</span>
|
||||||
<time datetime={post.data.date.toISOString().slice(0, 10)}>{formatDate(post.data.date, lang)}</time>
|
<time datetime={item.date.toISOString().slice(0, 10)}>{formatDate(item.date, lang)}</time>
|
||||||
<span>{ui.insights.readingTime(post.data.readingTime)}</span>
|
<span>{ui.insights.readingTime(item.entry.data.readingTime)}</span>
|
||||||
</p>
|
</p>
|
||||||
<h2 class="h2">{post.data.title}</h2>
|
<h2 class="h2">{item.entry.data.title}</h2>
|
||||||
{post.data.subtitle && <p class="muted">{post.data.subtitle}</p>}
|
{item.entry.data.subtitle && <p class="muted">{item.entry.data.subtitle}</p>}
|
||||||
{post.data.lang !== lang && <p class="small muted">{ui.insights.inEnglish}</p>}
|
{item.entry.data.lang !== lang && <p class="small muted">{ui.insights.inEnglish}</p>}
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
) : (
|
||||||
|
<li class="post" data-type={feedType(item)}>
|
||||||
|
<a class="post__link" href={localePath(lang, `/insights/${item.entry.id}`)} lang={item.entry.data.lang !== lang ? item.entry.data.lang : undefined}>
|
||||||
|
<div class="post__media mention__panel" aria-hidden="true">
|
||||||
|
<span class="mention__source">{item.entry.data.source}</span>
|
||||||
|
<span class="mention__platform">{item.entry.data.platform}</span>
|
||||||
|
</div>
|
||||||
|
<div class="post__text">
|
||||||
|
<p class="small muted post__meta">
|
||||||
|
<span>{m.category(item.entry.data.platform)}</span>
|
||||||
|
<time datetime={item.date.toISOString().slice(0, 10)}>{formatDate(item.date, lang)}</time>
|
||||||
|
<span>{m.source(item.entry.data.source)}</span>
|
||||||
|
</p>
|
||||||
|
<h2 class="h2">{item.entry.data.title}</h2>
|
||||||
|
<p class="muted mention__summary">{item.entry.data.summary}</p>
|
||||||
|
{item.entry.data.lang !== lang && <p class="small muted">{item.entry.data.lang === 'en' ? m.inEnglish : m.inGerman}</p>}
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -54,6 +95,29 @@ const posts = (await getCollection('insights')).sort((a, b) => b.data.date.value
|
|||||||
</section>
|
</section>
|
||||||
</Base>
|
</Base>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
/* Filter the list by type. The choice lives in ?type= so a filtered view can be linked;
|
||||||
|
without script every entry shows. */
|
||||||
|
const bar = document.querySelector<HTMLElement>('[data-filters]');
|
||||||
|
const items = document.querySelectorAll<HTMLElement>('[data-feed] > li');
|
||||||
|
if (bar) {
|
||||||
|
const inputs = bar.querySelectorAll<HTMLInputElement>('input[name="insights-type"]');
|
||||||
|
const keys = [...inputs].map((i) => i.value);
|
||||||
|
const apply = (key: string, push: boolean) => {
|
||||||
|
if (!keys.includes(key)) key = 'all';
|
||||||
|
inputs.forEach((i) => { i.checked = i.value === key; });
|
||||||
|
items.forEach((li) => { li.hidden = key !== 'all' && li.dataset.type !== key; });
|
||||||
|
if (push) {
|
||||||
|
const url = new URL(location.href);
|
||||||
|
if (key === 'all') url.searchParams.delete('type'); else url.searchParams.set('type', key);
|
||||||
|
history.replaceState(null, '', url);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
bar.addEventListener('change', (e) => apply((e.target as HTMLInputElement).value, true));
|
||||||
|
apply(new URL(location.href).searchParams.get('type') ?? 'all', false);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.index__title { grid-column: 1 / -1; }
|
.index__title { grid-column: 1 / -1; }
|
||||||
.index__lead { grid-column: 1 / -1; max-width: var(--measure); color: var(--ink-muted); }
|
.index__lead { grid-column: 1 / -1; max-width: var(--measure); color: var(--ink-muted); }
|
||||||
@@ -62,6 +126,9 @@ const posts = (await getCollection('insights')).sort((a, b) => b.data.date.value
|
|||||||
.index__lead { grid-column: 1 / span 5; }
|
.index__lead { grid-column: 1 / span 5; }
|
||||||
}
|
}
|
||||||
.section--list { padding-bottom: var(--section); }
|
.section--list { padding-bottom: var(--section); }
|
||||||
|
/* Filter row: the shared segmented choice (base.css), as on the research list. */
|
||||||
|
.filters { padding-bottom: var(--space-6); }
|
||||||
|
.post[hidden] { display: none; }
|
||||||
.posts { border-top: var(--hairline) solid var(--line); }
|
.posts { border-top: var(--hairline) solid var(--line); }
|
||||||
.post { border-bottom: var(--hairline) solid var(--line); }
|
.post { border-bottom: var(--hairline) solid var(--line); }
|
||||||
.post__link {
|
.post__link {
|
||||||
@@ -74,6 +141,18 @@ const posts = (await getCollection('insights')).sort((a, b) => b.data.date.value
|
|||||||
.post__img, :global(.post__img) { width: 100%; height: auto; aspect-ratio: 3 / 2; object-fit: cover; }
|
.post__img, :global(.post__img) { width: 100%; height: auto; aspect-ratio: 3 / 2; object-fit: cover; }
|
||||||
.post__text { display: grid; gap: var(--space-4); align-content: start; }
|
.post__text { display: grid; gap: var(--space-4); align-content: start; }
|
||||||
.post__meta { display: flex; flex-wrap: wrap; gap: var(--space-4); }
|
.post__meta { display: flex; flex-wrap: wrap; gap: var(--space-4); }
|
||||||
|
/* A mention has no cover: a typographic panel names who posted it and where. */
|
||||||
|
.mention__panel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
aspect-ratio: 3 / 2;
|
||||||
|
padding: var(--space-5);
|
||||||
|
background: var(--paper-deep);
|
||||||
|
}
|
||||||
|
.mention__source { font-size: var(--step-title); line-height: var(--leading-title); letter-spacing: var(--track-h2); color: var(--ink-muted); }
|
||||||
|
.mention__platform { font-size: var(--step-small); line-height: var(--leading-small); color: var(--ink-muted); }
|
||||||
|
.mention__summary { max-width: var(--measure); }
|
||||||
@media (min-width: 48rem) {
|
@media (min-width: 48rem) {
|
||||||
.post__link { grid-template-columns: 5fr 7fr; }
|
.post__link { grid-template-columns: 5fr 7fr; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
---
|
||||||
|
import type { CollectionEntry } from 'astro:content';
|
||||||
|
import { render } from 'astro:content';
|
||||||
|
import Base from '../layouts/Base.astro';
|
||||||
|
import { formatDate, localePath, t, type Lang } from '../i18n/ui';
|
||||||
|
|
||||||
|
interface Props { lang: Lang; mention: CollectionEntry<'mentions'> }
|
||||||
|
const { lang, mention } = Astro.props;
|
||||||
|
const ui = t(lang);
|
||||||
|
const m = ui.insights.mention;
|
||||||
|
const d = mention.data;
|
||||||
|
/* Optional notes of our own below the front matter. */
|
||||||
|
const hasBody = Boolean(mention.body?.trim());
|
||||||
|
const { Content } = await render(mention);
|
||||||
|
---
|
||||||
|
<Base lang={lang} title={d.title} description={d.summary}>
|
||||||
|
<article class="mention">
|
||||||
|
<header class="section section--intro">
|
||||||
|
<div class="container grid">
|
||||||
|
<div class="mention__back">
|
||||||
|
<a class="link" href={localePath(lang, '/insights')}>{ui.insights.back}</a>
|
||||||
|
</div>
|
||||||
|
<div class="mention__head">
|
||||||
|
<p class="small muted mention__meta">
|
||||||
|
<span>{m.category(d.platform)}</span>
|
||||||
|
<time datetime={d.date.toISOString().slice(0, 10)}>{formatDate(d.date, lang)}</time>
|
||||||
|
<span>{m.source(d.source)}</span>
|
||||||
|
</p>
|
||||||
|
<h1 class="h2 mention__title" lang={d.lang !== lang ? d.lang : undefined}>{d.title}</h1>
|
||||||
|
<p class="mention__summary muted" lang={d.lang !== lang ? d.lang : undefined}>{d.summary}</p>
|
||||||
|
{hasBody && <div class="prose mention__notes" lang={d.lang !== lang ? d.lang : undefined}><Content /></div>}
|
||||||
|
<p><a class="link" href={d.url} rel="noopener">{m.open(d.platform)}</a></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{d.embed && (
|
||||||
|
<section class="mention__embed-section" aria-label={m.embedTitle(d.source, d.platform)}>
|
||||||
|
<div class="container grid">
|
||||||
|
<div class="embed hud-frame" data-embed data-src={d.embed} data-title={m.embedTitle(d.source, d.platform)} style={`--embed-h: ${d.embedHeight}px`}>
|
||||||
|
<div class="embed__gate" data-embed-gate>
|
||||||
|
<p class="embed__source">{d.source}</p>
|
||||||
|
<p class="small muted embed__consent">
|
||||||
|
{m.consent(d.platform)} <a href={localePath(lang, '/privacy')}>{m.privacy}</a>
|
||||||
|
</p>
|
||||||
|
<button class="button" type="button" data-embed-load>{m.load(d.platform)}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
</article>
|
||||||
|
</Base>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
/* Two-click embed: nothing is requested from the platform until the visitor asks for it. */
|
||||||
|
document.querySelectorAll<HTMLElement>('[data-embed]').forEach((box) => {
|
||||||
|
const button = box.querySelector<HTMLButtonElement>('[data-embed-load]');
|
||||||
|
button?.addEventListener('click', () => {
|
||||||
|
const frame = document.createElement('iframe');
|
||||||
|
frame.src = box.dataset.src!;
|
||||||
|
frame.title = box.dataset.title ?? '';
|
||||||
|
frame.loading = 'lazy';
|
||||||
|
frame.className = 'embed__frame';
|
||||||
|
frame.setAttribute('allowfullscreen', '');
|
||||||
|
box.querySelector('[data-embed-gate]')?.remove();
|
||||||
|
box.append(frame);
|
||||||
|
box.toggleAttribute('data-loaded', true);
|
||||||
|
frame.focus();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.mention__back { grid-column: 1 / -1; }
|
||||||
|
.mention__head { grid-column: 1 / -1; display: grid; gap: var(--space-5); justify-items: start; }
|
||||||
|
.mention__meta { display: flex; flex-wrap: wrap; gap: var(--space-4); }
|
||||||
|
.mention__title { text-wrap: balance; }
|
||||||
|
.mention__summary { font-size: var(--step-h3); letter-spacing: var(--track-h3); line-height: var(--leading-h3); max-width: var(--measure); }
|
||||||
|
|
||||||
|
.mention__embed-section { padding-bottom: var(--section); }
|
||||||
|
.embed {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
display: grid;
|
||||||
|
aspect-ratio: 4 / 3;
|
||||||
|
background: var(--paper-deep);
|
||||||
|
}
|
||||||
|
/* Loaded: the frame takes the post's height. The jump answers the visitor's own click. */
|
||||||
|
.embed[data-loaded] { aspect-ratio: auto; }
|
||||||
|
.embed[data-loaded] { background: var(--paper); }
|
||||||
|
.embed__gate {
|
||||||
|
display: grid;
|
||||||
|
align-content: end;
|
||||||
|
justify-items: start;
|
||||||
|
gap: var(--space-5);
|
||||||
|
padding: var(--space-6);
|
||||||
|
}
|
||||||
|
.embed__source { font-size: var(--step-title); line-height: var(--leading-title); letter-spacing: var(--track-h2); color: var(--ink-muted); }
|
||||||
|
.embed__consent { max-width: 34em; }
|
||||||
|
.embed__consent a { color: var(--ink); text-decoration: underline; text-decoration-thickness: var(--hairline); text-underline-offset: 0.2em; }
|
||||||
|
.embed :global(.embed__frame) { width: 100%; height: var(--embed-h); border: 0; display: block; }
|
||||||
|
|
||||||
|
@media (min-width: 48rem) {
|
||||||
|
.mention__back { grid-column: 1 / span 2; }
|
||||||
|
.mention__head { grid-column: 3 / span 8; }
|
||||||
|
/* Half the grid, centred: columns 4 to 9. */
|
||||||
|
.embed { grid-column: 4 / span 6; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,11 +1,17 @@
|
|||||||
---
|
---
|
||||||
import { getCollection } from 'astro:content';
|
import { getCollection } from 'astro:content';
|
||||||
import Post from '../../_Post.astro';
|
import Post from '../../_Post.astro';
|
||||||
|
import Mention from '../../_Mention.astro';
|
||||||
|
|
||||||
|
/* Essays and mentions share /insights/<slug>; a slug used by both would be a defect. */
|
||||||
export async function getStaticPaths() {
|
export async function getStaticPaths() {
|
||||||
const posts = await getCollection('insights');
|
const posts = (await getCollection('insights')).map((post) => ({ params: { slug: post.id }, props: { post } }));
|
||||||
return posts.map((post) => ({ params: { slug: post.id }, props: { post } }));
|
const mentions = (await getCollection('mentions')).map((mention) => ({ params: { slug: mention.id }, props: { mention } }));
|
||||||
|
const taken = new Set(posts.map((p) => p.params.slug));
|
||||||
|
const clash = mentions.find((m) => taken.has(m.params.slug));
|
||||||
|
if (clash) throw new Error(`Insights slug used by an essay and a mention: ${clash.params.slug}`);
|
||||||
|
return [...posts, ...mentions];
|
||||||
}
|
}
|
||||||
const { post } = Astro.props;
|
const { post, mention } = Astro.props;
|
||||||
---
|
---
|
||||||
<Post lang="de" post={post} />
|
{post ? <Post lang="de" post={post} /> : <Mention lang="de" mention={mention!} />}
|
||||||
@@ -20,6 +20,9 @@ const ui = t('de');
|
|||||||
<h2>Benachrichtigung zur CipherVerifier-Alpha</h2>
|
<h2>Benachrichtigung zur CipherVerifier-Alpha</h2>
|
||||||
<p>Wenn Sie sich für die Benachrichtigung zur Alpha von CipherVerifier eintragen, speichern wir Ihre E-Mail-Adresse und den Zeitpunkt Ihrer Einwilligung, um Ihnen mitzuteilen, wann die erste Alpha veröffentlicht ist. Wir nutzen sie für nichts anderes und geben sie nicht an Dritte weiter. Rechtsgrundlage ist Ihre Einwilligung nach Art. 6 Abs. 1 lit. a DSGVO. Sie können Ihre Einwilligung jederzeit per E-Mail an <a href="mailto:office@ciphercell.de">office@ciphercell.de</a> widerrufen; wir löschen Ihre Adresse dann. Nach dem Versand der Benachrichtigung löschen wir sie ebenfalls.</p>
|
<p>Wenn Sie sich für die Benachrichtigung zur Alpha von CipherVerifier eintragen, speichern wir Ihre E-Mail-Adresse und den Zeitpunkt Ihrer Einwilligung, um Ihnen mitzuteilen, wann die erste Alpha veröffentlicht ist. Wir nutzen sie für nichts anderes und geben sie nicht an Dritte weiter. Rechtsgrundlage ist Ihre Einwilligung nach Art. 6 Abs. 1 lit. a DSGVO. Sie können Ihre Einwilligung jederzeit per E-Mail an <a href="mailto:office@ciphercell.de">office@ciphercell.de</a> widerrufen; wir löschen Ihre Adresse dann. Nach dem Versand der Benachrichtigung löschen wir sie ebenfalls.</p>
|
||||||
|
|
||||||
|
<h2>Eingebettete LinkedIn-Beiträge</h2>
|
||||||
|
<p>Einige Seiten unter Einblicke können einen Beitrag von LinkedIn (LinkedIn Ireland Unlimited Company, Wilton Place, Dublin 2, Irland) anzeigen. Beim Aufruf der Seite wird der Beitrag nicht geladen. Erst wenn Sie auf die Schaltfläche zum Laden klicken, verbindet sich Ihr Browser mit den Servern von LinkedIn; diese erhalten Ihre IP-Adresse und die aufgerufene Seite und können Cookies setzen. Sind Sie bei LinkedIn angemeldet, kann LinkedIn den Besuch Ihrem Konto zuordnen. Rechtsgrundlage ist Ihre Einwilligung nach Art. 6 Abs. 1 lit. a DSGVO, die Sie mit diesem Klick erteilen. Zur Verarbeitung durch LinkedIn siehe dessen Datenschutzrichtlinie unter <a href="https://de.linkedin.com/legal/privacy-policy" rel="noopener">linkedin.com/legal/privacy-policy</a>.</p>
|
||||||
|
|
||||||
<h2>Recht auf Auskunft, Berichtigung, Sperrung oder Löschung</h2>
|
<h2>Recht auf Auskunft, Berichtigung, Sperrung oder Löschung</h2>
|
||||||
<p>Nach dem anwendbaren Datenschutzrecht haben Sie ein Recht auf unentgeltliche Auskunft über Ihre gespeicherten Daten sowie ggf. ein Recht auf Berichtigung, Sperrung oder Löschung dieser Daten. Wenden Sie sich bei weitergehenden Fragen zum Datenschutz und zur Verarbeitung Ihrer personenbezogenen Daten, bei Auskünften oder zur Ausübung Ihres Rechts auf Berichtigung, Sperrung oder Löschung oder für den Fall des Widerrufs einer Einwilligung bitte per E-Mail an <a href="mailto:office@ciphercell.de">office@ciphercell.de</a>. Selbstverständlich können Sie auch die im Impressum angegebenen Kontaktdaten oder das Kontaktformular auf der Website nutzen. CipherCell verarbeitet und nutzt die von Ihnen mitgeteilten Informationen ausschließlich für die Bearbeitung Ihres Anliegens.</p>
|
<p>Nach dem anwendbaren Datenschutzrecht haben Sie ein Recht auf unentgeltliche Auskunft über Ihre gespeicherten Daten sowie ggf. ein Recht auf Berichtigung, Sperrung oder Löschung dieser Daten. Wenden Sie sich bei weitergehenden Fragen zum Datenschutz und zur Verarbeitung Ihrer personenbezogenen Daten, bei Auskünften oder zur Ausübung Ihres Rechts auf Berichtigung, Sperrung oder Löschung oder für den Fall des Widerrufs einer Einwilligung bitte per E-Mail an <a href="mailto:office@ciphercell.de">office@ciphercell.de</a>. Selbstverständlich können Sie auch die im Impressum angegebenen Kontaktdaten oder das Kontaktformular auf der Website nutzen. CipherCell verarbeitet und nutzt die von Ihnen mitgeteilten Informationen ausschließlich für die Bearbeitung Ihres Anliegens.</p>
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
---
|
---
|
||||||
import { getCollection } from 'astro:content';
|
import { getCollection } from 'astro:content';
|
||||||
import Post from '../_Post.astro';
|
import Post from '../_Post.astro';
|
||||||
|
import Mention from '../_Mention.astro';
|
||||||
|
|
||||||
|
/* Essays and mentions share /insights/<slug>; a slug used by both would be a defect. */
|
||||||
export async function getStaticPaths() {
|
export async function getStaticPaths() {
|
||||||
const posts = await getCollection('insights');
|
const posts = (await getCollection('insights')).map((post) => ({ params: { slug: post.id }, props: { post } }));
|
||||||
return posts.map((post) => ({ params: { slug: post.id }, props: { post } }));
|
const mentions = (await getCollection('mentions')).map((mention) => ({ params: { slug: mention.id }, props: { mention } }));
|
||||||
|
const taken = new Set(posts.map((p) => p.params.slug));
|
||||||
|
const clash = mentions.find((m) => taken.has(m.params.slug));
|
||||||
|
if (clash) throw new Error(`Insights slug used by an essay and a mention: ${clash.params.slug}`);
|
||||||
|
return [...posts, ...mentions];
|
||||||
}
|
}
|
||||||
const { post } = Astro.props;
|
const { post, mention } = Astro.props;
|
||||||
---
|
---
|
||||||
<Post lang="en" post={post} />
|
{post ? <Post lang="en" post={post} /> : <Mention lang="en" mention={mention!} />}
|
||||||
@@ -20,6 +20,9 @@ const ui = t('en');
|
|||||||
<h2>CipherVerifier alpha notification</h2>
|
<h2>CipherVerifier alpha notification</h2>
|
||||||
<p>If you register for the CipherVerifier alpha notification, we store your email address and the time of your consent in order to tell you when the first alpha is released. We use it for nothing else and do not pass it on to third parties. The legal basis is your consent under Art. 6 para. 1 lit. a GDPR. You can withdraw your consent at any time by email to <a href="mailto:office@ciphercell.de">office@ciphercell.de</a>; we then delete your address. We also delete it once the notification has been sent.</p>
|
<p>If you register for the CipherVerifier alpha notification, we store your email address and the time of your consent in order to tell you when the first alpha is released. We use it for nothing else and do not pass it on to third parties. The legal basis is your consent under Art. 6 para. 1 lit. a GDPR. You can withdraw your consent at any time by email to <a href="mailto:office@ciphercell.de">office@ciphercell.de</a>; we then delete your address. We also delete it once the notification has been sent.</p>
|
||||||
|
|
||||||
|
<h2>Embedded LinkedIn posts</h2>
|
||||||
|
<p>Some pages in Insights can show a post from LinkedIn (LinkedIn Ireland Unlimited Company, Wilton Place, Dublin 2, Ireland). The post is not loaded when you open the page. Only when you click the button to load it does your browser connect to LinkedIn's servers, which receive your IP address and the page you are on and may set cookies; if you are logged in to LinkedIn, it can link the visit to your account. The legal basis is your consent under Art. 6 para. 1 lit. a GDPR, given by that click. For LinkedIn's processing, see its privacy policy at <a href="https://www.linkedin.com/legal/privacy-policy" rel="noopener">linkedin.com/legal/privacy-policy</a>.</p>
|
||||||
|
|
||||||
<h2>Right to information, correction, blocking, or deletion</h2>
|
<h2>Right to information, correction, blocking, or deletion</h2>
|
||||||
<p>Under applicable data protection law, you have the right to obtain information about your stored data free of charge and, if necessary, the right to correct, block, or delete this data. If you have any further questions about data protection and the processing of your personal data, if you require information, or if you wish to exercise your right to correction, blocking, or deletion, or in the event of revocation of consent, please send an email to <a href="mailto:office@ciphercell.de">office@ciphercell.de</a>. You can also use the contact details provided in the legal notice or the contact form on the website. CipherCell processes and uses the information you provide exclusively for the purpose of processing your request.</p>
|
<p>Under applicable data protection law, you have the right to obtain information about your stored data free of charge and, if necessary, the right to correct, block, or delete this data. If you have any further questions about data protection and the processing of your personal data, if you require information, or if you wish to exercise your right to correction, blocking, or deletion, or in the event of revocation of consent, please send an email to <a href="mailto:office@ciphercell.de">office@ciphercell.de</a>. You can also use the contact details provided in the legal notice or the contact form on the website. CipherCell processes and uses the information you provide exclusively for the purpose of processing your request.</p>
|
||||||
|
|
||||||
|
|||||||
@@ -201,6 +201,34 @@ p { overflow-wrap: break-word; }
|
|||||||
.night .button { background: var(--paper); color: var(--night); }
|
.night .button { background: var(--paper); color: var(--night); }
|
||||||
.night .button:hover { background: var(--paper-deep); }
|
.night .button:hover { background: var(--paper-deep); }
|
||||||
|
|
||||||
|
/* Segmented choice: a row of radio options in the menu bar's language, hairline cells and mono
|
||||||
|
caps, the checked one filled with ink. Used for the research filters and the Insights filter.
|
||||||
|
Each option draws its own frame and overlaps its neighbours by one line, so a wrapped row
|
||||||
|
never leaves a rule running past its last option. */
|
||||||
|
.choice-group { border: 0; padding: 0; margin: 0; min-width: 0; display: grid; gap: var(--space-2); }
|
||||||
|
.choice-group__legend { padding: 0; color: var(--ink-muted); font-size: var(--step-small); }
|
||||||
|
.choices { display: flex; flex-wrap: wrap; padding: 0 var(--hairline) var(--hairline) 0; }
|
||||||
|
.choice { position: relative; display: flex; margin: 0 calc(-1 * var(--hairline)) calc(-1 * var(--hairline)) 0; }
|
||||||
|
.choice input { position: absolute; opacity: 0; inset: 0; margin: 0; cursor: pointer; }
|
||||||
|
.choice > span {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-2);
|
||||||
|
padding: var(--space-3) var(--space-4);
|
||||||
|
border: var(--hairline) solid var(--line);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: var(--step-micro);
|
||||||
|
font-weight: var(--weight-medium);
|
||||||
|
letter-spacing: var(--track-caps);
|
||||||
|
text-transform: uppercase;
|
||||||
|
line-height: 1;
|
||||||
|
transition: background-color var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
|
||||||
|
}
|
||||||
|
.choice:hover > span { background: color-mix(in srgb, var(--ink) var(--tint-hover), transparent); }
|
||||||
|
.choice input:checked + span { background: var(--ink); color: var(--paper); }
|
||||||
|
.choice input:focus-visible + span { outline: var(--focus-width) solid var(--ink); outline-offset: calc(-1 * var(--focus-width) - 2px); }
|
||||||
|
.choice__n { color: inherit; opacity: 0.6; }
|
||||||
|
|
||||||
/* Prose: long-form text */
|
/* Prose: long-form text */
|
||||||
.prose { max-width: var(--measure); }
|
.prose { max-width: var(--measure); }
|
||||||
.prose > * + * { margin-top: var(--space-5); }
|
.prose > * + * { margin-top: var(--space-5); }
|
||||||
|
|||||||
Reference in new issue
Block a user