feat: flyt sitet fra Astro til Zola
Samme indhold, URL'er og udseende, bygget af en enkelt zola-binær og Tailwind standalone-CLI uden Node. Kanonisk domæne er asger.weirsoe.dk. Blogindlæg ligger som page bundles med frontmatter Zola forstår, så de gamle stempler er ugyldige, indtil scripts/omstempl.py er kørt med PUBLISH_KEY. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
7e13068dd2
commit
e58a11c2b4
@@ -0,0 +1,8 @@
|
||||
{% extends "base.html" %}
|
||||
{% block main %}
|
||||
<div class="py-20 text-center">
|
||||
<h1 class="text-6xl font-bold tracking-tight">404</h1>
|
||||
<p class="mt-4 text-muted">Siden findes ikke.</p>
|
||||
<a href="/" class="mt-8 wt-btn wt-btn--primary wt-btn--lg">Til forsiden</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,112 @@
|
||||
{%- set preview = get_env(name="PW_PREVIEW", default="") == "1" -%}
|
||||
{%- set seo = load_data(path="data/seo.json") -%}
|
||||
{%- set lang = "da" -%}
|
||||
{%- set noindex = preview -%}
|
||||
{%- set alternates = [] -%}
|
||||
{%- set titel = "" -%}
|
||||
{%- set desc = config.description -%}
|
||||
{%- set sti = "/404" -%}
|
||||
{%- set artikel = false -%}
|
||||
{%- if page -%}{%- set meta = page -%}{%- elif section -%}{%- set meta = section -%}{%- endif -%}
|
||||
{%- if current_path -%}{%- set sti = current_path | trim_end(pat="/") -%}{%- endif -%}
|
||||
{%- if sti == "" -%}{%- set sti = "/" -%}{%- endif -%}
|
||||
{%- if meta -%}
|
||||
{%- if meta.title -%}{%- set titel = meta.title -%}{%- endif -%}
|
||||
{%- if meta.description -%}{%- set desc = meta.description -%}{%- endif -%}
|
||||
{%- if meta.extra.lang -%}{%- set lang = meta.extra.lang -%}{%- endif -%}
|
||||
{%- if meta.extra.noindex -%}{%- set noindex = true -%}{%- endif -%}
|
||||
{%- if meta.extra.alternates -%}{%- set alternates = meta.extra.alternates -%}{%- endif -%}
|
||||
{%- if page and page.date -%}{%- set artikel = true -%}{%- endif -%}
|
||||
{%- elif term -%}
|
||||
{%- set titel = "#" ~ term.name -%}{%- set desc = "Indlæg om " ~ term.name ~ "." -%}{%- set noindex = true -%}
|
||||
{%- elif taxonomy -%}
|
||||
{%- set titel = "Tags" -%}{%- set desc = "Alle emner på bloggen." -%}{%- set noindex = true -%}
|
||||
{%- else -%}
|
||||
{%- set titel = "404" -%}
|
||||
{%- endif -%}
|
||||
{%- if titel -%}{%- set sidetitel = titel ~ " · " ~ config.title -%}{%- else -%}{%- set sidetitel = config.title ~ " · Datalog & Technical Solution Architect" -%}{%- endif -%}
|
||||
{%- set kanonisk = config.extra.site ~ sti -%}
|
||||
<!doctype html>
|
||||
<html lang="{{ lang }}"{% if preview %} data-base="/udkast"{% endif %}>
|
||||
<head>
|
||||
{% include "partials/banner.html" %}
|
||||
{% include "partials/theme.html" %}
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>{{ sidetitel }}</title>
|
||||
<meta name="description" content="{{ desc }}" />
|
||||
<meta name="author" content="Asger Weirsøe" />
|
||||
<meta name="google-site-verification" content="l5LfnJZFSkVCauhwN9pND_gvxdnZJbx3FM6fpEK8nzE" />
|
||||
<meta name="robots" content="{% if noindex %}noindex, follow{% else %}index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1{% endif %}" />
|
||||
<link rel="canonical" href="{{ kanonisk | safe }}" />
|
||||
|
||||
<!-- Open Graph -->
|
||||
<meta property="og:type" content="{% if artikel %}article{% else %}website{% endif %}" />
|
||||
<meta property="og:site_name" content="{{ config.title }}" />
|
||||
<meta property="og:title" content="{{ sidetitel }}" />
|
||||
<meta property="og:description" content="{{ desc }}" />
|
||||
<meta property="og:url" content="{{ kanonisk | safe }}" />
|
||||
<meta property="og:locale" content="{% if lang == "en" %}en_GB{% else %}da_DK{% endif %}" />
|
||||
{%- for a in alternates %}
|
||||
<link rel="alternate" hreflang="{{ a.lang }}" href="{{ (config.extra.site ~ a.path) | safe }}" />
|
||||
{%- if a.lang == "da" %}
|
||||
<link rel="alternate" hreflang="x-default" href="{{ (config.extra.site ~ a.path) | safe }}" />
|
||||
{%- endif %}
|
||||
{%- endfor %}
|
||||
<meta property="og:image" content="{{ (config.extra.site ~ "/og.png") | safe }}" />
|
||||
<meta property="og:image:width" content="1200" />
|
||||
<meta property="og:image:height" content="630" />
|
||||
<meta property="og:image:alt" content="{{ sidetitel }}" />
|
||||
{%- if artikel %}
|
||||
<meta property="article:published_time" content="{{ page.date | date(format="%Y-%m-%dT%H:%M:%S.000Z") }}" />
|
||||
<meta property="article:modified_time" content="{{ (page.updated or page.date) | date(format="%Y-%m-%dT%H:%M:%S.000Z") }}" />
|
||||
<meta property="article:author" content="Asger Weirsøe" />
|
||||
{%- for t in page.taxonomies.tags or page.extra.tags or [] %}
|
||||
<meta property="article:tag" content="{{ t }}" />
|
||||
{%- endfor %}
|
||||
{%- endif %}
|
||||
|
||||
<!-- Twitter -->
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:title" content="{{ sidetitel }}" />
|
||||
<meta name="twitter:description" content="{{ desc }}" />
|
||||
<meta name="twitter:image" content="{{ (config.extra.site ~ "/og.png") | safe }}" />
|
||||
|
||||
<meta name="theme-color" content="#0b0f17" media="(prefers-color-scheme: dark)" />
|
||||
<meta name="theme-color" content="#f6f8fb" media="(prefers-color-scheme: light)" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||
<link rel="manifest" href="/site.webmanifest" />
|
||||
<link rel="sitemap" href="/sitemap-index.xml" />
|
||||
<link rel="alternate" type="application/rss+xml" title="{{ config.title }}" href="/rss.xml" />
|
||||
<link rel="me" href="https://mastodon.social/@asgerweirsoee" />
|
||||
<link rel="stylesheet" href="{{ get_url(path="css/site.css", cachebust=true) | replace(from=config.base_url, to="") | safe }}" />
|
||||
|
||||
{{ <ld g={seo.baseGraph} /> }}
|
||||
{%- block jsonld %}{% endblock %}
|
||||
|
||||
{% include "partials/matomo.html" %}
|
||||
<script type="speculationrules">{"prefetch":[{"where":{"href_matches":"/*"},"eagerness":"moderate"}]}</script>
|
||||
{%- if not preview %}
|
||||
<script src="/registerSW.js"></script>
|
||||
{%- endif %}
|
||||
</head>
|
||||
<body class="min-h-screen flex flex-col antialiased">
|
||||
{%- if preview %}
|
||||
<div class="sticky top-0 z-50 bg-amber-400 px-4 py-2 text-center text-sm font-semibold text-black">
|
||||
UDKAST, ikke udgivet. <a href="/udkast/" class="underline">Tilbage til godkendelse</a>
|
||||
</div>
|
||||
{%- endif %}
|
||||
{%- set nav = config.extra.nav_en if lang == "en" else config.extra.nav %}
|
||||
{%- set hjem = "/en" if lang == "en" else "/" %}
|
||||
{%- set andet = [a for a in alternates if a.lang != lang] | first %}
|
||||
{% include "partials/header.html" %}
|
||||
<main class="mx-auto w-full max-w-3xl flex-1 px-5 py-12">
|
||||
{% block main %}{% endblock %}
|
||||
</main>
|
||||
{% include "partials/footer.html" %}
|
||||
{% include "partials/palette.html" %}
|
||||
{% include "partials/cookie.html" %}
|
||||
{% include "partials/konami.html" %}
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,22 @@
|
||||
{% component ld(g) %}<script type="application/ld+json">{{ g | json_encode | safe }}</script>{% endcomponent ld %}
|
||||
|
||||
{% component faq(items) %}<script type="application/ld+json">{"@context":"https://schema.org","@type":"FAQPage","inLanguage":"da-DK","mainEntity":[{% for it in items %}{"@type":"Question","name":{{ it.q | json_encode | safe }},"acceptedAnswer":{"@type":"Answer","text":{{ it.a | json_encode | safe }}}}{% if not loop.last %},{% endif %}{% endfor %}]}</script>{% endcomponent faq %}
|
||||
|
||||
{% component crumbs(n1, u1, n2, u2, n3 = "", u3 = "") %}<script type="application/ld+json">{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":{{ n1 | json_encode | safe }},"item":{{ u1 | json_encode | safe }}},{"@type":"ListItem","position":2,"name":{{ n2 | json_encode | safe }},"item":{{ u2 | json_encode | safe }}}{% if n3 %},{"@type":"ListItem","position":3,"name":{{ n3 | json_encode | safe }},"item":{{ u3 | json_encode | safe }}}{% endif %}]}</script>{% endcomponent crumbs %}
|
||||
|
||||
{% component collection(url, name, description) %}<script type="application/ld+json">{"@context":"https://schema.org","@type":"CollectionPage","@id":{{ url | json_encode | safe }},"url":{{ url | json_encode | safe }},"name":{{ name | json_encode | safe }},"description":{{ description | json_encode | safe }},"inLanguage":"da-DK","isPartOf":{"@id":"https://asger.weirsoe.dk/#website"},"about":{"@id":"https://asger.weirsoe.dk/#person"}}</script>{% endcomponent collection %}
|
||||
|
||||
{% component postcard(p) %}
|
||||
<article class="group wt-card wt-card--hover">
|
||||
<a href="{{ p.path | trim_end(pat="/") | safe }}" class="block">
|
||||
<div class="text-xs text-muted">{{ p.date | date(format="d. MMMM y", locale="da") }}</div>
|
||||
<h3 class="mt-1 text-lg font-semibold tracking-tight group-hover:text-accent">{{ p.title }}</h3>
|
||||
{% if p.description %}<p class="mt-2 text-sm text-muted">{{ p.description }}</p>{% endif %}
|
||||
</a>
|
||||
{%- if p.taxonomies.tags %}
|
||||
<div class="mt-3 flex flex-wrap gap-1.5">
|
||||
{% for t in p.taxonomies.tags %}<a href="/tags/{{ t }}" class="wt-chip">#{{ t }}</a>{% endfor %}
|
||||
</div>
|
||||
{%- endif %}
|
||||
</article>
|
||||
{% endcomponent postcard %}
|
||||
@@ -0,0 +1,94 @@
|
||||
{% extends "base.html" %}
|
||||
{% block jsonld %}
|
||||
{%- set url = config.extra.site ~ (page.path | trim_end(pat="/")) -%}
|
||||
{%- set tags = page.taxonomies.tags or [] -%}
|
||||
<script type="application/ld+json">{"@context":"https://schema.org","@type":"BlogPosting","headline":{{ page.title | json_encode | safe }},"description":{{ (page.description or "") | json_encode | safe }},"url":{{ url | json_encode | safe }},"mainEntityOfPage":{{ url | json_encode | safe }},"datePublished":"{{ page.date | date(format="%Y-%m-%dT%H:%M:%S.000Z") }}","dateModified":"{{ (page.updated or page.date) | date(format="%Y-%m-%dT%H:%M:%S.000Z") }}","inLanguage":"da-DK","image":"https://asger.weirsoe.dk/og.png","keywords":{{ tags | join(sep=", ") | json_encode | safe }},"articleSection":{{ (tags[0] if tags else "Blog") | json_encode | safe }},"wordCount":{{ page.word_count }},"author":{"@id":"https://asger.weirsoe.dk/#person"},"publisher":{"@id":"https://asger.weirsoe.dk/#person"},"isPartOf":{"@id":"https://asger.weirsoe.dk/#website"}}</script>
|
||||
{{ <crumbs n1="Forside" u1={config.extra.site ~ "/"} n2="Blog" u2={config.extra.site ~ "/blog"} n3={page.title} u3={url} /> }}
|
||||
{% endblock %}
|
||||
{% block main %}
|
||||
<nav aria-label="Brødkrumme" class="text-sm text-muted">
|
||||
<a href="/" class="hover:text-accent">Forside</a>
|
||||
<span class="px-1.5 text-muted/60">/</span>
|
||||
<a href="/blog" class="hover:text-accent">Blog</a>
|
||||
</nav>
|
||||
<article class="mt-4">
|
||||
<header class="border-b border-border pb-6">
|
||||
<div class="text-sm text-muted">{{ page.date | date(format="d. MMMM y", locale="da") }}</div>
|
||||
<h1 class="mt-2 text-4xl font-bold tracking-tight">{{ page.title }}</h1>
|
||||
{%- if page.taxonomies.tags %}
|
||||
<div class="mt-4 flex flex-wrap gap-1.5">
|
||||
{% for t in page.taxonomies.tags %}<a href="/tags/{{ t }}" class="wt-chip">#{{ t }}</a>{% endfor %}
|
||||
</div>
|
||||
{%- endif %}
|
||||
</header>
|
||||
{#- Billeder i bundlen serveres som webp; markdown bruger bare . -#}
|
||||
{%- set indhold = page.content %}
|
||||
{%- for a in page.assets %}
|
||||
{%- if a is ending_with(pat=".png") or a is ending_with(pat=".jpg") or a is ending_with(pat=".jpeg") or a is ending_with(pat=".webp") or a is ending_with(pat=".avif") %}
|
||||
{%- set fil = a | split(pat="/") | last %}
|
||||
{%- set b = resize_image(path="blog/" ~ page.slug ~ "/" ~ fil, width=1200, height=10000, op="fit", format="webp", quality=80) %}
|
||||
{%- set_global indhold = indhold | replace(from='src="' ~ fil ~ '"', to='src="' ~ (b.url | replace(from=config.base_url, to="")) ~ '" width="' ~ b.width ~ '" height="' ~ b.height ~ '" loading="lazy" decoding="async"') %}
|
||||
{%- endif %}
|
||||
{%- endfor %}
|
||||
<div class="prose mt-8">{{ indhold | safe }}</div>
|
||||
</article>
|
||||
{%- if not preview %}
|
||||
<section class="mt-16 border-t border-border pt-8">
|
||||
<h2 class="text-2xl font-bold tracking-tight">Kommentarer</h2>
|
||||
<comentario-comments class="mt-6 block" live-update="true"></comentario-comments>
|
||||
</section>
|
||||
<script>
|
||||
(function () {
|
||||
if (!window.__comentarioLoaded) {
|
||||
window.__comentarioLoaded = true;
|
||||
var s = document.createElement('script');
|
||||
s.src = 'https://comentario.weircon.dk/comentario.js';
|
||||
s.defer = true;
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
{%- endif %}
|
||||
<script>
|
||||
// Mermaid-diagrammer: ```mermaid-blokke rendres client-side. Scriptet (self-hosted)
|
||||
// hentes KUN på indlæg der faktisk har diagrammer.
|
||||
(function () {
|
||||
var blocks = document.querySelectorAll('code[data-lang="mermaid"]');
|
||||
if (!blocks.length) return;
|
||||
var s = document.createElement('script');
|
||||
s.src = (document.documentElement.dataset.base || '') + '/js/mermaid.min.js';
|
||||
s.onload = async function () {
|
||||
mermaid.initialize({ startOnLoad: false,
|
||||
theme: document.documentElement.dataset.theme === 'light' ? 'default' : 'dark' });
|
||||
var i = 0;
|
||||
for (var code of blocks) {
|
||||
try {
|
||||
var r = await mermaid.render('mmd-' + i++, code.textContent);
|
||||
var el = document.createElement('div');
|
||||
el.className = 'mermaid-diagram';
|
||||
el.innerHTML = r.svg;
|
||||
code.closest('pre').replaceWith(el);
|
||||
} catch (_) { /* lad kodeblokken stå hvis diagrammet ikke parser */ }
|
||||
}
|
||||
};
|
||||
document.head.appendChild(s);
|
||||
})();
|
||||
</script>
|
||||
<script>
|
||||
(function () {
|
||||
document.querySelectorAll('.prose pre').forEach(function (pre) {
|
||||
if (pre.querySelector('.copy-btn')) return;
|
||||
var b = document.createElement('button');
|
||||
b.className = 'copy-btn'; b.type = 'button'; b.textContent = 'kopiér';
|
||||
b.addEventListener('click', function () {
|
||||
var code = pre.querySelector('code');
|
||||
navigator.clipboard.writeText(code ? code.innerText : pre.innerText).then(function () {
|
||||
b.textContent = 'kopieret ✓';
|
||||
setTimeout(function () { b.textContent = 'kopiér'; }, 1500);
|
||||
});
|
||||
});
|
||||
pre.appendChild(b);
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,10 @@
|
||||
<!--
|
||||
_ ____ ____ _____ ____
|
||||
/ \ / ___| / ___| ____| _ \
|
||||
/ _ \ \___ \| | _| _| | |_) |
|
||||
/ ___ \ ___) | |_| | |___| _ <
|
||||
/_/ \_\____/ \____|_____|_| \_\
|
||||
|
||||
Statisk Astro, self-hosted på Proxmox.
|
||||
Prøv Cmd/Ctrl+K.. eller den gamle kode: op op ned ned venstre højre venstre højre B A.
|
||||
-->
|
||||
@@ -0,0 +1,64 @@
|
||||
<div id="chat-log" class="mt-8 space-y-3" aria-live="polite"></div>
|
||||
|
||||
<form id="chat-form" class="mt-6 flex gap-3" data-thinking="{% if lang == "en" %}…thinking{% else %}…tænker{% endif %}" data-offline="{% if lang == "en" %}The chat is offline right now.. try again later.{% else %}Chatten er offline lige nu.. prøv senere.{% endif %}" data-unreachable="{% if lang == "en" %}Could not reach the chat.. try again in a moment.{% else %}Kunne ikke nå chatten.. prøv igen om lidt.{% endif %}">
|
||||
<input id="chat-input" type="text" required maxlength="500" autocomplete="off"
|
||||
placeholder="{% if lang == "en" %}Ask about experience, incident response, Peppol…{% else %}Spørg om erfaring, Peppol, homelab…{% endif %}"
|
||||
class="flex-1 wt-input wt-input--lg" />
|
||||
<button type="submit" class="wt-btn wt-btn--primary wt-btn--lg">Send</button>
|
||||
</form>
|
||||
<p class="mt-3 text-xs text-muted/80">{% if lang == "en" %}Answers are written by Claude (Anthropic) from my public CV.. check important details with me directly.{% else %}Svarene skrives af Claude (Anthropic) ud fra mit offentlige CV.. tjek vigtige detaljer med mig direkte.{% endif %}</p>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
var form = document.getElementById('chat-form');
|
||||
if (!form || form.__wired) return; form.__wired = true;
|
||||
var history = [];
|
||||
function esc(s) {
|
||||
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
}
|
||||
function linkify(text) {
|
||||
var html = esc(text);
|
||||
html = html.replace(/\b(https?:\/\/[^\s<]+[^\s<.,!?:;)])/g,
|
||||
'<a href="$1" target="_blank" rel="noopener" class="text-accent underline">$1</a>');
|
||||
html = html.replace(/\b([A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,})\b/g,
|
||||
'<a href="mailto:$1" class="text-accent underline">$1</a>');
|
||||
return html;
|
||||
}
|
||||
function track(action, q) {
|
||||
try { (window._paq = window._paq || []).push(['trackEvent', 'Chat', action, q]); } catch (_) {}
|
||||
}
|
||||
function bubble(role, text) {
|
||||
var log = document.getElementById('chat-log');
|
||||
var d = document.createElement('div');
|
||||
d.className = 'wt-card text-sm whitespace-pre-wrap ' + (role === 'user' ? 'wt-card--alt ml-8' : 'mr-8');
|
||||
if (role === 'user') d.textContent = text; else d.innerHTML = linkify(text);
|
||||
log.appendChild(d);
|
||||
d.scrollIntoView({ behavior: 'smooth', block: 'end' });
|
||||
return d;
|
||||
}
|
||||
form.addEventListener('submit', async function (e) {
|
||||
e.preventDefault();
|
||||
var input = document.getElementById('chat-input');
|
||||
var q = input.value.trim(); if (!q) return;
|
||||
input.value = '';
|
||||
bubble('user', q);
|
||||
history.push({ role: 'user', content: q });
|
||||
var wait = bubble('assistant', form.dataset.thinking);
|
||||
var btn = form.querySelector('button'); btn.disabled = true;
|
||||
try {
|
||||
var r = await fetch('/api/chat', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ messages: history.slice(-10) }),
|
||||
});
|
||||
var data = await r.json();
|
||||
if (!r.ok) { wait.textContent = data.error || form.dataset.offline; track('afvist', q); return; }
|
||||
wait.innerHTML = linkify(data.reply);
|
||||
history.push({ role: 'assistant', content: data.reply });
|
||||
track('besvaret', q);
|
||||
} catch (_) {
|
||||
wait.textContent = form.dataset.unreachable;
|
||||
track('fejl', q);
|
||||
} finally { btn.disabled = false; input.focus(); }
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
@@ -0,0 +1,40 @@
|
||||
<div
|
||||
id="cookie-banner"
|
||||
hidden
|
||||
class="fixed inset-x-0 bottom-0 z-[60] px-4 pb-4"
|
||||
role="dialog"
|
||||
aria-live="polite"
|
||||
aria-label="{% if lang == "en" %}Cookie notice{% else %}Cookie-besked{% endif %}"
|
||||
>
|
||||
<div class="mx-auto flex max-w-3xl flex-col gap-3 wt-card wt-card--raised sm:flex-row sm:items-center sm:justify-between">
|
||||
<p class="text-sm text-muted">
|
||||
{% if lang == "en" %}This site uses cookies to improve your experience and keep anonymous visitor statistics. By using the site you accept this.{% else %}Denne side bruger cookies til at forbedre din oplevelse og lave anonym statistik over besøg. Ved at bruge siden accepterer du det.{% endif %}
|
||||
<a href="/cookies" class="text-accent underline">{% if lang == "en" %}Read more (Danish){% else %}Læs mere{% endif %}</a>.
|
||||
</p>
|
||||
<button
|
||||
id="cookie-ok"
|
||||
class="shrink-0 self-start wt-btn wt-btn--primary sm:self-auto"
|
||||
>OK</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
var KEY = 'wec_cookie_ack';
|
||||
function wire() {
|
||||
var b = document.getElementById('cookie-banner');
|
||||
if (!b) return;
|
||||
try { if (localStorage.getItem(KEY)) { b.hidden = true; return; } } catch (_) {}
|
||||
b.hidden = false;
|
||||
var ok = document.getElementById('cookie-ok');
|
||||
if (ok && !ok.__wired) {
|
||||
ok.__wired = true;
|
||||
ok.addEventListener('click', function () {
|
||||
try { localStorage.setItem(KEY, '1'); } catch (_) {}
|
||||
b.hidden = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
wire();
|
||||
})();
|
||||
</script>
|
||||
@@ -0,0 +1,17 @@
|
||||
<footer class="mt-24 border-t border-border/70">
|
||||
<p class="mx-auto max-w-3xl px-5 pt-6 text-center text-xs text-muted/70 sm:text-left">
|
||||
<span class="font-mono">{% if lang == "en" %}static html · no js frameworks · self-hosted on proxmox · works offline{% else %}statisk html · ingen js-frameworks · self-hosted på proxmox · virker offline{% endif %}</span>
|
||||
</p>
|
||||
<div class="mx-auto flex max-w-3xl flex-col items-center gap-3 px-5 py-6 text-sm text-muted sm:flex-row sm:justify-between">
|
||||
<p>© {{ now() | date(format="%Y") }} {{ config.title }}</p>
|
||||
<div class="flex flex-wrap justify-center gap-4">
|
||||
{%- for s in config.extra.socials %}
|
||||
<a class="transition-colors hover:text-accent" href="{{ s.href | safe }}" rel="me noopener" target="_blank">{{ s.label }}</a>
|
||||
{%- endfor %}
|
||||
<a class="transition-colors hover:text-accent" href="/rss.xml">RSS</a>
|
||||
<a class="transition-colors hover:text-accent" href="/uses">Uses</a>
|
||||
<a class="transition-colors hover:text-accent" href="/colophon">{% if lang == "en" %}Colophon{% else %}Kolofon{% endif %}</a>
|
||||
<a class="transition-colors hover:text-accent" href="/cookies">Cookies</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
@@ -0,0 +1,42 @@
|
||||
<header class="sticky top-0 z-50 border-b border-border/70 backdrop-blur-md bg-bg/70">
|
||||
<nav class="mx-auto flex max-w-3xl items-center justify-between px-5 py-3.5">
|
||||
<a href="{{ hjem }}" class="font-bold tracking-tight text-lg">{{ config.title }}<span class="text-accent">.</span></a>
|
||||
<div class="flex items-center gap-1">
|
||||
<ul class="hidden sm:flex items-center gap-1 text-sm">
|
||||
{%- for item in nav %}
|
||||
<li>
|
||||
<a href="{{ item.href | safe }}" class="wt-nav-link"{% if item.href == "/" or item.href == "/en" %}{% if sti == item.href %} aria-current="page"{% endif %}{% elif sti is starting_with(pat=item.href) %} aria-current="page"{% endif %}>{{ item.label }}</a>
|
||||
</li>
|
||||
{%- endfor %}
|
||||
</ul>
|
||||
{%- if andet %}
|
||||
<a href="{{ andet.path | safe }}" hreflang="{{ andet.lang }}" lang="{{ andet.lang }}" class="wt-chip mr-1">{% if andet.lang == "en" %}English{% else %}Dansk{% endif %}</a>
|
||||
{%- endif %}
|
||||
<button type="button" data-palette-open class="theme-toggle" title="Søg (⌘K / Ctrl K)" aria-label="Åbn søgning og kommandopalette">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<circle cx="11" cy="11" r="7" />
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65" />
|
||||
</svg>
|
||||
</button>
|
||||
<button type="button" data-theme-toggle class="theme-toggle ml-1" title="Skift mellem lyst og mørkt tema" aria-label="Skift tema">
|
||||
<svg class="sun-and-moon" aria-hidden="true" width="22" height="22" viewBox="0 0 24 24">
|
||||
<mask class="moon" id="moon-mask">
|
||||
<rect x="0" y="0" width="100%" height="100%" fill="white" />
|
||||
<circle cx="24" cy="10" r="6" fill="black" />
|
||||
</mask>
|
||||
<circle class="sun" cx="12" cy="12" r="6" mask="url(#moon-mask)" fill="currentColor" />
|
||||
<g class="sun-beams" stroke="currentColor">
|
||||
<line x1="12" y1="1" x2="12" y2="3" />
|
||||
<line x1="12" y1="21" x2="12" y2="23" />
|
||||
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64" />
|
||||
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
|
||||
<line x1="1" y1="12" x2="3" y2="12" />
|
||||
<line x1="21" y1="12" x2="23" y2="12" />
|
||||
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36" />
|
||||
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
|
||||
</g>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
@@ -0,0 +1,19 @@
|
||||
<script>
|
||||
// Konami-kode → terminal-tema
|
||||
(() => {
|
||||
if (window.__konami) return; window.__konami = true;
|
||||
const seq = ['ArrowUp','ArrowUp','ArrowDown','ArrowDown','ArrowLeft','ArrowRight','ArrowLeft','ArrowRight','b','a'];
|
||||
let pos = 0;
|
||||
document.addEventListener('keydown', (e) => {
|
||||
pos = e.key === seq[pos] ? pos + 1 : (e.key === seq[0] ? 1 : 0);
|
||||
if (pos !== seq.length) return;
|
||||
pos = 0;
|
||||
document.documentElement.dataset.theme = 'terminal';
|
||||
localStorage.setItem('theme', 'terminal');
|
||||
document.body.animate(
|
||||
[{ transform: 'rotate(0)' }, { transform: 'rotate(360deg)' }],
|
||||
{ duration: 900, easing: 'ease-in-out' });
|
||||
console.log('%c> adgang givet. velkommen til terminalen.', 'color:#4bff7e;font-family:monospace;font-size:14px');
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
@@ -0,0 +1,38 @@
|
||||
<script>
|
||||
var _paq = (window._paq = window._paq || []);
|
||||
_paq.push(['setDomains', ['*.weirsoe.dk', '*.xn--weirse-fya.dk']]);
|
||||
_paq.push(['setVisitorCookieTimeout', 31536000]);
|
||||
_paq.push(['enableLinkTracking']);
|
||||
(function () {
|
||||
try {
|
||||
var w = screen.width, h = screen.height, dpr = Math.round(window.devicePixelRatio || 1);
|
||||
var k = Math.min(w, h) + 'x' + Math.max(w, h) + '@' + dpr;
|
||||
var ua = navigator.userAgent || '';
|
||||
var isApple = /iPhone|iPad|iPod/.test(ua) || (/Macintosh/.test(ua) && navigator.maxTouchPoints > 1);
|
||||
var iphone = {
|
||||
'320x568@2': 'iPhone SE(1)/5s',
|
||||
'375x667@2': 'iPhone SE(2/3)/6/7/8',
|
||||
'414x736@3': 'iPhone 6+/7+/8+ Plus',
|
||||
'375x812@3': 'iPhone X/XS/11Pro/12mini/13mini',
|
||||
'390x844@3': 'iPhone 12/13/14/16e (6,1 tomme notch)',
|
||||
'393x852@3': 'iPhone 14Pro/15/15Pro/16',
|
||||
'402x874@3': 'iPhone 16Pro',
|
||||
'414x896@2': 'iPhone XR/11',
|
||||
'414x896@3': 'iPhone XSMax/11ProMax',
|
||||
'428x926@3': 'iPhone 12ProMax/13ProMax/14Plus',
|
||||
'430x932@3': 'iPhone 14ProMax/15Plus/15ProMax/16Plus',
|
||||
'440x956@3': 'iPhone 16ProMax'
|
||||
};
|
||||
var model = iphone[k] ? iphone[k] : (isApple ? ('Apple ukendt ' + k) : ('Andet ' + k + ' ' + (navigator.platform || '?')));
|
||||
_paq.push(['setCustomDimension', 2, model]);
|
||||
} catch (e) {}
|
||||
})();
|
||||
_paq.push(['trackPageView']);
|
||||
(function () {
|
||||
var u = '/_a/';
|
||||
_paq.push(['setTrackerUrl', u + 'a']);
|
||||
_paq.push(['setSiteId', '2']);
|
||||
var d = document, g = d.createElement('script'), s = d.getElementsByTagName('script')[0];
|
||||
g.async = true; g.src = u + 'a.js'; s.parentNode.insertBefore(g, s);
|
||||
})();
|
||||
</script>
|
||||
@@ -0,0 +1,98 @@
|
||||
<div id="palette" class="palette-overlay" hidden>
|
||||
<div class="palette-box" role="dialog" aria-modal="true" aria-label="Kommandopalette">
|
||||
<input id="palette-input" class="palette-input" type="text" placeholder="Søg sider, indlæg eller kommandoer…"
|
||||
autocomplete="off" spellcheck="false" />
|
||||
<ul id="palette-list" class="palette-list" role="listbox"></ul>
|
||||
<div class="palette-hint">↑↓ navigér · ↵ vælg · esc luk</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
if (window.__palette) return; window.__palette = true;
|
||||
let index = null, items = [], sel = 0;
|
||||
const base = document.documentElement.dataset.base || '';
|
||||
|
||||
const staticActions = [
|
||||
{ type: 'action', title: 'Tema: mørkt', run: () => setTheme('dark') },
|
||||
{ type: 'action', title: 'Tema: lyst', run: () => setTheme('light') },
|
||||
{ type: 'action', title: 'Tema: terminal', run: () => setTheme('terminal') },
|
||||
{ type: 'action', title: 'Kopiér email (asger@weirsoe.dk)', run: () => navigator.clipboard?.writeText('asger@weirsoe.dk') },
|
||||
{ type: 'link', title: 'LinkedIn ↗', url: 'https://www.linkedin.com/in/ag-weirs%C3%B8e/', ext: true },
|
||||
{ type: 'link', title: 'Mastodon ↗', url: 'https://mastodon.social/@asgerweirsoee', ext: true },
|
||||
{ type: 'link', title: 'Gitea ↗', url: 'https://gitea.weircon.dk/agw', ext: true },
|
||||
{ type: 'link', title: 'RSS-feed', url: '/rss.xml' },
|
||||
];
|
||||
function setTheme(t) {
|
||||
document.documentElement.dataset.theme = t;
|
||||
localStorage.setItem('theme', t);
|
||||
}
|
||||
const el = (id) => document.getElementById(id);
|
||||
|
||||
async function ensureIndex() {
|
||||
if (index) return;
|
||||
try { index = await (await fetch(base + '/search-index.json')).json(); } catch { index = []; }
|
||||
}
|
||||
function score(q, it) {
|
||||
const hay = (it.title + ' ' + (it.extra || '')).toLowerCase();
|
||||
if (!q) return it.type === 'page' || it.type === 'action' ? 1 : 0.5;
|
||||
if (hay.startsWith(q)) return 3;
|
||||
if (hay.includes(q)) return 2;
|
||||
// simpel subsekvens-match (fzf-agtig)
|
||||
let i = 0; for (const c of hay) if (c === q[i]) i++;
|
||||
return i === q.length ? 1 : 0;
|
||||
}
|
||||
function refresh() {
|
||||
const q = el('palette-input').value.trim().toLowerCase();
|
||||
items = [...(index || []), ...staticActions]
|
||||
.map((it) => ({ it, s: score(q, it) }))
|
||||
.filter((x) => x.s > 0)
|
||||
.sort((a, b) => b.s - a.s)
|
||||
.slice(0, 9)
|
||||
.map((x) => x.it);
|
||||
sel = 0;
|
||||
el('palette-list').innerHTML = items.map((it, i) => {
|
||||
const badge = it.type === 'post' ? 'blog' : it.type === 'action' ? 'kommando' : it.ext ? 'link' : 'side';
|
||||
return `<li class="palette-item${i === sel ? ' is-selected' : ''}" data-i="${i}" role="option">
|
||||
<span>${it.title.replace(/</g, '<')}</span><span class="palette-badge">${badge}</span></li>`;
|
||||
}).join('') || '<li class="palette-item palette-empty">Ingen resultater</li>';
|
||||
}
|
||||
function paint() {
|
||||
el('palette-list').querySelectorAll('.palette-item').forEach((li, i) =>
|
||||
li.classList.toggle('is-selected', i === sel));
|
||||
}
|
||||
function choose(i) {
|
||||
const it = items[i]; if (!it) return;
|
||||
close();
|
||||
if (it.run) it.run();
|
||||
else if (it.ext) window.open(it.url, '_blank', 'noopener');
|
||||
else window.location.href = base + it.url;
|
||||
}
|
||||
async function open() {
|
||||
await ensureIndex();
|
||||
el('palette').hidden = false;
|
||||
el('palette-input').value = '';
|
||||
refresh();
|
||||
el('palette-input').focus();
|
||||
}
|
||||
function close() { const p = el('palette'); if (p) p.hidden = true; }
|
||||
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') { e.preventDefault(); el('palette')?.hidden ? open() : close(); return; }
|
||||
const p = el('palette');
|
||||
if (!p || p.hidden) return;
|
||||
if (e.key === 'Escape') close();
|
||||
else if (e.key === 'ArrowDown') { e.preventDefault(); sel = Math.min(sel + 1, items.length - 1); paint(); }
|
||||
else if (e.key === 'ArrowUp') { e.preventDefault(); sel = Math.max(sel - 1, 0); paint(); }
|
||||
else if (e.key === 'Enter') { e.preventDefault(); choose(sel); }
|
||||
});
|
||||
document.addEventListener('input', (e) => { if (e.target?.id === 'palette-input') refresh(); });
|
||||
document.addEventListener('click', (e) => {
|
||||
const trigger = e.target.closest?.('[data-palette-open]');
|
||||
if (trigger) { e.preventDefault(); el('palette')?.hidden ? open() : close(); return; }
|
||||
if (e.target === el('palette')) close();
|
||||
const li = e.target.closest?.('.palette-item[data-i]');
|
||||
if (li) choose(+li.dataset.i);
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
@@ -0,0 +1,3 @@
|
||||
{%- set a1 = resize_image(path="asger.png", width=260, op="fit_width", format="webp", quality=80) -%}
|
||||
{%- set a2 = resize_image(path="asger.png", width=520, op="fit_width", format="webp", quality=80) -%}
|
||||
<img src="{{ a1.url | replace(from=config.base_url, to="") | safe }}" srcset="{{ a1.url | replace(from=config.base_url, to="") | safe }} 1x, {{ a2.url | replace(from=config.base_url, to="") | safe }} 2x" alt="Asger Weirsøe" loading="eager" decoding="async" width="{{ a1.width }}" height="{{ a1.height }}" class="mx-auto h-44 w-44 rounded-2xl border border-border object-cover shadow-xl sm:h-56 sm:w-56" />
|
||||
@@ -0,0 +1,17 @@
|
||||
<script>
|
||||
(() => {
|
||||
const apply = () => {
|
||||
const t = localStorage.getItem('theme') ||
|
||||
(window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark');
|
||||
document.documentElement.dataset.theme = t;
|
||||
};
|
||||
apply();
|
||||
document.addEventListener('click', (e) => {
|
||||
const btn = e.target.closest('[data-theme-toggle]');
|
||||
if (!btn) return;
|
||||
const next = document.documentElement.dataset.theme === 'light' ? 'dark' : 'light';
|
||||
document.documentElement.dataset.theme = next;
|
||||
localStorage.setItem('theme', next);
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
@@ -0,0 +1,45 @@
|
||||
{% extends "base.html" %}
|
||||
{% block jsonld %}
|
||||
{%- set en = page.extra.lang == "en" -%}
|
||||
{%- set url = config.extra.site ~ (page.path | trim_end(pat="/")) -%}
|
||||
{%- set repo = "https://gitea.weircon.dk/agw/" ~ page.extra.repo -%}
|
||||
<script type="application/ld+json">{"@context":"https://schema.org","@type":"SoftwareSourceCode","@id":{{ (url ~ "#software") | json_encode | safe }},"url":{{ url | json_encode | safe }},"name":{{ page.title | json_encode | safe }},"description":{{ page.description | json_encode | safe }},"codeRepository":{{ repo | json_encode | safe }},"programmingLanguage":{{ page.extra.sprog | json_encode | safe }},"license":{{ page.extra.licens | json_encode | safe }},"runtimePlatform":{{ page.extra.platforme | json_encode | safe }},"sameAs":[{% for n in page.extra.aur %}"https://aur.archlinux.org/packages/{{ n }}"{% if not loop.last %},{% endif %}{% endfor %}],"dateCreated":"{{ page.date | date(format="%Y-%m-%dT%H:%M:%S.000Z") }}","dateModified":"{{ (page.updated or page.date) | date(format="%Y-%m-%dT%H:%M:%S.000Z") }}","keywords":{{ page.extra.tags | json_encode | safe }},"inLanguage":"{% if en %}en{% else %}da-DK{% endif %}","author":{"@id":"https://asger.weirsoe.dk/#person"},"maintainer":{"@id":"https://asger.weirsoe.dk/#person"}}</script>
|
||||
{%- if en %}
|
||||
{{ <crumbs n1="Home" u1={config.extra.site ~ "/en"} n2="Projects" u2={config.extra.site ~ "/en/projects"} n3={page.title} u3={url} /> }}
|
||||
{%- else %}
|
||||
{{ <crumbs n1="Forside" u1={config.extra.site ~ "/"} n2="Projekter" u2={config.extra.site ~ "/projekter"} n3={page.title} u3={url} /> }}
|
||||
{%- endif %}
|
||||
{% endblock %}
|
||||
{% block main %}
|
||||
{%- set en = page.extra.lang == "en" -%}
|
||||
<div class="text-sm text-muted">
|
||||
<nav aria-label="{% if en %}Breadcrumb{% else %}Brødkrumme{% endif %}">
|
||||
<a href="{% if en %}/en{% else %}/{% endif %}" class="hover:text-accent">{% if en %}Home{% else %}Forside{% endif %}</a>
|
||||
<span class="px-1.5 text-muted/60">/</span>
|
||||
<a href="{% if en %}/en/projects{% else %}/projekter{% endif %}" class="hover:text-accent">{% if en %}Projects{% else %}Projekter{% endif %}</a>
|
||||
</nav>
|
||||
</div>
|
||||
<article class="mt-4">
|
||||
<header class="border-b border-border pb-6">
|
||||
<h1 class="text-4xl font-bold tracking-tight">{{ page.title }}</h1>
|
||||
<p class="mt-3 max-w-2xl text-lg text-muted">{{ page.description }}</p>
|
||||
<dl class="mt-5 flex flex-wrap gap-x-6 gap-y-2 text-sm">
|
||||
<div><dt class="inline text-muted">{% if en %}Language{% else %}Sprog{% endif %} </dt><dd class="inline">{{ page.extra.sprog }}</dd></div>
|
||||
<div><dt class="inline text-muted">{% if en %}Licence{% else %}Licens{% endif %} </dt><dd class="inline">{{ page.extra.licens }}</dd></div>
|
||||
{%- if page.extra.platforme %}
|
||||
<div><dt class="inline text-muted">{% if en %}Runs on{% else %}Kører på{% endif %} </dt><dd class="inline">{{ page.extra.platforme | join(sep=", ") }}</dd></div>
|
||||
{%- endif %}
|
||||
</dl>
|
||||
<div class="mt-5 flex flex-wrap gap-2">
|
||||
<a href="https://gitea.weircon.dk/agw/{{ page.extra.repo }}" class="wt-chip">{% if en %}Source on Gitea{% else %}Kildekode på Gitea{% endif %}</a>
|
||||
{%- for n in page.extra.aur %}
|
||||
<a href="https://aur.archlinux.org/packages/{{ n }}" class="wt-chip">AUR: {{ n }}</a>
|
||||
{%- endfor %}
|
||||
{%- if [n for n in page.extra.aur if n is ending_with(pat="-bin")] %}
|
||||
<a href="/tarballz/" class="wt-chip">{% if en %}Prebuilt packages{% else %}Færdigbyggede pakker{% endif %}</a>
|
||||
{%- endif %}
|
||||
</div>
|
||||
</header>
|
||||
<div class="prose mt-8">{{ page.content | safe }}</div>
|
||||
</article>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"><channel><title>{{ config.title }}</title><description>{{ config.description }}</description><link>{{ config.extra.site | safe }}</link><language>da-dk</language>
|
||||
{%- for p in pages %}{% if p.components[0] == "blog" %}<item><title>{{ p.title }}</title><link>{{ config.extra.site ~ p.path | trim_end(pat="/") | safe }}</link><guid isPermaLink="true">{{ config.extra.site ~ p.path | trim_end(pat="/") | safe }}</guid><description>{{ p.description }}</description><pubDate>{{ p.date | date(format="%a, %d %b %Y %H:%M:%S GMT") }}</pubDate></item>{% endif %}{% endfor %}</channel></rss>
|
||||
@@ -0,0 +1,4 @@
|
||||
[{"type":"page","title":"Forside","url":"/"},{"type":"page","title":"CV","url":"/cv"},{"type":"page","title":"CV (English)","url":"/en/cv"},{"type":"page","title":"Home (English)","url":"/en"},{"type":"page","title":"Ask my CV (English)","url":"/en/chat"},{"type":"page","title":"Projekter","url":"/projekter"},
|
||||
{%- for p in pages %}{% if p.components[0] == "projekter" %}{"type":"page","title":{{ ("Projekt: " ~ p.title) | json_encode | safe }},"url":"{{ p.path | trim_end(pat="/") }}","extra":{{ [p.extra.sprog, ...p.extra.aur, ...p.extra.tags] | join(sep=" ") | json_encode | safe }}},{% elif p.components[0] == "en" %}{"type":"page","title":{{ ("Project: " ~ p.title ~ " (English)") | json_encode | safe }},"url":"{{ p.path | trim_end(pat="/") }}","extra":{{ [p.extra.sprog, ...p.extra.aur, ...p.extra.tags] | join(sep=" ") | json_encode | safe }}},{% endif %}{% endfor -%}
|
||||
{"type":"page","title":"Færdigbyggede pakker (tarballz)","url":"/tarballz/"},{"type":"page","title":"Blog","url":"/blog"},{"type":"page","title":"Tags","url":"/tags"},{"type":"page","title":"NemHandel-opslag","url":"/nemhandel"},{"type":"page","title":"Profil-opslag (Nemhandel & Peppol)","url":"/profiler"},{"type":"page","title":"UBL-tjek & identifikator-afkoder","url":"/ubl"},{"type":"page","title":"Spørg mit CV (AI-chat)","url":"/chat"},{"type":"page","title":"Status (homelab)","url":"/status"},{"type":"page","title":"Now","url":"/now"},{"type":"page","title":"Uses","url":"/uses"},{"type":"page","title":"Kolofon","url":"/colophon"},{"type":"page","title":"Cookies","url":"/cookies"}
|
||||
{%- for p in pages %}{% if p.components[0] == "blog" %},{"type":"post","title":{{ p.title | json_encode | safe }},"url":"{{ p.path | trim_end(pat="/") }}","extra":{{ p.taxonomies.tags | default(value=[]) | join(sep=" ") | json_encode | safe }}}{% endif %}{% endfor %}]
|
||||
@@ -0,0 +1,17 @@
|
||||
{% extends "base.html" %}
|
||||
{% block main %}
|
||||
<div class="flex items-baseline justify-between">
|
||||
<h1 class="text-4xl font-bold tracking-tight">Blog</h1>
|
||||
<a href="/tags" class="text-sm text-accent hover:underline">Tags</a>
|
||||
</div>
|
||||
<div class="mt-8 grid gap-4">
|
||||
{% for p in section.pages %}
|
||||
{{ <postcard p={p} /> }}
|
||||
{% endfor %}
|
||||
{% if not section.pages %}<p class="text-muted">Ingen indlæg endnu.</p>{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block jsonld %}
|
||||
{{ <collection url={config.extra.site ~ "/blog"} name="Blog · Asger Weirsøe" description="Indlæg om selvhosting, Proxmox, AI og udvikling." /> }}
|
||||
{{ <crumbs n1="Forside" u1={config.extra.site ~ "/"} n2="Blog" u2={config.extra.site ~ "/blog"} /> }}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,11 @@
|
||||
{% extends "base.html" %}
|
||||
{% block main %}
|
||||
<h1 class="text-4xl font-bold tracking-tight">Ask my CV</h1>
|
||||
<p class="mt-4 max-w-2xl text-muted">
|
||||
A small chat with <strong class="text-text">Claude</strong> from Anthropic. It has read my CV and this
|
||||
site and answers questions about me.. try <em>“what incident response experience does Asger have?”</em>
|
||||
or <em>“what runs in his homelab?”</em>. It only answers questions about me.
|
||||
</p>
|
||||
|
||||
{% include "partials/chat.html" %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,11 @@
|
||||
{% extends "base.html" %}
|
||||
{% block main %}
|
||||
<h1 class="text-4xl font-bold tracking-tight">Spørg mit CV</h1>
|
||||
<p class="mt-4 max-w-2xl text-muted">
|
||||
En lille chat med <strong class="text-text">Claude</strong> fra Anthropic, der har læst mit CV og denne
|
||||
side og svarer på spørgsmål om mig.. fx <em>“hvad har Asger lavet med Peppol?”</em> eller
|
||||
<em>“hvilken stack kører hans homelab?”</em>. Den svarer kun på spørgsmål om mig.
|
||||
</p>
|
||||
|
||||
{% include "partials/chat.html" %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,34 @@
|
||||
{% extends "base.html" %}
|
||||
{% block main %}
|
||||
{% set d = load_data(path="data/colophon.json") %}
|
||||
<h1 class="text-4xl font-bold tracking-tight">Kolofon</h1>
|
||||
<p class="mt-4 max-w-2xl text-muted">
|
||||
Hele kæden fra commit til din skærm kører på udstyr jeg selv drifter. Ingen CDN’er,
|
||||
ingen tredjeparts-fonts, ingen eksterne trackere.. bare en statisk side der gør hvad den får besked på.
|
||||
</p>
|
||||
|
||||
<div class="mt-8 wt-card wt-card--flush">
|
||||
<table class="w-full text-sm">
|
||||
<tbody>
|
||||
{% for row in d.stack %}
|
||||
<tr class="border-b border-border/60 last:border-0">
|
||||
<td class="w-40 bg-surface/60 px-4 py-3 font-semibold align-top">{{ row.k }}</td>
|
||||
<td class="px-4 py-3 text-muted">{{ row.v }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<section class="mt-10">
|
||||
<h2 class="text-sm font-semibold uppercase tracking-widest text-accent">Sikkerhed</h2>
|
||||
<p class="mt-3 max-w-2xl text-sm text-muted">
|
||||
Fundet noget der ser forkert ud? Skriv til <a class="text-accent hover:underline" href="mailto:asger@weirsoe.dk">asger@weirsoe.dk</a>.
|
||||
Der ligger også en <a class="text-accent hover:underline" href="/.well-known/security.txt">security.txt</a> (RFC 9116).
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<p class="mt-10 text-sm text-muted">
|
||||
Se også <a class="text-accent hover:underline" href="/uses">uses</a>.
|
||||
</p>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,41 @@
|
||||
{% extends "base.html" %}
|
||||
{% block main %}
|
||||
<h1 class="text-4xl font-bold tracking-tight">Cookies</h1>
|
||||
<p class="mt-4 max-w-2xl text-muted">
|
||||
Denne side bruger cookies til at forbedre din oplevelse og lave anonym statistik over,
|
||||
hvordan siden bruges. <strong class="text-text">Ved at bruge siden accepterer du, at der
|
||||
sættes cookies.</strong> Du kan altid slette dem igen i din browser.
|
||||
</p>
|
||||
|
||||
<h2 class="mt-10 text-2xl font-bold tracking-tight">Hvad bruger vi dem til?</h2>
|
||||
<div class="mt-5 grid gap-4">
|
||||
<div class="wt-card">
|
||||
<h3 class="font-semibold tracking-tight">Statistik & analyse</h3>
|
||||
<p class="mt-2 text-sm text-muted">
|
||||
Jeg bruger <strong class="text-text">Matomo</strong> til at se hvor mange der besøger siden,
|
||||
og hvordan den bruges, så jeg kan gøre den bedre. Matomo kører på mine egne servere, dine
|
||||
data bliver <strong class="text-text">ikke delt med tredjeparter</strong> og ikke solgt.
|
||||
</p>
|
||||
<ul class="mt-3 space-y-1 text-sm text-muted">
|
||||
<li><code class="rounded bg-surface-2 px-1.5 py-0.5">_pk_id</code>: et tilfældigt id der tæller dig som samme besøgende over tid (op til 1 år).</li>
|
||||
<li><code class="rounded bg-surface-2 px-1.5 py-0.5">_pk_ses</code>: markerer dit aktuelle besøg (udløber efter ca. 30 minutter).</li>
|
||||
<li><code class="rounded bg-surface-2 px-1.5 py-0.5">WEC_VID</code>: et tilsvarende besøgs-id på mine egne tjenester.</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="wt-card">
|
||||
<h3 class="font-semibold tracking-tight">Funktionelt</h3>
|
||||
<p class="mt-2 text-sm text-muted">
|
||||
Husker dine valg på siden. Det er ikke sporing, og det forlader aldrig din browser.
|
||||
</p>
|
||||
<ul class="mt-3 space-y-1 text-sm text-muted">
|
||||
<li><code class="rounded bg-surface-2 px-1.5 py-0.5">theme</code>: dit valgte tema (lyst/mørkt/terminal).</li>
|
||||
<li><code class="rounded bg-surface-2 px-1.5 py-0.5">wec_cookie_ack</code>: husker at du har set denne cookie-besked, så den ikke dukker op igen.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="mt-8 text-sm text-muted">
|
||||
Spørgsmål? Skriv til <a href="mailto:asger@weirsoe.dk" class="text-accent underline">asger@weirsoe.dk</a>.
|
||||
</p>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,142 @@
|
||||
{% extends "base.html" %}
|
||||
{% block main %}
|
||||
{% set d = load_data(path="data/cv-en.json") %}
|
||||
<h1 class="text-4xl font-bold tracking-tight">CV</h1>
|
||||
<div class="mt-3 flex flex-wrap gap-x-4 gap-y-1 text-sm text-muted">
|
||||
<span>Aalborg, Denmark</span>
|
||||
<a class="text-accent hover:underline" href="mailto:asger@weirsoe.dk">asger@weirsoe.dk</a>
|
||||
<a class="text-accent hover:underline" href="https://www.linkedin.com/in/ag-weirs%C3%B8e/">LinkedIn</a>
|
||||
</div>
|
||||
|
||||
<p class="mt-6 max-w-2xl text-muted">
|
||||
Technical Solution Architect who has worked on Danish national e-invoicing infrastructure
|
||||
since 2021. I have led responses to production outages and security vulnerabilities,
|
||||
coordinating engineers, hosting providers and the client, and I built an
|
||||
information security management system around ISO/IEC 27001 with procedures for incident
|
||||
response, escalation and post-incident review. M.Sc. in Computer Science, specialising in
|
||||
security in distributed systems, and currently studying business administration alongside work.
|
||||
</p>
|
||||
|
||||
<section class="mt-10">
|
||||
<h2 class="text-sm font-semibold uppercase tracking-widest text-accent">Areas of expertise</h2>
|
||||
<p class="mt-4 max-w-2xl text-muted">
|
||||
I work with <strong class="text-text">electronic invoicing</strong> and e-procurement. At CGI I helped
|
||||
develop the open source eDelivery solution for NemHandel, built on the open source access point
|
||||
<strong class="text-text">Oxalis</strong> (Java). Today I am a Technical Solution Architect at Digisense A/S,
|
||||
working on solution architecture in the same field: <strong class="text-text">Peppol</strong> (BIS,
|
||||
Access Point and eDelivery/AS4), <strong class="text-text">NemHandel</strong>,
|
||||
<strong class="text-text">OIOUBL/UBL</strong> and the European e-invoicing standard EN 16931.
|
||||
</p>
|
||||
<div class="mt-4 flex flex-wrap gap-2">
|
||||
{% for k in ["Peppol", "Peppol Access Point", "Oxalis", "NemHandel", "E-invoicing", "OIOUBL / UBL", "eDelivery (AS4)", "EN 16931", "E-procurement"] %}
|
||||
<span class="wt-tag">{{ k }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="mt-10">
|
||||
<h2 class="text-sm font-semibold uppercase tracking-widest text-accent">Incident response and compliance</h2>
|
||||
<p class="mt-4 max-w-2xl text-muted">
|
||||
When something breaks in production, the technical fix is only half of it. The other half is
|
||||
getting the right people moving, keeping the client informed and learning from it afterwards.
|
||||
During the <strong class="text-text">July 2024 CrowdStrike outage</strong> a database failure hit our
|
||||
hosted production platform just as most of the team’s workstations went down. As tech lead on
|
||||
CGI’s NemHandel team I coordinated by phone with the hosting provider and the one developer whose
|
||||
machine still worked, and the database was restored that afternoon. When the critical SnakeYAML
|
||||
vulnerability <strong class="text-text">CVE-2022-1471</strong> affected the NemHandel reference
|
||||
implementation, we notified the client immediately, mitigation guidance went out to operators,
|
||||
and we shipped a fixed release within two weeks.
|
||||
</p>
|
||||
<p class="mt-3 max-w-2xl text-muted">
|
||||
At Digisense I built the information security management system around
|
||||
<strong class="text-text">ISO/IEC 27001</strong>: incident management and escalation procedures,
|
||||
post-incident reviews and an incident register. That is the work that has to be in place before
|
||||
anything is on fire, so nobody has to invent the response as it happens.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="mt-10">
|
||||
<h2 class="text-sm font-semibold uppercase tracking-widest text-accent">Experience</h2>
|
||||
<div class="mt-4 space-y-4">
|
||||
{% for e in d.work %}
|
||||
<div class="wt-card">
|
||||
<h3 class="font-semibold">{{ e.org }} <span class="font-normal text-muted">· {{ e.place }}</span></h3>
|
||||
<div class="mt-3 space-y-3">
|
||||
{% for r in e.roles %}
|
||||
<div>
|
||||
<div class="flex flex-wrap items-baseline justify-between gap-2">
|
||||
<span class="font-medium text-text">{{ r.title }}</span>
|
||||
<span class="text-xs text-muted">{{ r.when }}</span>
|
||||
</div>
|
||||
<ul class="mt-1 list-disc pl-5 text-sm text-muted">
|
||||
{% for d in r.details %}<li>{{ d }}</li>{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="mt-10">
|
||||
<h2 class="text-sm font-semibold uppercase tracking-widest text-accent">Education</h2>
|
||||
<div class="mt-4 space-y-4">
|
||||
{% for u in d.education %}
|
||||
<div class="wt-card">
|
||||
<div class="flex flex-wrap items-baseline justify-between gap-2">
|
||||
<h3 class="font-semibold">{{ u.what }}</h3><span class="text-xs text-muted">{{ u.when }}</span>
|
||||
</div>
|
||||
<p class="text-sm text-muted">{{ u.where }}</p>
|
||||
{% if u.note %}<p class="mt-1 text-sm text-muted/80">{{ u.note }}</p>{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="mt-10">
|
||||
<h2 class="text-sm font-semibold uppercase tracking-widest text-accent">Publication</h2>
|
||||
<div class="mt-4 wt-card">
|
||||
<a href="https://arxiv.org/abs/2310.00999" target="_blank" rel="noopener" class="font-medium hover:text-accent">
|
||||
CGAAL: Distributed On-The-Fly ATL Model Checker with Heuristics
|
||||
</a>
|
||||
<p class="mt-1 text-sm text-muted">
|
||||
Carlsen, Frydenskov, Jensen, Rasmussen, Sørensen, <span class="text-text">Weirsøe</span>, Jensen & Larsen.
|
||||
<em>EPTCS</em>, vol. 390 (2023), pp. 99–114. <span class="text-muted/80">arXiv:2310.00999</span>
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="mt-10">
|
||||
<h2 class="text-sm font-semibold uppercase tracking-widest text-accent">Skills</h2>
|
||||
<div class="mt-4 space-y-4">
|
||||
{% for s in d.skills %}
|
||||
<div>
|
||||
<h3 class="text-sm font-medium text-text">{{ s.k }}</h3>
|
||||
<div class="mt-2 flex flex-wrap gap-2">
|
||||
{% for k in s.v %}<span class="wt-tag">{{ k }}</span>{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="mt-10 grid gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<h2 class="text-sm font-semibold uppercase tracking-widest text-accent">Languages</h2>
|
||||
<ul class="mt-4 space-y-2 text-sm">
|
||||
<li class="wt-card wt-card--compact">Danish (native)</li>
|
||||
<li class="wt-card wt-card--compact">English (fluent, spoken and written)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-sm font-semibold uppercase tracking-widest text-accent">Projects and interests</h2>
|
||||
<p class="mt-4 text-sm text-muted">
|
||||
I run a self-hosted Proxmox environment with around 50 containers and virtual machines,
|
||||
single sign-on through Authentik, central logging and monitoring. My family uses it every day.
|
||||
I have contributed to qutebrowser and Stregsystemet,
|
||||
and I am a licensed radio amateur (call sign <strong class="text-text">OZ0AGW</strong>).
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,193 @@
|
||||
{% extends "base.html" %}
|
||||
{% block main %}
|
||||
{% set d = load_data(path="data/cv.json") %}
|
||||
<h1 class="text-4xl font-bold tracking-tight">CV</h1>
|
||||
<div class="mt-3 flex flex-wrap gap-x-4 gap-y-1 text-sm text-muted">
|
||||
<span>Nørresundby, Danmark</span>
|
||||
<a class="text-accent hover:underline" href="mailto:asger@weirsoe.dk">asger@weirsoe.dk</a>
|
||||
<a class="text-accent hover:underline" href="https://www.linkedin.com/in/ag-weirs%C3%B8e/">LinkedIn</a>
|
||||
</div>
|
||||
|
||||
<p class="mt-6 max-w-2xl text-muted">
|
||||
Datalog (cand.scient.) fra Aalborg Universitet med en bred profil. Jeg forener teknisk
|
||||
indsigt med forretningsforståelse og er god til at få komplekse løsninger til at lande i
|
||||
praksis, særligt inden for elektronisk fakturering. Jeg har stået for hændelseshåndtering
|
||||
på national e-faktura-infrastruktur og bygget et ISMS efter ISO/IEC 27001. Karrieren gik fra fuld-stack udvikling
|
||||
hos BilligVVS til konsulent- og Teknisk Lead-rollen hos CGI, og i dag er jeg Technical
|
||||
Solution Architect hos Digisense A/S, hvor jeg bygger bro mellem kunder, forretning og
|
||||
udviklingsteams. Ved siden af læser jeg HD i Erhvervsøkonomi for at koble teknik og
|
||||
forretning endnu tættere.
|
||||
</p>
|
||||
|
||||
<section class="mt-10">
|
||||
<h2 class="text-sm font-semibold uppercase tracking-widest text-accent">Fagområder</h2>
|
||||
<p class="mt-4 max-w-2xl text-muted">
|
||||
Jeg arbejder med <strong class="text-text">elektronisk fakturering</strong> (e-faktura / electronic
|
||||
invoicing) og e-procurement. Hos CGI var jeg med til at udvikle den open source eDelivery-løsning
|
||||
til NemHandel, der er bygget på den open source-baserede access point <strong class="text-text">Oxalis</strong> (Java). I dag er jeg
|
||||
Technical Solution Architect hos Digisense A/S og arbejder med løsningsarkitektur på området:
|
||||
<strong class="text-text">Peppol</strong> (BIS, Access Point og eDelivery/AS4),
|
||||
<strong class="text-text">NemHandel</strong>, <strong class="text-text">OIOUBL/UBL</strong> og den
|
||||
europæiske e-faktura-standard EN 16931.
|
||||
</p>
|
||||
<div class="mt-4 flex flex-wrap gap-2">
|
||||
{% for k in ["Peppol", "Peppol Access Point", "Oxalis", "NemHandel", "e-faktura", "Electronic invoicing", "OIOUBL / UBL", "e-Delivery (AS4)", "EN 16931", "e-procurement"] %}
|
||||
<span class="wt-tag">{{ k }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="mt-10">
|
||||
<h2 class="text-sm font-semibold uppercase tracking-widest text-accent">Hændelseshåndtering og compliance</h2>
|
||||
<p class="mt-4 max-w-2xl text-muted">
|
||||
Når noget går ned i produktion, er det tekniske kun halvdelen. Den anden halvdel er at få de
|
||||
rigtige folk i gang, holde kunden orienteret og bagefter lære noget af det. Det har jeg gjort i
|
||||
praksis som teknisk lead på NemHandel hos CGI. Under <strong class="text-text">CrowdStrike-nedbruddet
|
||||
i juli 2024</strong> ramte en databasefejl produktionen, samtidig med at de fleste af teamets maskiner
|
||||
var nede.. så vi koordinerede over telefonen med hostingleverandøren og den ene udvikler, hvis maskine
|
||||
stadig virkede, og databasen var tilbage samme eftermiddag. Da den kritiske SnakeYAML-sårbarhed
|
||||
<strong class="text-text">CVE-2022-1471</strong> ramte NemHandel-referenceimplementeringen, orienterede
|
||||
vi kunden med det samme, der gik vejledning ud til dem der kørte løsningen, og vi havde en rettet
|
||||
version ude inden for to uger.
|
||||
</p>
|
||||
<p class="mt-3 max-w-2xl text-muted">
|
||||
Hos Digisense har jeg bygget virksomhedens ledelsessystem for informationssikkerhed efter
|
||||
<strong class="text-text">ISO/IEC 27001</strong>, med procedurer for hændelseshåndtering og eskalering,
|
||||
evaluering efter hændelser og et hændelsesregister. Det er arbejdet der skal ligge klar før det brænder,
|
||||
så ingen skal opfinde responsen undervejs. Interessen er ikke ny: min kandidat er specialiseret i
|
||||
sikkerhed i distribuerede systemer.
|
||||
</p>
|
||||
<div class="mt-4 flex flex-wrap gap-2">
|
||||
{% for k in ["Incident response", "Hændelseshåndtering", "Eskalering", "Post-incident review", "Sårbarhedshåndtering", "Interessentkommunikation", "ISO/IEC 27001", "ISMS", "Informationssikkerhed"] %}
|
||||
<span class="wt-tag">{{ k }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="mt-10">
|
||||
<h2 class="text-sm font-semibold uppercase tracking-widest text-accent">Erhvervserfaring</h2>
|
||||
<div class="mt-4 space-y-4">
|
||||
{% for e in d.erhverv %}
|
||||
<div class="wt-card">
|
||||
<h3 class="font-semibold">{{ e.org }} <span class="font-normal text-muted">· {{ e.sted }}</span></h3>
|
||||
<div class="mt-3 space-y-3">
|
||||
{% for r in e.roller %}
|
||||
<div>
|
||||
<div class="flex flex-wrap items-baseline justify-between gap-2">
|
||||
<span class="font-medium text-text">{{ r.titel }}</span>
|
||||
<span class="text-xs text-muted">{{ r.naar }}</span>
|
||||
</div>
|
||||
<ul class="mt-1 list-disc pl-5 text-sm text-muted">
|
||||
{% for d in r.detaljer %}<li>{{ d }}</li>{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="mt-10">
|
||||
<h2 class="text-sm font-semibold uppercase tracking-widest text-accent">Uddannelse</h2>
|
||||
<div class="mt-4 space-y-4">
|
||||
{% for u in d.uddannelse %}
|
||||
<div class="wt-card">
|
||||
<div class="flex flex-wrap items-baseline justify-between gap-2">
|
||||
<h3 class="font-semibold">{{ u.hvad }}</h3><span class="text-xs text-muted">{{ u.naar }}</span>
|
||||
</div>
|
||||
<p class="text-sm text-muted">{{ u.hvor }}</p>
|
||||
{% if u.note %}<p class="mt-1 text-sm text-muted/80">{{ u.note }}</p>{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="mt-10">
|
||||
<h2 class="text-sm font-semibold uppercase tracking-widest text-accent">Licenser</h2>
|
||||
<div class="mt-4 space-y-4">
|
||||
{% for l in d.licenser %}
|
||||
<div class="wt-card">
|
||||
<div class="flex flex-wrap items-baseline justify-between gap-2">
|
||||
<h3 class="font-semibold">{{ l.hvad }}</h3><span class="text-xs text-muted">{{ l.naar }}</span>
|
||||
</div>
|
||||
<p class="text-sm text-muted">{{ l.hvor }}</p>
|
||||
{% if l.note %}<p class="mt-1 text-sm text-muted">{{ l.note }}</p>{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="mt-10">
|
||||
<h2 class="text-sm font-semibold uppercase tracking-widest text-accent">Publikationer</h2>
|
||||
<div class="mt-4 wt-card">
|
||||
<a href="https://arxiv.org/abs/2310.00999" target="_blank" rel="noopener" class="font-medium hover:text-accent">
|
||||
CGAAL: Distributed On-The-Fly ATL Model Checker with Heuristics
|
||||
</a>
|
||||
<p class="mt-1 text-sm text-muted">
|
||||
Carlsen, Frydenskov, Jensen, Rasmussen, Sørensen, <span class="text-text">Weirsøe</span>, Jensen & Larsen.
|
||||
<em>EPTCS</em>, vol. 390 (2023), s. 99–114. <span class="text-muted/80">arXiv:2310.00999</span>
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="mt-10">
|
||||
<h2 class="text-sm font-semibold uppercase tracking-widest text-accent">Frivilligt arbejde</h2>
|
||||
<div class="mt-4 space-y-4">
|
||||
{% for f in d.frivilligt %}
|
||||
<div class="wt-card">
|
||||
<div class="flex flex-wrap items-baseline justify-between gap-2">
|
||||
<h3 class="font-semibold">{{ f.hvad }}</h3><span class="text-xs text-muted">{{ f.naar }}</span>
|
||||
</div>
|
||||
<p class="mt-1 text-sm text-muted">{{ f.note }}</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="mt-10 grid gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<h2 class="text-sm font-semibold uppercase tracking-widest text-accent">Primære kompetencer</h2>
|
||||
<div class="mt-4 flex flex-wrap gap-2">
|
||||
{% for k in d.primaere %}<span class="wt-tag">{{ k }}</span>{% endfor %}
|
||||
</div>
|
||||
<h2 class="mt-8 text-sm font-semibold uppercase tracking-widest text-accent">Sekundære</h2>
|
||||
<div class="mt-4 flex flex-wrap gap-2">
|
||||
{% for k in d.sekundaere %}<span class="wt-tag wt-muted">{{ k }}</span>{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2 class="text-sm font-semibold uppercase tracking-widest text-accent">Sprog</h2>
|
||||
<ul class="mt-4 space-y-2 text-sm">
|
||||
{% for s in d.sprog %}<li class="wt-card wt-card--compact">{{ s }}</li>{% endfor %}
|
||||
</ul>
|
||||
<h2 class="mt-8 text-sm font-semibold uppercase tracking-widest text-accent">Interesser</h2>
|
||||
<p class="mt-4 text-sm text-muted">
|
||||
Selvhosting og drift: jeg har gennem mange år bygget og vedligeholdt et homelab med
|
||||
<a class="text-accent hover:underline" href="https://www.proxmox.com/" target="_blank" rel="noopener"> Proxmox</a>,
|
||||
bag SSO via <a class="text-accent hover:underline" href="https://goauthentik.io/" target="_blank" rel="noopener">Authentik</a>.
|
||||
Det har ført til bidrag i open source-projekter som Qutebrowser og Stregsystemet (IT-studerendes betalingssystem på AAU).
|
||||
</p>
|
||||
<p class="mt-3 text-sm text-muted">
|
||||
Jeg er desuden licenseret radioamatør med <strong class="text-text">B-licens</strong> og kaldesignalet
|
||||
<strong class="text-text">OZ0AGW</strong>.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="mt-10">
|
||||
<h2 class="text-sm font-semibold uppercase tracking-widest text-accent">Natur og jagt</h2>
|
||||
<div class="mt-4 wt-card">
|
||||
<p class="text-muted">
|
||||
Jeg har jagttegn, men jagten er mest anledningen. Det jeg tager ud efter, er at stå op
|
||||
længe før resten af landet og træde ud i den stilhed, der kun findes tidligt om morgenen,
|
||||
og så se solen komme op og disen lette hen over markerne.
|
||||
</p>
|
||||
<p class="mt-3 text-muted">
|
||||
Planter, fugle, årstidernes skiften: jeg kan lide at kunne sætte navn på det jeg ser, og
|
||||
at lægge mærke til hvad der har ændret sig siden sidst. Ender turen uden at der bliver
|
||||
nedlagt noget, har den været lige så meget værd.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,59 @@
|
||||
{% extends "base.html" %}
|
||||
{% block main %}
|
||||
{% set d = load_data(path="data/forside-en.json") %}
|
||||
<section class="grid items-center gap-8 py-6 sm:grid-cols-[1fr_auto]">
|
||||
<div>
|
||||
<p class="font-medium text-accent">Hi, I am</p>
|
||||
<h1 class="mt-1 text-5xl font-bold tracking-tight sm:text-6xl">Asger Weirsøe</h1>
|
||||
<p class="mt-3 text-xl font-semibold text-accent sm:text-2xl">Computer scientist & Technical Solution Architect</p>
|
||||
<p class="mt-5 max-w-xl text-lg text-muted">
|
||||
M.Sc. in Computer Science from Aalborg University and Technical Solution Architect at Digisense A/S.
|
||||
I have spent my career on Danish national <strong class="text-text">e-invoicing infrastructure</strong>,
|
||||
where I have led the response when production broke and built the processes that make the next
|
||||
response better. In my spare time I run my own Proxmox homelab, tinker with AI projects and get on
|
||||
the air as a radio amateur (OZ0AGW).
|
||||
</p>
|
||||
<div class="mt-7 flex flex-wrap gap-3">
|
||||
<a href="/en/cv" class="wt-btn wt-btn--primary wt-btn--lg">See my CV</a>
|
||||
<a href="/en/chat" class="wt-btn wt-btn--lg">Ask my CV</a>
|
||||
<a href="/en/projects" class="wt-btn wt-btn--lg">Projects</a>
|
||||
</div>
|
||||
</div>
|
||||
{% include "partials/portraet.html" %}
|
||||
</section>
|
||||
|
||||
<section class="mt-14 grid gap-4 sm:grid-cols-3">
|
||||
{% for h in d.highlights %}
|
||||
<div class="wt-card">
|
||||
<h2 class="font-semibold tracking-tight">{{ h.title }}</h2>
|
||||
<p class="mt-2 text-sm text-muted">{{ h.body }}</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</section>
|
||||
|
||||
<section class="mt-16">
|
||||
<h2 class="text-2xl font-bold tracking-tight">More than a CV</h2>
|
||||
<div class="mt-5 grid gap-4 sm:grid-cols-2">
|
||||
{% for c in d.kort %}
|
||||
<a href="{{ c.href | safe }}" class="group wt-card wt-card--hover">
|
||||
<h3 class="font-semibold tracking-tight group-hover:text-accent">{{ c.title }} →</h3>
|
||||
<p class="mt-2 text-sm text-muted">{{ c.desc }}</p>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<p class="mt-3 text-xs text-muted/80">Press <kbd class="wt-kbd">Ctrl/⌘ K</kbd> to jump anywhere, or run <code class="rounded bg-surface-2 px-1.5 py-0.5">curl asger.weirsoe.dk</code> in a terminal.</p>
|
||||
</section>
|
||||
|
||||
<section class="mt-16">
|
||||
<h2 class="text-2xl font-bold tracking-tight">Frequently asked questions</h2>
|
||||
<div class="mt-5 grid gap-3">
|
||||
{% for f in d.faqs %}
|
||||
<details class="group wt-card">
|
||||
<summary class="cursor-pointer list-none font-semibold tracking-tight marker:hidden">{{ f.q }}</summary>
|
||||
<p class="mt-3 text-sm text-muted">{{ f.a }}</p>
|
||||
</details>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<p class="mt-6 text-sm text-muted">The blog and the e-invoicing tools are in Danish.</p>
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,80 @@
|
||||
{% extends "base.html" %}
|
||||
{% block main %}
|
||||
{% set d = load_data(path="data/forside.json") %}
|
||||
<section class="grid items-center gap-8 py-6 sm:grid-cols-[1fr_auto]">
|
||||
<div>
|
||||
<p class="font-medium text-accent">Hej, jeg er</p>
|
||||
<h1 class="mt-1 text-5xl font-bold tracking-tight sm:text-6xl">Asger Weirsøe</h1>
|
||||
<p class="mt-3 text-xl font-semibold text-accent sm:text-2xl">Datalog & Technical Solution Architect</p>
|
||||
<p class="mt-5 max-w-xl text-lg text-muted">
|
||||
Datalog (cand.scient.) fra Aalborg Universitet og Technical Solution Architect hos Digisense A/S.
|
||||
Til daglig bygger jeg bro mellem forretning og teknik og får <strong class="text-text">elektronisk fakturering</strong>
|
||||
som Peppol og NemHandel til at virke i praksis. I min fritid drifter jeg mit eget
|
||||
Proxmox-homelab, roder med AI-projekter og går på luften som radioamatør (OZ0AGW).
|
||||
</p>
|
||||
<div class="mt-7 flex flex-wrap gap-3">
|
||||
<a href="/cv" class="wt-btn wt-btn--primary wt-btn--lg">Se mit CV</a>
|
||||
<a href="/projekter" class="wt-btn wt-btn--lg">Projekter</a>
|
||||
<a href="/blog" class="wt-btn wt-btn--lg">Blog</a>
|
||||
</div>
|
||||
</div>
|
||||
{% include "partials/portraet.html" %}
|
||||
</section>
|
||||
|
||||
<section class="mt-14 grid gap-4 sm:grid-cols-3">
|
||||
{% for h in d.highlights %}
|
||||
<div class="wt-card">
|
||||
<h2 class="font-semibold tracking-tight">{{ h.title }}</h2>
|
||||
<p class="mt-2 text-sm text-muted">{{ h.body }}</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</section>
|
||||
|
||||
<section class="mt-16">
|
||||
<h2 class="text-2xl font-bold tracking-tight">Mere end et CV</h2>
|
||||
<div class="mt-5 grid gap-4 sm:grid-cols-2">
|
||||
{% for c in d.kort %}
|
||||
<a href="{{ c.href | safe }}" class="group wt-card wt-card--hover">
|
||||
<h3 class="font-semibold tracking-tight group-hover:text-accent">{{ c.title }} →</h3>
|
||||
<p class="mt-2 text-sm text-muted">{{ c.desc }}</p>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<p class="mt-3 text-xs text-muted/80">Tryk <kbd class="wt-kbd">Ctrl/⌘ K</kbd> og hop direkte til alt, eller <code class="rounded bg-surface-2 px-1.5 py-0.5">curl asger.weirsoe.dk</code> i en terminal.</p>
|
||||
</section>
|
||||
|
||||
<section class="mt-16">
|
||||
<h2 class="text-2xl font-bold tracking-tight">Ofte stillede spørgsmål</h2>
|
||||
<div class="mt-5 grid gap-3">
|
||||
{% for f in d.faqs %}
|
||||
<details class="group wt-card">
|
||||
<summary class="cursor-pointer list-none font-semibold tracking-tight marker:hidden">
|
||||
{{ f.q }}
|
||||
</summary>
|
||||
<p class="mt-3 text-sm text-muted">{{ f.a }}</p>
|
||||
</details>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{% set blog = get_section(path="blog/_index.md") %}
|
||||
{% set posts = blog.pages[:3] %}
|
||||
{% if posts %}
|
||||
<section class="mt-16">
|
||||
<div class="flex items-baseline justify-between">
|
||||
<h2 class="text-2xl font-bold tracking-tight">Seneste indlæg</h2>
|
||||
<a href="/blog" class="text-sm text-accent hover:underline">Alle indlæg →</a>
|
||||
</div>
|
||||
<div class="mt-5 grid gap-4">
|
||||
{% for p in posts %}
|
||||
{{ <postcard p={p} /> }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% block jsonld %}
|
||||
{{ <ld g={seo.profilePage} /> }}
|
||||
{% set d = load_data(path="data/forside.json") %}
|
||||
{{ <faq items={d.faqs} /> }}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,113 @@
|
||||
{% extends "base.html" %}
|
||||
{% block main %}
|
||||
<h1 class="text-4xl font-bold tracking-tight">NemHandel-opslag</h1>
|
||||
<p class="mt-4 max-w-2xl text-muted">
|
||||
Tast et CVR-nummer og se om virksomheden er registreret i NemHandelsregisteret, og hvilke
|
||||
<strong class="text-text">profilpakker</strong> den understøtter. Det er et øjebliksbillede,
|
||||
ikke en autoritativ kilde.
|
||||
</p>
|
||||
|
||||
<form id="pep-form" class="mt-8 flex gap-3">
|
||||
<input id="pep-cvr" type="text" inputmode="numeric" pattern="[0-9]{{ 8 }}" maxlength="8" required
|
||||
placeholder="CVR (8 cifre)" autocomplete="off"
|
||||
class="w-44 wt-input wt-input--lg" />
|
||||
<button type="submit" class="wt-btn wt-btn--primary wt-btn--lg">Slå op</button>
|
||||
</form>
|
||||
|
||||
<div id="pep-result" class="mt-8" hidden>
|
||||
<div class="wt-card">
|
||||
<div class="flex flex-wrap items-baseline justify-between gap-2">
|
||||
<h2 id="pep-name" class="text-xl font-semibold tracking-tight"></h2>
|
||||
<span id="pep-badge" class="wt-tag"></span>
|
||||
</div>
|
||||
<p id="pep-cvr-line" class="mt-1 text-sm text-muted"></p>
|
||||
<div id="pep-caps" class="mt-4"></div>
|
||||
</div>
|
||||
</div>
|
||||
<p id="pep-error" class="mt-8 text-sm text-muted" hidden></p>
|
||||
<div id="peppol-net" class="mt-4" hidden></div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
var form = document.getElementById('pep-form');
|
||||
if (!form || form.__wired) return; form.__wired = true;
|
||||
var esc = function (s) { return String(s).replace(/[&<>"]/g, function (c) {
|
||||
return { '&': '&', '<': '<', '>': '>', '"': '"' }[c]; }); };
|
||||
var PROF = {
|
||||
'urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0': 'Peppol BIS Billing 3.0',
|
||||
'urn:fdc:peppol.eu:poacc:trns:order:3': 'Ordering (Order)',
|
||||
'urn:fdc:peppol.eu:poacc:trns:order_response:3': 'Order Response',
|
||||
'urn:fdc:peppol.eu:poacc:trns:despatch_advice:3': 'Despatch Advice',
|
||||
'urn:fdc:peppol.eu:poacc:trns:catalogue:3': 'Catalogue',
|
||||
'urn:fdc:peppol.eu:poacc:trns:invoice_response:3': 'Invoice Response',
|
||||
'urn:fdc:peppol.eu:poacc:trns:mlr:3': 'Message Level Response',
|
||||
};
|
||||
async function loadPeppol(cvr) {
|
||||
var box = document.getElementById('peppol-net'); box.hidden = false;
|
||||
box.innerHTML = '<div class="wt-card"><p class="text-sm text-muted">Slår op i Peppol-netværket…</p></div>';
|
||||
try {
|
||||
var r = await fetch('/api/peppol/smp/' + encodeURIComponent(cvr));
|
||||
var d = await r.json();
|
||||
if (!r.ok) { box.innerHTML = '<div class="wt-card"><p class="text-sm text-muted">Peppol-opslag ikke tilgængeligt lige nu.</p></div>'; return; }
|
||||
if (!d.on_peppol) {
|
||||
box.innerHTML = '<div class="wt-card"><div class="flex items-baseline justify-between gap-2"><h2 class="text-xl font-semibold tracking-tight">Peppol-netværk</h2><span class="wt-tag wt-muted">Ikke fundet</span></div><p class="mt-2 text-sm text-muted">Virksomheden blev ikke fundet i Peppol Directory via CVR-opslag (kan stadig være på Peppol under et andet identifikator-scheme).</p></div>';
|
||||
return;
|
||||
}
|
||||
var docs = (d.doctypes || []).map(function (dt) {
|
||||
var label = PROF[dt.customization] || dt.customization;
|
||||
return '<span class="wt-tag">' + esc(dt.document) + ' · ' + esc(label) + '</span>';
|
||||
}).join('');
|
||||
box.innerHTML = '<div class="wt-card">' +
|
||||
'<div class="flex flex-wrap items-baseline justify-between gap-2"><h2 class="text-xl font-semibold tracking-tight">Peppol-netværk</h2>' +
|
||||
'<span class="wt-tag wt-tag--accent">På Peppol</span></div>' +
|
||||
'<p class="mt-1 text-sm text-muted">' + esc(d.name || '') + (d.country ? ' · ' + esc(d.country) : '') +
|
||||
' · <span class="font-mono text-xs">' + esc(d.participant_id || '') + '</span>' + (d.reg_date ? ' · reg. ' + esc(d.reg_date) : '') + '</p>' +
|
||||
'<p class="mt-4 text-xs font-semibold uppercase tracking-widest text-accent">Kan modtage</p>' +
|
||||
'<div class="mt-2 flex flex-wrap gap-2">' + (docs || '<span class="text-sm text-muted">ingen dokumenttyper</span>') + '</div>' +
|
||||
'<p class="mt-3 text-xs text-muted/80">Live fra Peppol Directory via egen proxy, intet gemt.</p></div>';
|
||||
} catch (_) {
|
||||
box.innerHTML = '<div class="wt-card"><p class="text-sm text-muted">Kunne ikke nå Peppol-opslaget.</p></div>';
|
||||
}
|
||||
}
|
||||
form.addEventListener('submit', async function (e) {
|
||||
e.preventDefault();
|
||||
var res = document.getElementById('pep-result'), err = document.getElementById('pep-error');
|
||||
res.hidden = true; err.hidden = true;
|
||||
var cvr = document.getElementById('pep-cvr').value.trim();
|
||||
var btn = form.querySelector('button'); btn.disabled = true; btn.textContent = 'Slår op…';
|
||||
loadPeppol(cvr);
|
||||
try {
|
||||
var r = await fetch('/api/nemhandel/' + encodeURIComponent(cvr));
|
||||
var data = await r.json();
|
||||
if (!r.ok) { err.textContent = data.error || 'Noget gik galt.. prøv igen.'; err.hidden = false; return; }
|
||||
document.getElementById('pep-name').textContent = data.name || 'Ukendt navn';
|
||||
document.getElementById('pep-cvr-line').textContent = 'CVR ' + data.cvr;
|
||||
var badge = document.getElementById('pep-badge');
|
||||
badge.textContent = data.registered ? 'Registreret i NemHandel' : 'Ikke registreret';
|
||||
badge.className = 'wt-tag ' + (data.registered ? 'wt-tag--accent' : 'wt-muted');
|
||||
var caps = document.getElementById('pep-caps');
|
||||
if (data.registered && (data.profiles.length || data.networks.length)) {
|
||||
caps.innerHTML =
|
||||
(data.networks.length ? '<p class="text-xs font-semibold uppercase tracking-widest text-accent">Netværk</p>' +
|
||||
'<div class="mt-2 flex flex-wrap gap-2">' + data.networks.map(function (n) {
|
||||
return '<span class="wt-tag">' + esc(n) + '</span>'; }).join('') + '</div>' : '') +
|
||||
(data.profiles.length ? '<p class="mt-4 text-xs font-semibold uppercase tracking-widest text-accent">Profilpakker</p>' +
|
||||
'<div class="mt-2 flex flex-wrap gap-2">' + data.profiles.map(function (p) {
|
||||
return '<span class="wt-tag">' + esc(p) + '</span>'; }).join('') + '</div>' : '');
|
||||
} else {
|
||||
caps.innerHTML = '<p class="text-sm text-muted">' + (data.registered
|
||||
? 'Registreret, men ingen profildata i seneste opdatering.'
|
||||
: 'Virksomheden findes i CVR, men er ikke registreret som e-faktura-modtager.') + '</p>';
|
||||
}
|
||||
res.hidden = false;
|
||||
} catch (_) {
|
||||
err.textContent = 'Kunne ikke nå opslagstjenesten.. prøv igen om lidt.'; err.hidden = false;
|
||||
} finally { btn.disabled = false; btn.textContent = 'Slå op'; }
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
||||
<p class="mt-10 text-xs text-muted/80">
|
||||
API: <code class="rounded bg-surface-2 px-1.5 py-0.5">curl https://asger.weirsoe.dk/api/nemhandel/<cvr></code> (rate-limited).
|
||||
</p>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,32 @@
|
||||
{% extends "base.html" %}
|
||||
{% block main %}
|
||||
{% set d = load_data(path="data/now.json") %}
|
||||
{% set g = load_data(path="data/gitea.json") %}
|
||||
<h1 class="text-4xl font-bold tracking-tight">Now</h1>
|
||||
<p class="mt-2 text-sm text-muted">Senest opdateret {{ g.updated }} · à la <a class="text-accent hover:underline" href="https://nownownow.com/about" target="_blank" rel="noopener">nownownow.com</a></p>
|
||||
|
||||
<section class="mt-8">
|
||||
<h2 class="text-sm font-semibold uppercase tracking-widest text-accent">Lige nu</h2>
|
||||
<ul class="mt-4 space-y-3">
|
||||
{% for f in d.focus %}
|
||||
<li class="wt-card text-sm">{{ f }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class="mt-10">
|
||||
<h2 class="text-sm font-semibold uppercase tracking-widest text-accent">Git-aktivitet</h2>
|
||||
<p class="mt-2 text-sm text-muted">{{ g.total }} bidrag i mit eget Gitea det seneste år, de seneste {{ g.weeks_shown }} uger:</p>
|
||||
<div class="mt-4 overflow-x-auto wt-card">
|
||||
<div class="heatmap" role="img" aria-label="Aktivitets-heatmap over git-bidrag">
|
||||
{% for w in g.weeks %}
|
||||
<div class="hm-col">
|
||||
{% for c in w %}<span class="hm-cell hm-{{ c.level }}" title="{{ c.date }}: {{ c.n }} bidrag"></span>{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<p class="mt-2 text-xs text-muted/80">Offentlige projekter ligger på <a class="text-accent hover:underline" href="https://gitea.weircon.dk/agw" target="_blank" rel="noopener">gitea.weircon.dk</a>.</p>
|
||||
</section>
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,14 @@
|
||||
<!doctype html>
|
||||
<html lang="da">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="robots" content="noindex, follow" />
|
||||
<meta http-equiv="refresh" content="0; url=/nemhandel" />
|
||||
<link rel="canonical" href="https://asger.weirsoe.dk/nemhandel" />
|
||||
<title>NemHandel-opslag</title>
|
||||
</head>
|
||||
<body>
|
||||
<p>Siden er flyttet til <a href="/nemhandel">/nemhandel</a>…</p>
|
||||
<script>location.replace('/nemhandel');</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,156 @@
|
||||
{% extends "base.html" %}
|
||||
{% block main %}
|
||||
{% set d = load_data(path="data/profiles.json") %}
|
||||
<h1 class="text-4xl font-bold tracking-tight">Profil-opslag</h1>
|
||||
<p class="mt-4 max-w-2xl text-muted">
|
||||
Browse og søg i e-dokumentprofilerne for <strong class="text-text">Nemhandel OIOUBL</strong> og
|
||||
<strong class="text-text">Peppol BIS/PINT</strong>: profil-/proces-id, customization-id, hvilke
|
||||
dokumenter profilen indeholder og typisk anvendelse. Klik på et id for at kopiere det.
|
||||
</p>
|
||||
<p class="mt-3 max-w-2xl text-sm text-muted">
|
||||
De danske profiler står i samme rækkefølge og under samme gruppenavne som i
|
||||
<strong class="text-text">Nemhandelsregistrets</strong> profil-dropdown (“Profiler for: …”), så du kan
|
||||
slå direkte op mellem registrering og dokumentindhold.
|
||||
</p>
|
||||
|
||||
<div class="mt-8 flex flex-col gap-4 sm:flex-row sm:items-center">
|
||||
<div class="relative flex-1">
|
||||
<span class="pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-muted">⌕</span>
|
||||
<input
|
||||
id="p-search"
|
||||
type="search"
|
||||
placeholder="Søg profil, id, dokument…"
|
||||
autocomplete="off"
|
||||
aria-label="Søg profiler"
|
||||
class="w-full wt-input wt-input--lg pl-9"
|
||||
/>
|
||||
</div>
|
||||
<span id="p-count" class="text-sm text-muted">{{ d.profiles | length }} profiler</span>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 flex flex-wrap gap-2">
|
||||
<button type="button" class="wt-btn wt-btn--ghost wt-btn--sm" data-std="" aria-pressed="true">Alle</button>
|
||||
{% for s in d.standarder %}<button type="button" class="wt-btn wt-btn--ghost wt-btn--sm" data-std="{{ s }}" aria-pressed="false">{{ s }}</button>{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<span class="mr-2 text-xs text-muted">Nemhandelsregistrets kategori:</span>
|
||||
<span class="mt-1 inline-flex flex-wrap gap-2 align-middle">
|
||||
<button type="button" class="wt-btn wt-btn--ghost wt-btn--sm" data-kat="" aria-pressed="true">Alle</button>
|
||||
{% for k in d.kategorier %}<button type="button" class="wt-btn wt-btn--ghost wt-btn--sm" data-kat="{{ k }}" aria-pressed="false">{{ k }}</button>{% endfor %}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 space-y-3">
|
||||
{%- for p in d.profiles %}
|
||||
<article
|
||||
data-profile
|
||||
data-standard="{{ p.standard }}"
|
||||
data-kategori="{{ p.kategori }}"
|
||||
data-search="{{ (p.standard ~ " " ~ p.kategori ~ " " ~ p.navn ~ " " ~ p.id ~ " " ~ p.customization ~ " " ~ (p.dokumenter | join(sep=" ")) ~ " " ~ p.anvendelse ~ " " ~ p.bemaerkninger) | lower }}"
|
||||
class="wt-card wt-card--hover"
|
||||
>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span class="wt-tag wt-tag--accent">
|
||||
{{ p.standard }}
|
||||
</span>
|
||||
{% if p.kategori %}
|
||||
<span
|
||||
class="wt-tag wt-muted"
|
||||
title="Gruppen profilen står under i Nemhandelsregistrets dropdown"
|
||||
>
|
||||
Register: {{ p.kategori }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<h2 class="mt-2 text-lg font-semibold tracking-tight">{{ p.navn }}</h2>
|
||||
|
||||
<dl class="mt-3 grid gap-1 text-xs sm:grid-cols-[10rem_1fr]">
|
||||
<dt class="text-muted">Profil/proces-id</dt>
|
||||
<dd>
|
||||
<button type="button" data-copy="{{ p.id }}" title="Klik for at kopiere" class="copy break-all text-left font-mono">
|
||||
{{ p.id }}
|
||||
</button>
|
||||
</dd>
|
||||
<dt class="text-muted">Customization-id</dt>
|
||||
<dd>
|
||||
<button type="button" data-copy="{{ p.customization }}" title="Klik for at kopiere" class="copy break-all text-left font-mono">
|
||||
{{ p.customization }}
|
||||
</button>
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
<div class="mt-3 flex flex-wrap gap-1.5">
|
||||
{% for doc in p.dokumenter %}
|
||||
<span class="wt-tag">{{ doc }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<p class="mt-3 text-sm text-muted">{{ p.anvendelse }}</p>
|
||||
<p class="mt-2 text-xs text-muted">
|
||||
<span class="font-semibold text-muted">Bemærk:</span> {{ p.bemaerkninger }}
|
||||
</p>
|
||||
<p class="mt-3 text-xs"><a href="{{ p.kilde.url | safe }}" target="_blank" rel="noopener" class="text-accent/80 transition-colors hover:text-accent">Kilde: {{ p.kilde.label }} ↗</a></p>
|
||||
</article>
|
||||
{%- endfor %}
|
||||
</div>
|
||||
|
||||
<p id="p-none" class="mt-6 text-sm text-muted" hidden>Ingen profiler matcher.</p>
|
||||
|
||||
<p class="mt-10 text-xs text-muted">
|
||||
Kilder: OIOUBL Profiler-guiden (G26) v1.3 (dokumentlister, tabel 1-3), Nemhandelsregistret
|
||||
(gruppering og profilnavne) og Peppol BIS/PINT (docs.peppol.eu). * = ApplicationResponse bruges kun
|
||||
til afvisning; det forretningsmæssige svar på en ordre er altid OrderResponse/OrderResponseSimple.
|
||||
Øjebliksbillede.. verificér altid id'er mod den officielle dokumentation før brug.
|
||||
</p>
|
||||
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
var cards = [].slice.call(document.querySelectorAll('[data-profile]'));
|
||||
var search = document.getElementById('p-search');
|
||||
var stdChips = [].slice.call(document.querySelectorAll('button[data-std]'));
|
||||
var katChips = [].slice.call(document.querySelectorAll('button[data-kat]'));
|
||||
var count = document.getElementById('p-count');
|
||||
var none = document.getElementById('p-none');
|
||||
var q = '', std = '', kat = '';
|
||||
function apply() {
|
||||
var shown = 0;
|
||||
cards.forEach(function (c) {
|
||||
var okS = !std || c.getAttribute('data-standard') === std;
|
||||
var okK = !kat || c.getAttribute('data-kategori') === kat;
|
||||
var okQ = !q || c.getAttribute('data-search').indexOf(q) > -1;
|
||||
var hit = okS && okK && okQ;
|
||||
c.hidden = !hit;
|
||||
if (hit) shown++;
|
||||
});
|
||||
count.textContent = shown + (shown === 1 ? ' profil' : ' profiler');
|
||||
none.hidden = shown > 0;
|
||||
}
|
||||
search.addEventListener('input', function () { q = search.value.trim().toLowerCase(); apply(); });
|
||||
function wire(group, attr, set) {
|
||||
group.forEach(function (b) {
|
||||
b.addEventListener('click', function () {
|
||||
set(b.getAttribute(attr) || '');
|
||||
group.forEach(function (x) { x.setAttribute('aria-pressed', x === b ? 'true' : 'false'); });
|
||||
apply();
|
||||
});
|
||||
});
|
||||
}
|
||||
wire(stdChips, 'data-std', function (v) { std = v; });
|
||||
wire(katChips, 'data-kat', function (v) { kat = v; });
|
||||
document.addEventListener('click', function (e) {
|
||||
var btn = e.target.closest && e.target.closest('[data-copy]');
|
||||
if (!btn || !navigator.clipboard) return;
|
||||
navigator.clipboard.writeText(btn.getAttribute('data-copy')).then(function () {
|
||||
var orig = btn.textContent;
|
||||
btn.textContent = 'kopieret ✓';
|
||||
setTimeout(function () { btn.textContent = orig; }, 1200);
|
||||
}).catch(function () {});
|
||||
});
|
||||
var qp = new URLSearchParams(location.search).get('q');
|
||||
if (qp) { search.value = qp; q = qp.trim().toLowerCase(); }
|
||||
apply();
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,21 @@
|
||||
{% extends "base.html" %}
|
||||
{% block main %}
|
||||
<h1 class="text-4xl font-bold tracking-tight">Projects</h1>
|
||||
<p class="mt-3 text-muted">Tools I maintain and publish as packages. Everything else lives on my <a class="text-accent hover:underline" href="https://gitea.weircon.dk/agw">Gitea</a>.</p>
|
||||
<div class="mt-8 grid gap-4 sm:grid-cols-2">
|
||||
{% for p in section.pages %}
|
||||
<a href="{{ p.path | trim_end(pat="/") | safe }}"
|
||||
class="group wt-card wt-card--hover">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="font-semibold tracking-tight group-hover:text-accent">{{ p.title }}</h3>
|
||||
{% if p.extra.aur %}<span class="text-xs text-muted">AUR</span>{% endif %}
|
||||
</div>
|
||||
<p class="mt-2 line-clamp-3 text-sm text-muted">{{ p.description }}</p>
|
||||
<div class="mt-3 text-xs text-accent">{{ p.extra.sprog }} · Read more →</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% block jsonld %}
|
||||
{{ <collection url={config.extra.site ~ "/en/projects"} name="Projects" description="Tools I write and publish on the AUR, Homebrew and Scoop." /> }}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,35 @@
|
||||
{% extends "base.html" %}
|
||||
{% block main %}
|
||||
{% set g = load_data(path="data/gitea.json") %}
|
||||
<h1 class="text-4xl font-bold tracking-tight">Projekter</h1>
|
||||
<p class="mt-3 text-muted">Hentet live fra min <a class="text-accent hover:underline" href="https://gitea.weircon.dk/agw">Gitea</a>.</p>
|
||||
|
||||
{% if g.error %}<p class="mt-8 wt-card text-muted">Kunne ikke hente repositories lige nu.</p>{% endif %}
|
||||
|
||||
<div class="mt-8 grid gap-4 sm:grid-cols-2">
|
||||
{%- for r in g.repos %}
|
||||
{%- set side = [s for s in section.pages if s.extra.repo == r.name] | first %}
|
||||
{%- if side %}
|
||||
<a href="{{ side.path | trim_end(pat="/") | safe }}"
|
||||
class="group wt-card wt-card--hover">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="font-semibold tracking-tight group-hover:text-accent">{{ side.title }}</h3>
|
||||
{% if side.extra.aur %}<span class="text-xs text-muted">AUR</span>{% endif %}
|
||||
</div>
|
||||
<p class="mt-2 line-clamp-3 text-sm text-muted">{{ side.description }}</p>
|
||||
<div class="mt-3 text-xs text-accent">{{ side.extra.sprog }} · Læs mere →</div>
|
||||
</a>
|
||||
{%- else %}
|
||||
<a href="{{ r.html_url | safe }}" target="_blank" rel="noopener"
|
||||
class="group wt-card wt-card--hover">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="font-semibold tracking-tight group-hover:text-accent">{{ r.name }}</h3>
|
||||
{% if r.stars_count > 0 %}<span class="text-xs text-muted">★ {{ r.stars_count }}</span>{% endif %}
|
||||
</div>
|
||||
<p class="mt-2 line-clamp-3 text-sm text-muted">{% if r.description %}{{ r.description }}{% else %}Ingen beskrivelse.{% endif %}</p>
|
||||
{% if r.language %}<div class="mt-3 text-xs text-accent">{{ r.language }}</div>{% endif %}
|
||||
</a>
|
||||
{%- endif %}
|
||||
{%- endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,16 @@
|
||||
{% extends "base.html" %}
|
||||
{% block main %}
|
||||
<h1 class="text-4xl font-bold tracking-tight">Status</h1>
|
||||
<p class="mt-4 max-w-2xl text-muted">
|
||||
Live status for de offentlige dele af mit homelab, målt af en self-hosted
|
||||
<a class="text-accent hover:underline" href="https://uptime.kuma.pet" target="_blank" rel="noopener">Uptime Kuma</a>.
|
||||
</p>
|
||||
|
||||
<div class="mt-8 wt-card wt-card--flush">
|
||||
<iframe src="https://status.weircon.dk/status/weircon" title="Uptime Kuma statusside" loading="lazy"
|
||||
class="h-[70vh] w-full" style="border:0"></iframe>
|
||||
</div>
|
||||
<p class="mt-3 text-sm text-muted">
|
||||
Vises siden ikke? Åbn den direkte: <a class="text-accent hover:underline" href="https://status.weircon.dk/status/weircon" target="_blank" rel="noopener">https://status.weircon.dk/status/weircon</a>
|
||||
</p>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,184 @@
|
||||
{% extends "base.html" %}
|
||||
{% block main %}
|
||||
{% set d = load_data(path="data/profiles.json") %}
|
||||
<h1 class="text-4xl font-bold tracking-tight">UBL-tjek</h1>
|
||||
<p class="mt-4 max-w-2xl text-muted">
|
||||
Træk en <strong class="text-text">UBL/OIOUBL-faktura</strong> (eller andet UBL-dokument) ind, eller indsæt en
|
||||
<strong class="text-text">CustomizationID/ProfileID</strong>, så afkoder den profilen, syntaksen og
|
||||
EN 16931-status og kobler til <a href="/profiler" class="text-accent">profil-opslaget</a>.
|
||||
Alt sker <strong class="text-text">lokalt i din browser</strong>; intet sendes nogen steder.
|
||||
</p>
|
||||
<p class="mt-3 max-w-2xl text-muted">
|
||||
Vil du læse hele fakturaen eller have den vedhæftede PDF ud, så har jeg skrevet
|
||||
<a href="/projekter/ubl-tools" class="text-accent hover:underline">ubl-tools</a> til terminalen,
|
||||
også på Windows og macOS.
|
||||
</p>
|
||||
|
||||
<div id="drop" class="mt-8 wt-dropzone">
|
||||
<p class="text-muted">Træk en <code class="rounded bg-surface-2 px-1.5 py-0.5">.xml</code>-fil hertil, eller
|
||||
<label class="cursor-pointer text-accent hover:underline">vælg en fil<input id="file" type="file" accept=".xml,text/xml,application/xml" class="hidden" /></label>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<details class="mt-3">
|
||||
<summary class="cursor-pointer text-sm text-muted">…eller indsæt XML / en identifikator manuelt</summary>
|
||||
<textarea id="paste" rows="6" placeholder="Indsæt UBL-XML, eller bare en CustomizationID/ProfileID…"
|
||||
class="mt-2 w-full wt-input wt-input--lg font-mono text-xs"></textarea>
|
||||
<p class="mt-2"><button id="analyze" type="button" class="wt-btn wt-btn--primary">Afkod</button></p>
|
||||
</details>
|
||||
|
||||
<div id="result" class="mt-6" hidden></div>
|
||||
<p id="err" class="mt-6 text-sm" hidden></p>
|
||||
|
||||
<p class="mt-10 text-xs text-muted/80">
|
||||
Heuristik mod kendte profiler (samme datagrundlag som <a href="/profiler" class="text-accent">/profiler</a>) +
|
||||
EN 16931-/Peppol-/OIOUBL-mønstre. Privat: ingen upload, ingen sporing af indholdet.
|
||||
</p>
|
||||
|
||||
<script type="application/json" id="profiles-data" >[{% for p in d.profiles %}{"standard":{{ p.standard | json_encode | safe }},"navn":{{ p.navn | json_encode | safe }},"id":{{ p.id | json_encode | safe }},"customization":{{ p.customization | json_encode | safe }},"dokumenter":{{ p.dokumenter | json_encode | safe }}}{% if not loop.last %},{% endif %}{% endfor %}]</script>
|
||||
<script>
|
||||
(function () {
|
||||
var PROFILES = JSON.parse(document.getElementById('profiles-data').textContent || '[]');
|
||||
var drop = document.getElementById('drop'), fileInput = document.getElementById('file'),
|
||||
paste = document.getElementById('paste'), result = document.getElementById('result'),
|
||||
err = document.getElementById('err');
|
||||
var esc = function (s) { return String(s == null ? '' : s).replace(/[&<>"]/g, function (c) {
|
||||
return { '&': '&', '<': '<', '>': '>', '"': '"' }[c]; }); };
|
||||
|
||||
// Electronic Address Scheme (ISO 6523): de gængse, DK/Norden i fokus.
|
||||
var EAS = {
|
||||
'0088': 'GLN (GS1 Global Location Number)',
|
||||
'0184': 'DK:CVR (dansk CVR-nummer)',
|
||||
'0198': 'DK:SE (dansk SE-nummer)',
|
||||
'0096': 'DK:P (dansk P-nummer)',
|
||||
'0192': 'NO:ORG (norsk organisasjonsnummer)',
|
||||
'0007': 'SE:ORG (svensk organisationsnummer)',
|
||||
'0208': 'BE:CBE (belgisk virksomhedsnr.)',
|
||||
'0151': 'AU:ABN', '0060': 'DUNS', '0235': 'JP', '0211': 'IT:PA',
|
||||
'9915': 'Peppol test-deltager', '9930': 'DE:LWID (Leitweg-ID)',
|
||||
};
|
||||
|
||||
function findEl(doc, name) {
|
||||
var all = doc.getElementsByTagName('*');
|
||||
for (var i = 0; i < all.length; i++) if (all[i].localName === name) return all[i];
|
||||
return null;
|
||||
}
|
||||
function txt(doc, name) { var e = findEl(doc, name); return e ? (e.textContent || '').trim() : ''; }
|
||||
|
||||
// afkod en CustomizationID/ProfileID-streng til menneskesprog
|
||||
function decode(custom, profile) {
|
||||
var notes = [], standard = null, match = null;
|
||||
var c = (custom || '').trim(), p = (profile || '').trim();
|
||||
|
||||
// 1) eksakt match mod kendte profiler
|
||||
match = PROFILES.find(function (x) { return c && x.customization === c; })
|
||||
|| PROFILES.find(function (x) { return p && x.id === p; })
|
||||
|| PROFILES.find(function (x) { return c && x.id === c; });
|
||||
|
||||
// 2) mønster-heuristik (også når der ikke er fuldt match)
|
||||
var blob = (c + ' ' + p).toLowerCase();
|
||||
if (blob.indexOf('en16931') > -1) notes.push('EN 16931-kompatibel (CIUS af den europæiske kernefaktura)');
|
||||
if (blob.indexOf('peppol.eu') > -1 || blob.indexOf('fdc:peppol') > -1) { standard = 'Peppol BIS'; notes.push('Peppol-netværket'); }
|
||||
if (blob.indexOf('pint') > -1) { standard = 'Peppol PINT'; notes.push('PINT (Peppol International)'); }
|
||||
if (blob.indexOf('billing:3.0') > -1) notes.push('Billing 3.0-proces');
|
||||
if (blob.indexOf('selfbilling') > -1) notes.push('Selvfakturering (køber udsteder)');
|
||||
if (blob.indexOf('oioubl') > -1) { standard = standard || 'OIOUBL (NemHandel)'; notes.push('OIOUBL, dansk UBL-tilpasning'); }
|
||||
if (/procurement-/i.test(p)) { standard = standard || 'OIOUBL (NemHandel)'; if (/r-|r$|simr|advr/i.test(p)) notes.push("'R' = svar påkrævet"); }
|
||||
|
||||
return { match: match, standard: match ? match.standard : standard, notes: notes };
|
||||
}
|
||||
|
||||
function badge(text, kind) {
|
||||
return '<span class="wt-tag' + (kind === 'ok' ? ' wt-tag--accent' : '') + '">' + esc(text) + '</span>';
|
||||
}
|
||||
function row(dt, dd) {
|
||||
return '<div class="grid gap-1 sm:grid-cols-[10rem_1fr]"><dt class="text-muted">' + esc(dt) +
|
||||
'</dt><dd class="break-all font-mono">' + (dd || '<span class="text-muted">—</span>') + '</dd></div>';
|
||||
}
|
||||
|
||||
function renderXml(doc) {
|
||||
var root = doc.documentElement;
|
||||
var docType = root ? root.localName : '?';
|
||||
var ublv = txt(doc, 'UBLVersionID');
|
||||
var custom = txt(doc, 'CustomizationID');
|
||||
var profile = txt(doc, 'ProfileID');
|
||||
var id = txt(doc, 'ID');
|
||||
var dec = decode(custom, profile);
|
||||
|
||||
// EAS på parter
|
||||
var endpoints = [];
|
||||
var eps = doc.getElementsByTagName('*');
|
||||
for (var i = 0; i < eps.length; i++) {
|
||||
if (eps[i].localName === 'EndpointID') {
|
||||
var scheme = eps[i].getAttribute('schemeID') || '';
|
||||
endpoints.push({ scheme: scheme, val: (eps[i].textContent || '').trim() });
|
||||
}
|
||||
}
|
||||
var epHtml = endpoints.slice(0, 4).map(function (e) {
|
||||
return '<div>' + badge((EAS[e.scheme] || ('EAS ' + e.scheme || 'ukendt')), '') +
|
||||
' <span class="font-mono text-xs">' + esc(e.val) + '</span></div>';
|
||||
}).join('');
|
||||
|
||||
var head = dec.match
|
||||
? '<div class="flex flex-wrap items-center gap-2">' + badge(dec.match.standard, 'ok') +
|
||||
'</div><h2 class="mt-2 text-xl font-semibold tracking-tight">' + esc(dec.match.navn) + '</h2>'
|
||||
: '<h2 class="text-xl font-semibold tracking-tight">' + esc(docType) +
|
||||
(dec.standard ? ' <span class="text-muted text-base font-normal">· ' + esc(dec.standard) + '</span>' : '') + '</h2>';
|
||||
|
||||
result.innerHTML =
|
||||
'<div class="wt-card">' + head +
|
||||
'<dl class="mt-3 space-y-1 text-xs">' +
|
||||
row('Dokumenttype', '<span class="font-semibold">' + esc(docType) + '</span> (UBL ' + (esc(ublv) || '2.x') + ')') +
|
||||
row('ID', esc(id)) +
|
||||
row('ProfileID', esc(profile)) +
|
||||
row('CustomizationID', esc(custom)) +
|
||||
'</dl>' +
|
||||
(dec.notes.length ? '<div class="mt-3 flex flex-wrap gap-1.5">' + dec.notes.map(function (n) { return badge(n, ''); }).join('') + '</div>' : '') +
|
||||
(epHtml ? '<p class="mt-3 text-xs text-muted">Parter (EAS):</p><div class="mt-1 space-y-1">' + epHtml + '</div>' : '') +
|
||||
(dec.match ? '<p class="mt-3 text-xs"><a class="text-accent hover:underline" href="/profiler?q=' + encodeURIComponent(dec.match.id) + '">Se profilen i /profiler ↗</a></p>'
|
||||
: '<p class="mt-3 text-xs text-muted">Ingen eksakt match i profil-databasen, afkodet ud fra mønstre.</p>') +
|
||||
'</div>';
|
||||
result.hidden = false; err.hidden = true;
|
||||
}
|
||||
|
||||
function renderId(s) {
|
||||
var dec = decode(s, s);
|
||||
result.innerHTML = '<div class="wt-card">' +
|
||||
(dec.match ? '<div class="flex flex-wrap items-center gap-2">' + badge(dec.match.standard, 'ok') +
|
||||
'</div><h2 class="mt-2 text-xl font-semibold tracking-tight">' + esc(dec.match.navn) + '</h2>'
|
||||
: '<h2 class="text-xl font-semibold tracking-tight">Identifikator</h2>') +
|
||||
'<dl class="mt-3 space-y-1 text-xs">' + row('Streng', esc(s)) + '</dl>' +
|
||||
(dec.notes.length ? '<div class="mt-3 flex flex-wrap gap-1.5">' + dec.notes.map(function (n) { return badge(n, ''); }).join('') + '</div>'
|
||||
: '<p class="mt-3 text-xs text-muted">Genkendte ingen kendte mønstre.</p>') +
|
||||
(dec.match ? '<p class="mt-3 text-xs"><a class="text-accent hover:underline" href="/profiler?q=' + encodeURIComponent(dec.match.id) + '">Se profilen i /profiler ↗</a></p>' : '') +
|
||||
'</div>';
|
||||
result.hidden = false; err.hidden = true;
|
||||
}
|
||||
|
||||
function analyze(text) {
|
||||
text = (text || '').trim();
|
||||
if (!text) return;
|
||||
if (text.indexOf('<') === 0 || text.indexOf('<?xml') === 0) {
|
||||
var doc = new DOMParser().parseFromString(text, 'application/xml');
|
||||
if (doc.getElementsByTagName('parsererror').length) {
|
||||
err.textContent = 'Kunne ikke parse XML.. er filen velformet?'; err.hidden = false; result.hidden = true; return;
|
||||
}
|
||||
renderXml(doc);
|
||||
} else {
|
||||
renderId(text);
|
||||
}
|
||||
}
|
||||
|
||||
// fil-input + drag/drop
|
||||
function readFile(f) { var r = new FileReader(); r.onload = function () { analyze(String(r.result)); }; r.readAsText(f); }
|
||||
fileInput.addEventListener('change', function () { if (fileInput.files[0]) readFile(fileInput.files[0]); });
|
||||
['dragenter', 'dragover'].forEach(function (ev) { drop.addEventListener(ev, function (e) {
|
||||
e.preventDefault(); drop.classList.add('is-over'); }); });
|
||||
['dragleave', 'drop'].forEach(function (ev) { drop.addEventListener(ev, function (e) {
|
||||
e.preventDefault(); drop.classList.remove('is-over'); }); });
|
||||
drop.addEventListener('drop', function (e) { var f = e.dataTransfer && e.dataTransfer.files[0]; if (f) readFile(f); });
|
||||
document.getElementById('analyze').addEventListener('click', function () { analyze(paste.value); });
|
||||
paste.addEventListener('keydown', function (e) { if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') analyze(paste.value); });
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,23 @@
|
||||
{% extends "base.html" %}
|
||||
{% block main %}
|
||||
{% set d = load_data(path="data/uses.json") %}
|
||||
<h1 class="text-4xl font-bold tracking-tight">Uses</h1>
|
||||
<p class="mt-4 max-w-2xl text-muted">
|
||||
Inspireret af <a class="text-accent hover:underline" href="https://uses.tech" target="_blank" rel="noopener">uses.tech</a>:
|
||||
det jeg faktisk bruger. Kort version: alt der kan selvhostes, bliver selvhostet.
|
||||
</p>
|
||||
|
||||
{% for s in d.sections %}
|
||||
<section class="mt-10">
|
||||
<h2 class="text-sm font-semibold uppercase tracking-widest text-accent">{{ s.title }}</h2>
|
||||
<div class="mt-4 space-y-3">
|
||||
{% for it in s.items %}
|
||||
<div class="wt-card">
|
||||
<span class="font-semibold">{{ it.name }}</span>
|
||||
<p class="mt-1 text-sm text-muted">{{ it.note }}</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,11 @@
|
||||
{% extends "base.html" %}
|
||||
{% block main %}
|
||||
<h1 class="text-4xl font-bold tracking-tight">Tags</h1>
|
||||
<div class="mt-8 flex flex-wrap gap-2">
|
||||
{%- for t in terms | sort(attribute="page_count") | reverse %}
|
||||
<a href="/tags/{{ t.slug }}" class="wt-chip">#{{ t.name }} <span class="text-muted">({{ t.page_count }})</span></a>
|
||||
{%- else %}
|
||||
<p class="text-muted">Ingen tags endnu.</p>
|
||||
{%- endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,8 @@
|
||||
{% extends "base.html" %}
|
||||
{% block main %}
|
||||
<a href="/tags" class="text-sm text-muted hover:text-accent">← Alle tags</a>
|
||||
<h1 class="mt-3 text-4xl font-bold tracking-tight">#{{ term.name }}</h1>
|
||||
<div class="mt-8 grid gap-4">
|
||||
{% for p in term.pages %}{{ <postcard p={p} /> }}{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user