Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 87 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,88 @@
/node_modules
# Dependencies
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Build outputs
app.js
dist/
build/

# SSL certificates (local development only)
ssl/
*.pem
*.key
*.crt

# Environment variables
.env
.env.local
.env.development.local
.env.test.local
.env.production.local

# IDE/Editor files
.vscode/
.idea/
*.swp
*.swo
*~

# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db

# Logs
logs
*.log

# Runtime data
pids
*.pid
*.seed
*.pid.lock

# Coverage directory used by tools like istanbul
coverage/
*.lcov

# nyc test coverage
.nyc_output

# Dependency directories
jspm_packages/

# Optional npm cache directory
.npm

# Optional eslint cache
.eslintcache

# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/

# Optional REPL history
.node_repl_history

# Output of 'npm pack'
*.tgz

# Yarn Integrity file
.yarn-integrity

# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache

# Temporary folders
tmp/
temp/
68 changes: 68 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,72 @@ npm run build
* The `Hosted code URL` is where you've deployed the element to in step 1.
* Add the URL slug element's codename into configuration.

## Local Development with HTTPS

For local development and testing with Kontent.ai (which requires HTTPS), this project includes SSL certificate generation and HTTPS server support.

### Quick Start

```bash
# First time setup - generate SSL certificates
npm run setup:ssl

# Build and start HTTPS server
npm run dev:https
```

**Note:** If you get "Could not find certificate ssl/cert.pem" error, run `npm run setup:ssl` first.

This will:
1. Generate self-signed SSL certificates (if needed)
2. Build the TypeScript code
3. Start an HTTPS server on port 8443

### Available URLs
- `https://localhost:8443`
- `https://127.0.0.1:8443`

### Available Scripts

- `npm run setup:ssl` - Generate SSL certificates for HTTPS development
- `npm run serve:https` - Start HTTPS server on port 8443
- `npm run dev:https` - Build and start HTTPS server
- `npm run serve` - Start HTTP server on port 8080
- `npm run dev` - Build with watch mode (for development)
- `npm run build` - Build for production

### SSL Certificate Setup

**Manual SSL Certificate Generation:**

If you prefer to generate certificates manually or the npm script doesn't work:

```bash
# Create ssl directory
mkdir -p ssl

# Generate self-signed certificate and private key
openssl req -x509 -newkey rsa:4096 -keyout ssl/key.pem -out ssl/cert.pem -days 365 -nodes -subj "/C=US/ST=State/L=City/O=Organization/CN=localhost"
```

The SSL certificates will be created in the `ssl/` folder:
- `ssl/cert.pem` - SSL certificate
- `ssl/key.pem` - Private key

**Requirements:**
- OpenSSL must be installed on your system
- On macOS: OpenSSL comes pre-installed
- On Windows: Install via [Git for Windows](https://git-scm.com/download/win) or [OpenSSL for Windows](https://slproweb.com/products/Win32OpenSSL.html)
- On Linux: Install via package manager (`sudo apt-get install openssl` or similar)

**Important:** Your browser will show a security warning for self-signed certificates. Click "Advanced" → "Proceed to localhost" to continue.

### Using with Kontent.ai

1. Start the HTTPS server: `npm run dev:https`
2. In Kontent.ai, use `https://localhost:8443` as your custom element URL
3. Accept the browser security warning when prompted

## Configuration

The custom element may only be used for content types that contain the URL slug element. The configuration of the custom element looks like this:
Expand All @@ -38,6 +104,8 @@ Netlify has made this easy. If you click the deploy button below, it will guide

[![Deploy to Netlify](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/ondrabus/kontent-url-slug-history-custom-element)

Don't forget to add `npm run build` to the build command in Netlify settings, so that the TypeScript code is compiled before deployment.

## What is Saved?

The value is an array of strings (old URL slugs).
Expand Down
63 changes: 36 additions & 27 deletions app.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,25 @@
import { createDeliveryClient, DeliveryClient } from "@kentico/kontent-delivery";
import { createDeliveryClient, DeliveryClient } from "@kontent-ai/delivery-sdk";
import { IContext } from "./customElementModels/IContext";
import { ICustomElement } from "./customElementModels/ICustomElement";
import { IElement } from "./customElementModels/IElement";

const customElement = window['CustomElement'] as ICustomElement
let urlSlugElementCodename: string = null
let contentItemCodename: string = null
let currentPublishedUrlSlug: string = null
let pagePublished: boolean = null
let urlSlugElementCodename: string | null = null
let contentItemCodename: string | null = null
let currentPublishedUrlSlug: string | null = null
let pagePublished: boolean | null = null
let initialized: boolean = false
let deliveryClient: DeliveryClient = null
let projectId: string = null
let deliveryClient: DeliveryClient | null = null
let projectId: string | null = null
let history: string[] = []
let disabled: boolean = false
const label = document.querySelector("#slug-container")

const initCustomElement = (element: IElement, context: IContext) => {
if (!element.config || !context){
throw new Error('Element and context must be defined.')
}

urlSlugElementCodename = element.config['urlSlugElementCodename']
if (!urlSlugElementCodename){
throw new Error('The "urlSlugElementCodename" must be defined in custom element config.')
Expand All @@ -24,9 +28,9 @@ const initCustomElement = (element: IElement, context: IContext) => {
contentItemCodename = context.item.codename
disabled = element.disabled

history = JSON.parse(element.value) ?? []
history = element.value ? JSON.parse(element.value) : []
deliveryClient = createDeliveryClient({
projectId: projectId,
environmentId: projectId,
globalHeaders: () => [
{
header: 'X-KC-Wait-For-Loading-New-Content',
Expand All @@ -42,7 +46,7 @@ const initCustomElement = (element: IElement, context: IContext) => {
.toPromise()
.then(res => {
if (res.response.status == 200){
currentPublishedUrlSlug = res.data.item.elements[urlSlugElementCodename].value
currentPublishedUrlSlug = res.data.item.elements[urlSlugElementCodename as string].value
pagePublished = true
}})
.catch(err => {
Expand All @@ -55,7 +59,7 @@ const initCustomElement = (element: IElement, context: IContext) => {
})
} else {
initialized = true
document.querySelector('.manual-input').remove()
document.querySelector('.manual-input')?.remove()
displayHistory()
}
} catch (error) {
Expand All @@ -65,8 +69,8 @@ const initCustomElement = (element: IElement, context: IContext) => {
}

const elementChanged = (changedElementCodenames: string[]) => {
if (changedElementCodenames.includes(urlSlugElementCodename)) {
customElement.getElementValue(urlSlugElementCodename, processNewUrlSlug)
if (changedElementCodenames.includes(urlSlugElementCodename as string)) {
customElement.getElementValue(urlSlugElementCodename as string, processNewUrlSlug)
}
}

Expand Down Expand Up @@ -103,31 +107,32 @@ const addSlugToHistory = (urlSlug: string) => {
displayHistory()
}

document.querySelector("#add-button").addEventListener(
'click',
(e) => {
const urlSlug = (document.querySelector("input[name=newUrlSlug]") as HTMLInputElement).value
if (!urlSlug){
alert("The URL slug cannot be empty")
return
}
addSlugToHistory(urlSlug)
document.getElementById('manual-input-form')?.addEventListener('submit', (e) => {
e.preventDefault();
const urlSlug = (document.querySelector("input[name=newUrlSlug]") as HTMLInputElement).value
if (!urlSlug){
alert("The URL slug cannot be empty")
return
}
)
addSlugToHistory(urlSlug)
});

const displayHistory = () => {
if (!label) {
return;
}
if (!initialized) {
label.innerHTML = '(loading...)'
return
}

if (Array.isArray(history) && history.length > 0){
if (Array.isArray(history) && history.length > 0 && label){
label.innerHTML = `<div class="list">
${history.map(historySlugItem => {
if (disabled || historySlugItem !== currentPublishedUrlSlug){
return `<div>
${!disabled ? `<div>
<button class='btn btn--secondary btn--s' data-slug='${historySlugItem}'>
<button type="button" class='btn btn--secondary btn--s' data-slug='${historySlugItem}'>
<span>remove</span>
</button>
</div>` : ''}
Expand All @@ -152,8 +157,12 @@ const displayHistory = () => {
.forEach(btn =>
btn.addEventListener(
'click',
(e) =>
removeSlugFromHistory((e.currentTarget as HTMLButtonElement).getAttribute('data-slug'))))
(e) => {
const slug = (e.currentTarget as HTMLButtonElement).getAttribute('data-slug')
if (slug) {
removeSlugFromHistory(slug)
}
}))
}
}

Expand Down
12 changes: 6 additions & 6 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<script type="text/javascript">
window["CustomElement"] = CustomElement
</script>
<link rel="stylesheet" href="https://kentico.github.io/kontent-custom-element-samples/shared/custom-element-v2.css">
<link rel="stylesheet" href="./styles.css">
<style>
body
{
Expand Down Expand Up @@ -55,15 +55,15 @@
<div id="slug-container">
<i class="icon-rotate-double-right"></i> initializing...
</div>
<div class="manual-input">
<p class="u-spacing-l action-medium">Manual input</p>
<form id="manual-input-form" class="manual-input">
<label for="newUrlSlug" class="u-spacing-l action-medium">Manual input</label>
<div>
<div class="text-field">
<input class="text-field__input" type="text" name="newUrlSlug" placeholder="Enter url slug" />
<input id="newUrlSlug" class="text-field__input" type="text" name="newUrlSlug" placeholder="Enter url slug">
</div>
<button class="btn btn--primary" id="add-button">Add</button>
<button type="submit" class="btn btn--primary" id="add-button">Add</button>
</div>
</div>
</form>
<script src="./app.js"></script>
</body>
</html>
Binary file added kontent-ai-icons-v3.0.1.woff2
Binary file not shown.
Loading