Svelte 및 kentico kontent.ai
색인
메인 사이트를 준비하세요
One prerequisite for this whole shenanigan to actually work, is that you have your live site up and running.
Another important step is that you have a way of referencing your ssr content to an kontent.ai id. The way I did it was by using data-system-id
in the ssr site.
폴카가 있는 프록시 서버
The node server (I used polka, but express or any similar should work as well) is a very simple one.
I check if I get a call with a ?previewId={id}
, which will have the kentico id.
const dir = join(__dirname, '../public'); //dir for public
const serve = serveStatic(dir);
polka()
.use('/preview', serve)
.get('*', async (req, res) => {
let url = req.originalUrl;
const isMainRequest = url.match(/(\?|&)previewId=/) !== null;
// some magic 🦄
})
.listen(PORT, (err) => {
if (err) throw err;
console.log(`> Running on localhost:${PORT}`);
});
All requests, that are not our main request we will just proxy.
if (!isMainRequest) {
return request
.get(url)
.auth(usr, pwd, false) // if server needs basic auth
.pipe(res);
}
For our main request it is important, that we remove our custom Url parameter
const toRemove = url.match(/[\?|&](previewId=.*?$|&)/)[1];
url = url
.replace(toRemove, '')
.replace(/\/\?$/, '');
After that we can handle our main request and inject our js/css bundles at the end of our html
// get requested site from live server
const resp = await fetch(url, {headers});
let text = await resp.text();
// add script tag before </body>
if (text.includes('<html')) {
const bundles = `
<script src="/preview/bundle.js" async></script>
<link rel="stylesheet" href="/preview/bundle.css">
`;
if(text.includes('</body>')) {
text = text.replace('</body>', `${bundles}</body>`)
} else {
// cloudflare eg. minifies html
// by truncating last closing tags
text += bundles;
}
}
// return response
return res.end(text);
귀하의 사이트를 단순화하십시오
The best choice for the frontend in my opinion (especially for such small an powerful tool) is svelte.
I leaves a small footprint comes with huge capabilities and is ideal if you want to run a tool on top of another site.
The basic svelte setup (with ts) looks something like this:
<!-- App.svelte -->
<script lang="ts">
import { onMount } from 'svelte';
// INIT VARS
let preview = true;
let addMode = false;
let toggleFuncs = new Map();
let arrayOfCmsNodes = [];
let overlays = [];
onMount(() => {
// some init stuff
});
</script>
<main>
</main>
CSS can be totally custom. In my project I put the tools in the bottom right corner, but this is just my preference, so I'll leave them out.
In the onMount function I initialize the app by getting the previewId and setting up all available dom nodes that have cms capability. (in my case I excluded child cms components)
// App.svelte
onMount(() => {
// get param from url
const url = new URL(document.URL);
const id = url.searchParams.get('previewId');
loadPreview(id);
const tempArr = [];
document.querySelectorAll('[data-system-id]')
.forEach((node: HTMLElement) => {
if (node.dataset.systemId === id) return;
// for nested this needs to exclude children data-system-id
if((node.parentNode as HTMLElement).closest('[data-system-id]') !== null) return;
tempArr.push(node);
});
arrayOfCmsNodes = tempArr;
});
As you can see, the next step was to call loadPreview(id)
. This will get the preview data from Kontent.ai
// App.svelte
import { getPreviewContent } from './service/kontent';
import { getToggle } from './service/toggleFunctionGenerator';
const loadPreview = async (id: string) => {
if (!id) return;
const content = await getPreviewContent(id);
if (!content?.items?.length) return;
const toggle = getToggle(id, content);
if (!toggle) return;
toggleFuncs.set(id, toggle);
if(preview) toggle();
}
To get the content you just need to fetch the content by id from https://preview-deliver.kontent.ai/${projectId}/items?system.id=${key}
by setting an authorization header with your preview key.
const headers = {
'authorization': `Bearer ${previewKey}`
};
미리보기 콘텐츠를 전환 가능하게 만들기
As we want the content to not be only replaced, but toggle between live and preview version, we need to generate a toggle function.
For switching between those states I created a simple toggle switch and function.
<!-- App.svelte -->
<script lang="ts">
import Toggle from './components/Toggle.svelte';
const togglePreviews = () => {
preview = !preview
toggleFuncs.forEach(func => func());
}
</script>
<main>
<Toggle
{preview}
{togglePreviews} />
</main>
Setting up the toggle function was a little bit more complex, but in the end it is really easy to add more entries.
// .service/toggleFunctionGenerator.ts
import {
replaceText,
} from './replaceContent';
import {
getToogleDataByType,
} from './toggleConfig';
const getNodeBySystemId = (id: string) => document.querySelector(`[data-system-id='${id}']`);
const handleType = (type: string, id: string, elements: IKElements, modularContent: IKModularContent): { (): void} => {
const node = getNodeBySystemId(id);
if (!node) return null;
const {
textReplace,
} = getToogleDataByType(type, elements);
const children = Object.keys(modularContent).length
? Object.entries(modularContent)
.map(([key, value]) => handleType(value.system.type, value.system.id, value.elements, {}))
.filter((child) => !!child)
: [];
const toggleFunc = () => {
if (textReplace) replaceText(node, textReplace);
};
return toggleFunc;
};
export const getToggle = (id: string, content: IKContent) => {
const item = content.items[0];
return handleType(item.system.type, id, item.elements, content.modular_content)
};
By wrapping everything into a toggle function, we keep the state available inside of it. As kontent.ai will return a lot of data that will not be used, I decided to explicitly save the data that I need. I this inside of getToogleDataByType
.
// .service/toggleConfig.ts
// in my project I have 6 different data generators, so they ended up in a new file
const getGenericElements = (elements: IKElements, keyMapper: IKeyValue): IReplacer[] => {
const tempArr: IReplacer[] = [];
Object.entries(keyMapper).forEach(([key, querySelector]) => {
const data = elements[key]
if (!data) return;
tempArr.push({
querySelector,
value: data.value,
});
});
return tempArr;
};
// Toggle Data Config
const myType = (elements: IKElements): IToggleData => {
const textKeyMapper: IKeyValue = {
my_title: '.js-my_title',
};
return {
textReplace: getGenericElements(elements, textKeyMapper),
}
};
export const getToogleDataByType = (type: string, elements: IKElements): IToggleData => {
const callRegistry = {
myType: myType,
}
const caller = callRegistry[type];
return caller
? Object.assign({}, caller(elements))
: {};
}
Each replacer will give us an array with objects that will match the preview value with the dom selector (or whatever other stuff you can think of).
So how does the data generation actually translate to updating the dom when the toggle function is called?
It is basically just getting and saving the old value and setting the new one.
// .service/replaceContent.ts
const getElementByQuerySelector = (node: Element, querySelector: string): any => querySelector === null
? node
: node.querySelector(querySelector);
export const replaceText = (node: Element, textElements: IReplacer[]) => {
textElements.forEach(({querySelector, value}, i) => {
const element = getElementByQuerySelector(node, querySelector);
if (!element) return;
const old = element.textContent;
element.textContent = value;
textElements[i].value = old;
});
};
So we've got the basics up and running. But to only have one id previewed is a little boring.
더 많은 CMS 항목 추가
As we alread have an array of cms nodes, setting this up should be fairly easy. ☺
We just need an overlay and handle the add click with the already existing setup.
<!-- App.svelte -->
<script lang="ts">
import AddButton from './components/AddButton.svelte';
import AddBox from './components/AddBox.svelte';
const handleAddClick = (idToAdd: string) => {
handleAddMode();
loadPreview(idToAdd);
arrayOfCmsNodes = arrayOfCmsNodes.filter((node: HTMLElement) => node.dataset.systemId !== idToAdd);
}
const handleAddMode = () => {
addMode = !addMode;
if (addMode) {
arrayOfCmsNodes.forEach((node: HTMLElement) => {
const {top, height, left, width} = node.getBoundingClientRect();
overlays.push({
id: node.dataset.systemId,
top: top + window.scrollY,
height: height,
left: left,
width: width,
});
})
overlays = overlays;
} else {
overlays = [];
}
}
</script>
<main>
{#if arrayOfCmsNodes.length}
<AddButton
{addMode}
{handleAddMode} />
{/if}
</main>
{#each overlays as {id, top, height, left, width}}
<AddBox
{id}
{top}
{height}
{left}
{width}
{handleAddClick} />
{/each}
I know this part was by far the easiest one, but is adds a lot of value to the functionality, so I wanted to include it here.
Thank you for reading and I hope you can take away something or are inspired for your own project.
학점
표지 이미지: https://unsplash.com/@marvelous
Reference
이 문제에 관하여(Svelte 및 kentico kontent.ai), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://dev.to/dreitzner/cms-preview-with-svelte-and-kentico-kontent-ai-3aen텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)