B2B-88: add starter kit structure and elements
This commit is contained in:
3
packages/cms/wordpress/README.md
Normal file
3
packages/cms/wordpress/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# CMS/Wordpress - @kit/wordpress
|
||||
|
||||
Please refer to the [documentation](https://makerkit.dev/docs/next-supabase-turbo/content/wordpress).
|
||||
48
packages/cms/wordpress/docker-compose.yml
Normal file
48
packages/cms/wordpress/docker-compose.yml
Normal file
@@ -0,0 +1,48 @@
|
||||
services:
|
||||
db:
|
||||
# We use a mariadb image which supports both amd64 & arm64 architecture
|
||||
image: mariadb:10.6.4-focal
|
||||
# If you really want to use MySQL, uncomment the following line
|
||||
#image: mysql:8.0.27
|
||||
command: '--default-authentication-plugin=mysql_native_password'
|
||||
volumes:
|
||||
- db_data:/var/lib/mysql
|
||||
restart: always
|
||||
environment:
|
||||
- MYSQL_ROOT_PASSWORD=somewordpress
|
||||
- MYSQL_DATABASE=wordpress
|
||||
- MYSQL_USER=wordpress
|
||||
- MYSQL_PASSWORD=wordpress
|
||||
expose:
|
||||
- 3306
|
||||
- 33060
|
||||
wordpress:
|
||||
image: wordpress:latest
|
||||
ports:
|
||||
- 8080:80
|
||||
restart: always
|
||||
working_dir: /var/www/html
|
||||
volumes:
|
||||
- ./wp-content:/var/www/html/wp-content
|
||||
environment:
|
||||
- WORDPRESS_DB_HOST=db
|
||||
- WORDPRESS_DB_USER=wordpress
|
||||
- WORDPRESS_DB_PASSWORD=wordpress
|
||||
- WORDPRESS_DB_NAME=wordpress
|
||||
- WORDPRESS_DEBUG=1
|
||||
- WORDPRESS_CONFIG_EXTRA = |
|
||||
define('FS_METHOD', 'direct');
|
||||
/** disable wp core auto update */
|
||||
define('WP_AUTO_UPDATE_CORE', false);
|
||||
|
||||
/** local environment settings */
|
||||
define('WP_CACHE', false);
|
||||
define('ENVIRONMENT', 'local');
|
||||
|
||||
/** force site home url */
|
||||
if(!defined('WP_HOME')) {
|
||||
define('WP_HOME', 'http://localhost');
|
||||
define('WP_SITEURL', WP_HOME);
|
||||
}
|
||||
volumes:
|
||||
db_data:
|
||||
3
packages/cms/wordpress/eslint.config.mjs
Normal file
3
packages/cms/wordpress/eslint.config.mjs
Normal file
@@ -0,0 +1,3 @@
|
||||
import eslintConfigBase from '@kit/eslint-config/base.js';
|
||||
|
||||
export default eslintConfigBase;
|
||||
1
packages/cms/wordpress/node_modules/@kit/cms-types
generated
vendored
Symbolic link
1
packages/cms/wordpress/node_modules/@kit/cms-types
generated
vendored
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../types
|
||||
1
packages/cms/wordpress/node_modules/@kit/eslint-config
generated
vendored
Symbolic link
1
packages/cms/wordpress/node_modules/@kit/eslint-config
generated
vendored
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../../../tooling/eslint
|
||||
1
packages/cms/wordpress/node_modules/@kit/prettier-config
generated
vendored
Symbolic link
1
packages/cms/wordpress/node_modules/@kit/prettier-config
generated
vendored
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../../../tooling/prettier
|
||||
1
packages/cms/wordpress/node_modules/@kit/tsconfig
generated
vendored
Symbolic link
1
packages/cms/wordpress/node_modules/@kit/tsconfig
generated
vendored
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../../../tooling/typescript
|
||||
1
packages/cms/wordpress/node_modules/@kit/ui
generated
vendored
Symbolic link
1
packages/cms/wordpress/node_modules/@kit/ui
generated
vendored
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../../ui
|
||||
1
packages/cms/wordpress/node_modules/@types/node
generated
vendored
Symbolic link
1
packages/cms/wordpress/node_modules/@types/node
generated
vendored
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../../../node_modules/.pnpm/@types+node@22.15.30/node_modules/@types/node
|
||||
1
packages/cms/wordpress/node_modules/@types/react
generated
vendored
Symbolic link
1
packages/cms/wordpress/node_modules/@types/react
generated
vendored
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../../../node_modules/.pnpm/@types+react@19.1.4/node_modules/@types/react
|
||||
1
packages/cms/wordpress/node_modules/wp-types
generated
vendored
Symbolic link
1
packages/cms/wordpress/node_modules/wp-types
generated
vendored
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../../node_modules/.pnpm/wp-types@4.68.0/node_modules/wp-types
|
||||
34
packages/cms/wordpress/package.json
Normal file
34
packages/cms/wordpress/package.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "@kit/wordpress",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"scripts": {
|
||||
"clean": "git clean -xdf .turbo node_modules",
|
||||
"format": "prettier --check \"**/*.{ts,tsx}\"",
|
||||
"lint": "eslint .",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"start": "docker compose up"
|
||||
},
|
||||
"prettier": "@kit/prettier-config",
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./renderer": "./src/content-renderer.tsx"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@kit/cms-types": "workspace:*",
|
||||
"@kit/eslint-config": "workspace:*",
|
||||
"@kit/prettier-config": "workspace:*",
|
||||
"@kit/tsconfig": "workspace:*",
|
||||
"@kit/ui": "workspace:*",
|
||||
"@types/node": "^22.15.18",
|
||||
"@types/react": "19.1.4",
|
||||
"wp-types": "^4.68.0"
|
||||
},
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
"*": [
|
||||
"src/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
5
packages/cms/wordpress/src/content-renderer.tsx
Normal file
5
packages/cms/wordpress/src/content-renderer.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import React from 'react';
|
||||
|
||||
export function WordpressContentRenderer(props: { content: unknown }) {
|
||||
return <div dangerouslySetInnerHTML={{ __html: props.content as string }} />;
|
||||
}
|
||||
1
packages/cms/wordpress/src/index.ts
Normal file
1
packages/cms/wordpress/src/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './wp-client';
|
||||
423
packages/cms/wordpress/src/wp-client.ts
Normal file
423
packages/cms/wordpress/src/wp-client.ts
Normal file
@@ -0,0 +1,423 @@
|
||||
import type {
|
||||
WP_Post_Status_Name,
|
||||
WP_REST_API_Category,
|
||||
WP_REST_API_Post,
|
||||
WP_REST_API_Tag,
|
||||
} from 'wp-types';
|
||||
|
||||
import { Cms, CmsClient } from '@kit/cms-types';
|
||||
|
||||
import GetTagsOptions = Cms.GetTagsOptions;
|
||||
|
||||
/**
|
||||
* Creates a new WordpressClient instance.
|
||||
*
|
||||
* @param {string} apiUrl - The URL of the Wordpress API.
|
||||
* @returns {WordpressClient} A new WordpressClient instance.
|
||||
*/
|
||||
export function createWordpressClient(
|
||||
apiUrl = process.env.WORDPRESS_API_URL as string,
|
||||
) {
|
||||
return new WordpressClient(apiUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* @name WordpressClient
|
||||
* @description Represents a client for interacting with a Wordpress CMS.
|
||||
* Implements the CmsClient interface.
|
||||
*/
|
||||
class WordpressClient implements CmsClient {
|
||||
constructor(private readonly apiUrl: string) {}
|
||||
|
||||
/**
|
||||
* Retrieves content items from a CMS based on the provided options.
|
||||
*
|
||||
* @param {Cms.GetContentItemsOptions} options - The options to customize the retrieval of content items.
|
||||
*/
|
||||
async getContentItems(options: Cms.GetContentItemsOptions) {
|
||||
const queryParams = new URLSearchParams({
|
||||
_embed: 'true',
|
||||
});
|
||||
|
||||
if (options?.limit && options.limit !== Infinity) {
|
||||
queryParams.append('per_page', options.limit.toString());
|
||||
}
|
||||
|
||||
if (options?.offset) {
|
||||
queryParams.append('offset', options.offset.toString());
|
||||
}
|
||||
|
||||
if (options.sortBy) {
|
||||
const sortBy = mapSortByParam(options.sortBy);
|
||||
|
||||
if (sortBy) {
|
||||
queryParams.append('orderby', sortBy);
|
||||
}
|
||||
}
|
||||
|
||||
if (options.sortDirection) {
|
||||
queryParams.append('order', options.sortDirection);
|
||||
}
|
||||
|
||||
if (options?.categories) {
|
||||
const ids = await this.getCategories({
|
||||
slugs: options.categories,
|
||||
}).then((categories) => categories.map((category) => category.id));
|
||||
|
||||
if (ids.length) {
|
||||
queryParams.append('categories', ids.join(','));
|
||||
} else {
|
||||
console.warn(
|
||||
'No categories found for the provided slugs',
|
||||
options.categories,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (options?.tags) {
|
||||
const allTags = [...options.tags, options.language ?? ''].filter(Boolean);
|
||||
|
||||
const ids = await this.getTags({
|
||||
slugs: allTags,
|
||||
}).then((tags) => tags.map((tag) => tag.id));
|
||||
|
||||
if (ids.length) {
|
||||
queryParams.append('tags', ids.join(','));
|
||||
} else {
|
||||
console.warn('No tags found for the provided slugs', options.tags);
|
||||
}
|
||||
}
|
||||
|
||||
if (options?.parentIds && options.parentIds.length > 0) {
|
||||
queryParams.append('parent', options.parentIds.join(','));
|
||||
}
|
||||
|
||||
const endpoints = [
|
||||
`/wp-json/wp/v2/posts?${queryParams.toString()}`,
|
||||
`/wp-json/wp/v2/pages?${queryParams.toString()}`,
|
||||
];
|
||||
|
||||
const endpoint =
|
||||
options.collection === 'posts' ? endpoints[0] : endpoints[1];
|
||||
|
||||
const url = `${this.apiUrl}${endpoint}`;
|
||||
|
||||
const response = await fetch(url);
|
||||
const totalHeader = response.headers.get('X-WP-Total');
|
||||
|
||||
const total = totalHeader ? Number(totalHeader) : 0;
|
||||
const results = (await response.json()) as WP_REST_API_Post[];
|
||||
|
||||
const status = options.status ?? 'published';
|
||||
|
||||
const postsResults = await Promise.allSettled(
|
||||
results.map(async (item: WP_REST_API_Post) => {
|
||||
let parentId: string | undefined;
|
||||
|
||||
if (!item) {
|
||||
throw new Error('Failed to fetch content items');
|
||||
}
|
||||
|
||||
if (item.parent) {
|
||||
parentId = item.parent.toString();
|
||||
}
|
||||
|
||||
const mappedStatus = mapToStatus(item.status as WP_Post_Status_Name);
|
||||
|
||||
if (mappedStatus !== status) {
|
||||
throw new Error('Status does not match');
|
||||
}
|
||||
|
||||
const categories = await this.getCategoriesByIds(item.categories ?? []);
|
||||
const tags = await this.getTagsByIds(item.tags ?? []);
|
||||
const image = item.featured_media ? this.getFeaturedMedia(item) : '';
|
||||
const order = item.menu_order ?? 0;
|
||||
|
||||
return {
|
||||
id: item.id.toString(),
|
||||
title: item.title.rendered,
|
||||
label: item.title.rendered,
|
||||
content: item.content.rendered,
|
||||
description: item.excerpt.rendered,
|
||||
image,
|
||||
url: item.link,
|
||||
slug: item.slug,
|
||||
publishedAt: item.date,
|
||||
status: mappedStatus ?? 'draft',
|
||||
categories: categories,
|
||||
tags: tags,
|
||||
parentId,
|
||||
order,
|
||||
children: [],
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
const posts = postsResults
|
||||
.filter((item) => item.status === 'fulfilled')
|
||||
.map((item) => item.value);
|
||||
|
||||
return {
|
||||
total,
|
||||
items: posts,
|
||||
};
|
||||
}
|
||||
|
||||
async getContentItemBySlug({
|
||||
slug,
|
||||
collection,
|
||||
status,
|
||||
}: {
|
||||
slug: string;
|
||||
collection: string;
|
||||
status?: Cms.ContentItemStatus;
|
||||
}) {
|
||||
const searchParams = new URLSearchParams({
|
||||
_embed: 'true',
|
||||
slug,
|
||||
});
|
||||
|
||||
const endpoints = [
|
||||
`/wp-json/wp/v2/posts?${searchParams.toString()}`,
|
||||
`/wp-json/wp/v2/pages?${searchParams.toString()}`,
|
||||
];
|
||||
|
||||
const endpoint = collection === 'posts' ? endpoints[0] : endpoints[1];
|
||||
|
||||
const responses = await fetch(this.apiUrl + endpoint).then(
|
||||
(res) => res.json() as Promise<WP_REST_API_Post[]>,
|
||||
);
|
||||
|
||||
const item = responses[0];
|
||||
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
|
||||
const mappedStatus = status
|
||||
? mapToStatus(item.status as WP_Post_Status_Name)
|
||||
: undefined;
|
||||
const statusMatch = status ? mappedStatus === status : true;
|
||||
|
||||
if (!statusMatch) {
|
||||
return;
|
||||
}
|
||||
|
||||
const categories = await this.getCategoriesByIds(item.categories ?? []);
|
||||
const tags = await this.getTagsByIds(item.tags ?? []);
|
||||
const image = item.featured_media ? this.getFeaturedMedia(item) : '';
|
||||
|
||||
return {
|
||||
id: item.id.toString(),
|
||||
image,
|
||||
order: item.menu_order ?? 0,
|
||||
url: item.link,
|
||||
description: item.excerpt.rendered,
|
||||
children: [],
|
||||
title: item.title.rendered,
|
||||
label: item.title.rendered,
|
||||
content: item.content.rendered,
|
||||
slug: item.slug,
|
||||
publishedAt: item.date,
|
||||
status: mappedStatus ?? 'draft',
|
||||
categories,
|
||||
tags,
|
||||
parentId: item.parent?.toString(),
|
||||
};
|
||||
}
|
||||
|
||||
async getCategoryBySlug(slug: string) {
|
||||
const url = `${this.apiUrl}/wp-json/wp/v2/categories?slug=${slug}`;
|
||||
const response = await fetch(url);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const item = data[0] as WP_REST_API_Category;
|
||||
|
||||
return {
|
||||
id: item.id.toString(),
|
||||
name: item.name,
|
||||
slug: item.slug,
|
||||
};
|
||||
}
|
||||
|
||||
async getTagBySlug(slug: string) {
|
||||
const url = `${this.apiUrl}/wp-json/wp/v2/tags?slug=${slug}`;
|
||||
const response = await fetch(url);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const item = data[0] as WP_REST_API_Tag;
|
||||
|
||||
return {
|
||||
id: item.id.toString(),
|
||||
name: item.name,
|
||||
slug: item.slug,
|
||||
};
|
||||
}
|
||||
|
||||
async getCategories(options?: Cms.GetCategoriesOptions) {
|
||||
const queryParams = new URLSearchParams();
|
||||
|
||||
if (options?.limit) {
|
||||
queryParams.append('per_page', options.limit.toString());
|
||||
}
|
||||
|
||||
if (options?.offset) {
|
||||
queryParams.append('offset', options.offset.toString());
|
||||
}
|
||||
|
||||
if (options?.slugs) {
|
||||
const slugs = options.slugs.join(',');
|
||||
|
||||
queryParams.append('slug', slugs);
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
`${this.apiUrl}/wp-json/wp/v2/categories?${queryParams.toString()}`,
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('Failed to fetch categories', await response.json());
|
||||
|
||||
throw new Error('Failed to fetch categories');
|
||||
}
|
||||
|
||||
const data = (await response.json()) as WP_REST_API_Category[];
|
||||
|
||||
return data.map((item) => ({
|
||||
id: item.id.toString(),
|
||||
name: item.name,
|
||||
slug: item.slug,
|
||||
}));
|
||||
}
|
||||
|
||||
async getTags(options: GetTagsOptions) {
|
||||
const queryParams = new URLSearchParams();
|
||||
|
||||
if (options?.limit) {
|
||||
queryParams.append('per_page', options.limit.toString());
|
||||
}
|
||||
|
||||
if (options?.offset) {
|
||||
queryParams.append('offset', options.offset.toString());
|
||||
}
|
||||
|
||||
if (options?.slugs) {
|
||||
const slugs = options.slugs.join(',');
|
||||
queryParams.append('slug', slugs);
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
`${this.apiUrl}/wp-json/wp/v2/tags?${queryParams.toString()}`,
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
console.error('Failed to fetch tags', await response.json());
|
||||
|
||||
throw new Error('Failed to fetch tags');
|
||||
}
|
||||
|
||||
const data = (await response.json()) as WP_REST_API_Tag[];
|
||||
|
||||
return data.map((item) => ({
|
||||
id: item.id.toString(),
|
||||
name: item.name,
|
||||
slug: item.slug,
|
||||
}));
|
||||
}
|
||||
|
||||
private async getTagsByIds(ids: number[]) {
|
||||
const promises = ids.map((id) =>
|
||||
fetch(`${this.apiUrl}/wp-json/wp/v2/tags/${id}`),
|
||||
);
|
||||
|
||||
const responses = await Promise.all(promises);
|
||||
|
||||
const data = (await Promise.all(
|
||||
responses.map((response) => response.json()),
|
||||
)) as WP_REST_API_Tag[];
|
||||
|
||||
return data.map((item) => ({
|
||||
id: item.id.toString(),
|
||||
name: item.name,
|
||||
slug: item.slug,
|
||||
}));
|
||||
}
|
||||
|
||||
private async getCategoriesByIds(ids: number[]) {
|
||||
const promises = ids.map((id) =>
|
||||
fetch(`${this.apiUrl}/wp-json/wp/v2/categories/${id}`),
|
||||
);
|
||||
|
||||
const responses = await Promise.all(promises);
|
||||
|
||||
const data = (await Promise.all(
|
||||
responses.map((response) => response.json()),
|
||||
)) as WP_REST_API_Category[];
|
||||
|
||||
return data.map((item) => ({
|
||||
id: item.id.toString(),
|
||||
name: item.name,
|
||||
slug: item.slug,
|
||||
}));
|
||||
}
|
||||
|
||||
private getFeaturedMedia(post: WP_REST_API_Post) {
|
||||
const embedded = post._embedded ?? {
|
||||
'wp:featuredmedia': [],
|
||||
};
|
||||
|
||||
const media = embedded['wp:featuredmedia'] ?? [];
|
||||
const item = media?.length > 0 ? media[0] : null;
|
||||
|
||||
return item
|
||||
? (
|
||||
item as {
|
||||
source_url: string;
|
||||
}
|
||||
).source_url
|
||||
: '';
|
||||
}
|
||||
}
|
||||
|
||||
function mapSortByParam(sortBy: string) {
|
||||
switch (sortBy) {
|
||||
case 'publishedAt':
|
||||
return 'date';
|
||||
case 'title':
|
||||
return 'title';
|
||||
case 'slug':
|
||||
return 'slug';
|
||||
case 'order':
|
||||
return 'menu_order';
|
||||
default:
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function mapToStatus(status: WP_Post_Status_Name): Cms.ContentItemStatus {
|
||||
const Draft = 'draft' as WP_Post_Status_Name;
|
||||
const Publish = 'publish' as WP_Post_Status_Name;
|
||||
const Pending = 'pending' as WP_Post_Status_Name;
|
||||
|
||||
switch (status) {
|
||||
case Draft:
|
||||
return 'draft';
|
||||
|
||||
case Publish:
|
||||
return 'published';
|
||||
|
||||
case Pending:
|
||||
return 'pending';
|
||||
|
||||
default:
|
||||
return 'draft';
|
||||
}
|
||||
}
|
||||
8
packages/cms/wordpress/tsconfig.json
Normal file
8
packages/cms/wordpress/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "@kit/tsconfig/base.json",
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
<?php
|
||||
|
||||
<footer>
|
||||
</footer>
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
function register_category_and_tag_with_pages(){
|
||||
/*add categories and tags to pages*/
|
||||
register_taxonomy_for_object_type('category', 'page');
|
||||
register_taxonomy_for_object_type('post_tag', 'page');
|
||||
}
|
||||
add_action( 'init', 'register_category_and_tag_with_pages');
|
||||
|
||||
function register_pre_get_category_and_tag_with_pages( $query ) {
|
||||
|
||||
if ( is_admin() || ! $query->is_main_query() ) {
|
||||
return;
|
||||
}
|
||||
/*view categories and tags archive pages */
|
||||
if($query->is_category && $query->is_main_query()){
|
||||
$query->set('post_type', array( 'post', 'page'));
|
||||
}
|
||||
if($query->is_tag && $query->is_main_query()){
|
||||
$query->set('post_type', array( 'post', 'page'));
|
||||
}
|
||||
}
|
||||
add_action( 'pre_get_posts', 'register_pre_get_category_and_tag_with_pages');
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html <?php language_attributes(); ?>>
|
||||
<head>
|
||||
<meta charset="<?php bloginfo( 'charset' ); ?>">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<?php wp_head(); ?>
|
||||
</head>
|
||||
|
||||
<body <?php body_class(); ?>>
|
||||
|
||||
<div class="wrap">
|
||||
<div id="primary" class="content-area">
|
||||
<main id="main" class="site-main" role="main">
|
||||
|
||||
<?php
|
||||
if (have_posts()) :
|
||||
/* Start the Loop */
|
||||
while (have_posts()) : the_post();
|
||||
?>
|
||||
<div>
|
||||
<a href="<?php the_permalink(); ?>"><h3><?php the_title(); ?></h3></a>
|
||||
</div>
|
||||
<?php
|
||||
endwhile;
|
||||
/* End the Loop */
|
||||
else :
|
||||
// Nothing
|
||||
endif;
|
||||
?>
|
||||
|
||||
</main><!-- #main -->
|
||||
</div><!-- #primary -->
|
||||
</div><!-- .wrap -->
|
||||
|
||||
<?php get_footer(); ?>
|
||||
<?php wp_footer(); ?>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user